From 071bf3943e5aba4cc2fb96810cb45cb9c37b203a Mon Sep 17 00:00:00 2001
From: louismorgner
Date: Mon, 8 Dec 2025 17:23:43 +0200
Subject: [PATCH 1/2] feat: add onboarding flow UI with metrics tracking step
- Add 6-step onboarding flow with split-screen layout
- Implement organization name, member import, team setup, role creation, KPI/metrics, and finish steps
- Add data source dropdown and goal type toggle (absolute/relative) for metrics
- Add numeric input validation for goal fields
- Note: This is UI-only implementation, no data persistence yet
---
.../onboarding/_components/finish-step.tsx | 127 ++++
.../_components/import-members-step.tsx | 151 +++++
src/app/onboarding/_components/kpi-step.tsx | 286 ++++++++
.../onboarding/_components/org-name-step.tsx | 99 +++
.../_components/progress-indicator.tsx | 53 ++
.../_components/role-creation-step.tsx | 136 ++++
.../onboarding/_components/step-visuals.tsx | 623 ++++++++++++++++++
.../_components/team-setup-step.tsx | 102 +++
src/app/onboarding/layout.tsx | 14 +
src/app/onboarding/page.tsx | 94 +++
10 files changed, 1685 insertions(+)
create mode 100644 src/app/onboarding/_components/finish-step.tsx
create mode 100644 src/app/onboarding/_components/import-members-step.tsx
create mode 100644 src/app/onboarding/_components/kpi-step.tsx
create mode 100644 src/app/onboarding/_components/org-name-step.tsx
create mode 100644 src/app/onboarding/_components/progress-indicator.tsx
create mode 100644 src/app/onboarding/_components/role-creation-step.tsx
create mode 100644 src/app/onboarding/_components/step-visuals.tsx
create mode 100644 src/app/onboarding/_components/team-setup-step.tsx
create mode 100644 src/app/onboarding/layout.tsx
create mode 100644 src/app/onboarding/page.tsx
diff --git a/src/app/onboarding/_components/finish-step.tsx b/src/app/onboarding/_components/finish-step.tsx
new file mode 100644
index 00000000..750a7295
--- /dev/null
+++ b/src/app/onboarding/_components/finish-step.tsx
@@ -0,0 +1,127 @@
+"use client";
+
+import Link from "next/link";
+
+import { motion } from "framer-motion";
+import { ArrowRight, Check } from "lucide-react";
+
+interface FinishStepProps {
+ onBack: () => void;
+}
+
+export function FinishStep({ onBack }: FinishStepProps) {
+ // Mock team ID - will be replaced with actual ID when hooked up
+ const mockTeamId = "demo-team";
+
+ return (
+
+ {/* Success icon */}
+
+
+
+
+
+ you're all set
+
+
+ your organization is ready. let's build your first role canvas.
+
+
+ {/* Summary of what was created */}
+
+
+
+
+ ORGANIZATION
+
+
+ created
+
+
+
+
+
+ TEAM
+
+
+ created
+
+
+
+
+
+ FIRST ROLE
+
+
+ defined
+
+
+
+
+
+
+
+ open role canvas
+
+
+
+
+
+ you can always add more roles, teams, and metrics later from your
+ dashboard.
+
+
+ );
+}
diff --git a/src/app/onboarding/_components/import-members-step.tsx b/src/app/onboarding/_components/import-members-step.tsx
new file mode 100644
index 00000000..dd5e073a
--- /dev/null
+++ b/src/app/onboarding/_components/import-members-step.tsx
@@ -0,0 +1,151 @@
+"use client";
+
+import { useState } from "react";
+
+import { motion } from "framer-motion";
+
+interface ImportMembersStepProps {
+ onNext: () => void;
+ onBack: () => void;
+}
+
+export function ImportMembersStep({ onNext, onBack }: ImportMembersStepProps) {
+ const [selectedIntegration, setSelectedIntegration] = useState<
+ "slack" | "google" | null
+ >(null);
+
+ return (
+
+
+ bring in your team
+
+
+ connect your workspace to automatically import team members and keep
+ everything in sync.
+
+
+
+ {/* Google Workspace option */}
+
+
+ {/* Slack option */}
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/onboarding/_components/kpi-step.tsx b/src/app/onboarding/_components/kpi-step.tsx
new file mode 100644
index 00000000..9b47993f
--- /dev/null
+++ b/src/app/onboarding/_components/kpi-step.tsx
@@ -0,0 +1,286 @@
+"use client";
+
+import { useState } from "react";
+
+import { motion } from "framer-motion";
+
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
+
+interface KpiStepProps {
+ onNext: () => void;
+ onBack: () => void;
+}
+
+type GoalType = "absolute" | "relative";
+
+export function KpiStep({ onNext, onBack }: KpiStepProps) {
+ const [kpiName, setKpiName] = useState("");
+ const [integrationSource, setIntegrationSource] = useState("");
+ const [goalType, setGoalType] = useState("absolute");
+ const [absoluteGoal, setAbsoluteGoal] = useState("");
+ const [relativeGoal, setRelativeGoal] = useState("");
+ const [relativePeriod, setRelativePeriod] = useState("month");
+
+ const handleAbsoluteGoalChange = (e: React.ChangeEvent) => {
+ const value = e.target.value;
+ // Allow empty string, numbers, and decimal point
+ if (value === "" || /^\d*\.?\d*$/.test(value)) {
+ setAbsoluteGoal(value);
+ }
+ };
+
+ const handleRelativeGoalChange = (e: React.ChangeEvent) => {
+ const value = e.target.value;
+ // Allow empty string, numbers, and decimal point
+ if (value === "" || /^\d*\.?\d*$/.test(value)) {
+ setRelativeGoal(value);
+ }
+ };
+
+ const integrationOptions = [
+ { value: "manual", label: "manual entry" },
+ { value: "google-sheets", label: "google sheets" },
+ { value: "github", label: "github" },
+ { value: "posthog", label: "posthog" },
+ { value: "youtube", label: "youtube" },
+ { value: "slack", label: "slack" },
+ ];
+
+ return (
+
+
+ track what matters
+
+
+ connect a key metric to this role. what number should they move?
+
+
+ {/* Example metrics */}
+
+ {["MRR", "NPS score", "conversion rate", "churn %", "active users"].map(
+ (example) => (
+
+ ),
+ )}
+
+
+
+ {/* Metric Name */}
+
+
+ setKpiName(e.target.value)}
+ placeholder="e.g., monthly recurring revenue"
+ className="border-border text-foreground placeholder:text-muted-foreground/50 focus:border-foreground/30 w-full rounded-sm border bg-transparent px-4 py-3 font-sans transition-colors focus:outline-none"
+ style={{ letterSpacing: "-0.02em" }}
+ autoFocus
+ />
+
+
+ {/* Integration Source */}
+
+
+
+
+
+ {/* Goal Type Toggle */}
+
+
+ {
+ if (value) setGoalType(value as GoalType);
+ }}
+ variant="outline"
+ className="w-full"
+ >
+
+
+ absolute
+
+
+
+
+ relative
+
+
+
+
+
+ {/* Goal Inputs */}
+ {goalType === "absolute" ? (
+
+
+
+
+ enter a numeric target value.
+
+
+ ) : (
+
+
+
+
+
+
+ %
+
+
+
+
+
+
+
+
+ track growth as a percentage increase over time.
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/onboarding/_components/org-name-step.tsx b/src/app/onboarding/_components/org-name-step.tsx
new file mode 100644
index 00000000..50dd85a2
--- /dev/null
+++ b/src/app/onboarding/_components/org-name-step.tsx
@@ -0,0 +1,99 @@
+"use client";
+
+import { useState } from "react";
+
+import { motion } from "framer-motion";
+
+interface OrgNameStepProps {
+ onNext: () => void;
+}
+
+export function OrgNameStep({ onNext }: OrgNameStepProps) {
+ const [orgName, setOrgName] = useState("");
+ const [orgUrl, setOrgUrl] = useState("");
+ const canContinue = orgName.trim().length > 0;
+
+ return (
+
+
+ welcome to ryō
+
+
+ let's begin by setting up your organization. this will only take a
+ few minutes.
+
+
+
+
+
+ setOrgName(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && canContinue) {
+ onNext();
+ }
+ }}
+ placeholder="acme inc."
+ className="border-border text-foreground placeholder:text-muted-foreground/50 focus:border-foreground/30 w-full rounded-sm border bg-transparent px-4 py-3 font-sans transition-colors focus:outline-none"
+ style={{ letterSpacing: "-0.02em" }}
+ autoFocus
+ />
+
+
+
+
+
setOrgUrl(e.target.value)}
+ placeholder="https://acme.com"
+ className="border-border text-foreground placeholder:text-muted-foreground/50 focus:border-foreground/30 w-full rounded-sm border bg-transparent px-4 py-3 font-sans transition-colors focus:outline-none"
+ style={{ letterSpacing: "-0.02em" }}
+ />
+
+ we'll use this to learn more about your company and personalize
+ your experience.
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/onboarding/_components/progress-indicator.tsx b/src/app/onboarding/_components/progress-indicator.tsx
new file mode 100644
index 00000000..06631906
--- /dev/null
+++ b/src/app/onboarding/_components/progress-indicator.tsx
@@ -0,0 +1,53 @@
+"use client";
+
+import { motion } from "framer-motion";
+
+interface ProgressIndicatorProps {
+ currentStep: number;
+ totalSteps: number;
+}
+
+export function ProgressIndicator({
+ currentStep,
+ totalSteps,
+}: ProgressIndicatorProps) {
+ return (
+
+ {Array.from({ length: totalSteps }, (_, i) => {
+ const stepNumber = i + 1;
+ const isActive = stepNumber === currentStep;
+ const isCompleted = stepNumber < currentStep;
+
+ return (
+
+
+ {isActive && (
+
+ )}
+
+ );
+ })}
+
+ );
+}
diff --git a/src/app/onboarding/_components/role-creation-step.tsx b/src/app/onboarding/_components/role-creation-step.tsx
new file mode 100644
index 00000000..137f2474
--- /dev/null
+++ b/src/app/onboarding/_components/role-creation-step.tsx
@@ -0,0 +1,136 @@
+"use client";
+
+import { useState } from "react";
+
+import { motion } from "framer-motion";
+
+interface RoleCreationStepProps {
+ onNext: () => void;
+ onBack: () => void;
+}
+
+export function RoleCreationStep({ onNext, onBack }: RoleCreationStepProps) {
+ const [title, setTitle] = useState("");
+ const [purpose, setPurpose] = useState("");
+ const [accountabilities, setAccountabilities] = useState("");
+
+ const canContinue = title.trim().length > 0 && purpose.trim().length > 0;
+
+ return (
+
+
+ define your first role
+
+
+ forget job titles. think about what your company needs to succeed, then
+ work backwards.
+
+
+
+
+
+
setTitle(e.target.value)}
+ placeholder="what does your company need?"
+ className="border-border text-foreground placeholder:text-muted-foreground/50 focus:border-foreground/30 w-full rounded-sm border bg-transparent px-4 py-3 font-sans transition-colors focus:outline-none"
+ style={{ letterSpacing: "-0.02em" }}
+ autoFocus
+ />
+
+ think outcomes, not job titles. "revenue driver" beats
+ "sales rep".
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/onboarding/_components/step-visuals.tsx b/src/app/onboarding/_components/step-visuals.tsx
new file mode 100644
index 00000000..8c35b732
--- /dev/null
+++ b/src/app/onboarding/_components/step-visuals.tsx
@@ -0,0 +1,623 @@
+"use client";
+
+import { motion } from "framer-motion";
+
+// Shared animation config
+const containerVariants = {
+ initial: { opacity: 0 },
+ animate: { opacity: 1 },
+ exit: { opacity: 0 },
+};
+
+// Step 1: Organization building visualization
+export function OrgNameVisual() {
+ return (
+
+ {/* Grid background */}
+
+
+ {/* Abstract building blocks */}
+
+
+
+
+
+ {/* Label */}
+
+
+ YOUR ORGANIZATION
+
+
+
+
+ );
+}
+
+// Step 2: Connected people visualization
+export function ImportMembersVisual() {
+ return (
+
+
+ {/* Center node */}
+
+
+
+
+ {/* Orbiting nodes */}
+ {[0, 60, 120, 180, 240, 300].map((angle, i) => {
+ const radius = 80;
+ const x = Math.cos((angle * Math.PI) / 180) * radius;
+ const y = Math.sin((angle * Math.PI) / 180) * radius;
+ return (
+
+
+
+ );
+ })}
+
+ {/* Connection lines */}
+
+
+ {/* Label */}
+
+
+ TEAM MEMBERS
+
+
+
+
+ );
+}
+
+// Step 3: Team structure visualization
+export function TeamSetupVisual() {
+ return (
+
+
+ {/* Team cards */}
+ {["product", "growth", "engineering"].map((team, i) => (
+
+
+
+ ))}
+
+ {/* Label */}
+
+
+ ORGANIZE BY PURPOSE
+
+
+
+
+ );
+}
+
+// Step 4: Educational breakdown visualization
+export function RoleCreationVisual() {
+ return (
+
+
+
+ INSTEAD OF "MARKETING MANAGER"...
+
+
+
+ →
+
+
+ TITLE
+
+
+ what your company needs
+
+
+ "growth engine"
+
+
+
+
+ →
+
+
+ PURPOSE
+
+
+ why this role exists
+
+
+ "to acquire customers efficiently"
+
+
+
+
+ →
+
+
+ ACCOUNTABILITIES
+
+
+ what they own
+
+
+ "manage paid acquisition, optimize funnel"
+
+
+
+
+
+
+ );
+}
+
+// Step 5: KPI/Metric visualization
+export function KpiVisual() {
+ const dataPoints = [35, 42, 38, 55, 48, 62, 58, 72];
+
+ return (
+
+
+ {/* Metric header */}
+
+
+
+ monthly recurring revenue
+
+
+ {/* Value and chart */}
+
+
+
+ $47k
+
+
+ +12%
+
+
+
+ {/* Mini chart */}
+
+
+
+
+
+
+
+ `L${i * 11.4},${40 - p * 0.5}`).join(" ")} L80,${40 - dataPoints[7] * 0.5} L80,40 L0,40 Z`}
+ fill="url(#kpiGradient)"
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ transition={{ delay: 0.7, duration: 0.3 }}
+ />
+ `L${i * 11.4},${40 - p * 0.5}`).join(" ")}`}
+ fill="none"
+ stroke="rgb(142, 157, 172)"
+ strokeWidth="2"
+ initial={{ pathLength: 0 }}
+ animate={{ pathLength: 1 }}
+ transition={{ delay: 0.5, duration: 0.6 }}
+ />
+
+
+
+ {/* Target */}
+
+
+ TARGET: $100K
+
+
+
+
+ );
+}
+
+// Step 6: Completion celebration visualization
+export function FinishVisual() {
+ return (
+
+
+ {/* Connected structure */}
+
+
+ {/* Nodes */}
+
+ ORG
+
+
+
+
+ TEAM
+
+
+
+
+
+ ROLE
+
+
+
+
+
+ KPI
+
+
+
+ {/* Checkmark */}
+
+
+
+
+ {/* Label */}
+
+
+ READY TO GO
+
+
+
+
+ );
+}
diff --git a/src/app/onboarding/_components/team-setup-step.tsx b/src/app/onboarding/_components/team-setup-step.tsx
new file mode 100644
index 00000000..2bdfaa84
--- /dev/null
+++ b/src/app/onboarding/_components/team-setup-step.tsx
@@ -0,0 +1,102 @@
+"use client";
+
+import { useState } from "react";
+
+import { motion } from "framer-motion";
+
+interface TeamSetupStepProps {
+ onNext: () => void;
+ onBack: () => void;
+}
+
+export function TeamSetupStep({ onNext, onBack }: TeamSetupStepProps) {
+ const [teamName, setTeamName] = useState("");
+ const [teamDescription, setTeamDescription] = useState("");
+ const canContinue = teamName.trim().length > 0;
+
+ return (
+
+
+ create your first team
+
+
+ teams help you organize roles around a shared purpose—like product,
+ growth, or operations.
+
+
+
+
+
+ setTeamName(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && canContinue) {
+ onNext();
+ }
+ }}
+ placeholder="e.g., product, growth, engineering"
+ className="border-border text-foreground placeholder:text-muted-foreground/50 focus:border-foreground/30 w-full rounded-sm border bg-transparent px-4 py-3 font-sans transition-colors focus:outline-none"
+ style={{ letterSpacing: "-0.02em" }}
+ autoFocus
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/onboarding/layout.tsx b/src/app/onboarding/layout.tsx
new file mode 100644
index 00000000..484684a1
--- /dev/null
+++ b/src/app/onboarding/layout.tsx
@@ -0,0 +1,14 @@
+import { type Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: "Get Started",
+ description: "Set up your organization in ryō",
+};
+
+export default function OnboardingLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return {children}
;
+}
diff --git a/src/app/onboarding/page.tsx b/src/app/onboarding/page.tsx
new file mode 100644
index 00000000..f4639a9f
--- /dev/null
+++ b/src/app/onboarding/page.tsx
@@ -0,0 +1,94 @@
+"use client";
+
+import { useState } from "react";
+
+import { AnimatePresence } from "framer-motion";
+
+import { FinishStep } from "./_components/finish-step";
+import { ImportMembersStep } from "./_components/import-members-step";
+import { KpiStep } from "./_components/kpi-step";
+import { OrgNameStep } from "./_components/org-name-step";
+import { ProgressIndicator } from "./_components/progress-indicator";
+import { RoleCreationStep } from "./_components/role-creation-step";
+import {
+ FinishVisual,
+ ImportMembersVisual,
+ KpiVisual,
+ OrgNameVisual,
+ RoleCreationVisual,
+ TeamSetupVisual,
+} from "./_components/step-visuals";
+import { TeamSetupStep } from "./_components/team-setup-step";
+
+const TOTAL_STEPS = 6;
+
+export default function OnboardingPage() {
+ const [currentStep, setCurrentStep] = useState(1);
+
+ const goToNext = () =>
+ setCurrentStep((prev) => Math.min(prev + 1, TOTAL_STEPS));
+ const goBack = () => setCurrentStep((prev) => Math.max(prev - 1, 1));
+
+ return (
+
+ {/* Left side - Form */}
+
+
+
+
+
+
+ {currentStep === 1 && (
+
+ )}
+ {currentStep === 2 && (
+
+ )}
+ {currentStep === 3 && (
+
+ )}
+ {currentStep === 4 && (
+
+ )}
+ {currentStep === 5 && (
+
+ )}
+ {currentStep === 6 && }
+
+
+
+
+
+ {/* Right side - Visualization */}
+
+
+
+ {currentStep === 1 && }
+ {currentStep === 2 && }
+ {currentStep === 3 && }
+ {currentStep === 4 && }
+ {currentStep === 5 && }
+ {currentStep === 6 && }
+
+
+
+
+ );
+}
From 86a5432ab062dbdeb170ac36e8a4cb78846c6255 Mon Sep 17 00:00:00 2001
From: louismorgner
Date: Mon, 8 Dec 2025 17:24:57 +0200
Subject: [PATCH 2/2] feat: add pre-selection buttons for team names
- Add quick-select buttons for common team names (growth, product, operations)
- Improve UX by allowing users to quickly select example team names
- Matches the pattern used in metrics step
---
.../_components/team-setup-step.tsx | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/src/app/onboarding/_components/team-setup-step.tsx b/src/app/onboarding/_components/team-setup-step.tsx
index 2bdfaa84..fb1bb970 100644
--- a/src/app/onboarding/_components/team-setup-step.tsx
+++ b/src/app/onboarding/_components/team-setup-step.tsx
@@ -35,6 +35,25 @@ export function TeamSetupStep({ onNext, onBack }: TeamSetupStepProps) {
growth, or operations.
+ {/* Example team names */}
+
+ {["growth", "product", "operations"].map((example) => (
+
+ ))}
+
+