Files
community-rule/docs/create-flow.md
T
adilalloandCursor b6afdb6e32 Let Use without changes from a template collect identity instead of the full questionnaire.
Direct catalog picks walk name, description, and photo, then stakeholders; leftover drafts no longer skip that path. In-flow template picks keep community identity. Exit on a direct template preview leaves immediately because nothing has been collected yet.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 17:00:17 -06:00

134 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Create rule flow (custom wizard) — canonical reference
Product/engineering reference for the **custom** “Create rule” experience: URL order, persistence, and entry points. **Canon wizard** alignment (docs, `[screenId]` routing, footer progress) shipped under **[CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo)** (**Done**). **Draft resume vs profile**, server hydration, and “new rule” vs **continue draft** are tracked with **[CR-86](https://linear.app/community-rule/issue/CR-86/backend-profile-dashboard-account-figma-profile)** (profile dashboard). See also [docs/guides/backend-linear-tickets.md](guides/backend-linear-tickets.md) **Ticket 17**.
---
## Product stages (Figma)
The Figma **Create Community** sequence is the **source of truth** for the first segment of the wizard (eight frames). After **`review`**, the flow continues with **Create Custom CommunityRule** and **Review and complete** stages. The shipped URL sequence in [`FLOW_STEP_ORDER`](../app/(app)/create/utils/flowSteps.ts) **follows that trajectory**; stages are a **product** slice of that linear order, not separate routers today.
| Stage (Figma) | Purpose (summary) | `CreateFlowStep` values (in order) |
| --- | --- | --- |
| **Create Community** | Intro, naming, structure, context, size, upload, save progress (email), then community review. | `informational``community-name``community-structure``community-context``community-size``community-upload``community-save``review` |
| **Create Custom CommunityRule** | Author the CommunityRule content and structure (core values + four card-stack categories). | `core-values``communication-methods``membership-methods``decision-approaches``conflict-management` |
| **Review and complete** | Stakeholders, final card, publish, success. | `confirm-stakeholders``final-review``completed` |
Treat these stages as the **canonical product sections** when adding chrome (e.g. stage headers, progress copy), breaking work across teams, or reusing flows in other surfaces. **Layout kind** is **not** encoded in the URL; it lives in [`CREATE_FLOW_SCREEN_REGISTRY`](../app/(app)/create/utils/createFlowScreenRegistry.ts) (Figma node id + `layoutKind` per step). Figma defines eight layout kinds: **informational**, **text**, **select**, **upload**, **review**, **card**, **right-rail**, **completed**`CreateFlowLayoutKind` and [`app/(app)/create/screens/`](../app/(app)/create/screens/) mirror that list (one folder per kind; multiple steps may share a kind, e.g. several **select** screens).
**Create from template (future):** A full **template-driven** create path is **not** finalized; it will likely live on **additional route(s)** (and may reuse these stages where it overlaps the custom trajectory). Today, **`/create/review-template/[slug]`** is only an auxiliary **preview** in the create shell; it is **not** a Figma stage and not the final template-create entry. See **Out of scope** in [CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo).
---
## Step order and URLs
Order is defined in code by [`FLOW_STEP_ORDER`](../app/(app)/create/utils/flowSteps.ts) and the [`CreateFlowStep`](../app/(app)/create/types.ts) type. Wizard steps use a **single dynamic route**: [`app/(app)/create/[screenId]/page.tsx`](../app/(app)/create/[screenId]/page.tsx), which validates `screenId` and renders [`CreateFlowScreenView`](../app/(app)/create/screens/CreateFlowScreenView.tsx). Implementation files are grouped under [`app/(app)/create/screens/`](../app/(app)/create/screens/) by Figma **layout kind** (subfolders: informational, text, select, upload, review, card, right-rail, completed). **`/create`** redirects to the first step.
| Order | Figma stage | Step ID (`screenId`) | Path |
| ----: | ----------- | -------------------- | ---- |
| 1 | Create Community | `informational` | `/create/informational` |
| 2 | Create Community | `community-name` | `/create/community-name` |
| 3 | Create Community | `community-structure` | `/create/community-structure` |
| 4 | Create Community | `community-context` | `/create/community-context` |
| 5 | Create Community | `community-size` | `/create/community-size` |
| 6 | Create Community | `community-upload` | `/create/community-upload` |
| 7 | Create Community | `community-save` | `/create/community-save` |
| 8 | Create Community (review frame) | `review` | `/create/review` |
| 9 | Create Custom CommunityRule | `core-values` | `/create/core-values` |
| 10 | Create Custom CommunityRule | `communication-methods` | `/create/communication-methods` |
| 11 | Create Custom CommunityRule | `membership-methods` | `/create/membership-methods` |
| 12 | Create Custom CommunityRule | `decision-approaches` | `/create/decision-approaches` |
| 13 | Create Custom CommunityRule | `conflict-management` | `/create/conflict-management` |
| 14 | Review and complete | `confirm-stakeholders` | `/create/confirm-stakeholders` |
| 15 | Review and complete | `final-review` | `/create/final-review` |
| 16 | Review and complete | `completed` | `/create/completed` |
**Primary entry:** marketing header **Create rule** and profile **Create new custom Rule** both run **`prepareFreshCreateFlowEntry`** then navigate to **`/create`**, which redirects to **`/create/informational`** (see [`Top.container.tsx`](../app/components/navigation/Top/Top.container.tsx) and [`ProfilePageClient.tsx`](../app/(app)/profile/ProfilePageClient.tsx)).
Active step for chrome and navigation is resolved from the pathname via [`parseCreateFlowScreenFromPathname`](../app/(app)/create/utils/flowSteps.ts) inside [`useCreateFlowNavigation`](../app/(app)/create/hooks/useCreateFlowNavigation.ts).
### Custom-rule facet catalog and `/create` path helpers (CR-92)
**[`CUSTOM_RULE_FACETS`](../lib/create/customRuleFacets.ts)** is the single table for template category keys, `CreateFlowStep`, footer confirm bindings, `selected*` / strip keys, and the four `GET /api/create-flow/methods?section=` ids. Update that module (and related tests) when adding or renaming a custom-rule facet instead of scattering new switches across `applyTemplatePrefill`, `customRuleConfirmFooterSteps`, `buildFinalReviewCategories`, `stripCustomRuleSelectionFields`, etc.
**[`createFlowPaths`](../app/(app)/create/utils/createFlowPaths.ts)** centralises `/create/{step}` builders and shared constants (`CREATE_ROUTES`, `syncDraft` helpers). Prefer these over raw string literals in layout, hooks, and redirects.
Wizard step → React screen rendering lives in [`createFlowScreenComponents.tsx`](../app/(app)/create/screens/createFlowScreenComponents.tsx) (`renderCreateFlowScreen`), paired with [`CREATE_FLOW_SCREEN_REGISTRY`](../app/(app)/create/utils/createFlowScreenRegistry.ts) for Figma/layout metadata.
### Stakeholder emails (`confirm-stakeholders`)
The step persists **`stakeholderEmails`** on `CreateFlowState` (validated on `PUT /api/drafts/me`). On **first publish** (`POST /api/rules`) **with a session**, the server records [`RuleStakeholder`](../prisma/schema.prisma) rows and emails each address a one-time link to [`GET /api/invites/rule-stakeholder/verify`](../app/api/invites/rule-stakeholder/verify/route.ts). Guests can still publish; invites are skipped until they sign in. Opening the link creates or signs in the account for that email and redirects to the public rule; the rule also appears on the invitees **profile** with **view** access (not manage). **After publish**, owners manage invites from **`/create/edit-rule`** via **Manage Stakeholders**, which opens **`/create/confirm-stakeholders?reviewReturn=edit-rule&manageStakeholders=1`** (same screen layout as the pre-publish step, backed by [`GET` / `POST` `DELETE` / resend](../app/api/rules/[id]/stakeholders/route.ts)). **`PATCH /api/rules/[id]`** still does not read stakeholder emails from the wizard draft.
### Fresh start vs continue draft (signed-in + sync)
**Established pattern:** anonymous and signed-in users should see the **same** wizard when starting a **new** rule from marketing or profile: empty state at the first step, with no surprise reload of old work. Both can **publish** (guests get an unlisted public `/rules/{id}` with no owner). Both see **Save & Exit** from `community-structure` onward (and `edit-rule`). On **`/create/completed`**, guests still get **Save & Exit** (keep-this-rule magic-link modal); signed-in users get **Exit**. Wizard guests open the save-progress magic-link modal; signed-in users may **PUT** `/api/drafts/me` when `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true`.
- **New rule entry** (always a clean slate): call [`prepareFreshCreateFlowEntry`](../app/(app)/create/utils/prepareFreshCreateFlowEntry.ts) **before** `router.push` into `/create` or `/create/review-template/...`. It clears **`create-flow-anonymous`** and the core-value-details `localStorage` key; when sync is on, it **`DELETE`s `/api/drafts/me`** so [`SignedInDraftHydration`](../app/(app)/create/SignedInDraftHydration.tsx) does not rehydrate a stale server draft after local storage was wiped.
- **Continue saved draft** (profile): do **not** call `prepareFreshCreateFlowEntry`. Clear the same `localStorage` keys **only** (see [`ProfilePageClient`](../app/(app)/profile/ProfilePageClient.tsx) `handleContinueDraft`) so the client mirror is empty, then navigate to **`/create/{savedStep}`**. Hydration loads the server draft; the URL may be corrected to `currentStep` when it differs from the path.
- **Local-only wipe** without touching the server: [`clearCreateFlowPersistedDrafts`](../app/(app)/create/utils/clearCreateFlowPersistedDrafts.ts) (same two `localStorage` keys). Prefer **`prepareFreshCreateFlowEntry`** for any user-facing “start new rule” navigation so signed-in + sync stays aligned with anonymous.
Call sites for **`prepareFreshCreateFlowEntry`**: [`Top.container.tsx`](../app/components/navigation/Top/Top.container.tsx) (Create rule), profile **Create new custom Rule** ([`ProfilePageClient.tsx`](../app/(app)/profile/ProfilePageClient.tsx)), home **Popular templates** ([`RuleStack.container.tsx`](../app/components/sections/RuleStack/RuleStack.container.tsx)), and **direct** `/templates` template picks ([`TemplatesPageClient.tsx`](../app/(marketing)/templates/TemplatesPageClient.tsx)) when **`fromFlow` is absent**.
---
## Auxiliary route (not a wizard step or Figma stage)
| Path | Purpose |
| --- | --- |
| `/create/review-template/[slug]` | Template preview in the create shell; uses the same layout/footer chrome as other create pages but **is not** part of `FLOW_STEP_ORDER` **or** the three Figma stages above. |
From that page, **Customize** pre-fills the custom-rule selections on the current `CreateFlowState` (via [`buildTemplateCustomizePrefill`](../lib/create/applyTemplatePrefill.ts)) and routes to **`/create/core-values`** when the community name (`state.title`) is already set, otherwise to **`/create/informational`**. Name-only is the gate because other community-stage fields (e.g. `communityStructureChipSnapshots`) are sticky once the user lands on those screens; a non-empty title is also the minimum bar [`buildPublishPayload`](../lib/create/buildPublishPayload.ts) enforces, so the two checks stay aligned. No query-param plumbing: state persists via the usual anonymous/server-draft mirrors.
**Exit on template preview.** Direct catalog / marketing entry (`/create/review-template/[slug]` without `?fromFlow=1`) has nothing to save — top-nav **Exit** clears client state and goes home, with no save-progress modal and no leave confirm. In-flow (`?fromFlow=1`) still has community progress, so Exit keeps the usual save / leave-confirm path.
**Use without changes** writes the template's `body.sections` into `state.sections` (chip titles only; bodies are empty in seeded templates), resets any prior Customize chip selections so they don't bleed into `document.coreValues`, and skips custom-rule authoring. It does **not** copy the template catalog `description` into `state.summary` — the published rule summary comes from **`communityContext` first**, then `summary`, when the user publishes. At publish, [`buildPublishPayload`](../lib/create/buildPublishPayload.ts) derives `methodSelections` from those section titles, merges preset copy into `document.sections`, and emits structured `methodSelections`. The user then exits via the normal **`final-review → handleFinalize → publishRule`** pipeline. Guests can publish without signing in (`POST /api/rules` with `userId` null); stakeholder invites are sent only when a session exists.
**Entering a template before community stage is done.** Both handlers apply their side effects eagerly (prefill for Customize; `sections` for Use without changes) *and* pin a `pendingTemplateAction: { slug, mode }` when the community questionnaire still needs to run.
- **Customize** routes to `/create/informational` and walks the full Create Community questionnaire. Once the user reaches `/create/review`, [`CommunityReviewScreen`](../app/(app)/create/screens/review/CommunityReviewScreen.tsx) reads the action on mount, clears it via `updateState`, and `router.replace`s to `/create/core-values`. `replace` (not `push`) keeps `community-save` as the Back-button target from that destination.
- **Use without changes (direct from a template)** — home Popular templates, `/templates` without `fromFlow`, or a pasted review-template URL. Always routes to `/create/community-name` and walks **name → description → optional photo**, then `/create/confirm-stakeholders`. A leftover draft title is **not** “community already done”; those identity fields are cleared so cache cannot skip or pre-fill the path. Intro, structure, size, save, community review, and custom-rule authoring are skipped. Back from name returns to template review.
- **Use without changes (in-flow)** — `/create/review``/templates?fromFlow=1` → template. Community identity is already collected, so this action seeds the template and routes to `/create/confirm-stakeholders`. Back returns to template review.
The action for Customize is cleared on the first fire so later direct visits to `/create/review` render normally. The review screen is not part of the Use-without-changes identity path. If they land on `/create/review` with a leftover Use-without-changes pin, the screen `replace`s to `community-name`.
**Direct entry vs in-flow template pick.** The same `/create/review-template/[slug]` URL is reached from two different origins. We disambiguate at the *click site*, not on the review-template page. **Direct** picks call [`prepareFreshCreateFlowEntry`](../app/(app)/create/utils/prepareFreshCreateFlowEntry.ts) **before** navigation (local + server draft when sync is on — see **Fresh start vs continue draft** above). **In-flow** picks skip that call so the users community-stage state survives the detour. Because `CreateFlowProvider` reads `localStorage` in its `useState` initializer, clearing **before** `push` means a direct entry mounts without stale anonymous keys; signed-in users also avoid a stale server draft overwriting the empty mirror.
| Origin | Click-site behavior | URL the user lands on |
| --- | --- | --- |
| Home marketing "Popular templates" ([`RuleStack.container.tsx`](../app/components/sections/RuleStack/RuleStack.container.tsx)) | always `await prepareFreshCreateFlowEntry()` then navigate | `/create/review-template/[slug]` |
| `/templates` index ([`TemplatesPageClient.tsx`](../app/(marketing)/templates/TemplatesPageClient.tsx)) visited directly / via pasted URL | `fromFlow` absent → `await prepareFreshCreateFlowEntry()` then navigate | `/create/review-template/[slug]` |
| In-flow: `/create/review` footer "Create from template" → `/templates?fromFlow=1` → template click | `fromFlow=1` → no fresh-entry prep | `/create/review-template/[slug]?fromFlow=1` |
Only one `?fromFlow=1` marker exists on `/templates`. Template clicks forward it onto `/create/review-template/[slug]?fromFlow=1` so **Use without changes** can tell in-flow (skip identity) from a direct catalog start (collect name / description / photo). **Customize** still branches on `state.title` (full questionnaire vs `core-values`).
**Resume from profile** remains explicit-only: **Continue** clears local mirrors then opens `/create/{step}` so [`SignedInDraftHydration`](../app/(app)/create/SignedInDraftHydration.tsx) can load `/api/drafts/me` when the client buffer is empty. There is no automatic “pick template from marketing → silently merge server draft” path.
**Final-review Rule category chips** are derived from `CreateFlowState` via [`buildFinalReviewCategoriesFromState`](../lib/create/buildFinalReviewCategories.ts): for the Customize / plain custom-rule path it resolves `selected{Communication,Membership,DecisionApproach,ConflictManagement}MethodIds` against the curated method presets in `messages/en/create/customRule/*.json`, and `buildCoreValuesForDocument` supplies the `Values` row from `coreValuesChipsSnapshot` + `selectedCoreValueIds`. For the Use-without-changes path the template body lives in `state.sections`; the helper renders `categoryName` + entry titles directly. The demo chips shipped in `finalReview.json` remain the fallback only when nothing in state resolves to any chip (e.g. direct navigation for development).
**Starting the wizard from a template at `final-review` directly** is out of scope until a dedicated product ticket ships. A **full create-from-template** experience will **likely use separate route(s)** when product and eng define it (may still align conceptually with the same three stages where behavior overlaps the custom path).
---
## Persistence and exit
| Mode | Where progress lives | Save & Exit / publish |
| --- | --- | --- |
| **Anonymous** | `localStorage` key **`create-flow-anonymous`** | **Save & Exit** (from `community-structure` onward, plus `edit-rule`) opens the save-progress magic-link modal; after verify, optional **PUT** `/api/drafts/me` when `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` (see Tickets 45 in [guides/backend-linear-tickets.md](guides/backend-linear-tickets.md)). **Publish** `POST`s `/api/rules` without a session (`userId` null) and sets httpOnly **`cr_rule_claim`**. Landing on **`/create/completed`** opens a **keep-this-rule** magic-link modal (no draft transfer); the top nav stays **Save & Exit** so they can reopen it (**Continue without saving** from that control leaves the flow). After Publish, skip on the auto-opened modal stays on completed. **Edit** shows when a last-published rule is in session storage. The public URL works; the row is omitted from `GET /api/rules` until claimed. Signing in on the same browser attaches `userId` (profile, edit, invites). |
| **Signed-in** | In-memory React state in **`CreateFlowContext`** | **Save & Exit** from the **`community-structure`** step onward (and `edit-rule`) may **PUT** `/api/drafts/me` when sync is on. **Completed** is **Exit**. **Publish** stores the rule with **`userId`**. **Sign out** is on profile, not in the create top nav. |
Details and edge cases (conflict confirm, banners, `?syncDraft=1`) match **Ticket 4**, **Ticket 5**, and [`docs/guides/backend-roadmap.md`](guides/backend-roadmap.md) §12.
---
## Known implementation gaps
- **Profile + drafts (CR-86):** The profile page lists the server draft. **Continue** clears anonymous `localStorage` (and core-value details) then deep-links to `/create/{currentStep}` so hydration loads the server draft. **Create new custom Rule** and marketing **Create rule** use **`prepareFreshCreateFlowEntry`** (local + `DELETE /api/drafts/me` when sync is on) before opening the wizard so signed-in behavior matches a fresh anonymous start. `SignedInDraftHydration` may `router.replace` to the saved step when it applies a server draft so the URL matches hydrated state. Remaining edge cases (e.g. template review routes) are handled when they surface in QA.
- **Inner “text/select shells”:** deferred until Create Community is stable; screens use **`CreateFlowStepShell`** only for Stage 1.
---
## Related docs
- [docs/guides/backend-roadmap.md](guides/backend-roadmap.md) §12 — Frontend hook-up
- [docs/guides/backend-linear-tickets.md](guides/backend-linear-tickets.md) — Tickets 4, 5, 6, 17; profile/drafts — Ticket 15 / **CR-86**