Template flow cleaned up

This commit is contained in:
adilallo
2026-04-20 16:45:15 -06:00
parent d3bb8cdd0f
commit c08cd62872
32 changed files with 1545 additions and 254 deletions
@@ -0,0 +1,22 @@
import { clearAnonymousCreateFlowStorage } from "./anonymousDraftStorage";
import { clearCoreValueDetailsLocalStorage } from "./coreValueDetailsLocalStorage";
/**
* Wipe the anonymous in-progress create-flow draft from `localStorage` (both
* the main `create-flow-anonymous` blob and the separate core-value details
* key). Intended for call sites that navigate **into** the create flow from
* outside and want a fresh slate — today that's the marketing "Popular
* templates" click handler on the home page and the `/templates` index page
* (when not in-flow). `CreateFlowProvider` reads `localStorage` during its
* `useState` initializer, so clearing *before* pushing the next route means
* the provider mounts empty and the Create Community stage starts clean.
*
* Note: this only touches localStorage. It does **not** delete the
* authenticated user's server draft (`/api/drafts/me`). Server drafts are
* loaded deliberately from the profile page, not re-hydrated into the flow
* on every entry, so there's nothing to wipe here for signed-in users.
*/
export function clearCreateFlowPersistedDrafts(): void {
clearAnonymousCreateFlowStorage();
clearCoreValueDetailsLocalStorage();
}
@@ -0,0 +1,24 @@
/**
* Typography + padding overrides applied to the primary/secondary buttons
* rendered inside `CreateFlowFooter`. The footer slot expects a compact
* size regardless of the default `<Button size="xsmall">` output, and both
* the Create Community / Custom Rule / Review flows and the template-review
* footer share the same override string — keeping it here prevents drift
* between those two call sites.
*
* The `!` prefixes bypass Button's own size tokens; the extra spacing vars
* mirror the Figma compact footer button spec. When the design system
* exposes a native size that matches, this module should collapse.
*/
export const CREATE_FLOW_FOOTER_BUTTON_CLASS =
"md:!text-[14px] md:!leading-[16px] !text-[12px] !leading-[14px] " +
"!px-[var(--spacing-measures-spacing-200,8px)] md:!px-[var(--spacing-measures-spacing-250,10px)] " +
"!py-[var(--spacing-measures-spacing-200,8px)] md:!py-[var(--spacing-measures-spacing-250,10px)]";
/**
* Template-review "Use without changes" (ghost variant) renders on a dark
* backdrop and needs an explicit text-color override in addition to the
* shared compact sizing. Composed from the base class so any future tweak
* to typography/padding propagates automatically.
*/
export const CREATE_FLOW_FOOTER_BUTTON_ON_DARK_CLASS = `${CREATE_FLOW_FOOTER_BUTTON_CLASS} !text-white`;
@@ -0,0 +1,69 @@
import type { CreateFlowState, CreateFlowStep } from "../types";
import type footerMessages from "../../../../messages/en/create/footer.json";
type FooterMessageKey = keyof typeof footerMessages;
/**
* Binding for each Custom Rule stage step whose footer primary button
* gates the user on "has at least one chip selected?". All five screens
* render the same `<Button …>`; only the disable predicate and the
* footer message differ — this table is the single source of truth for
* both, so `CreateFlowLayoutClient` can render one JSX block for the
* whole group.
*
* `selectionIds` returns the currently-selected ids array from flow
* state for that step (empty array when nothing has been selected or
* the field hasn't been touched). Returning a fresh array on empty is
* fine: these are read-only length checks, not memo keys.
*
* Note: the Confirm Stakeholders step has its own dedicated label copy
* and is not gated on a selection count, so it stays out of this table.
* Template-review and Community Save also have bespoke two-button
* layouts and are intentionally excluded.
*/
export type CustomRuleConfirmFooterStep = {
step: Extract<
CreateFlowStep,
| "core-values"
| "communication-methods"
| "membership-methods"
| "decision-approaches"
| "conflict-management"
>;
footerMessageKey: FooterMessageKey;
selectionIds: (state: CreateFlowState) => readonly string[];
};
export const CUSTOM_RULE_CONFIRM_FOOTER_STEPS: readonly CustomRuleConfirmFooterStep[] =
[
{
step: "core-values",
footerMessageKey: "confirmCoreValues",
selectionIds: (s) => s.selectedCoreValueIds ?? [],
},
{
step: "communication-methods",
footerMessageKey: "confirmCommunication",
selectionIds: (s) => s.selectedCommunicationMethodIds ?? [],
},
{
step: "membership-methods",
footerMessageKey: "confirmMembership",
selectionIds: (s) => s.selectedMembershipMethodIds ?? [],
},
{
step: "decision-approaches",
footerMessageKey: "confirmDecisionApproaches",
selectionIds: (s) => s.selectedDecisionApproachIds ?? [],
},
{
step: "conflict-management",
footerMessageKey: "confirmConflictManagement",
selectionIds: (s) => s.selectedConflictManagementIds ?? [],
},
] as const;
export const CUSTOM_RULE_CONFIRM_FOOTER_STEP_BY_STEP: ReadonlyMap<
CreateFlowStep,
CustomRuleConfirmFooterStep
> = new Map(CUSTOM_RULE_CONFIRM_FOOTER_STEPS.map((e) => [e.step, e]));
@@ -1,27 +0,0 @@
import type { CreateFlowState } from "../types";
const IGNORED_KEYS = new Set<string>(["currentStep"]);
function valueIndicatesUserInput(value: unknown): boolean {
if (value === undefined || value === null) return false;
if (typeof value === "string") return value.trim().length > 0;
if (typeof value === "boolean") return value;
if (typeof value === "number") return Number.isFinite(value);
if (Array.isArray(value)) return value.length > 0;
if (typeof value === "object") {
return Object.keys(value as object).length > 0;
}
return false;
}
/**
* True once the user has entered meaningful create-flow data (not only navigation metadata).
* Used to show "Save & Exit" vs a plain "Exit" that confirms data loss.
*/
export function hasCreateFlowUserInput(state: CreateFlowState): boolean {
for (const key of Object.keys(state)) {
if (IGNORED_KEYS.has(key)) continue;
if (valueIndicatesUserInput(state[key])) return true;
}
return false;
}