diff --git a/AI.md b/AI.md
index 2667f56..9ceaf11 100644
--- a/AI.md
+++ b/AI.md
@@ -676,6 +676,14 @@ The core domain in `packages/core/src/domain/` covers the entities the plan call
- replacing the stale read-only flow report with `docs/gph-user-flows-review.md`, updating `Readme.md`, and adding focused regressions across AppShell, CommandPalette, WorkItemModal, ProjectsListView, BoardView, TableView, and RoadmapView
- verifying the finished pass with 53 focused UI tests, the complete 230-test repository suite, all workspace TypeScript checks, a successful production web build, and matched-state desktop/mobile browser comparisons
- aligning the Playwright project bootstrap with the calmer launcher behavior through a shared `createProjectFromLauncher()` helper, so E2E coverage chooses `New project` before submitting the creation modal instead of depending on the removed automatic first-run modal
+- added the guided New user tutorial by:
+ - placing a `New user tutorial` link directly beneath the launcher’s Demo project explanation and registering `/tutorial` in both the web and desktop shells without treating it as an active project route
+ - adding a focused tutorial landing surface that sets expectations, lists the covered workspace areas, and refuses to replace a dirty or otherwise unsaved active project
+ - opening a disposable `buildDemoProject()` bundle named `Tutorial Project`, keeping its storage trust unsaved, and clearly explaining that tutorial changes remain sample data unless explicitly exported
+ - adding an external tutorial state store persisted in `sessionStorage`, tied to the exact generated tutorial project id so the guide ends instead of leaking into another project
+ - mounting a non-modal, keyboard-readable tutorial panel in `AppShell` with 13 steps, progress semantics, Back/Next/Exit controls, responsive bottom-sheet behavior, and real route transitions across Overview, Board, Backlog, Table, Roadmap, Calendar, Docs, Bug Triage, My Work, Search/commands, Trash, and Settings
+ - synchronizing the wizard with manual sidebar navigation so users can skip ahead or revisit a feature without being forced back to the previous route
+ - adding focused launcher/tutorial component coverage plus a full Playwright journey that advances through every tutorial stop and verifies the final handoff back to Overview
## Open follow-on planning
diff --git a/Readme.md b/Readme.md
index 609c1c4..ae57de4 100644
--- a/Readme.md
+++ b/Readme.md
@@ -44,6 +44,7 @@ For real project work, run the repo locally or use the future release builds.
Grillo is past the skeleton stage. The current app includes:
- Workspace launcher with new, open/import, demo, recent-project, template preview, explicit browser-vs-folder storage guidance, and a calm first-run state that waits for the user to choose a path.
+- A guided New user tutorial beneath the demo entry that opens safe sample data and walks through all 12 core workspace surfaces in a resumable 13-step wizard.
- Overview, board, backlog, table, roadmap, calendar, docs, bug triage, my work, search, trash, and settings surfaces.
- Board-local column management for adding unmapped workflow statuses, removing columns without deleting their statuses or work items, and creating a new categorized status and column together.
- Explicit save-state UI in the project header, including save destination, dirty state, save failures, manual save/retry, switch project, and guarded close-project actions.
diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx
index 1ea6b9c..b8883e3 100644
--- a/apps/desktop/src/main.tsx
+++ b/apps/desktop/src/main.tsx
@@ -14,6 +14,7 @@ import {
ProjectsListView,
OpenProjectView,
DemoFolderView,
+ NewUserTutorialView,
ThemeProvider,
useRestoreProjectSession
} from "@gph/ui";
@@ -35,6 +36,7 @@ function App() {
} />
} />
} />
+ } />
} />
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
index 9fd1f22..0006358 100644
--- a/apps/web/src/main.tsx
+++ b/apps/web/src/main.tsx
@@ -14,6 +14,7 @@ import {
ProjectsListView,
OpenProjectView,
DemoFolderView,
+ NewUserTutorialView,
ThemeProvider,
useRestoreProjectSession
} from "@gph/ui";
@@ -42,6 +43,7 @@ function App() {
} />
} />
} />
+ } />
} />
diff --git a/docs/gph-user-flows-review.md b/docs/gph-user-flows-review.md
index b25e725..4639699 100644
--- a/docs/gph-user-flows-review.md
+++ b/docs/gph-user-flows-review.md
@@ -2,6 +2,8 @@
**Review date:** 2026-07-16
+**Feature update:** 2026-07-17 — added the guided New user tutorial described below.
+
**Scope:** The complete browser product journey, reviewed in the running app at desktop and mobile widths, followed by implementation and regression verification.
**Primary environment:** Local web app, demo workspace, 1280 x 720 desktop viewport, and 390 x 844 mobile viewport.
@@ -16,7 +18,7 @@ The audit did expose several interaction-level problems that source review alone
### 1. Workspace launcher, first run, open/import, and demo
-**Journey:** Open Grillo -> review launcher -> create a project, open/import a bundle, or enter the demo -> arrive at Overview.
+**Journey:** Open Grillo -> review launcher -> create a project, open/import a bundle, enter the demo, or start the New user tutorial -> arrive at Overview.
**Health after this pass:** Good.
@@ -24,6 +26,7 @@ The audit did expose several interaction-level problems that source review alone
- When no project is open, the sidebar contains only Projects, Open, and Demo. Project views and Settings no longer lead users into guarded routes that cannot work yet.
- Closing an unsaved demo still uses the existing confirmation dialog, but confirming now returns to the clean launcher without immediately reopening project creation.
- Disabled primary actions now look inactive instead of retaining the full green emphasis of an available action.
+- The Demo project explanation now includes a `New user tutorial` link. Its landing page opens a disposable demo only when existing unsaved work is protected, then a 13-step wizard moves through every core project surface with visible progress, Back/Next/Exit controls, and manual sidebar-route synchronization.
### 2. Create, save, switch, and close project
@@ -136,6 +139,7 @@ The audit did expose several interaction-level problems that source review alone
- Reflowed work-item metadata for mobile and replaced native multi-select labels with checkbox chips.
- Shortened Bug Triage action copy without losing item-specific accessible labels.
- Improved disabled primary-action styling and mobile Overview density.
+- Added the safe demo-backed New user tutorial entry, landing page, persistent route-aware wizard, and responsive desktop/mobile presentation.
## Verification
diff --git a/packages/ui/src/AppShell.tsx b/packages/ui/src/AppShell.tsx
index 3b4ba20..bf987b1 100644
--- a/packages/ui/src/AppShell.tsx
+++ b/packages/ui/src/AppShell.tsx
@@ -36,6 +36,7 @@ import { Button, ConfirmDialog, HelpTip, IconButton, InlineAlert, ToastProvider,
import { PROJECT_NAV_ITEMS } from "./nav-config";
import { useWorkspaceStore } from "./store/workspace-store";
import { hasRegisteredSavedRoute, savedViewsForBundle, viewRoute } from "./views/planning/view-helpers";
+import { NewUserTutorial } from "./tutorial";
export type AppShellProps = {
appMode: "web" | "desktop";
@@ -455,7 +456,8 @@ function AppShellFrame({ appMode, appDistribution = "local", children }: AppShel
location.pathname !== "/" &&
!location.pathname.startsWith("/projects") &&
!location.pathname.startsWith("/open") &&
- !location.pathname.startsWith("/demo"),
+ !location.pathname.startsWith("/demo") &&
+ !location.pathname.startsWith("/tutorial"),
[location.pathname]
);
@@ -664,6 +666,7 @@ function AppShellFrame({ appMode, appDistribution = "local", children }: AppShel
/>
) : null}
+
);
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 06c988a..4cac979 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -26,3 +26,4 @@ export * from "./views/search";
export * from "./views/settings";
export * from "./views/projects";
export * from "./work-item";
+export * from "./tutorial";
diff --git a/packages/ui/src/theme/global.css b/packages/ui/src/theme/global.css
index e30619f..f3b49fd 100644
--- a/packages/ui/src/theme/global.css
+++ b/packages/ui/src/theme/global.css
@@ -1845,6 +1845,25 @@ a.btn {
background: var(--color-bg-muted);
border: 1px solid var(--color-border-subtle);
}
+.workspace-tutorial-link {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ width: fit-content;
+ margin-top: 2px;
+ color: var(--color-accent);
+ font-size: var(--font-size-sm);
+ font-weight: 700;
+}
+.workspace-tutorial-link:hover {
+ color: var(--color-accent-hover);
+ text-decoration: underline;
+ text-underline-offset: 3px;
+}
+.workspace-tutorial-link svg {
+ width: 17px;
+ height: 17px;
+}
.workspace-inline-note {
padding: 10px 12px;
border-radius: var(--radius-sm);
@@ -1861,6 +1880,278 @@ a.btn {
font-size: var(--font-size-sm);
}
+/* ---- New user tutorial ---- */
+.tutorial-landing {
+ flex: 1;
+ display: grid;
+ grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.65fr);
+ align-items: stretch;
+ gap: var(--space-4);
+ width: min(1080px, 100%);
+ margin: 0 auto;
+ padding: clamp(20px, 4vw, 48px);
+ overflow: auto;
+}
+.tutorial-landing-card,
+.tutorial-landing-features {
+ border: 1px solid var(--color-border-subtle);
+ border-radius: var(--radius-lg);
+ background: var(--color-bg-surface);
+ box-shadow: var(--shadow-1);
+}
+.tutorial-landing-card {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: var(--space-4);
+ padding: clamp(24px, 5vw, 52px);
+}
+.tutorial-landing-icon {
+ display: grid;
+ place-items: center;
+ width: 58px;
+ height: 58px;
+ border-radius: var(--radius-lg);
+ background: var(--color-accent-soft);
+ color: var(--color-accent);
+}
+.tutorial-landing-icon svg {
+ width: 30px;
+ height: 30px;
+}
+.tutorial-landing-copy {
+ max-width: 680px;
+}
+.tutorial-landing-kicker,
+.tutorial-step-label {
+ color: var(--color-accent);
+ font-size: var(--font-size-xs);
+ font-weight: 800;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+.tutorial-landing-copy h1,
+.tutorial-landing-features h2 {
+ margin: 6px 0 10px;
+}
+.tutorial-landing-copy p {
+ margin: 0;
+ color: var(--color-text-secondary);
+ line-height: 1.65;
+}
+.tutorial-landing-benefits {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: var(--space-2);
+}
+.tutorial-landing-benefits > div {
+ display: flex;
+ align-items: flex-start;
+ gap: 9px;
+ min-width: 0;
+ padding: 12px;
+ border: 1px solid var(--color-border-subtle);
+ border-radius: var(--radius-sm);
+ background: var(--color-bg-muted);
+}
+.tutorial-landing-benefits svg {
+ flex: 0 0 auto;
+ width: 18px;
+ height: 18px;
+ color: var(--color-accent);
+}
+.tutorial-landing-benefits span {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ min-width: 0;
+}
+.tutorial-landing-benefits small {
+ color: var(--color-text-secondary);
+ line-height: 1.35;
+}
+.tutorial-landing-actions {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ flex-wrap: wrap;
+}
+.tutorial-landing-note {
+ margin: calc(var(--space-2) * -1) 0 0;
+}
+.tutorial-landing-features {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: var(--space-4);
+ padding: clamp(24px, 4vw, 40px);
+ background: linear-gradient(155deg, var(--color-bg-surface), var(--color-bg-muted));
+}
+.tutorial-feature-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+.tutorial-feature-list span {
+ padding: 7px 10px;
+ border: 1px solid var(--color-border-subtle);
+ border-radius: 999px;
+ background: var(--color-bg-surface);
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-xs);
+ font-weight: 700;
+}
+.new-user-tutorial {
+ position: fixed;
+ right: 20px;
+ bottom: 20px;
+ z-index: 45;
+ display: flex;
+ flex-direction: column;
+ width: min(430px, calc(100vw - 40px));
+ max-height: calc(100vh - 96px);
+ overflow: hidden;
+ border: 1px solid var(--color-border-strong);
+ border-radius: var(--radius-lg);
+ background: var(--color-bg-elevated);
+ box-shadow: var(--shadow-3);
+}
+.new-user-tutorial-header,
+.new-user-tutorial-footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-2);
+ padding: var(--space-3);
+}
+.new-user-tutorial-header {
+ padding-bottom: 10px;
+}
+.new-user-tutorial-brand {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ min-width: 0;
+}
+.new-user-tutorial-icon {
+ display: grid;
+ place-items: center;
+ flex: 0 0 auto;
+ width: 36px;
+ height: 36px;
+ border-radius: var(--radius-sm);
+ background: var(--color-accent-soft);
+ color: var(--color-accent);
+}
+.new-user-tutorial-icon svg {
+ width: 20px;
+ height: 20px;
+}
+.new-user-tutorial-progress {
+ height: 4px;
+ margin: 0 var(--space-3);
+ overflow: hidden;
+ border-radius: 999px;
+ background: var(--color-bg-muted);
+}
+.new-user-tutorial-progress span {
+ display: block;
+ height: 100%;
+ border-radius: inherit;
+ background: var(--color-accent);
+ transition: width 180ms ease-out;
+}
+.new-user-tutorial-body {
+ overflow: auto;
+ padding: var(--space-4) var(--space-3) var(--space-3);
+}
+.new-user-tutorial-step-meta {
+ align-items: baseline;
+ gap: var(--space-2);
+}
+.new-user-tutorial-body h2 {
+ margin: 7px 0 8px;
+ font-size: 20px;
+ line-height: 1.3;
+}
+.new-user-tutorial-body h2:focus {
+ outline: none;
+}
+.new-user-tutorial-body > p {
+ margin: 0;
+ line-height: 1.55;
+}
+.new-user-tutorial-try {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr);
+ gap: 10px;
+ margin-top: var(--space-3);
+ padding: 12px;
+ border: 1px solid var(--color-border-subtle);
+ border-radius: var(--radius-sm);
+ background: var(--color-accent-soft);
+}
+.new-user-tutorial-try > svg {
+ width: 18px;
+ height: 18px;
+ color: var(--color-accent);
+}
+.new-user-tutorial-try p {
+ margin: 3px 0 0;
+ color: var(--color-text-secondary);
+ font-size: var(--font-size-xs);
+ line-height: 1.45;
+}
+.new-user-tutorial-footer {
+ border-top: 1px solid var(--color-border-subtle);
+ background: var(--color-bg-surface);
+}
+
+@media (max-width: 820px) {
+ .tutorial-landing {
+ grid-template-columns: 1fr;
+ }
+ .tutorial-landing-benefits {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 640px) {
+ .tutorial-landing {
+ padding: 14px;
+ }
+ .tutorial-landing-card,
+ .tutorial-landing-features {
+ padding: 22px;
+ }
+ .new-user-tutorial {
+ right: 0;
+ bottom: 0;
+ left: 0;
+ width: 100%;
+ max-height: min(72vh, 580px);
+ border-right: 0;
+ border-bottom: 0;
+ border-left: 0;
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
+ }
+ .new-user-tutorial-header,
+ .new-user-tutorial-footer {
+ padding-right: 14px;
+ padding-left: 14px;
+ }
+ .new-user-tutorial-body {
+ padding-right: 14px;
+ padding-left: 14px;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .new-user-tutorial-progress span {
+ transition: none;
+ }
+}
+
/* ---- Item detail ---- */
.item-detail {
display: flex;
diff --git a/packages/ui/src/tutorial/NewUserTutorial.test.tsx b/packages/ui/src/tutorial/NewUserTutorial.test.tsx
new file mode 100644
index 0000000..08f69f4
--- /dev/null
+++ b/packages/ui/src/tutorial/NewUserTutorial.test.tsx
@@ -0,0 +1,125 @@
+import { cleanup, render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, beforeEach, describe, expect, it } from "vitest";
+import { Link, MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
+import { buildProjectFromTemplate } from "@gph/core";
+import { useProjectStore } from "../store/project-store";
+import { NewUserTutorial } from "./NewUserTutorial";
+import { NewUserTutorialView } from "./NewUserTutorialView";
+import {
+ endNewUserTutorial,
+ getNewUserTutorialSnapshot,
+ startNewUserTutorial
+} from "./tutorial-state";
+
+function LocationProbe() {
+ const location = useLocation();
+ return
{location.pathname}
;
+}
+
+describe("NewUserTutorial", () => {
+ beforeEach(() => {
+ cleanup();
+ endNewUserTutorial();
+ sessionStorage.clear();
+ useProjectStore.setState({
+ bundle: null,
+ storageKey: null,
+ storagePath: null,
+ storageTrust: "unsaved",
+ isDirty: false,
+ lastSource: null,
+ saveStatus: "idle",
+ lastSavedAt: null,
+ saveError: null
+ });
+ });
+
+ afterEach(() => {
+ cleanup();
+ endNewUserTutorial();
+ });
+
+ it("starts a disposable demo and opens the guided overview", async () => {
+ render(
+
+
+ } />
+ } />
+
+
+ );
+
+ await userEvent.click(screen.getByRole("button", { name: "Start tutorial" }));
+
+ await waitFor(() => {
+ expect(screen.getByTestId("location")).toHaveTextContent("/overview");
+ });
+ expect(useProjectStore.getState().bundle?.project.name).toBe("Tutorial Project");
+ expect(useProjectStore.getState().storageTrust).toBe("unsaved");
+ expect(getNewUserTutorialSnapshot()).toEqual({
+ active: true,
+ projectId: useProjectStore.getState().bundle?.project.id,
+ stepIndex: 0
+ });
+ });
+
+ it("protects an existing unsaved project", () => {
+ useProjectStore.setState({
+ bundle: buildProjectFromTemplate("software-project", "Unsaved work"),
+ storageTrust: "unsaved",
+ isDirty: true
+ });
+
+ render(
+
+
+
+ );
+
+ expect(screen.getByRole("button", { name: "Start tutorial" })).toBeDisabled();
+ expect(screen.getByText(/Save or close the current unsaved project/i)).toBeInTheDocument();
+ });
+
+ it("moves forward and backward through real feature routes", async () => {
+ startNewUserTutorial("tutorial-project");
+ render(
+
+
+
+
+ );
+
+ const tutorial = screen.getByRole("dialog", { name: "New user tutorial" });
+ expect(screen.getByRole("heading", { name: "See the whole project at a glance" })).toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole("button", { name: "Open Board" }));
+ expect(screen.getByTestId("location")).toHaveTextContent("/board");
+ expect(screen.getByRole("heading", { name: "Move work through its workflow" })).toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole("button", { name: "Back" }));
+ expect(screen.getByTestId("location")).toHaveTextContent("/overview");
+ expect(tutorial).toBeInTheDocument();
+
+ await userEvent.click(screen.getByRole("button", { name: "Exit tutorial" }));
+ expect(screen.queryByRole("dialog", { name: "New user tutorial" })).not.toBeInTheDocument();
+ });
+
+ it("follows manual workspace navigation without forcing a route", async () => {
+ startNewUserTutorial("tutorial-project");
+ render(
+
+ Open Calendar manually
+
+
+
+ );
+
+ await userEvent.click(screen.getByRole("link", { name: "Open Calendar manually" }));
+
+ await waitFor(() => {
+ expect(screen.getByRole("heading", { name: "See commitments by date" })).toBeInTheDocument();
+ });
+ expect(screen.getByRole("progressbar", { name: "Tutorial progress" })).toHaveAttribute("aria-valuenow", "6");
+ });
+});
diff --git a/packages/ui/src/tutorial/NewUserTutorial.tsx b/packages/ui/src/tutorial/NewUserTutorial.tsx
new file mode 100644
index 0000000..51d7e25
--- /dev/null
+++ b/packages/ui/src/tutorial/NewUserTutorial.tsx
@@ -0,0 +1,245 @@
+import { useEffect, useRef, useSyncExternalStore } from "react";
+import { ArrowLeft, ArrowRight, GraduationCap, Lightbulb } from "lucide-react";
+import { useLocation, useNavigate } from "react-router-dom";
+import { Button } from "../components";
+import {
+ endNewUserTutorial,
+ getNewUserTutorialSnapshot,
+ setNewUserTutorialStep,
+ subscribeToNewUserTutorial
+} from "./tutorial-state";
+
+export type NewUserTutorialStep = {
+ description: string;
+ label: string;
+ nextLabel: string;
+ route: string;
+ title: string;
+ tryThis: string;
+};
+
+const NEW_USER_TUTORIAL_STEPS: NewUserTutorialStep[] = [
+ {
+ label: "Overview",
+ title: "See the whole project at a glance",
+ description: "Overview turns project activity into a quick health check: what is moving, what is blocked, and what needs attention next.",
+ tryThis: "Scan the status totals, upcoming work, and milestone progress in the tutorial project.",
+ route: "/overview",
+ nextLabel: "Open Board"
+ },
+ {
+ label: "Board",
+ title: "Move work through its workflow",
+ description: "Board groups items by status so the team can see flow, open details, and move work forward without losing context.",
+ tryThis: "Open a card to explore descriptions, labels, assignees, comments, and activity history.",
+ route: "/board",
+ nextLabel: "Open Backlog"
+ },
+ {
+ label: "Backlog",
+ title: "Prioritize what comes next",
+ description: "Backlog gives planning work a quieter list view with priority, status, due dates, and ownership aligned for fast scanning.",
+ tryThis: "Compare the urgent bug with the release work and decide what the team should pull next.",
+ route: "/backlog",
+ nextLabel: "Open Table"
+ },
+ {
+ label: "Table",
+ title: "Edit many details efficiently",
+ description: "Table is the dense operational view for sorting, filtering, choosing columns, and updating selected work in bulk.",
+ tryThis: "Select a row to reveal bulk edit controls, then open Columns to tailor the view.",
+ route: "/table",
+ nextLabel: "Open Roadmap"
+ },
+ {
+ label: "Roadmap",
+ title: "Plan milestones over time",
+ description: "Roadmap connects milestones and scheduled work so release timing, scope, and dependencies stay visible.",
+ tryThis: "Use a milestone grip with the pointer or arrow keys to adjust its target date.",
+ route: "/roadmap",
+ nextLabel: "Open Calendar"
+ },
+ {
+ label: "Calendar",
+ title: "See commitments by date",
+ description: "Calendar places due work into a seven-day planning surface with an agenda for the details that matter now.",
+ tryThis: "Choose a day to create a dated item, or open existing work from the agenda.",
+ route: "/calendar",
+ nextLabel: "Open Docs"
+ },
+ {
+ label: "Docs",
+ title: "Keep decisions beside delivery",
+ description: "Docs stores project notes, briefs, and checklists next to the work they explain, including direct links back to items.",
+ tryThis: "Open the Launch checklist and follow one of its linked work items.",
+ route: "/docs",
+ nextLabel: "Open Bug triage"
+ },
+ {
+ label: "Bug triage",
+ title: "Turn reports into actionable bugs",
+ description: "Bug triage brings severity, reproduction steps, expected behavior, and disposition actions into one review flow.",
+ tryThis: "Inspect the responsive-header report and review its severity and reproduction context.",
+ route: "/bugs",
+ nextLabel: "Open My work"
+ },
+ {
+ label: "My work",
+ title: "Focus each person on their queue",
+ description: "My work groups assigned items around a selected team member so personal priorities stay clear without hiding project context.",
+ tryThis: "Switch between Alex and Sam to see how the same project becomes a personal queue.",
+ route: "/mywork",
+ nextLabel: "Open Search"
+ },
+ {
+ label: "Search & commands",
+ title: "Find anything and move quickly",
+ description: "Search finds project work and documents, while the command palette jumps to views and common actions from anywhere.",
+ tryThis: "Search for \"first-run,\" then use Search commands in the header or press Ctrl K.",
+ route: "/search",
+ nextLabel: "Open Trash"
+ },
+ {
+ label: "Trash",
+ title: "Recover work before it is gone",
+ description: "Trash keeps deleted items recoverable so cleanup does not immediately become permanent data loss.",
+ tryThis: "Notice the restore path; permanent deletion stays a separate, deliberate action.",
+ route: "/trash",
+ nextLabel: "Open Settings"
+ },
+ {
+ label: "Settings",
+ title: "Shape Grillo around the project",
+ description: "Settings controls workflows, item types, members, labels, views, automation, storage, and portable imports or exports.",
+ tryThis: "Browse the tabs to see which parts are project structure and which parts help move or protect data.",
+ route: "/settings",
+ nextLabel: "Wrap up"
+ },
+ {
+ label: "Complete",
+ title: "You know your way around Grillo",
+ description: "You have visited every core workspace surface. The tutorial project remains open, so you can keep experimenting with sample data.",
+ tryThis: "Create an item with the C shortcut, use Ctrl K to jump views, or return to Projects when you are ready for your own workspace.",
+ route: "/overview",
+ nextLabel: "Finish tutorial"
+ }
+];
+
+export function NewUserTutorial({ activeProjectId }: { activeProjectId: string | null }) {
+ const state = useSyncExternalStore(
+ subscribeToNewUserTutorial,
+ getNewUserTutorialSnapshot,
+ getNewUserTutorialSnapshot
+ );
+ const location = useLocation();
+ const navigate = useNavigate();
+ const titleRef = useRef(null);
+ const lastStepIndex = NEW_USER_TUTORIAL_STEPS.length - 1;
+ const stepIndex = Math.min(state.stepIndex, lastStepIndex);
+ const step = NEW_USER_TUTORIAL_STEPS[stepIndex];
+ const tutorialProjectAvailable = Boolean(activeProjectId && activeProjectId === state.projectId);
+
+ useEffect(() => {
+ if (state.active && !tutorialProjectAvailable) {
+ endNewUserTutorial();
+ }
+ }, [state.active, tutorialProjectAvailable]);
+
+ useEffect(() => {
+ if (!state.active || stepIndex === lastStepIndex) return;
+ const routeStepIndex = NEW_USER_TUTORIAL_STEPS
+ .slice(0, lastStepIndex)
+ .findIndex((entry) => entry.route === location.pathname);
+ if (routeStepIndex >= 0 && routeStepIndex !== stepIndex) {
+ setNewUserTutorialStep(routeStepIndex);
+ }
+ }, [lastStepIndex, location.pathname, state.active, stepIndex]);
+
+ useEffect(() => {
+ if (state.active) titleRef.current?.focus();
+ }, [state.active, stepIndex]);
+
+ if (!state.active || !tutorialProjectAvailable) return null;
+
+ const goToStep = (nextStepIndex: number) => {
+ const boundedIndex = Math.max(0, Math.min(nextStepIndex, lastStepIndex));
+ setNewUserTutorialStep(boundedIndex);
+ navigate(NEW_USER_TUTORIAL_STEPS[boundedIndex].route);
+ };
+
+ const advance = () => {
+ if (stepIndex === lastStepIndex) {
+ endNewUserTutorial();
+ return;
+ }
+ goToStep(stepIndex + 1);
+ };
+
+ return (
+
+ );
+}
diff --git a/packages/ui/src/tutorial/NewUserTutorialView.tsx b/packages/ui/src/tutorial/NewUserTutorialView.tsx
new file mode 100644
index 0000000..f7956d0
--- /dev/null
+++ b/packages/ui/src/tutorial/NewUserTutorialView.tsx
@@ -0,0 +1,101 @@
+import { buildDemoProject } from "@gph/core";
+import { CheckCircle2, Clock3, GraduationCap, ShieldCheck } from "lucide-react";
+import { Link, useNavigate } from "react-router-dom";
+import { Button, InlineAlert } from "../components";
+import { useProjectStore } from "../store/project-store";
+import { startNewUserTutorial } from "./tutorial-state";
+
+const TUTORIAL_FEATURES = [
+ "Overview",
+ "Board",
+ "Backlog",
+ "Table",
+ "Roadmap",
+ "Calendar",
+ "Docs",
+ "Bug triage",
+ "My work",
+ "Search",
+ "Trash",
+ "Settings"
+];
+
+export function NewUserTutorialView() {
+ const navigate = useNavigate();
+ const bundle = useProjectStore((state) => state.bundle);
+ const isDirty = useProjectStore((state) => state.isDirty);
+ const storageTrust = useProjectStore((state) => state.storageTrust);
+ const setBundle = useProjectStore((state) => state.setBundle);
+ const hasUnprotectedWork = Boolean(bundle) && (isDirty || storageTrust === "unsaved");
+
+ const startTutorial = () => {
+ if (hasUnprotectedWork) return;
+ const tutorialBundle = buildDemoProject("Tutorial Project");
+ setBundle(tutorialBundle, {
+ storageKey: null,
+ storagePath: null,
+ storageTrust: "unsaved"
+ });
+ startNewUserTutorial(tutorialBundle.project.id);
+ navigate("/overview");
+ };
+
+ return (
+
+
+
+
+
+
+ New user tutorial
+
Learn Grillo by doing
+
+ Open a disposable sample project and follow a guided wizard through every core workspace surface.
+ You can interact with the demo, move backward, skip ahead from the sidebar, or exit whenever you are ready.
+
+
+
+
+
+
+ About 5 minutes13 focused steps
+
+
+
+ Safe sample dataYour projects are not changed
+
+
+
+ Learn by clickingExplore the real product
+
+
+
+ {hasUnprotectedWork ? (
+
+ Save or close the current unsaved project before starting the tutorial. This keeps your in-progress work protected.
+
+ ) : null}
+
+
+
+ Back to projects
+
+
+ The tutorial project stays browser-local and is not saved unless you explicitly export it.
+