App reorganization
This commit is contained in:
@@ -1,59 +1,51 @@
|
||||
---
|
||||
description: Figma ↔ codebase prop alignment & normalization for design-system components
|
||||
description: Component prop conventions — lowercase-canonical enums, Figma traceability
|
||||
globs: app/components/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Component prop alignment
|
||||
|
||||
Figma emits PascalCase enum values (`"Standard"`, `"Inverse"`); the codebase
|
||||
stores lowercase (`"standard"`, `"inverse"`). Components must accept **both**,
|
||||
normalize to lowercase internally, and never break existing consumer call
|
||||
sites.
|
||||
Figma is the source of truth for component **design** (existence, variants,
|
||||
visual specification). The codebase implements those components using
|
||||
idiomatic TypeScript naming. Enum props are **lowercase** in code; PascalCase
|
||||
is a Figma-side concern only.
|
||||
|
||||
## Scope
|
||||
## Enum prop convention
|
||||
|
||||
Applies to enum-like string props: `variant`, `size`, `state`, `mode`, `type`,
|
||||
`position`, `alignment`, `status`, `color`, `palette`. **Skip** for free-form
|
||||
strings (URLs, classNames), booleans, numbers, or internal-only props.
|
||||
|
||||
## Pattern
|
||||
- Types use lowercase string unions: `"small" | "medium" | "large"`.
|
||||
- Do NOT add PascalCase variants to type unions.
|
||||
- Do NOT call normalizers in containers. The container layer is for `memo`,
|
||||
derived state, prop defaults, and bound logic — not for casing translation.
|
||||
- Each enum prop has a sibling `<COMPONENT>_<PROP>_OPTIONS as const` array
|
||||
exported alongside the type. Storybook `argTypes` and any runtime guard
|
||||
consume that array as the single source of valid values.
|
||||
|
||||
```typescript
|
||||
// Type accepts both formats
|
||||
export type ComponentSizeValue = "small" | "medium" | "Small" | "Medium";
|
||||
|
||||
// Container normalizes via helpers from lib/propNormalization.ts
|
||||
import { normalizeSize } from "../../../lib/propNormalization";
|
||||
|
||||
const ComponentContainer = ({ size: sizeProp = "small" }: Props) => {
|
||||
const size = normalizeSize(sizeProp, SIZE_OPTIONS, "small");
|
||||
return <ComponentView size={size} />;
|
||||
};
|
||||
export const CHIP_PALETTE_OPTIONS = ["primary", "secondary"] as const;
|
||||
export type ChipPaletteValue = (typeof CHIP_PALETTE_OPTIONS)[number];
|
||||
```
|
||||
|
||||
## Normalizers
|
||||
|
||||
Use an existing helper from `lib/propNormalization.ts` before writing a new
|
||||
one. Generic: `normalizeMode`, `normalizeState`, `normalizeInputState`,
|
||||
`normalizeSize`, `normalizeAlignment`, `normalizeSmallMediumLargeSize`,
|
||||
`normalizeLabelVariant`. Component-specific variants are named
|
||||
`normalize<Component><Prop>` (e.g. `normalizeChipPalette`,
|
||||
`normalizeButtonState`) — add one there rather than inlining a switch in your
|
||||
container.
|
||||
|
||||
## Figma traceability
|
||||
|
||||
Every DS component's container docstring cites its Figma origin so designers
|
||||
and engineers can jump between the two. Format: `Figma: "<Component Path>"
|
||||
(<node-id>)`.
|
||||
- Container docstring (required on every DS container): `Figma:
|
||||
"<Component Path>" (<node-id>)`.
|
||||
- View root element: `data-figma-node="<id>"` when the view maps to a
|
||||
distinct Figma node.
|
||||
- For create-flow screens, node ids come from `CREATE_FLOW_SCREEN_REGISTRY`
|
||||
in `app/(app)/create/utils/createFlowScreenRegistry.ts`. For everything else,
|
||||
pull the node id from the Figma file directly. Use `TODO(figma)` as a
|
||||
placeholder rather than omitting the docstring entirely.
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Figma: "Control / Incrementer" (`17857:30943`). A compact [ - value + ]
|
||||
* row used for numeric step inputs…
|
||||
* Figma: "Control / Incrementer" (17857:30943). A compact [ - value + ]
|
||||
* row used for numeric step inputs.
|
||||
*/
|
||||
```
|
||||
|
||||
When the view renders a distinct Figma node, add `data-figma-node="<id>"` on
|
||||
the root element for quick DOM-to-design lookup.
|
||||
## Pasting from Figma
|
||||
|
||||
Figma's "Inspect → Code" output emits PascalCase. When importing a snippet,
|
||||
lowercase the enum values before committing — same pattern as removing
|
||||
inline pixel values in favor of design tokens.
|
||||
|
||||
@@ -22,24 +22,27 @@ app/components/controls/<Name>/
|
||||
**Container** (`<Name>.container.tsx`):
|
||||
|
||||
- Marked `"use client"`.
|
||||
- Receives `<Name>Props`; normalizes PascalCase enums via
|
||||
`lib/propNormalization.ts`; computes derived state (clamps, ids, bounds).
|
||||
- Renders `<<Name>View />` with already-normalized `<Name>ViewProps`.
|
||||
- Receives `<Name>Props`; computes derived state (clamps, ids, bounds, prop
|
||||
defaults) and bound event handlers.
|
||||
- Renders `<<Name>View />`. Containers do **not** translate prop casing —
|
||||
enum props are lowercase end-to-end (see `component-props.mdc`).
|
||||
- Default export: `memo(<Name>Container)` with `.displayName = "<Name>"`.
|
||||
- Carries the Figma docstring (`Figma: "<Path>" (<node-id>)`).
|
||||
|
||||
**View** (`<Name>.view.tsx`):
|
||||
|
||||
- Marked `"use client"`.
|
||||
- Pure render of `<Name>ViewProps`. No prop normalization, no data fetching,
|
||||
no derived business logic.
|
||||
- Default export: `memo(<Name>View)` with
|
||||
`.displayName = "<Name>View"`.
|
||||
- Pure render of `<Name>ViewProps`. No data fetching, no derived business
|
||||
logic, no enum casing translation.
|
||||
- Default export: `memo(<Name>View)` with `.displayName = "<Name>View"`.
|
||||
|
||||
**Types** (`<Name>.types.ts`):
|
||||
|
||||
- Export `<Name>Props` (consumer-facing, accepts PascalCase + lowercase).
|
||||
- Export `<Name>ViewProps` (already-normalized shape the view consumes).
|
||||
- Export any locally-defined value types (`<Name>SizeValue`, etc.).
|
||||
- Export `<Name>Props` (consumer-facing).
|
||||
- Export `<Name>ViewProps` (the shape the view consumes — typically a
|
||||
resolved superset of `<Name>Props`).
|
||||
- Export any locally-defined value types (`<Name>SizeValue`, etc.) sourced
|
||||
from the matching `*_OPTIONS` array in `lib/propNormalization.ts`.
|
||||
|
||||
**Index** (`index.tsx`):
|
||||
|
||||
@@ -51,10 +54,10 @@ export type { <Name>Props } from "./<Name>.types";
|
||||
## Single-file pattern (exception)
|
||||
|
||||
`app/components/buttons/*.tsx` and other trivially-presentational components
|
||||
can stay as a single file when they have **no enum prop normalization and no
|
||||
derived state** (e.g. `Button.tsx`, `InlineTextButton.tsx`). If you find
|
||||
yourself adding state, enum normalization, or more than a handful of props,
|
||||
promote it to the split pattern.
|
||||
can stay as a single file when they have **no derived state and only a
|
||||
handful of props** (e.g. `Button.tsx`, `InlineTextButton.tsx`). If you find
|
||||
yourself adding state, side effects, or enum logic, promote it to the split
|
||||
pattern.
|
||||
|
||||
## Wrapper / group components
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
description: Create-flow structure & design-system reuse guardrails
|
||||
globs: app/create/**/*.{ts,tsx},messages/en/create/**/*.json
|
||||
globs: app/(app)/create/**/*.{ts,tsx},messages/en/create/**/*.json
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
@@ -8,15 +8,15 @@ alwaysApply: false
|
||||
|
||||
## Folder & file layout
|
||||
|
||||
- Screens live in `app/create/screens/<layoutKind>/<StepIdPascal>Screen.tsx`
|
||||
- Screens live in `app/(app)/create/screens/<layoutKind>/<StepIdPascal>Screen.tsx`
|
||||
where `<layoutKind>` mirrors `CreateFlowLayoutKind` (`card`, `select`,
|
||||
`right-rail`, `completed`, …). File + export name is the **step id**, never
|
||||
the layout kind (e.g. `DecisionApproachesScreen`, not `RightRailScreen`).
|
||||
- Step id ↔ layout kind mapping is declared in
|
||||
`app/create/utils/createFlowScreenRegistry.ts`. Never branch on layout kind
|
||||
`app/(app)/create/utils/createFlowScreenRegistry.ts`. Never branch on layout kind
|
||||
inside a screen — pick the matching shell (`CreateFlowStepShell` /
|
||||
`CreateFlowTwoColumnSelectShell`).
|
||||
- Shared create-flow pieces go in `app/create/components/` (layout shells,
|
||||
- Shared create-flow pieces go in `app/(app)/create/components/` (layout shells,
|
||||
field composites). Generic primitives go in `app/components/`.
|
||||
|
||||
## Use the design system — don't hand-roll
|
||||
@@ -25,8 +25,8 @@ Reach for these before writing new markup:
|
||||
|
||||
| Need | Component |
|
||||
| --- | --- |
|
||||
| Labelled text-area section in a modal | `app/create/components/ModalTextAreaField` |
|
||||
| Toggle-chip row + inline "+ Add" input | `app/create/components/ApplicableScopeField` |
|
||||
| Labelled text-area section in a modal | `app/(app)/create/components/ModalTextAreaField` |
|
||||
| Toggle-chip row + inline "+ Add" input | `app/(app)/create/components/ApplicableScopeField` |
|
||||
| `[– value +]` numeric stepper (± label) | `app/components/controls/Incrementer` / `IncrementerBlock` |
|
||||
| Mid-paragraph "expand / see all" link button | `app/components/buttons/InlineTextButton` |
|
||||
| Help-icon + label above a control | `app/components/utility/InputLabel` (`helpIcon` prop) |
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
description: Custom hooks live in app/hooks; co-locate logic, document via TSDoc.
|
||||
globs: app/hooks/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Custom hooks
|
||||
|
||||
Reusable component logic lives in `app/hooks/`. Each hook is a small, focused
|
||||
module with a TSDoc block that doubles as the API reference (no separate doc
|
||||
file).
|
||||
|
||||
## File layout
|
||||
|
||||
- One file per hook: `app/hooks/use<Name>.ts`.
|
||||
- Re-export from `app/hooks/index.ts`. Consumers import from the barrel:
|
||||
`import { useFoo } from "../hooks";`.
|
||||
- Companion unit test (when there is non-trivial logic): `tests/unit/hooks/`.
|
||||
|
||||
## Authoring rules
|
||||
|
||||
- Marked as a regular function (`export function useFoo() {}`); React handles
|
||||
the `use*` naming convention.
|
||||
- Wrap exposed callbacks in `useCallback` and computed values in `useMemo`
|
||||
so consumers can list them in dependency arrays without churn.
|
||||
- Read DOM/browser APIs only inside `useEffect` so the hook stays SSR-safe.
|
||||
- Never throw on missing globals (e.g. `window`, `gtag`); guard and no-op.
|
||||
|
||||
## TSDoc — the only reference
|
||||
|
||||
Every exported hook gets a TSDoc block with:
|
||||
|
||||
- 1–2 sentence summary.
|
||||
- `@param` per argument and `@returns` describing the shape.
|
||||
- `@example` showing the typical call site.
|
||||
|
||||
```ts
|
||||
/**
|
||||
* Detect clicks outside a set of elements (e.g. close a dropdown).
|
||||
*
|
||||
* @param refs Elements that should NOT trigger the handler.
|
||||
* @param handler Invoked when a click lands outside every ref.
|
||||
* @param enabled Toggle without unmounting the consumer (default true).
|
||||
*
|
||||
* @example
|
||||
* useClickOutside([menuRef, buttonRef], () => setOpen(false), open);
|
||||
*/
|
||||
export function useClickOutside(
|
||||
refs: Array<RefObject<HTMLElement>>,
|
||||
handler: (event: MouseEvent | TouchEvent) => void,
|
||||
enabled = true,
|
||||
): void { /* ... */ }
|
||||
```
|
||||
|
||||
## Container/view consumption
|
||||
|
||||
Hooks belong in **container** files (per `component-structure.mdc`). Views
|
||||
stay pure and read derived values via props — never call hooks that touch
|
||||
state or side effects from a view.
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
description: App Router route organization (groups, layouts, chrome composition)
|
||||
globs: app/**/*.{ts,tsx}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Route organization
|
||||
|
||||
Top-level routes live inside **route groups** so each surface owns its own
|
||||
layout and chrome. Groups are wrapping folders in `(parens)` — they organize
|
||||
the file tree without affecting URLs.
|
||||
|
||||
## Group map
|
||||
|
||||
| Group | URL surface | Audience | Chrome |
|
||||
|---|---|---|---|
|
||||
| `app/(marketing)/` | `/`, `/learn`, `/blog`, `/templates`, future public pages | Public, indexable | TopNav (via root) + marketing `<Footer />` |
|
||||
| `app/(app)/` | `/create/*`, `/login`, `/profile`, future signed-in surfaces | Authenticated product | TopNav (via root) — no footer |
|
||||
| `app/(admin)/` | `/monitor`, future ops dashboards | Operators | TopNav (via root) — no footer |
|
||||
| `app/(dev)/` | `/components-preview`, future dev previews | Local dev (NODE_ENV gated) | TopNav (via root) — no footer |
|
||||
| `app/api/` | API routes | n/a | n/a |
|
||||
|
||||
Route folders **must not** sit loose at the top level of `app/`. If a new
|
||||
surface doesn't fit an existing group, add a new group rather than dropping
|
||||
the folder next to `(marketing)/`.
|
||||
|
||||
## Layout responsibilities
|
||||
|
||||
- **`app/layout.tsx`** — `<html>`, `<body>`, providers (`MessagesProvider`,
|
||||
`AuthModalProvider`), fonts, and `ConditionalNavigation`. Renders
|
||||
`{children}` directly inside the flex column. **Does not** render
|
||||
`<main>` — each group layout owns that.
|
||||
- **`app/(marketing)/layout.tsx`** — wraps with `<main className="flex-1">`
|
||||
and appends the public `<Footer />`.
|
||||
- **`app/(app)/layout.tsx`** / **`(admin)/layout.tsx`** / **`(dev)/layout.tsx`** —
|
||||
wrap with `<main className="flex-1">`. No footer.
|
||||
- **Nested layouts** (e.g. `(app)/create/layout.tsx`) compose feature-specific
|
||||
chrome inside the group's `<main>` — never render `<html>`, `<body>`,
|
||||
`<main>`, or providers.
|
||||
|
||||
If a route needs different chrome than its group provides, prefer adding a
|
||||
**nested layout** under that route — don't introduce pathname-sniffing
|
||||
client components. (`ConditionalNavigation` is the lone tolerated exception
|
||||
because it carries SSR session state; do not add new pathname-conditional
|
||||
chrome components.)
|
||||
|
||||
## Co-located component folders
|
||||
|
||||
Page-private server/client components that are **only** used by routes in a
|
||||
given group go in `_components/` inside that group:
|
||||
|
||||
```
|
||||
app/(marketing)/_components/MarketingRuleStackSection.tsx
|
||||
```
|
||||
|
||||
The leading underscore makes Next.js treat the folder as **private** — it's
|
||||
ignored by the router. Use this instead of letting page-only files sit next
|
||||
to `page.tsx`.
|
||||
|
||||
Components reused across groups belong in `app/components/<category>/`
|
||||
(see `component-structure.mdc`).
|
||||
|
||||
## Adding a new route
|
||||
|
||||
1. **Choose the group** by audience: marketing (public), app (signed-in),
|
||||
admin (operators), dev (local-only). When in doubt, ask whether the
|
||||
public marketing footer should appear — if yes, it's `(marketing)`.
|
||||
2. Create `app/(<group>)/<route>/page.tsx`. URLs do **not** include the
|
||||
group name.
|
||||
3. If the route needs its own chrome (e.g. a wizard header), add
|
||||
`app/(<group>)/<route>/layout.tsx`.
|
||||
4. If the route ships private helpers, put them in
|
||||
`app/(<group>)/<route>/_components/` (or
|
||||
`app/(<group>)/_components/` for group-wide page components).
|
||||
|
||||
## Splitting a group
|
||||
|
||||
Promote a sub-cluster to its own group only when **both** are true:
|
||||
|
||||
- It will hold ≥2 routes that share a layout, **or** it has a clearly
|
||||
distinct audience/access model (e.g. a future `(auth)/` for
|
||||
signup/forgot/verify alongside login).
|
||||
- Moving the routes pays for itself by replacing existing pathname
|
||||
conditionals or by composing real shared chrome — not just by tidying
|
||||
the folder list.
|
||||
|
||||
YAGNI applies: a group with one route and no shared layout is just a
|
||||
folder with parens.
|
||||
+19
-10
@@ -6,14 +6,22 @@ alwaysApply: false
|
||||
|
||||
# Where stories live
|
||||
|
||||
All stories live in the top-level `stories/` folder, mirroring the
|
||||
`app/components/` directory tree:
|
||||
All stories live in the top-level `stories/` folder. Two layout rules:
|
||||
|
||||
| Source | Story location |
|
||||
| --------------------------------- | -------------------------------------- |
|
||||
| `app/components/controls/Chip` | `stories/controls/Chip.stories.js` |
|
||||
| `app/components/buttons/Button` | `stories/buttons/Button.stories.js` |
|
||||
| `app/create/screens/.../FooScreen`| `stories/pages/FooPage.stories.js` |
|
||||
- **Design-system components** mirror `app/components/`. A component at
|
||||
`app/components/<bucket>/<Name>` gets `stories/<bucket>/<Name>.stories.js`.
|
||||
- **Create-flow material** has two carve-outs:
|
||||
- `stories/create-flow/` — shared create-flow pieces that aren't in
|
||||
`app/components/` (e.g. composed wizard fragments).
|
||||
- `stories/pages/` — integration stories that exercise an entire
|
||||
`app/(app)/create/screens/<...>` screen as it appears in the wizard.
|
||||
|
||||
| Source | Story location |
|
||||
| --------------------------------- | --------------------------------------- |
|
||||
| `app/components/controls/Chip` | `stories/controls/Chip.stories.js` |
|
||||
| `app/components/buttons/Button` | `stories/buttons/Button.stories.js` |
|
||||
| `app/(app)/create/screens/.../FooScreen`| `stories/pages/FooPage.stories.js` |
|
||||
| Shared create-flow fragment | `stories/create-flow/<Name>.stories.js` |
|
||||
|
||||
Do **not** colocate `*.stories.*` next to components. The Storybook config
|
||||
(`.storybook/main.js`) only globs `stories/**`.
|
||||
@@ -65,8 +73,9 @@ export const Default = { args: { variant: "filled" } };
|
||||
## `argTypes`
|
||||
|
||||
For every Figma enum prop (`variant`, `size`, `state`, `mode`, `palette`,
|
||||
…) expose a `select` control listing the **lowercase** option set — even
|
||||
though the prop accepts PascalCase too. See `.cursor/rules/component-props.mdc`.
|
||||
…) expose a `select` control listing the **lowercase** option set, sourced
|
||||
from the matching `*_OPTIONS` const in `lib/propNormalization.ts`. See
|
||||
`.cursor/rules/component-props.mdc`.
|
||||
|
||||
# Rely on the global preview — don't re-wrap
|
||||
|
||||
@@ -101,5 +110,5 @@ export const Interactive = {
|
||||
# Coverage expectation
|
||||
|
||||
Every new component in `app/components/**` ships with a story. Screens in
|
||||
`app/create/screens/**` ship with a `stories/pages/<Name>Page.stories.js`
|
||||
`app/(app)/create/screens/**` ship with a `stories/pages/<Name>Page.stories.js`
|
||||
entry. A new component without a story is considered incomplete.
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
# Agent guide
|
||||
|
||||
Orientation for AI coding agents working in this repo. Per-file conventions
|
||||
live in `.cursor/rules/*.mdc` (auto-loaded by Cursor; other agents should
|
||||
read them on demand). This file is the **map** — load it first, then load
|
||||
the rule(s) matching the file you're editing.
|
||||
|
||||
## What this is
|
||||
|
||||
Next.js 16 / React 19 app for community decision-making and governance.
|
||||
Single-locale (English) today; designed for i18n via `messages/`.
|
||||
|
||||
## Read before editing
|
||||
|
||||
| If you're touching… | Load this rule |
|
||||
| --- | --- |
|
||||
| `app/components/**` | `component-structure.mdc`, `component-props.mdc`, `tailwind-styling.mdc` |
|
||||
| `app/(app)/create/**` | `create-flow.mdc` (+ component rules) |
|
||||
| `app/api/**` | `api-routes.mdc` |
|
||||
| `app/hooks/**` | `hooks.mdc` |
|
||||
| `app/**/page.tsx` or `app/**/layout.tsx` | `routes.mdc` |
|
||||
| `messages/**` or any user-visible string | `localization.mdc` |
|
||||
| `tests/**` | `testing.mdc` |
|
||||
| `stories/**` | `storybook.mdc` |
|
||||
|
||||
When in doubt about file structure or naming, the rules win over your
|
||||
priors — they reflect deliberate decisions.
|
||||
|
||||
## Cross-cutting principles (no single rule owns these)
|
||||
|
||||
1. **Figma is the source of truth for design.** Container files carry a
|
||||
`Figma: "<Path>" (<node-id>)` docstring; views render Figma intent.
|
||||
Codebase naming uses lowercase conventions (see `component-props.mdc`)
|
||||
even when Figma uses PascalCase enum values.
|
||||
2. **Container / view split is the component pattern.** Never put state
|
||||
or side effects in a `*.view.tsx`. Hooks belong in containers.
|
||||
3. **All user-visible text lives in `messages/`.** Hardcoded strings in
|
||||
components are a bug — even for placeholders.
|
||||
4. **Tests live in `tests/`, not co-located.** Mirror the source path
|
||||
(`app/components/Foo` → `tests/components/Foo.test.tsx`).
|
||||
5. **Routes live inside groups** — `(marketing)`, `(app)`, `(admin)`,
|
||||
`(dev)`. Don't drop a new route folder loose at the top of `app/`.
|
||||
6. **No new pathname-sniffing chrome.** Compose chrome via group/nested
|
||||
layouts, not `usePathname()` checks. (`ConditionalNavigation` is the
|
||||
sole tolerated exception — it carries SSR session state.)
|
||||
|
||||
## Legacy / scaffolding
|
||||
|
||||
Some code exists temporarily while backend services are stood up:
|
||||
|
||||
- `NEXT_PUBLIC_ENABLE_BACKEND_SYNC` gating
|
||||
- `migrateLegacyCreateFlowState`, `LEGACY_LIVE_KEY`, `LEGACY_DRAFT_KEY`
|
||||
- `/create/right-rail` redirect
|
||||
- `docs/guides/backend-roadmap.md`, `backend-linear-tickets.md`,
|
||||
`template-recommendation-matrix.md`
|
||||
|
||||
**Do not delete** without an explicit ask. Do not add new code in this
|
||||
shape — when adding scaffolding, leave a `// TODO(legacy): …` with the
|
||||
removal trigger.
|
||||
|
||||
## Verification recipe
|
||||
|
||||
Run these (in order) before declaring a change done. They mirror CI:
|
||||
|
||||
```bash
|
||||
rm -rf .next # only if you moved/renamed routes or layouts
|
||||
npx tsc --noEmit # type check
|
||||
npx vitest run # unit + component (101 files / ~700 tests)
|
||||
npx next build # production build + route manifest
|
||||
```
|
||||
|
||||
For UI-only changes, also: `npm run storybook` and visually confirm.
|
||||
For E2E-relevant changes: `npm run e2e`.
|
||||
|
||||
## Where else to look
|
||||
|
||||
- [README.md](README.md) — human onboarding, scripts, project layout.
|
||||
- [CONTRIBUTING.md](CONTRIBUTING.md) — local Postgres + Prisma + magic-link
|
||||
setup, PR workflow.
|
||||
- [docs/README.md](docs/README.md) — index of user-facing docs.
|
||||
- [docs/create-flow.md](docs/create-flow.md) — wizard URL/persistence canon
|
||||
(read alongside `create-flow.mdc`).
|
||||
+58
-33
@@ -1,51 +1,76 @@
|
||||
# Contributing
|
||||
|
||||
## Backend (local)
|
||||
## Local backend
|
||||
|
||||
1. Copy [`.env.example`](.env.example) to `.env` and set `SESSION_SECRET` (at least 16 characters).
|
||||
2. `docker compose up -d postgres mailhog` — omit `mailhog` if you only need Postgres; with `SMTP_URL` unset, the **magic-link verify URL** is printed in the dev server log (see `.env.example`).
|
||||
3. Install dependencies: `npm ci`
|
||||
4. Apply migrations: `npx prisma migrate dev`
|
||||
5. (Optional) Seed curated rule templates: `npx prisma db seed` — requires `DATABASE_URL` and applied migrations. Safe to re-run; rows are upserted by `slug` so duplicates are not created.
|
||||
6. Run the app: `npm run dev`
|
||||
1. Copy [`.env.example`](.env.example) to `.env` and set `SESSION_SECRET`
|
||||
(at least 16 characters).
|
||||
2. `docker compose up -d postgres mailhog` — omit `mailhog` if you only
|
||||
need Postgres. Without `SMTP_URL`, the **magic-link verify URL** is
|
||||
printed in the dev server log.
|
||||
3. `npm ci`
|
||||
4. `npx prisma migrate dev`
|
||||
5. *(Optional)* `npx prisma db seed` — seeds curated rule templates.
|
||||
Idempotent; rows upsert by `slug`.
|
||||
6. `npm run dev`
|
||||
|
||||
Use `npx prisma studio` to inspect the database.
|
||||
|
||||
### Prisma migrations (important)
|
||||
### Prisma migrations
|
||||
|
||||
- **Do not edit** migration files that have **already been applied** to **staging, production, or any shared database**. Changing history breaks `migrate deploy` and other environments.
|
||||
- To fix a bad migration, add a **new** migration that corrects the schema. See [docs/backend-roadmap.md](docs/backend-roadmap.md) §8 for the full policy.
|
||||
- **Never edit** a migration that has already been applied to staging,
|
||||
production, or any shared database. Add a **new** migration that
|
||||
corrects the schema instead. Full policy:
|
||||
[docs/guides/backend-roadmap.md](docs/guides/backend-roadmap.md) §8.
|
||||
|
||||
### API routes (overview)
|
||||
### API routes
|
||||
|
||||
| Method | Path | Purpose |
|
||||
| ---------- | ------------------------------ | --------------------------------------------- |
|
||||
| GET | `/api/health` | Liveness / DB check |
|
||||
| GET | `/api/auth/session` | Current user or null |
|
||||
| POST | `/api/auth/magic-link/request` | Send sign-in link email |
|
||||
| GET | `/api/auth/magic-link/verify` | Validate token, set session cookie, redirect |
|
||||
| POST | `/api/auth/logout` | Clear session |
|
||||
| GET / PUT | `/api/drafts/me` | Load or save create-flow JSON (authenticated) |
|
||||
| GET / POST | `/api/rules` | List or publish rules (each **Finalize** creates a new published row until an update/edit-published API exists) |
|
||||
| GET | `/api/templates` | List curated templates |
|
||||
| Method | Path | Purpose |
|
||||
| --- | --- | --- |
|
||||
| GET | `/api/health` | Liveness / DB check. |
|
||||
| GET | `/api/auth/session` | Current user or null. |
|
||||
| POST | `/api/auth/magic-link/request` | Send sign-in link email. |
|
||||
| GET | `/api/auth/magic-link/verify` | Validate token, set cookie, redirect. |
|
||||
| POST | `/api/auth/logout` | Clear session. |
|
||||
| GET / PUT | `/api/drafts/me` | Load or save the create-flow draft. |
|
||||
| GET / POST | `/api/rules` | List or publish rules. |
|
||||
| GET | `/api/templates` | List curated templates. |
|
||||
|
||||
### Email magic link (sign-in)
|
||||
### Magic-link sign-in
|
||||
|
||||
- Open **[http://localhost:3000/login](http://localhost:3000/login)** or use **Log in** in the site header (modal or full page).
|
||||
- Enter email and request a link. Complete sign-in by opening the link in the **same browser** you use for the app (session cookie).
|
||||
- **No `SMTP_URL`:** the full **`GET /api/auth/magic-link/verify?...`** URL is printed in the **dev server terminal** — paste it into the browser address bar.
|
||||
- **Mailhog:** with Compose Mailhog running, set `SMTP_URL=smtp://localhost:1025` and open the link from the message in the Mailhog UI ([http://localhost:8025](http://localhost:8025)).
|
||||
|
||||
**Staging / production:** Sign-in links use the app’s origin. Ensure your reverse proxy sets **`Host`** (and TLS) so links in email match the URL users open. See [docs/backend-roadmap.md](docs/backend-roadmap.md) §9.
|
||||
- Visit **[/login](http://localhost:3000/login)** or use **Log in** in the
|
||||
site header.
|
||||
- Without `SMTP_URL`: copy the verify URL from the dev server terminal.
|
||||
- With Mailhog: set `SMTP_URL=smtp://localhost:1025` and open the message
|
||||
at [http://localhost:8025](http://localhost:8025).
|
||||
- Open the link in the **same browser** as the app (session cookie).
|
||||
|
||||
### Optional draft sync
|
||||
|
||||
Set `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` in `.env` so **signed-in** users can persist create-flow drafts to Postgres via **Save & Exit** and so **anonymous** progress can be **uploaded after magic-link sign-in** from the save-progress exit modal. Without it, server **PUT** `/api/drafts/me` is skipped; anonymous work stays in **browser `localStorage`**, but after sign-in with a `?syncDraft=1` return URL the app still **merges that local draft into the in-memory create flow** (no server write) so you can continue and publish.
|
||||
`NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` enables Postgres draft persistence
|
||||
via `PUT /api/drafts/me` for signed-in users and post-sign-in upload of
|
||||
anonymous drafts. Without it, anonymous progress stays in `localStorage`
|
||||
and signed-in progress stays in memory until **Save & Exit**.
|
||||
|
||||
### Create flow URLs (custom wizard)
|
||||
### Create flow
|
||||
|
||||
The **custom** create-rule wizard lives under **`/create/…`**. The header links to **`/create`**, which redirects to the first step. **Semantic** URL segments (e.g. `community-name`, `community-size`) match Figma intent; order is **`FLOW_STEP_ORDER`** in `app/create/utils/flowSteps.ts`, with UI from **`app/create/[screenId]/page.tsx`** and **`CREATE_FLOW_SCREEN_REGISTRY`** for Figma traceability. **Figma** stages: **Create Community** (through `review`), **Create Custom CommunityRule** (`communication-methods`–`right-rail`), **Review and complete** (`confirm-stakeholders`–`completed`). **`/create/review-template/[slug]`** is a template **preview** only. Full tables and persistence are in **[docs/create-flow.md](docs/create-flow.md)**; engineering tracking: Linear **CR-89** / Ticket 17 in [docs/backend-linear-tickets.md](docs/backend-linear-tickets.md).
|
||||
The custom wizard lives under `/create/…`. Step order, URLs, and Figma
|
||||
stage mapping are canon in [docs/create-flow.md](docs/create-flow.md).
|
||||
Engineering tracking: Linear **CR-89** /
|
||||
[docs/guides/backend-linear-tickets.md](docs/guides/backend-linear-tickets.md)
|
||||
Ticket 17.
|
||||
|
||||
## Frontend / tests
|
||||
## Frontend & tests
|
||||
|
||||
See [docs/TESTING_GUIDE.md](docs/TESTING_GUIDE.md) and the root [README.md](README.md).
|
||||
- Code conventions are enforced by `.cursor/rules/*.mdc` — Cursor surfaces
|
||||
the relevant rule when editing matching files.
|
||||
- See [docs/testing-guide.md](docs/testing-guide.md) for testing
|
||||
philosophy and `.cursor/rules/testing.mdc` for layout/helpers.
|
||||
|
||||
## Pull request workflow
|
||||
|
||||
1. Branch from `main`: `git checkout -b feature/<short-name>`.
|
||||
2. Make the change and add/update tests.
|
||||
3. `npm test && npm run e2e` (and `npm run storybook:build` if you touched
|
||||
stories).
|
||||
4. Commit using a clear message (`feat:`, `fix:`, `chore:`, …).
|
||||
5. Open a PR; CI runs unit, E2E, visual regression, and Lighthouse.
|
||||
|
||||
@@ -1,252 +1,64 @@
|
||||
# Community Rule
|
||||
|
||||
A Next.js application for community decision-making and governance documentation.
|
||||
A Next.js application for community decision-making and governance
|
||||
documentation.
|
||||
|
||||
## 📋 Requirements
|
||||
## Requirements
|
||||
|
||||
- **Node.js**: 20.0.0 or higher (LTS recommended)
|
||||
- **npm**: 10.0.0 or higher
|
||||
- Node.js **20+** (LTS)
|
||||
- npm **10+**
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
Run the development server:
|
||||
## Getting started
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
Open [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
Backend (Postgres, Prisma, API routes) setup is documented in [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
Backend setup (Postgres, Prisma, magic-link auth) is documented in
|
||||
[CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
## 🧪 Testing Framework
|
||||
## Common scripts
|
||||
|
||||
This project uses a simplified, component‑first testing model:
|
||||
| Command | What it does |
|
||||
| --- | --- |
|
||||
| `npm run dev` | Next.js dev server (Turbopack). |
|
||||
| `npm run build` / `npm start` | Production build / serve. |
|
||||
| `npm test` | Vitest unit + component tests with coverage. |
|
||||
| `npm run test:component` | Faster inner loop — components only. |
|
||||
| `npm run e2e` | Playwright E2E + visual regression. |
|
||||
| `npm run storybook` | Storybook on port 6006. |
|
||||
| `npm run lhci` | Lighthouse CI performance pass. |
|
||||
|
||||
- **Component tests (Vitest + RTL)** live in `tests/components/` with a single file per component.
|
||||
- **E2E tests (Playwright)** cover critical user journeys and visual regression.
|
||||
## Project layout
|
||||
|
||||
### Quick Test Commands
|
||||
|
||||
```bash
|
||||
# All component tests with coverage
|
||||
npm test
|
||||
|
||||
# Component tests only (new structure)
|
||||
npm run test:component
|
||||
|
||||
# E2E tests only
|
||||
npm run test:e2e
|
||||
```text
|
||||
app/ Next.js app router (routes, components, hooks, contexts)
|
||||
lib/ Shared library code (i18n, validation, utilities)
|
||||
messages/en/ Localized UI copy (see docs/guides/i18n-translation-workflow.md)
|
||||
prisma/ Database schema, migrations, seed
|
||||
public/ Static assets
|
||||
stories/ Storybook stories
|
||||
tests/ Vitest + Playwright suites
|
||||
docs/ User-facing documentation (start with docs/README.md)
|
||||
.cursor/rules/ Implementation conventions enforced by Cursor
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
## Tech stack
|
||||
|
||||
- ✅ **428 Unit Tests** (94.88% coverage - exceeds 85% target)
|
||||
- ✅ **92 E2E Tests** across 4 browsers
|
||||
- ✅ **23 Visual Regression Tests** per browser
|
||||
- ✅ **Performance Budgets** with Lighthouse CI
|
||||
- ✅ **WCAG 2.1 AA Compliance** with automated testing
|
||||
- ✅ **Bundle Analysis** with automated monitoring
|
||||
- ✅ **Web Vitals Tracking** with real-time metrics
|
||||
Next.js 16 · React 19 · TypeScript · Tailwind CSS 4 · Prisma · Vitest ·
|
||||
Playwright · Storybook 10 · Lighthouse CI.
|
||||
|
||||
### CI/CD Pipeline
|
||||
## Documentation
|
||||
|
||||
- **Gitea Actions** with 7 parallel jobs
|
||||
- **Cross-browser testing** (Chromium, Firefox, WebKit, Mobile)
|
||||
- **Visual regression testing**
|
||||
- **Performance monitoring**
|
||||
- **Code coverage reporting**
|
||||
- [docs/README.md](docs/README.md) — index of guides and rules.
|
||||
- [docs/create-flow.md](docs/create-flow.md) — create-rule wizard canon.
|
||||
- [docs/testing-guide.md](docs/testing-guide.md) — testing philosophy.
|
||||
- [CONTRIBUTING.md](CONTRIBUTING.md) — local backend, API routes, PR
|
||||
workflow.
|
||||
|
||||
📖 **For detailed testing documentation, see `docs/TESTING_GUIDE.md` and [docs/README.md](docs/README.md)**
|
||||
## License
|
||||
|
||||
## ⚡ Performance Optimizations
|
||||
|
||||
This project includes comprehensive performance optimizations for sub-2-second load times:
|
||||
|
||||
### Frontend Optimizations
|
||||
|
||||
- **✅ Code Splitting**: Dynamic imports for non-critical components
|
||||
- **✅ React.memo**: Applied to all 30+ components to prevent unnecessary re-renders
|
||||
- **✅ Image Optimization**: Enhanced `next/image` with lazy loading and blur placeholders
|
||||
- **✅ Font Optimization**: Preloading and fallbacks for all fonts
|
||||
- **✅ Bundle Analysis**: Real-time monitoring with performance budgets
|
||||
- **✅ Error Boundaries**: Comprehensive error handling
|
||||
|
||||
### Performance Monitoring
|
||||
|
||||
Performance testing is handled by:
|
||||
|
||||
- **Lighthouse CI** (`.lighthouserc.json`): Comprehensive performance testing in CI
|
||||
|
||||
```bash
|
||||
npm run lhci # Run Lighthouse CI
|
||||
npm run lhci:mobile # Mobile preset
|
||||
npm run lhci:desktop # Desktop preset
|
||||
npm run performance:budget # With performance budgets
|
||||
```
|
||||
|
||||
- **E2E Performance Tests** (`tests/e2e/performance.spec.ts`): Essential performance checks
|
||||
|
||||
```bash
|
||||
npm run e2e:performance # Run E2E performance tests
|
||||
```
|
||||
|
||||
- **Bundle Analysis**: Analyze bundle sizes
|
||||
```bash
|
||||
npm run bundle:analyze # Analyze bundle sizes
|
||||
```
|
||||
|
||||
### Performance Targets
|
||||
|
||||
- **Bundle Size**: <250KB gzipped (currently 101KB) ✅
|
||||
- **Core Web Vitals**: All metrics in "Good" range ✅
|
||||
- **Lighthouse Score**: >90 on all critical pages ✅
|
||||
- **Load Time**: <2 seconds on 3G connections ✅
|
||||
|
||||
## 📚 Storybook Development
|
||||
|
||||
This project includes Storybook for component development and documentation. The setup automatically detects the environment and applies the appropriate configuration.
|
||||
|
||||
### Local Development
|
||||
|
||||
For local Storybook development:
|
||||
|
||||
```bash
|
||||
npm run storybook:local
|
||||
# or simply
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
- Start Storybook at `http://localhost:6006`
|
||||
- Use relative paths for assets (no base path)
|
||||
|
||||
### GitHub Pages Deployment
|
||||
|
||||
For GitHub Pages deployment with base path:
|
||||
|
||||
```bash
|
||||
npm run storybook:build:github
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
- Build Storybook with `/communityrulestorybook/` base path
|
||||
- Generate files ready for GitHub Pages deployment
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
The CI pipeline automatically uses the GitHub Pages configuration when building Storybook.
|
||||
|
||||
### Configuration
|
||||
|
||||
The Storybook configuration automatically detects the environment:
|
||||
|
||||
- **Local development**: No base path, relative assets
|
||||
- **CI/Production**: Base path `/communityrulestorybook/` for GitHub Pages
|
||||
|
||||
## 📋 Available Scripts
|
||||
|
||||
### Development
|
||||
|
||||
- `npm run dev` - Start Next.js development server
|
||||
- `npm run build` - Build Next.js application for production
|
||||
- `npm run start` - Start Next.js production server
|
||||
|
||||
### Testing
|
||||
|
||||
- `npm test` - Run all component tests with coverage
|
||||
- `npm run test:component` - Run tests in `tests/components/` only
|
||||
- `npm run test:watch` - Run tests in watch mode
|
||||
- `npm run test:ui` - Run tests with UI
|
||||
- `npm run test:e2e` - Run E2E tests only
|
||||
- `npm run e2e` - Alias for Playwright E2E tests
|
||||
- `npm run e2e:ui` - Run E2E tests with UI
|
||||
- `npm run e2e:serve` - Start dev server and run E2E tests
|
||||
- `npm run lhci` - Run performance tests
|
||||
|
||||
### Storybook
|
||||
|
||||
- `npm run storybook:local` - Start Storybook for local development
|
||||
- `npm run storybook:github` - Start Storybook with GitHub Pages configuration
|
||||
- `npm run storybook:build` - Build Storybook for local deployment
|
||||
- `npm run storybook:build:github` - Build Storybook for GitHub Pages
|
||||
- `npm run storybook` - Start Storybook with current configuration
|
||||
|
||||
## 🏗️ Project Structure
|
||||
|
||||
```
|
||||
community-rule/
|
||||
├── app/ # Next.js app directory
|
||||
│ ├── components/ # React components
|
||||
│ ├── hooks/ # Custom React hooks
|
||||
│ ├── layout.tsx # Root layout
|
||||
│ └── page.tsx # Homepage
|
||||
├── config/ # Project-specific configuration
|
||||
│ ├── gitea-runner.yaml # Gitea runner configuration
|
||||
│ └── runner-config.yaml # Runner configuration
|
||||
├── docs/ # Documentation
|
||||
│ ├── README.md # Documentation index
|
||||
│ ├── TESTING_GUIDE.md # Testing guide
|
||||
│ ├── CUSTOM_HOOKS.md # Custom hooks documentation
|
||||
│ └── guides/ # Guides
|
||||
│ └── content-creation.md # Content creation guide
|
||||
├── scripts/ # Utility scripts
|
||||
│ ├── start-runner.sh # Start Gitea runner
|
||||
│ ├── status-runner.sh # Check runner status
|
||||
│ └── stop-runner.sh # Stop Gitea runner
|
||||
├── tests/ # Test files
|
||||
│ ├── components/ # Component tests (Vitest + RTL)
|
||||
│ ├── pages/ # Page-level tests
|
||||
│ ├── e2e/ # E2E tests (Playwright)
|
||||
│ ├── utils/ # Test utilities (componentTestSuite, etc.)
|
||||
│ ├── msw/ # MSW server setup
|
||||
│ └── accessibility/ # E2E accessibility checks
|
||||
├── .storybook/ # Storybook configuration
|
||||
├── .gitea/ # Gitea Actions workflows
|
||||
│ └── workflows/
|
||||
│ └── ci.yaml # CI/CD pipeline
|
||||
└── public/ # Static assets
|
||||
```
|
||||
|
||||
## 🔧 Technology Stack
|
||||
|
||||
- **Framework**: Next.js 16 + React 19
|
||||
- **Runtime**: Node.js 20+ (LTS)
|
||||
- **Styling**: Tailwind CSS 4
|
||||
- **Testing**: Vitest + Playwright + Lighthouse CI
|
||||
- **Documentation**: Storybook 10
|
||||
- **CI/CD**: Gitea Actions
|
||||
- **Hosting**: Gitea (Git hosting)
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
- **[Documentation Index](docs/README.md)** - Complete documentation guide
|
||||
- **[Testing Guide](docs/TESTING_GUIDE.md)** - Testing strategy, component tests, E2E tests, and accessibility
|
||||
- **[Custom Hooks](docs/CUSTOM_HOOKS.md)** - Documentation for custom React hooks
|
||||
- **[Content Creation Guide](docs/guides/content-creation.md)** - Guide for creating blog content
|
||||
- **[Storybook](http://localhost:6006)** - Component documentation (local)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
1. **Fork the repository**
|
||||
2. **Create a feature branch**: `git checkout -b feature/amazing-feature`
|
||||
3. **Write tests first** (see [Testing Guide](docs/TESTING_GUIDE.md))
|
||||
4. **Make your changes**
|
||||
5. **Run tests**: `npm test && npm run e2e`
|
||||
6. **Commit changes**: `git commit -m "feat: add amazing feature"`
|
||||
7. **Push to branch**: `git push origin feature/amazing-feature`
|
||||
8. **Create Pull Request**
|
||||
|
||||
### Development Workflow
|
||||
|
||||
- All changes must have tests
|
||||
- CI pipeline runs automatically on PRs
|
||||
- Visual regression tests ensure UI consistency
|
||||
- Performance budgets must be met
|
||||
- Accessibility standards must be maintained
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
[MIT](LICENSE).
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
// Operator/admin dashboards (e.g. `/monitor`) intentionally render without the
|
||||
// public marketing footer. Auth/access is enforced upstream.
|
||||
export default function AdminLayout({ children }: { children: ReactNode }) {
|
||||
return <main className="flex-1">{children}</main>;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import WebVitalsDashboard from "../../components/WebVitalsDashboard";
|
||||
import WebVitalsDashboard from "../../components/sections/WebVitalsDashboard";
|
||||
import TopNav from "../../components/navigation/TopNav";
|
||||
import Footer from "../../components/navigation/Footer";
|
||||
|
||||
|
||||
+13
-13
@@ -11,35 +11,35 @@ import { usePathname, useRouter } from "next/navigation";
|
||||
import { CreateFlowProvider, useCreateFlow } from "./context/CreateFlowContext";
|
||||
import { useCreateFlowNavigation } from "./hooks/useCreateFlowNavigation";
|
||||
import { useCreateFlowExit } from "./hooks/useCreateFlowExit";
|
||||
import CreateFlowTopNav from "../components/utility/CreateFlowTopNav";
|
||||
import CreateFlowTopNav from "../../components/utility/CreateFlowTopNav";
|
||||
import { getNextStep, getStepIndex } from "./utils/flowSteps";
|
||||
import { getProportionBarProgressForCreateFlowStep } from "./utils/createFlowProportionProgress";
|
||||
import {
|
||||
createFlowStepUsesCenteredTextLayout,
|
||||
createFlowStepUsesCardLayout,
|
||||
} from "./utils/createFlowScreenRegistry";
|
||||
import CreateFlowFooter from "../components/utility/CreateFlowFooter";
|
||||
import Button from "../components/buttons/Button";
|
||||
import { buildPublishPayload } from "../../lib/create/buildPublishPayload";
|
||||
import { isValidCreateFlowSaveEmail } from "../../lib/create/isValidCreateFlowSaveEmail";
|
||||
import CreateFlowFooter from "../../components/utility/CreateFlowFooter";
|
||||
import Button from "../../components/buttons/Button";
|
||||
import { buildPublishPayload } from "../../../lib/create/buildPublishPayload";
|
||||
import { isValidCreateFlowSaveEmail } from "../../../lib/create/isValidCreateFlowSaveEmail";
|
||||
import {
|
||||
fetchAuthSession,
|
||||
publishRule,
|
||||
requestMagicLink,
|
||||
} from "../../lib/create/api";
|
||||
import { safeInternalPath } from "../../lib/safeInternalPath";
|
||||
} from "../../../lib/create/api";
|
||||
import { safeInternalPath } from "../../../lib/safeInternalPath";
|
||||
import { setTransferPendingFlag } from "./utils/anonymousDraftStorage";
|
||||
import { writeLastPublishedRule } from "../../lib/create/lastPublishedRule";
|
||||
import { writeLastPublishedRule } from "../../../lib/create/lastPublishedRule";
|
||||
import {
|
||||
fetchTemplateBySlug,
|
||||
type RuleTemplateDto,
|
||||
} from "../../lib/create/fetchTemplates";
|
||||
import messages from "../../messages/en/index";
|
||||
import { useAuthModal } from "../contexts/AuthModalContext";
|
||||
import { useMessages, useTranslation } from "../contexts/MessagesContext";
|
||||
} from "../../../lib/create/fetchTemplates";
|
||||
import messages from "../../../messages/en/index";
|
||||
import { useAuthModal } from "../../contexts/AuthModalContext";
|
||||
import { useMessages, useTranslation } from "../../contexts/MessagesContext";
|
||||
import { PostLoginDraftTransfer } from "./PostLoginDraftTransfer";
|
||||
import { SignedInDraftHydration } from "./SignedInDraftHydration";
|
||||
import Alert from "../components/modals/Alert";
|
||||
import Alert from "../../components/modals/Alert";
|
||||
import {
|
||||
CreateFlowDraftSaveBannerProvider,
|
||||
useCreateFlowDraftSaveBanner,
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
} from "./utils/anonymousDraftStorage";
|
||||
import { useCreateFlow } from "./context/CreateFlowContext";
|
||||
import { parseCreateFlowScreenFromPathname } from "./utils/flowSteps";
|
||||
import { saveDraftToServer } from "../../lib/create/api";
|
||||
import messages from "../../messages/en/index";
|
||||
import { saveDraftToServer } from "../../../lib/create/api";
|
||||
import messages from "../../../messages/en/index";
|
||||
|
||||
const SYNC_ENABLED = process.env.NEXT_PUBLIC_ENABLE_BACKEND_SYNC === "true";
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import type { CreateFlowState } from "./types";
|
||||
import { createFlowStateHasKeys } from "../../lib/create/draftHydrationUtils";
|
||||
import { createFlowStateHasKeys } from "../../../lib/create/draftHydrationUtils";
|
||||
import {
|
||||
clearAnonymousCreateFlowStorage,
|
||||
hasTransferPendingFlag,
|
||||
readAnonymousCreateFlowState,
|
||||
} from "./utils/anonymousDraftStorage";
|
||||
import { useCreateFlow } from "./context/CreateFlowContext";
|
||||
import { fetchDraftFromServer } from "../../lib/create/api";
|
||||
import messages from "../../messages/en/index";
|
||||
import { fetchDraftFromServer } from "../../../lib/create/api";
|
||||
import messages from "../../../messages/en/index";
|
||||
|
||||
const SYNC_ENABLED = process.env.NEXT_PUBLIC_ENABLE_BACKEND_SYNC === "true";
|
||||
|
||||
+5
-5
@@ -8,8 +8,8 @@
|
||||
*/
|
||||
|
||||
import { memo, useState } from "react";
|
||||
import Chip from "../../components/controls/Chip";
|
||||
import InputLabel from "../../components/utility/InputLabel";
|
||||
import Chip from "../../../components/controls/Chip";
|
||||
import InputLabel from "../../../components/utility/InputLabel";
|
||||
|
||||
export interface ApplicableScopeFieldProps {
|
||||
/** Label rendered above the capsule row. */
|
||||
@@ -74,9 +74,9 @@ function ApplicableScopeFieldComponent({
|
||||
<Chip
|
||||
key={scope}
|
||||
label={scope}
|
||||
state={isSelected ? "Selected" : "Disabled"}
|
||||
palette="Default"
|
||||
size="S"
|
||||
state={isSelected ? "selected" : "disabled"}
|
||||
palette="default"
|
||||
size="s"
|
||||
disabled={false}
|
||||
onClick={() => onToggleScope(scope)}
|
||||
ariaLabel={`${isSelected ? "Deselect" : "Select"} ${scope}`}
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import HeaderLockup from "../../components/type/HeaderLockup";
|
||||
import type { HeaderLockupProps } from "../../components/type/HeaderLockup/HeaderLockup.types";
|
||||
import HeaderLockup from "../../../components/type/HeaderLockup";
|
||||
import type { HeaderLockupProps } from "../../../components/type/HeaderLockup/HeaderLockup.types";
|
||||
import { useCreateFlowMdUp } from "../hooks/useCreateFlowMdUp";
|
||||
|
||||
export type CreateFlowHeaderLockupProps = Omit<HeaderLockupProps, "size"> & {
|
||||
+2
-2
@@ -7,8 +7,8 @@
|
||||
*/
|
||||
|
||||
import { memo, useId } from "react";
|
||||
import TextArea from "../../components/controls/TextArea";
|
||||
import InputLabel from "../../components/utility/InputLabel";
|
||||
import TextArea from "../../../components/controls/TextArea";
|
||||
import InputLabel from "../../../components/utility/InputLabel";
|
||||
|
||||
export interface ModalTextAreaFieldProps {
|
||||
/** Label rendered above the text area. */
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import { useCallback } from "react";
|
||||
import type { CreateFlowState, CreateFlowStep } from "../types";
|
||||
import { saveDraftToServer } from "../../../lib/create/api";
|
||||
import messages from "../../../messages/en/index";
|
||||
import { saveDraftToServer } from "../../../../lib/create/api";
|
||||
import messages from "../../../../messages/en/index";
|
||||
|
||||
const SYNC_ENABLED = process.env.NEXT_PUBLIC_ENABLE_BACKEND_SYNC === "true";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMediaQuery } from "../../hooks/useMediaQuery";
|
||||
import { useMediaQuery } from "../../../hooks/useMediaQuery";
|
||||
|
||||
/** `--breakpoint-lg` (1024px); same SSR/first-paint pattern as `useCreateFlowMdUp`. */
|
||||
const CREATE_FLOW_MIN_WIDTH_LG = "(min-width: 1024px)";
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMediaQuery } from "../../hooks/useMediaQuery";
|
||||
import { useMediaQuery } from "../../../hooks/useMediaQuery";
|
||||
|
||||
/** `--breakpoint-md` (640px); same SSR/first-paint pattern as `useCreateFlowLgUp`. */
|
||||
const CREATE_FLOW_MIN_WIDTH_MD = "(min-width: 640px)";
|
||||
+5
-5
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { use, useEffect, useState } from "react";
|
||||
import { TemplateReviewCard } from "../../../components/cards/TemplateReviewCard";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import { TemplateReviewCard } from "../../../../components/cards/TemplateReviewCard";
|
||||
import { useTranslation } from "../../../../contexts/MessagesContext";
|
||||
import {
|
||||
fetchTemplateBySlug,
|
||||
isTemplatesFetchAborted,
|
||||
type RuleTemplateDto,
|
||||
} from "../../../../lib/create/fetchTemplates";
|
||||
import messages from "../../../../messages/en/index";
|
||||
import Alert from "../../../components/modals/Alert";
|
||||
} from "../../../../../lib/create/fetchTemplates";
|
||||
import messages from "../../../../../messages/en/index";
|
||||
import Alert from "../../../../components/modals/Alert";
|
||||
import {
|
||||
CREATE_FLOW_REVIEW_RULE_CARD_LAYOUT_CLASS,
|
||||
CreateFlowLockupCardStepShell,
|
||||
+4
-4
@@ -10,13 +10,13 @@
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import CardStack from "../../../components/utility/CardStack";
|
||||
import Create from "../../../components/modals/Create";
|
||||
import InlineTextButton from "../../../components/buttons/InlineTextButton";
|
||||
import CardStack from "../../../../components/utility/CardStack";
|
||||
import Create from "../../../../components/modals/Create";
|
||||
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
import {
|
||||
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
|
||||
+4
-4
@@ -12,13 +12,13 @@
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import CardStack from "../../../components/utility/CardStack";
|
||||
import Create from "../../../components/modals/Create";
|
||||
import InlineTextButton from "../../../components/buttons/InlineTextButton";
|
||||
import CardStack from "../../../../components/utility/CardStack";
|
||||
import Create from "../../../../components/modals/Create";
|
||||
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
import {
|
||||
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
|
||||
+4
-4
@@ -12,13 +12,13 @@
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import CardStack from "../../../components/utility/CardStack";
|
||||
import Create from "../../../components/modals/Create";
|
||||
import InlineTextButton from "../../../components/buttons/InlineTextButton";
|
||||
import CardStack from "../../../../components/utility/CardStack";
|
||||
import Create from "../../../../components/modals/Create";
|
||||
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
import {
|
||||
CREATE_FLOW_CARD_STACK_AREA_MAX_CLASS,
|
||||
+6
-6
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import CommunityRuleDocument from "../../../components/sections/CommunityRuleDocument";
|
||||
import type { CommunityRuleDocumentSection } from "../../../components/sections/CommunityRuleDocument/CommunityRuleDocument.types";
|
||||
import Alert from "../../../components/modals/Alert";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import { parseDocumentSectionsForDisplay } from "../../../../lib/create/buildPublishPayload";
|
||||
import { readLastPublishedRule } from "../../../../lib/create/lastPublishedRule";
|
||||
import CommunityRuleDocument from "../../../../components/sections/CommunityRuleDocument";
|
||||
import type { CommunityRuleDocumentSection } from "../../../../components/sections/CommunityRuleDocument/CommunityRuleDocument.types";
|
||||
import Alert from "../../../../components/modals/Alert";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { parseDocumentSectionsForDisplay } from "../../../../../lib/create/buildPublishPayload";
|
||||
import { readLastPublishedRule } from "../../../../../lib/create/lastPublishedRule";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import {
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import NumberedList from "../../../components/type/NumberedList";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import NumberedList from "../../../../components/type/NumberedList";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import RuleCard from "../../../components/cards/RuleCard";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import RuleCard from "../../../../components/cards/RuleCard";
|
||||
import { useTranslation } from "../../../../contexts/MessagesContext";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowLgUp } from "../../hooks/useCreateFlowLgUp";
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import RuleCard from "../../../components/cards/RuleCard";
|
||||
import type { Category } from "../../../components/cards/RuleCard/RuleCard.types";
|
||||
import { useMessages, useTranslation } from "../../../contexts/MessagesContext";
|
||||
import RuleCard from "../../../../components/cards/RuleCard";
|
||||
import type { Category } from "../../../../components/cards/RuleCard/RuleCard.types";
|
||||
import { useMessages, useTranslation } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import {
|
||||
+8
-8
@@ -14,14 +14,14 @@
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import DecisionMakingSidebar from "../../../components/utility/DecisionMakingSidebar";
|
||||
import CardStack from "../../../components/utility/CardStack";
|
||||
import Create from "../../../components/modals/Create";
|
||||
import IncrementerBlock from "../../../components/controls/IncrementerBlock";
|
||||
import InlineTextButton from "../../../components/buttons/InlineTextButton";
|
||||
import type { InfoMessageBoxItem } from "../../../components/utility/InfoMessageBox/InfoMessageBox.types";
|
||||
import type { CardStackItem } from "../../../components/utility/CardStack/CardStack.types";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import DecisionMakingSidebar from "../../../../components/utility/DecisionMakingSidebar";
|
||||
import CardStack from "../../../../components/utility/CardStack";
|
||||
import Create from "../../../../components/modals/Create";
|
||||
import IncrementerBlock from "../../../../components/controls/IncrementerBlock";
|
||||
import InlineTextButton from "../../../../components/buttons/InlineTextButton";
|
||||
import type { InfoMessageBoxItem } from "../../../../components/utility/InfoMessageBox/InfoMessageBox.types";
|
||||
import type { CardStackItem } from "../../../../components/utility/CardStack/CardStack.types";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||
+13
-13
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import MultiSelect from "../../../components/controls/MultiSelect";
|
||||
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import MultiSelect from "../../../../components/controls/MultiSelect";
|
||||
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoColumnSelectShell";
|
||||
@@ -14,13 +14,13 @@ function chipRowsFromLabels(
|
||||
return rows.map((row, i) => ({
|
||||
id: String(i + 1),
|
||||
label: row.label,
|
||||
state: "Unselected" as const,
|
||||
state: "unselected" as const,
|
||||
}));
|
||||
}
|
||||
|
||||
function selectedIdsFromOptions(options: ChipOption[]): string[] {
|
||||
return options
|
||||
.filter((o) => o.state === "Selected")
|
||||
.filter((o) => o.state === "selected")
|
||||
.map((o) => o.id);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function CommunitySizeSelectScreen() {
|
||||
const selected = new Set(state.selectedCommunitySizeIds ?? []);
|
||||
return base.map((opt) => ({
|
||||
...opt,
|
||||
state: selected.has(opt.id) ? ("Selected" as const) : ("Unselected" as const),
|
||||
state: selected.has(opt.id) ? ("selected" as const) : ("unselected" as const),
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -45,13 +45,13 @@ export function CommunitySizeSelectScreen() {
|
||||
const selected = new Set(state.selectedCommunitySizeIds ?? []);
|
||||
setCommunitySizeOptions((prev) =>
|
||||
prev.map((opt) =>
|
||||
opt.state === "Custom"
|
||||
opt.state === "custom"
|
||||
? opt
|
||||
: {
|
||||
...opt,
|
||||
state: selected.has(opt.id)
|
||||
? ("Selected" as const)
|
||||
: ("Unselected" as const),
|
||||
? ("selected" as const)
|
||||
: ("unselected" as const),
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -71,9 +71,9 @@ export function CommunitySizeSelectScreen() {
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "Selected"
|
||||
? ("Unselected" as const)
|
||||
: ("Selected" as const),
|
||||
opt.state === "selected"
|
||||
? ("unselected" as const)
|
||||
: ("selected" as const),
|
||||
}
|
||||
: opt,
|
||||
);
|
||||
@@ -83,7 +83,7 @@ export function CommunitySizeSelectScreen() {
|
||||
const multiSelectBlock = (
|
||||
<MultiSelect
|
||||
formHeader={false}
|
||||
size="M"
|
||||
size="m"
|
||||
options={communitySizeOptions}
|
||||
onChipClick={handleCommunitySizeClick}
|
||||
addButton={false}
|
||||
+25
-25
@@ -7,9 +7,9 @@ import {
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
} from "react";
|
||||
import MultiSelect from "../../../components/controls/MultiSelect";
|
||||
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import MultiSelect from "../../../../components/controls/MultiSelect";
|
||||
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import type { CommunityStructureChipSnapshotRow } from "../../types";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
@@ -17,7 +17,7 @@ import { CreateFlowTwoColumnSelectShell } from "../../components/CreateFlowTwoCo
|
||||
|
||||
function createListCustomHandlers(
|
||||
setList: Dispatch<SetStateAction<ChipOption[]>>,
|
||||
confirmState: "Unselected" | "Selected",
|
||||
confirmState: "unselected" | "selected",
|
||||
onInteraction?: () => void,
|
||||
) {
|
||||
const touch = () => onInteraction?.();
|
||||
@@ -26,7 +26,7 @@ function createListCustomHandlers(
|
||||
touch();
|
||||
setList((prev) => [
|
||||
...prev,
|
||||
{ id: crypto.randomUUID(), label: "", state: "Custom" },
|
||||
{ id: crypto.randomUUID(), label: "", state: "custom" },
|
||||
]);
|
||||
},
|
||||
onCustomChipConfirm: (chipId: string, value: string) => {
|
||||
@@ -52,7 +52,7 @@ function chipRowsFromLabels(
|
||||
return rows.map((row, i) => ({
|
||||
id: String(i + 1),
|
||||
label: row.label,
|
||||
state: "Unselected" as const,
|
||||
state: "unselected" as const,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -62,20 +62,20 @@ function applySavedSelection(
|
||||
): ChipOption[] {
|
||||
const selected = new Set(saved ?? []);
|
||||
return options.map((opt) =>
|
||||
opt.state === "Custom"
|
||||
opt.state === "custom"
|
||||
? opt
|
||||
: {
|
||||
...opt,
|
||||
state: selected.has(opt.id)
|
||||
? ("Selected" as const)
|
||||
: ("Unselected" as const),
|
||||
? ("selected" as const)
|
||||
: ("unselected" as const),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function selectedIdsFromOptions(options: ChipOption[]): string[] {
|
||||
return options
|
||||
.filter((o) => o.state === "Selected")
|
||||
.filter((o) => o.state === "selected")
|
||||
.map((o) => o.id);
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ export function CommunityStructureSelectScreen() {
|
||||
() =>
|
||||
createListCustomHandlers(
|
||||
setOrganizationTypeOptions,
|
||||
"Unselected",
|
||||
"unselected",
|
||||
markCreateFlowInteraction,
|
||||
),
|
||||
[markCreateFlowInteraction],
|
||||
@@ -203,7 +203,7 @@ export function CommunityStructureSelectScreen() {
|
||||
() =>
|
||||
createListCustomHandlers(
|
||||
setScaleOptions,
|
||||
"Unselected",
|
||||
"unselected",
|
||||
markCreateFlowInteraction,
|
||||
),
|
||||
[markCreateFlowInteraction],
|
||||
@@ -212,7 +212,7 @@ export function CommunityStructureSelectScreen() {
|
||||
() =>
|
||||
createListCustomHandlers(
|
||||
setMaturityOptions,
|
||||
"Unselected",
|
||||
"unselected",
|
||||
markCreateFlowInteraction,
|
||||
),
|
||||
[markCreateFlowInteraction],
|
||||
@@ -258,9 +258,9 @@ export function CommunityStructureSelectScreen() {
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "Selected"
|
||||
? ("Unselected" as const)
|
||||
: ("Selected" as const),
|
||||
opt.state === "selected"
|
||||
? ("unselected" as const)
|
||||
: ("selected" as const),
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
@@ -274,9 +274,9 @@ export function CommunityStructureSelectScreen() {
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "Selected"
|
||||
? ("Unselected" as const)
|
||||
: ("Selected" as const),
|
||||
opt.state === "selected"
|
||||
? ("unselected" as const)
|
||||
: ("selected" as const),
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
@@ -290,9 +290,9 @@ export function CommunityStructureSelectScreen() {
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "Selected"
|
||||
? ("Unselected" as const)
|
||||
: ("Selected" as const),
|
||||
opt.state === "selected"
|
||||
? ("unselected" as const)
|
||||
: ("selected" as const),
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
@@ -304,7 +304,7 @@ export function CommunityStructureSelectScreen() {
|
||||
<MultiSelect
|
||||
label={cs.organizationMultiSelect.label}
|
||||
showHelpIcon
|
||||
size="S"
|
||||
size="s"
|
||||
options={organizationTypeOptions}
|
||||
onChipClick={handleOrganizationTypeClick}
|
||||
{...organizationCustomHandlers}
|
||||
@@ -314,7 +314,7 @@ export function CommunityStructureSelectScreen() {
|
||||
<MultiSelect
|
||||
label={cs.scaleMultiSelect.label}
|
||||
showHelpIcon
|
||||
size="S"
|
||||
size="s"
|
||||
options={scaleOptions}
|
||||
onChipClick={handleScaleClick}
|
||||
{...scaleCustomHandlers}
|
||||
@@ -324,7 +324,7 @@ export function CommunityStructureSelectScreen() {
|
||||
<MultiSelect
|
||||
label={cs.maturityMultiSelect.label}
|
||||
showHelpIcon
|
||||
size="S"
|
||||
size="s"
|
||||
options={maturityOptions}
|
||||
onChipClick={handleMaturityClick}
|
||||
{...maturityCustomHandlers}
|
||||
+7
-7
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import MultiSelect from "../../../components/controls/MultiSelect";
|
||||
import Alert from "../../../components/modals/Alert";
|
||||
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import MultiSelect from "../../../../components/controls/MultiSelect";
|
||||
import Alert from "../../../../components/modals/Alert";
|
||||
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import { useTranslation } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
@@ -22,7 +22,7 @@ export function ConfirmStakeholdersScreen() {
|
||||
markCreateFlowInteraction();
|
||||
setStakeholderOptions((prev) => [
|
||||
...prev,
|
||||
{ id: crypto.randomUUID(), label: "", state: "Custom" },
|
||||
{ id: crypto.randomUUID(), label: "", state: "custom" },
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ export function ConfirmStakeholdersScreen() {
|
||||
markCreateFlowInteraction();
|
||||
setStakeholderOptions((prev) =>
|
||||
prev.map((opt) =>
|
||||
opt.id === chipId ? { ...opt, label: value, state: "Selected" } : opt,
|
||||
opt.id === chipId ? { ...opt, label: value, state: "selected" } : opt,
|
||||
),
|
||||
);
|
||||
};
|
||||
@@ -64,7 +64,7 @@ export function ConfirmStakeholdersScreen() {
|
||||
<MultiSelect
|
||||
formHeader={false}
|
||||
showHelpIcon={false}
|
||||
size="S"
|
||||
size="s"
|
||||
options={stakeholderOptions}
|
||||
onChipClick={handleChipClick}
|
||||
onAddClick={handleAddStakeholder}
|
||||
+22
-22
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import MultiSelect from "../../../components/controls/MultiSelect";
|
||||
import type { ChipOption } from "../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import TextArea from "../../../components/controls/TextArea";
|
||||
import Create from "../../../components/modals/Create";
|
||||
import ContentLockup from "../../../components/type/ContentLockup";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import MultiSelect from "../../../../components/controls/MultiSelect";
|
||||
import type { ChipOption } from "../../../../components/controls/MultiSelect/MultiSelect.types";
|
||||
import TextArea from "../../../../components/controls/TextArea";
|
||||
import Create from "../../../../components/modals/Create";
|
||||
import ContentLockup from "../../../../components/type/ContentLockup";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import type { CommunityStructureChipSnapshotRow } from "../../types";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
@@ -46,7 +46,7 @@ function chipRowsFromPresets(presets: readonly CoreValuePreset[]): ChipOption[]
|
||||
return presets.map((row, i) => ({
|
||||
id: String(i + 1),
|
||||
label: row.label,
|
||||
state: "Unselected" as const,
|
||||
state: "unselected" as const,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -56,20 +56,20 @@ function applySavedSelection(
|
||||
): ChipOption[] {
|
||||
const selected = new Set(saved ?? []);
|
||||
return options.map((opt) =>
|
||||
opt.state === "Custom"
|
||||
opt.state === "custom"
|
||||
? opt
|
||||
: {
|
||||
...opt,
|
||||
state: selected.has(opt.id)
|
||||
? ("Selected" as const)
|
||||
: ("Unselected" as const),
|
||||
? ("selected" as const)
|
||||
: ("unselected" as const),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function selectedIdsFromOptions(options: ChipOption[]): string[] {
|
||||
return options
|
||||
.filter((o) => o.state === "Selected")
|
||||
.filter((o) => o.state === "selected")
|
||||
.map((o) => o.id);
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ export function CoreValuesSelectScreen() {
|
||||
if (activeModalChipId && modalSession === "pending") {
|
||||
const next = coreValueOptions.map((opt) =>
|
||||
opt.id === activeModalChipId
|
||||
? { ...opt, state: "Unselected" as const }
|
||||
? { ...opt, state: "unselected" as const }
|
||||
: opt,
|
||||
);
|
||||
persistCoreValues(next);
|
||||
@@ -226,16 +226,16 @@ export function CoreValuesSelectScreen() {
|
||||
|
||||
const handleChipClick = (chipId: string) => {
|
||||
const target = coreValueOptions.find((o) => o.id === chipId);
|
||||
if (!target || target.state === "Custom") return;
|
||||
if (!target || target.state === "custom") return;
|
||||
|
||||
const selectedCount = coreValueOptions.filter(
|
||||
(o) => o.state === "Selected",
|
||||
(o) => o.state === "selected",
|
||||
).length;
|
||||
|
||||
if (target.state === "Selected") {
|
||||
if (target.state === "selected") {
|
||||
const next: ChipOption[] = coreValueOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, state: "Unselected" as const }
|
||||
? { ...opt, state: "unselected" as const }
|
||||
: opt,
|
||||
);
|
||||
persistCoreValues(next);
|
||||
@@ -246,7 +246,7 @@ export function CoreValuesSelectScreen() {
|
||||
|
||||
const next: ChipOption[] = coreValueOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, state: "Selected" as const }
|
||||
? { ...opt, state: "selected" as const }
|
||||
: opt,
|
||||
);
|
||||
persistCoreValues(next);
|
||||
@@ -259,7 +259,7 @@ export function CoreValuesSelectScreen() {
|
||||
setCoreValueOptions((prev) => {
|
||||
const next: ChipOption[] = [
|
||||
...prev,
|
||||
{ id: crypto.randomUUID(), label: "", state: "Custom" },
|
||||
{ id: crypto.randomUUID(), label: "", state: "custom" },
|
||||
];
|
||||
queueMicrotask(() => syncCoreValuesToDraft(next));
|
||||
return next;
|
||||
@@ -270,17 +270,17 @@ export function CoreValuesSelectScreen() {
|
||||
setCoreValueOptions((prev) => {
|
||||
const withLabel = prev.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, label: value, state: "Unselected" as const }
|
||||
? { ...opt, label: value, state: "unselected" as const }
|
||||
: opt,
|
||||
);
|
||||
const selectedCount = withLabel.filter(
|
||||
(o) => o.state === "Selected",
|
||||
(o) => o.state === "selected",
|
||||
).length;
|
||||
const canSelect = selectedCount < MAX_CORE_VALUES;
|
||||
const next = canSelect
|
||||
? withLabel.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, state: "Selected" as const }
|
||||
? { ...opt, state: "selected" as const }
|
||||
: opt,
|
||||
)
|
||||
: withLabel;
|
||||
@@ -343,7 +343,7 @@ export function CoreValuesSelectScreen() {
|
||||
>
|
||||
<MultiSelect
|
||||
formHeader={false}
|
||||
size="M"
|
||||
size="m"
|
||||
options={coreValueOptions}
|
||||
onChipClick={handleChipClick}
|
||||
onAddClick={addHandlers.onAddClick}
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, type HTMLInputTypeAttribute } from "react";
|
||||
import TextInput from "../../../components/controls/TextInput";
|
||||
import type { HeaderLockupJustificationValue } from "../../../components/type/HeaderLockup/HeaderLockup.types";
|
||||
import { useTranslation } from "../../../contexts/MessagesContext";
|
||||
import TextInput from "../../../../components/controls/TextInput";
|
||||
import type { HeaderLockupJustificationValue } from "../../../../components/type/HeaderLockup/HeaderLockup.types";
|
||||
import { useTranslation } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { useCreateFlowMdUp } from "../../hooks/useCreateFlowMdUp";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Upload from "../../../components/controls/Upload";
|
||||
import { useMessages } from "../../../contexts/MessagesContext";
|
||||
import Upload from "../../../../components/controls/Upload";
|
||||
import { useMessages } from "../../../../contexts/MessagesContext";
|
||||
import { useCreateFlow } from "../../context/CreateFlowContext";
|
||||
import { CreateFlowHeaderLockup } from "../../components/CreateFlowHeaderLockup";
|
||||
import { CreateFlowStepShell } from "../../components/CreateFlowStepShell";
|
||||
+1
-10
@@ -1,5 +1,5 @@
|
||||
import type { CreateFlowState } from "../types";
|
||||
import { migrateLegacyCreateFlowState } from "../../../lib/create/migrateLegacyCreateFlowState";
|
||||
import { migrateLegacyCreateFlowState } from "../../../../lib/create/migrateLegacyCreateFlowState";
|
||||
|
||||
/** Anonymous in-progress create flow (local only until magic-link transfer). */
|
||||
export const CREATE_FLOW_ANONYMOUS_KEY = "create-flow-anonymous" as const;
|
||||
@@ -75,15 +75,6 @@ export function hasTransferPendingFlag(): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
export function clearTransferPendingFlag(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
window.localStorage.removeItem(CREATE_FLOW_TRANSFER_PENDING_KEY);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
/** One-time cleanup of pre–anonymous-draft keys. */
|
||||
export function clearLegacyCreateFlowKeysOnce(): void {
|
||||
if (typeof window === "undefined") return;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { ProportionBarState } from "../../components/progress/ProportionBar/ProportionBar.types";
|
||||
import type { ProportionBarState } from "../../../components/progress/ProportionBar/ProportionBar.types";
|
||||
import type { CreateFlowStep } from "../types";
|
||||
import { FLOW_STEP_ORDER, getStepIndex } from "./flowSteps";
|
||||
|
||||
+3
-3
@@ -2,10 +2,10 @@ import type { CreateFlowStep } from "../types";
|
||||
|
||||
/**
|
||||
* Figma layout families for the create flow (not encoded in the URL).
|
||||
* `app/create/screens/<kind>/` mirrors these names: e.g. `layoutKind: "select"` → `screens/select/`,
|
||||
* `app/(app)/create/screens/<kind>/` mirrors these names: e.g. `layoutKind: "select"` → `screens/select/`,
|
||||
* `"card"` → `screens/card/` (compact card-stack frames, distinct from two-column chip selects).
|
||||
*/
|
||||
export type CreateFlowLayoutKind =
|
||||
type CreateFlowLayoutKind =
|
||||
| "informational"
|
||||
| "text"
|
||||
| "select"
|
||||
@@ -15,7 +15,7 @@ export type CreateFlowLayoutKind =
|
||||
| "right-rail"
|
||||
| "completed";
|
||||
|
||||
export interface CreateFlowScreenDefinition {
|
||||
interface CreateFlowScreenDefinition {
|
||||
layoutKind: CreateFlowLayoutKind;
|
||||
/** Figma node id (file Community-Rule-System), dev mode. */
|
||||
figmaNodeId: string;
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
// Signed-in product surfaces (`/create/*`, `/login`, `/profile`) intentionally
|
||||
// run without the marketing footer. Per-route chrome (e.g. CreateFlow's own
|
||||
// header/footer lockup) is composed in nested layouts.
|
||||
export default function AppLayout({ children }: { children: ReactNode }) {
|
||||
return <main className="flex-1">{children}</main>;
|
||||
}
|
||||
@@ -3,18 +3,19 @@
|
||||
import { Suspense } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslation } from "../contexts/MessagesContext";
|
||||
import Login from "../components/modals/Login";
|
||||
import LoginForm from "../components/modals/Login/LoginForm";
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
import Login from "../../components/modals/Login";
|
||||
import LoginForm from "../../components/modals/Login/LoginForm";
|
||||
|
||||
const loginPageBgClass =
|
||||
"min-h-[100dvh] bg-[var(--color-surface-inverse-brand-primary)]";
|
||||
|
||||
function LoginLoadingFallback() {
|
||||
const t = useTranslation("pages.login");
|
||||
return (
|
||||
<div className={`${loginPageBgClass} flex items-center justify-center`}>
|
||||
<p className="font-inter text-[14px] text-[var(--color-content-default-primary)]">
|
||||
Loading…
|
||||
{t("loadingFallback")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "../contexts/MessagesContext";
|
||||
import Button from "../components/buttons/Button";
|
||||
import { fetchAuthSession, logout } from "../../lib/create/api";
|
||||
import { useTranslation } from "../../contexts/MessagesContext";
|
||||
import Button from "../../components/buttons/Button";
|
||||
import { fetchAuthSession, logout } from "../../../lib/create/api";
|
||||
|
||||
export default function ProfilePageClient() {
|
||||
const t = useTranslation("pages.profile");
|
||||
@@ -14,27 +14,26 @@ let ruleCardIdCounter = 0;
|
||||
interface ChipData {
|
||||
id: string;
|
||||
label: string;
|
||||
state: "Unselected" | "Selected" | "Custom";
|
||||
palette: "Default" | "Inverse";
|
||||
size: "S" | "M";
|
||||
state: "unselected" | "selected" | "custom";
|
||||
palette: "default" | "inverse";
|
||||
size: "s" | "m";
|
||||
}
|
||||
|
||||
// MultiSelect example component with state management
|
||||
function MultiSelectExample({ size }: { size: "S" | "M" }) {
|
||||
function MultiSelectExample({ size }: { size: "s" | "m" }) {
|
||||
const [options, setOptions] = useState<
|
||||
Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
state: "Unselected" | "Selected" | "Custom";
|
||||
state: "unselected" | "selected" | "custom";
|
||||
}>
|
||||
>([
|
||||
{ id: "1", label: "1 member", state: "Unselected" },
|
||||
{ id: "2", label: "2-10 members", state: "Unselected" },
|
||||
{ id: "3", label: "10-24 members", state: "Unselected" },
|
||||
{ id: "4", label: "24-64 members", state: "Unselected" },
|
||||
{ id: "5", label: "64-128 members", state: "Unselected" },
|
||||
{ id: "6", label: "125-1000 members", state: "Unselected" },
|
||||
{ id: "7", label: "1000+ members", state: "Unselected" },
|
||||
{ id: "1", label: "1 member", state: "unselected" },
|
||||
{ id: "2", label: "2-10 members", state: "unselected" },
|
||||
{ id: "3", label: "10-24 members", state: "unselected" },
|
||||
{ id: "4", label: "24-64 members", state: "unselected" },
|
||||
{ id: "5", label: "64-128 members", state: "unselected" },
|
||||
{ id: "6", label: "125-1000 members", state: "unselected" },
|
||||
{ id: "7", label: "1000+ members", state: "unselected" },
|
||||
]);
|
||||
|
||||
const handleChipClick = (chipId: string) => {
|
||||
@@ -43,7 +42,7 @@ function MultiSelectExample({ size }: { size: "S" | "M" }) {
|
||||
opt.id === chipId
|
||||
? {
|
||||
...opt,
|
||||
state: opt.state === "Selected" ? "Unselected" : "Selected",
|
||||
state: opt.state === "selected" ? "unselected" : "selected",
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
@@ -52,14 +51,14 @@ function MultiSelectExample({ size }: { size: "S" | "M" }) {
|
||||
|
||||
const handleAddClick = () => {
|
||||
const newId = `custom-${Date.now()}`;
|
||||
setOptions((prev) => [...prev, { id: newId, label: "", state: "Custom" }]);
|
||||
setOptions((prev) => [...prev, { id: newId, label: "", state: "custom" }]);
|
||||
};
|
||||
|
||||
const handleCustomConfirm = (chipId: string, value: string) => {
|
||||
setOptions((prev) =>
|
||||
prev.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, label: value, state: "Selected" as const }
|
||||
? { ...opt, label: value, state: "selected" as const }
|
||||
: opt,
|
||||
),
|
||||
);
|
||||
@@ -72,7 +71,7 @@ function MultiSelectExample({ size }: { size: "S" | "M" }) {
|
||||
return (
|
||||
<div className="space-y-[var(--spacing-scale-016)]">
|
||||
<h3 className="font-inter text-[20px] leading-[24px] font-semibold text-[var(--color-content-default-primary)]">
|
||||
{size === "S" ? "Small (S)" : "Medium (M)"}
|
||||
{size === "s" ? "Small (S)" : "Medium (M)"}
|
||||
</h3>
|
||||
<MultiSelect
|
||||
label="Label"
|
||||
@@ -91,12 +90,12 @@ function MultiSelectExample({ size }: { size: "S" | "M" }) {
|
||||
|
||||
export default function ComponentsPreview() {
|
||||
const [chipStates, setChipStates] = useState<
|
||||
Record<string, "Unselected" | "Selected">
|
||||
Record<string, "unselected" | "selected">
|
||||
>({
|
||||
"default-s": "Unselected",
|
||||
"default-m": "Unselected",
|
||||
"inverse-s": "Unselected",
|
||||
"inverse-m": "Unselected",
|
||||
"default-s": "unselected",
|
||||
"default-m": "unselected",
|
||||
"inverse-s": "unselected",
|
||||
"inverse-m": "unselected",
|
||||
});
|
||||
|
||||
// Manage custom chips separately
|
||||
@@ -104,16 +103,16 @@ export default function ComponentsPreview() {
|
||||
{
|
||||
id: "custom-1",
|
||||
label: "",
|
||||
state: "Custom",
|
||||
palette: "Default",
|
||||
size: "S",
|
||||
state: "custom",
|
||||
palette: "default",
|
||||
size: "s",
|
||||
},
|
||||
{
|
||||
id: "custom-2",
|
||||
label: "",
|
||||
state: "Custom",
|
||||
palette: "Default",
|
||||
size: "M",
|
||||
state: "custom",
|
||||
palette: "default",
|
||||
size: "m",
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -124,7 +123,7 @@ export default function ComponentsPreview() {
|
||||
chipOptions: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
state: "Unselected" | "Selected" | "Custom";
|
||||
state: "unselected" | "selected" | "custom";
|
||||
}>;
|
||||
onChipClick?: (_categoryName: string, _chipId: string) => void;
|
||||
onAddClick?: (_categoryName: string) => void;
|
||||
@@ -139,11 +138,11 @@ export default function ComponentsPreview() {
|
||||
{
|
||||
name: "Values",
|
||||
chipOptions: [
|
||||
{ id: "values-1", label: "Consciousness", state: "Unselected" },
|
||||
{ id: "values-2", label: "Ecology", state: "Unselected" },
|
||||
{ id: "values-3", label: "Abundance", state: "Unselected" },
|
||||
{ id: "values-4", label: "Art", state: "Unselected" },
|
||||
{ id: "values-5", label: "Decisiveness", state: "Unselected" },
|
||||
{ id: "values-1", label: "Consciousness", state: "unselected" },
|
||||
{ id: "values-2", label: "Ecology", state: "unselected" },
|
||||
{ id: "values-3", label: "Abundance", state: "unselected" },
|
||||
{ id: "values-4", label: "Art", state: "unselected" },
|
||||
{ id: "values-5", label: "Decisiveness", state: "unselected" },
|
||||
],
|
||||
onChipClick: (categoryName: string, chipId: string) => {
|
||||
setRuleCardCategories((prev) =>
|
||||
@@ -156,9 +155,9 @@ export default function ComponentsPreview() {
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "Selected"
|
||||
? "Unselected"
|
||||
: "Selected",
|
||||
opt.state === "selected"
|
||||
? "unselected"
|
||||
: "selected",
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
@@ -176,7 +175,7 @@ export default function ComponentsPreview() {
|
||||
...cat,
|
||||
chipOptions: [
|
||||
...cat.chipOptions,
|
||||
{ id: newId, label: "", state: "Custom" },
|
||||
{ id: newId, label: "", state: "custom" },
|
||||
],
|
||||
}
|
||||
: cat,
|
||||
@@ -195,7 +194,7 @@ export default function ComponentsPreview() {
|
||||
...cat,
|
||||
chipOptions: cat.chipOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, label: value, state: "Selected" }
|
||||
? { ...opt, label: value, state: "selected" }
|
||||
: opt,
|
||||
),
|
||||
}
|
||||
@@ -220,7 +219,7 @@ export default function ComponentsPreview() {
|
||||
},
|
||||
{
|
||||
name: "Communication",
|
||||
chipOptions: [{ id: "comm-1", label: "Signal", state: "Unselected" }],
|
||||
chipOptions: [{ id: "comm-1", label: "Signal", state: "unselected" }],
|
||||
onChipClick: (categoryName: string, chipId: string) => {
|
||||
setRuleCardCategories((prev) =>
|
||||
prev.map((cat) =>
|
||||
@@ -232,9 +231,9 @@ export default function ComponentsPreview() {
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "Selected"
|
||||
? "Unselected"
|
||||
: "Selected",
|
||||
opt.state === "selected"
|
||||
? "unselected"
|
||||
: "selected",
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
@@ -252,7 +251,7 @@ export default function ComponentsPreview() {
|
||||
...cat,
|
||||
chipOptions: [
|
||||
...cat.chipOptions,
|
||||
{ id: newId, label: "", state: "Custom" },
|
||||
{ id: newId, label: "", state: "custom" },
|
||||
],
|
||||
}
|
||||
: cat,
|
||||
@@ -271,7 +270,7 @@ export default function ComponentsPreview() {
|
||||
...cat,
|
||||
chipOptions: cat.chipOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, label: value, state: "Selected" }
|
||||
? { ...opt, label: value, state: "selected" }
|
||||
: opt,
|
||||
),
|
||||
}
|
||||
@@ -297,7 +296,7 @@ export default function ComponentsPreview() {
|
||||
{
|
||||
name: "Membership",
|
||||
chipOptions: [
|
||||
{ id: "membership-1", label: "Open Admission", state: "Unselected" },
|
||||
{ id: "membership-1", label: "Open Admission", state: "unselected" },
|
||||
],
|
||||
onChipClick: (categoryName: string, chipId: string) => {
|
||||
setRuleCardCategories((prev) =>
|
||||
@@ -310,9 +309,9 @@ export default function ComponentsPreview() {
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "Selected"
|
||||
? "Unselected"
|
||||
: "Selected",
|
||||
opt.state === "selected"
|
||||
? "unselected"
|
||||
: "selected",
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
@@ -330,7 +329,7 @@ export default function ComponentsPreview() {
|
||||
...cat,
|
||||
chipOptions: [
|
||||
...cat.chipOptions,
|
||||
{ id: newId, label: "", state: "Custom" },
|
||||
{ id: newId, label: "", state: "custom" },
|
||||
],
|
||||
}
|
||||
: cat,
|
||||
@@ -349,7 +348,7 @@ export default function ComponentsPreview() {
|
||||
...cat,
|
||||
chipOptions: cat.chipOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, label: value, state: "Selected" }
|
||||
? { ...opt, label: value, state: "selected" }
|
||||
: opt,
|
||||
),
|
||||
}
|
||||
@@ -375,8 +374,8 @@ export default function ComponentsPreview() {
|
||||
{
|
||||
name: "Decision-making",
|
||||
chipOptions: [
|
||||
{ id: "decision-1", label: "Lazy Consensus", state: "Unselected" },
|
||||
{ id: "decision-2", label: "Modified Consensus", state: "Unselected" },
|
||||
{ id: "decision-1", label: "Lazy Consensus", state: "unselected" },
|
||||
{ id: "decision-2", label: "Modified Consensus", state: "unselected" },
|
||||
],
|
||||
onChipClick: (categoryName: string, chipId: string) => {
|
||||
setRuleCardCategories((prev) =>
|
||||
@@ -389,9 +388,9 @@ export default function ComponentsPreview() {
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "Selected"
|
||||
? "Unselected"
|
||||
: "Selected",
|
||||
opt.state === "selected"
|
||||
? "unselected"
|
||||
: "selected",
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
@@ -409,7 +408,7 @@ export default function ComponentsPreview() {
|
||||
...cat,
|
||||
chipOptions: [
|
||||
...cat.chipOptions,
|
||||
{ id: newId, label: "", state: "Custom" },
|
||||
{ id: newId, label: "", state: "custom" },
|
||||
],
|
||||
}
|
||||
: cat,
|
||||
@@ -428,7 +427,7 @@ export default function ComponentsPreview() {
|
||||
...cat,
|
||||
chipOptions: cat.chipOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, label: value, state: "Selected" }
|
||||
? { ...opt, label: value, state: "selected" }
|
||||
: opt,
|
||||
),
|
||||
}
|
||||
@@ -454,8 +453,8 @@ export default function ComponentsPreview() {
|
||||
{
|
||||
name: "Conflict management",
|
||||
chipOptions: [
|
||||
{ id: "conflict-1", label: "Code of Conduct", state: "Unselected" },
|
||||
{ id: "conflict-2", label: "Restorative Justice", state: "Unselected" },
|
||||
{ id: "conflict-1", label: "Code of Conduct", state: "unselected" },
|
||||
{ id: "conflict-2", label: "Restorative Justice", state: "unselected" },
|
||||
],
|
||||
onChipClick: (categoryName: string, chipId: string) => {
|
||||
setRuleCardCategories((prev) =>
|
||||
@@ -468,9 +467,9 @@ export default function ComponentsPreview() {
|
||||
? {
|
||||
...opt,
|
||||
state:
|
||||
opt.state === "Selected"
|
||||
? "Unselected"
|
||||
: "Selected",
|
||||
opt.state === "selected"
|
||||
? "unselected"
|
||||
: "selected",
|
||||
}
|
||||
: opt,
|
||||
),
|
||||
@@ -488,7 +487,7 @@ export default function ComponentsPreview() {
|
||||
...cat,
|
||||
chipOptions: [
|
||||
...cat.chipOptions,
|
||||
{ id: newId, label: "", state: "Custom" },
|
||||
{ id: newId, label: "", state: "custom" },
|
||||
],
|
||||
}
|
||||
: cat,
|
||||
@@ -507,7 +506,7 @@ export default function ComponentsPreview() {
|
||||
...cat,
|
||||
chipOptions: cat.chipOptions.map((opt) =>
|
||||
opt.id === chipId
|
||||
? { ...opt, label: value, state: "Selected" }
|
||||
? { ...opt, label: value, state: "selected" }
|
||||
: opt,
|
||||
),
|
||||
}
|
||||
@@ -560,45 +559,45 @@ export default function ComponentsPreview() {
|
||||
<Chip
|
||||
label="Small"
|
||||
state={chipStates["default-s"]}
|
||||
palette="Default"
|
||||
size="S"
|
||||
palette="default"
|
||||
size="s"
|
||||
onClick={() =>
|
||||
setChipStates((prev) => ({
|
||||
...prev,
|
||||
"default-s":
|
||||
prev["default-s"] === "Selected"
|
||||
? "Unselected"
|
||||
: "Selected",
|
||||
prev["default-s"] === "selected"
|
||||
? "unselected"
|
||||
: "selected",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<Chip
|
||||
label="Medium"
|
||||
state={chipStates["default-m"]}
|
||||
palette="Default"
|
||||
size="M"
|
||||
palette="default"
|
||||
size="m"
|
||||
onClick={() =>
|
||||
setChipStates((prev) => ({
|
||||
...prev,
|
||||
"default-m":
|
||||
prev["default-m"] === "Selected"
|
||||
? "Unselected"
|
||||
: "Selected",
|
||||
prev["default-m"] === "selected"
|
||||
? "unselected"
|
||||
: "selected",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<Chip
|
||||
label="Disabled"
|
||||
state="Disabled"
|
||||
palette="Default"
|
||||
size="S"
|
||||
state="disabled"
|
||||
palette="default"
|
||||
size="s"
|
||||
/>
|
||||
{customChips
|
||||
.filter((chip) => chip.palette === "Default")
|
||||
.filter((chip) => chip.palette === "default")
|
||||
.map((chip) => (
|
||||
<Chip
|
||||
key={chip.id}
|
||||
label={chip.state === "Custom" ? "" : chip.label}
|
||||
label={chip.state === "custom" ? "" : chip.label}
|
||||
state={chip.state}
|
||||
palette={chip.palette}
|
||||
size={chip.size}
|
||||
@@ -607,7 +606,7 @@ export default function ComponentsPreview() {
|
||||
setCustomChips((prev) =>
|
||||
prev.map((c) =>
|
||||
c.id === chip.id
|
||||
? { ...c, label: value, state: "Selected" }
|
||||
? { ...c, label: value, state: "selected" }
|
||||
: c,
|
||||
),
|
||||
);
|
||||
@@ -622,8 +621,8 @@ export default function ComponentsPreview() {
|
||||
e.stopPropagation();
|
||||
// Only toggle if the chip is in Selected or Unselected state (not Custom)
|
||||
if (
|
||||
chip.state === "Selected" ||
|
||||
chip.state === "Unselected"
|
||||
chip.state === "selected" ||
|
||||
chip.state === "unselected"
|
||||
) {
|
||||
setCustomChips((prev) =>
|
||||
prev.map((c) =>
|
||||
@@ -631,9 +630,9 @@ export default function ComponentsPreview() {
|
||||
? {
|
||||
...c,
|
||||
state:
|
||||
c.state === "Selected"
|
||||
? "Unselected"
|
||||
: "Selected",
|
||||
c.state === "selected"
|
||||
? "unselected"
|
||||
: "selected",
|
||||
}
|
||||
: c,
|
||||
),
|
||||
@@ -652,9 +651,9 @@ export default function ComponentsPreview() {
|
||||
{
|
||||
id: newId,
|
||||
label: "",
|
||||
state: "Custom",
|
||||
palette: "Default",
|
||||
size: "S",
|
||||
state: "custom",
|
||||
palette: "default",
|
||||
size: "s",
|
||||
},
|
||||
]);
|
||||
}}
|
||||
@@ -698,38 +697,38 @@ export default function ComponentsPreview() {
|
||||
<Chip
|
||||
label="Small"
|
||||
state={chipStates["inverse-s"]}
|
||||
palette="Inverse"
|
||||
size="S"
|
||||
palette="inverse"
|
||||
size="s"
|
||||
onClick={() =>
|
||||
setChipStates((prev) => ({
|
||||
...prev,
|
||||
"inverse-s":
|
||||
prev["inverse-s"] === "Selected"
|
||||
? "Unselected"
|
||||
: "Selected",
|
||||
prev["inverse-s"] === "selected"
|
||||
? "unselected"
|
||||
: "selected",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<Chip
|
||||
label="Medium"
|
||||
state={chipStates["inverse-m"]}
|
||||
palette="Inverse"
|
||||
size="M"
|
||||
palette="inverse"
|
||||
size="m"
|
||||
onClick={() =>
|
||||
setChipStates((prev) => ({
|
||||
...prev,
|
||||
"inverse-m":
|
||||
prev["inverse-m"] === "Selected"
|
||||
? "Unselected"
|
||||
: "Selected",
|
||||
prev["inverse-m"] === "selected"
|
||||
? "unselected"
|
||||
: "selected",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<Chip
|
||||
label="Disabled"
|
||||
state="Disabled"
|
||||
palette="Inverse"
|
||||
size="S"
|
||||
state="disabled"
|
||||
palette="inverse"
|
||||
size="s"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -959,10 +958,10 @@ export default function ComponentsPreview() {
|
||||
</h2>
|
||||
<div className="bg-[var(--color-surface-default-secondary)] rounded-[var(--radius-300,12px)] p-[var(--spacing-scale-032)] space-y-[var(--spacing-scale-024)]">
|
||||
{/* Small size */}
|
||||
<MultiSelectExample size="S" />
|
||||
<MultiSelectExample size="s" />
|
||||
|
||||
{/* Medium size */}
|
||||
<MultiSelectExample size="M" />
|
||||
<MultiSelectExample size="m" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
// Development-only previews (e.g. `/components-preview`) — no public chrome.
|
||||
// Routes here are gated by NODE_ENV checks at the page level.
|
||||
export default function DevLayout({ children }: { children: ReactNode }) {
|
||||
return <main className="flex-1">{children}</main>;
|
||||
}
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import { listRuleTemplatesFromDb } from "../../lib/server/ruleTemplates";
|
||||
import { GOVERNANCE_TEMPLATE_HOME_SLUGS } from "../../lib/templates/governanceTemplateCatalog";
|
||||
import { gridEntriesForSlugOrderWithCatalogFallback } from "../../lib/templates/templateGridPresentation";
|
||||
import { listRuleTemplatesFromDb } from "../../../lib/server/ruleTemplates";
|
||||
import { GOVERNANCE_TEMPLATE_HOME_SLUGS } from "../../../lib/templates/governanceTemplateCatalog";
|
||||
import { gridEntriesForSlugOrderWithCatalogFallback } from "../../../lib/templates/templateGridPresentation";
|
||||
|
||||
const RuleStack = dynamic(() => import("../components/sections/RuleStack"), {
|
||||
const RuleStack = dynamic(() => import("../../components/sections/RuleStack"), {
|
||||
loading: () => (
|
||||
<section className="py-[var(--spacing-scale-032)] min-h-[400px]" />
|
||||
),
|
||||
@@ -0,0 +1,21 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
// Site footer is part of the public marketing chrome only — not rendered for
|
||||
// signed-in product surfaces, admin dashboards, or dev previews. See
|
||||
// `.cursor/rules/routes.mdc` for the full chrome composition map.
|
||||
const Footer = dynamic(() => import("../components/navigation/Footer"), {
|
||||
loading: () => (
|
||||
<footer className="bg-[var(--color-surface-default-primary)] w-full min-h-[200px]" />
|
||||
),
|
||||
ssr: true,
|
||||
});
|
||||
|
||||
export default function MarketingLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import messages from "../../messages/en/index";
|
||||
import { getTranslation } from "../../lib/i18n/getTranslation";
|
||||
import HeroBanner from "../components/sections/HeroBanner";
|
||||
import AskOrganizer from "../components/sections/AskOrganizer";
|
||||
import { MarketingRuleStackSection } from "./MarketingRuleStackSection";
|
||||
import { MarketingRuleStackSection } from "./_components/MarketingRuleStackSection";
|
||||
|
||||
// Code split below-the-fold components to reduce initial bundle size
|
||||
const LogoWall = dynamic(() => import("../components/sections/LogoWall"), {
|
||||
|
||||
@@ -5,34 +5,26 @@ import type {
|
||||
ButtonPaletteValue,
|
||||
ButtonStateValue,
|
||||
} from "../../../lib/propNormalization";
|
||||
import {
|
||||
normalizeSize,
|
||||
normalizeButtonType,
|
||||
normalizeButtonPalette,
|
||||
} from "../../../lib/propNormalization";
|
||||
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
* Button type (Figma prop). Accepts both lowercase and PascalCase (case-insensitive).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
* Button type (Figma prop).
|
||||
* @default "filled"
|
||||
*/
|
||||
buttonType?: ButtonTypeValue;
|
||||
/**
|
||||
* Button palette (Figma prop). Accepts both lowercase and PascalCase (case-insensitive).
|
||||
* Figma uses "Invert", codebase uses "inverse" - both are supported.
|
||||
* Button palette (Figma prop).
|
||||
* @default "default"
|
||||
*/
|
||||
palette?: ButtonPaletteValue;
|
||||
/**
|
||||
* Button size. Accepts both lowercase and PascalCase (case-insensitive).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
* Button size.
|
||||
* @default "xsmall"
|
||||
*/
|
||||
size?: SizeValue;
|
||||
/**
|
||||
* Button state (Figma prop). Accepts both lowercase and PascalCase (case-insensitive).
|
||||
* Button state (Figma prop).
|
||||
* @default "default"
|
||||
*/
|
||||
state?: ButtonStateValue;
|
||||
@@ -83,12 +75,9 @@ const Button = memo<ButtonProps>(
|
||||
ariaLabel,
|
||||
...props
|
||||
}) => {
|
||||
// Normalize props
|
||||
const buttonType = normalizeButtonType(typeProp, "filled");
|
||||
const buttonPalette = normalizeButtonPalette(paletteProp, "default");
|
||||
const size = normalizeSize(sizeProp);
|
||||
// State prop is for Figma alignment - actual state is handled by CSS pseudo-classes
|
||||
// We accept it for API alignment but don't use it for styling (CSS handles states)
|
||||
const buttonType = typeProp ?? "filled";
|
||||
const buttonPalette = paletteProp ?? "default";
|
||||
const size = sizeProp;
|
||||
|
||||
// Map type + palette to variant string for styling (internal use only)
|
||||
const getVariantFromTypeAndPalette = (
|
||||
|
||||
@@ -3,80 +3,57 @@
|
||||
import { memo } from "react";
|
||||
import SectionNumber from "../sections/SectionNumber";
|
||||
|
||||
import { normalizeNumberCardSize } from "../../../lib/propNormalization";
|
||||
|
||||
export type NumberCardSizeValue =
|
||||
| "Small"
|
||||
| "Medium"
|
||||
| "Large"
|
||||
| "XLarge"
|
||||
| "small"
|
||||
| "medium"
|
||||
| "large"
|
||||
| "xlarge";
|
||||
export type NumberCardSizeValue = "small" | "medium" | "large" | "xlarge";
|
||||
|
||||
interface NumberCardProps {
|
||||
number: number;
|
||||
text: string;
|
||||
/**
|
||||
* Number card size. Accepts both PascalCase (Figma default) and lowercase (case-insensitive).
|
||||
* Figma uses PascalCase, codebase uses PascalCase - both are supported.
|
||||
*/
|
||||
size?: NumberCardSizeValue;
|
||||
iconShape?: string;
|
||||
iconColor?: string;
|
||||
}
|
||||
|
||||
const NumberCard = memo<NumberCardProps>(({ number, text, size: sizeProp }) => {
|
||||
// Base classes common to all sizes
|
||||
const baseClasses =
|
||||
"bg-[var(--color-surface-inverse-primary)] rounded-[12px] shadow-lg";
|
||||
|
||||
// If size prop is provided, use explicit size classes
|
||||
// Otherwise, use responsive breakpoints for backward compatibility
|
||||
if (sizeProp) {
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const size = normalizeNumberCardSize(sizeProp);
|
||||
// Size-specific classes
|
||||
const size = sizeProp;
|
||||
const sizeClasses = {
|
||||
Small: "flex flex-col items-end justify-center gap-4 p-5 relative",
|
||||
Medium: "flex flex-row items-center gap-8 p-8 relative",
|
||||
Large:
|
||||
small: "flex flex-col items-end justify-center gap-4 p-5 relative",
|
||||
medium: "flex flex-row items-center gap-8 p-8 relative",
|
||||
large:
|
||||
"flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative",
|
||||
XLarge:
|
||||
xlarge:
|
||||
"flex flex-col items-start justify-end gap-[22px] h-[238px] p-8 relative",
|
||||
};
|
||||
|
||||
// Text size classes
|
||||
const textClasses = {
|
||||
Small:
|
||||
small:
|
||||
"font-bricolage-grotesque font-medium text-[24px] leading-[32px] text-[#141414]",
|
||||
Medium:
|
||||
medium:
|
||||
"font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]",
|
||||
Large:
|
||||
large:
|
||||
"font-bricolage-grotesque font-medium text-[24px] leading-[24px] text-[#141414]",
|
||||
XLarge:
|
||||
xlarge:
|
||||
"font-bricolage-grotesque font-medium text-[32px] leading-[32px] text-[#141414]",
|
||||
};
|
||||
|
||||
// Section number wrapper classes - Small doesn't need a wrapper
|
||||
const sectionNumberWrapperClasses = {
|
||||
Small: "relative shrink-0",
|
||||
Medium: "flex justify-start flex-shrink-0",
|
||||
Large: "absolute top-8 right-8",
|
||||
XLarge: "absolute top-8 right-8",
|
||||
small: "relative shrink-0",
|
||||
medium: "flex justify-start flex-shrink-0",
|
||||
large: "absolute top-8 right-8",
|
||||
xlarge: "absolute top-8 right-8",
|
||||
};
|
||||
|
||||
// Content container classes
|
||||
const contentClasses = {
|
||||
Small: "min-w-full relative shrink-0",
|
||||
Medium: "flex-1",
|
||||
Large: "absolute bottom-8 left-8 right-16",
|
||||
XLarge: "absolute bottom-8 left-8 right-16",
|
||||
small: "min-w-full relative shrink-0",
|
||||
medium: "flex-1",
|
||||
large: "absolute bottom-8 left-8 right-16",
|
||||
xlarge: "absolute bottom-8 left-8 right-16",
|
||||
};
|
||||
|
||||
// Small variant has section number as direct child, others need wrapper
|
||||
if (size === "Small") {
|
||||
if (size === "small") {
|
||||
return (
|
||||
<div className={`${baseClasses} ${sizeClasses[size]}`}>
|
||||
{/* Section Number - Direct child for Small */}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import { memo } from "react";
|
||||
import { RuleCardView } from "./RuleCard.view";
|
||||
import type { RuleCardProps } from "./RuleCard.types";
|
||||
import { normalizeRuleCardSize } from "../../../../lib/propNormalization";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -33,8 +32,7 @@ const RuleCardContainer = memo<RuleCardProps>(
|
||||
logoAlt,
|
||||
communityInitials,
|
||||
}) => {
|
||||
// Normalize size prop
|
||||
const size = normalizeRuleCardSize(sizeProp, "L");
|
||||
const size = sizeProp ?? "L";
|
||||
|
||||
const handleClick = () => {
|
||||
// Basic analytics event tracking
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface RuleCardProps {
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
expanded?: boolean;
|
||||
size?: "XS" | "S" | "M" | "L" | "xs" | "s" | "m" | "l";
|
||||
size?: "XS" | "S" | "M" | "L";
|
||||
categories?: Category[];
|
||||
logoUrl?: string;
|
||||
logoAlt?: string;
|
||||
|
||||
@@ -261,8 +261,8 @@ export function RuleCardView({
|
||||
key={categoryIndex}
|
||||
label={category.name}
|
||||
showHelpIcon={false}
|
||||
size="S"
|
||||
palette="Inverse"
|
||||
size="s"
|
||||
palette="inverse"
|
||||
options={category.chipOptions}
|
||||
onChipClick={(chipId) => {
|
||||
category.onChipClick?.(category.name, chipId);
|
||||
|
||||
@@ -4,12 +4,10 @@ import { memo } from "react";
|
||||
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
|
||||
import ContentContainerView from "./ContentContainer.view";
|
||||
import type { ContentContainerProps } from "./ContentContainer.types";
|
||||
import { normalizeContentContainerSize } from "../../../../lib/propNormalization";
|
||||
|
||||
const ContentContainerContainer = memo<ContentContainerProps>(
|
||||
({ post, width = "200px", size: sizeProp = "responsive" }) => {
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const size = normalizeContentContainerSize(sizeProp);
|
||||
const size = sizeProp;
|
||||
// Get the corresponding icon based on the same logic as background images
|
||||
const getIconImage = (slug: string): string => {
|
||||
const icons = [
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import type { BlogPost } from "../../../../lib/content";
|
||||
|
||||
export type ContentContainerSizeValue =
|
||||
| "xs"
|
||||
| "responsive"
|
||||
| "Xs"
|
||||
| "Responsive";
|
||||
export type ContentContainerSizeValue = "xs" | "responsive";
|
||||
|
||||
export interface ContentContainerProps {
|
||||
post: BlogPost;
|
||||
width?: string;
|
||||
/**
|
||||
* Content container size. Accepts both lowercase and PascalCase (case-insensitive).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
* Content container size.
|
||||
*/
|
||||
size?: ContentContainerSizeValue;
|
||||
}
|
||||
|
||||
+1
-3
@@ -4,12 +4,10 @@ import { memo } from "react";
|
||||
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
|
||||
import ContentThumbnailTemplateView from "./ContentThumbnailTemplate.view";
|
||||
import type { ContentThumbnailTemplateProps } from "./ContentThumbnailTemplate.types";
|
||||
import { normalizeContentThumbnailVariant } from "../../../../lib/propNormalization";
|
||||
|
||||
const ContentThumbnailTemplateContainer = memo<ContentThumbnailTemplateProps>(
|
||||
({ post, className = "", variant: variantProp = "vertical" }) => {
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const variant = normalizeContentThumbnailVariant(variantProp);
|
||||
const variant = variantProp;
|
||||
// Get article-specific background image from frontmatter
|
||||
const getBackgroundImage = (
|
||||
post: ContentThumbnailTemplateProps["post"],
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import type { BlogPost } from "../../../../lib/content";
|
||||
|
||||
export type ContentThumbnailTemplateVariantValue =
|
||||
| "vertical"
|
||||
| "horizontal"
|
||||
| "Vertical"
|
||||
| "Horizontal";
|
||||
export type ContentThumbnailTemplateVariantValue = "vertical" | "horizontal";
|
||||
|
||||
export interface ContentThumbnailTemplateProps {
|
||||
post: BlogPost;
|
||||
className?: string;
|
||||
/**
|
||||
* Content thumbnail variant. Accepts both lowercase and PascalCase (case-insensitive).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
* Content thumbnail variant.
|
||||
*/
|
||||
variant?: ContentThumbnailTemplateVariantValue;
|
||||
slugOrder?: string[];
|
||||
|
||||
@@ -4,11 +4,11 @@ import { memo } from "react";
|
||||
import { useComponentId } from "../../../hooks";
|
||||
import { CheckboxView } from "./Checkbox.view";
|
||||
import type { CheckboxProps } from "./Checkbox.types";
|
||||
import {
|
||||
normalizeMode,
|
||||
normalizeState,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
/**
|
||||
* Figma: "Control / Checkbox" (TODO(figma)). Single boolean checkbox with
|
||||
* optional label, supporting standard and inverse modes.
|
||||
*/
|
||||
const CheckboxContainer = memo<CheckboxProps>(
|
||||
({
|
||||
checked = false,
|
||||
@@ -24,9 +24,8 @@ const CheckboxContainer = memo<CheckboxProps>(
|
||||
ariaLabel,
|
||||
...props
|
||||
}) => {
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const mode = normalizeMode(modeProp);
|
||||
const state = normalizeState(stateProp);
|
||||
const mode = modeProp;
|
||||
const state = stateProp;
|
||||
|
||||
const isInverse = mode === "inverse";
|
||||
const isStandard = mode === "standard";
|
||||
|
||||
@@ -2,15 +2,9 @@ import type { ModeValue, StateValue } from "../../../../lib/propNormalization";
|
||||
|
||||
export interface CheckboxProps {
|
||||
checked?: boolean;
|
||||
/**
|
||||
* Mode variant. Accepts both "standard"/"Standard" and "inverse"/"Inverse" (case-insensitive).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
*/
|
||||
/** Mode variant (Figma: Mode). */
|
||||
mode?: ModeValue;
|
||||
/**
|
||||
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
*/
|
||||
/** Visual state (Figma: State). */
|
||||
state?: StateValue;
|
||||
disabled?: boolean;
|
||||
label?: string;
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
import { memo, useCallback, useId, useState } from "react";
|
||||
import { CheckboxGroupView } from "./CheckboxGroup.view";
|
||||
import type { CheckboxGroupProps } from "./CheckboxGroup.types";
|
||||
import { normalizeMode } from "../../../../lib/propNormalization";
|
||||
|
||||
/**
|
||||
* Figma: "Control / CheckboxGroup" (TODO(figma)). Group of checkboxes sharing
|
||||
* a name that emits the array of currently selected values.
|
||||
*/
|
||||
const CheckboxGroupContainer = ({
|
||||
name,
|
||||
value,
|
||||
@@ -15,8 +18,7 @@ const CheckboxGroupContainer = ({
|
||||
className = "",
|
||||
...props
|
||||
}: CheckboxGroupProps) => {
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const mode = normalizeMode(modeProp);
|
||||
const mode = modeProp;
|
||||
// Generate unique ID for accessibility if not provided
|
||||
const generatedId = useId();
|
||||
const groupId = name || `checkbox-group-${generatedId}`;
|
||||
|
||||
@@ -12,8 +12,7 @@ export interface CheckboxGroupProps {
|
||||
value?: string[];
|
||||
onChange?: (_data: { value: string[] }) => void;
|
||||
/**
|
||||
* Mode variant. Accepts both "standard"/"Standard" and "inverse"/"Inverse" (case-insensitive).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
* Mode variant.
|
||||
*/
|
||||
mode?: ModeValue;
|
||||
disabled?: boolean;
|
||||
|
||||
@@ -3,18 +3,17 @@
|
||||
import { memo, useState, useEffect, useRef } from "react";
|
||||
import ChipView from "./Chip.view";
|
||||
import type { ChipProps } from "./Chip.types";
|
||||
import {
|
||||
normalizeChipPalette,
|
||||
normalizeChipSize,
|
||||
normalizeChipState,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
/**
|
||||
* Figma: "Control / Chip" (TODO(figma)). Compact pill-shaped tag with
|
||||
* selectable, removable, and inline-editable (custom) states.
|
||||
*/
|
||||
const ChipContainer = memo<ChipProps>(
|
||||
({
|
||||
label,
|
||||
state: stateProp = "Unselected",
|
||||
palette: paletteProp = "Default",
|
||||
size: sizeProp = "S",
|
||||
state: stateProp = "unselected",
|
||||
palette: paletteProp = "default",
|
||||
size: sizeProp = "s",
|
||||
className = "",
|
||||
disabled,
|
||||
onClick,
|
||||
@@ -23,9 +22,9 @@ const ChipContainer = memo<ChipProps>(
|
||||
onClose,
|
||||
ariaLabel,
|
||||
}) => {
|
||||
const state = normalizeChipState(stateProp);
|
||||
const palette = normalizeChipPalette(paletteProp);
|
||||
const size = normalizeChipSize(sizeProp);
|
||||
const state = stateProp;
|
||||
const palette = paletteProp;
|
||||
const size = sizeProp;
|
||||
|
||||
const isDisabled = disabled ?? state === "disabled";
|
||||
const isCustom = state === "custom";
|
||||
|
||||
@@ -7,38 +7,32 @@ import type {
|
||||
export interface ChipProps {
|
||||
label: string;
|
||||
/**
|
||||
* Visual state of the chip, aligned with Figma:
|
||||
* - "Unselected"
|
||||
* - "Selected"
|
||||
* - "Disabled"
|
||||
* - "Custom" (editable chips with check/close buttons)
|
||||
*
|
||||
* Accepts both PascalCase (Figma) and lowercase values.
|
||||
* Visual state of the chip:
|
||||
* - "unselected"
|
||||
* - "selected"
|
||||
* - "disabled"
|
||||
* - "custom" (editable chips with check/close buttons)
|
||||
*/
|
||||
state?: ChipStateValue;
|
||||
/**
|
||||
* Palette of the chip, aligned with Figma:
|
||||
* - "Default"
|
||||
* - "Inverse"
|
||||
*
|
||||
* Accepts both PascalCase (Figma) and lowercase values.
|
||||
* Palette of the chip:
|
||||
* - "default"
|
||||
* - "inverse"
|
||||
*/
|
||||
palette?: ChipPaletteValue;
|
||||
/**
|
||||
* Size of the chip, aligned with Figma:
|
||||
* - "S"
|
||||
* - "M"
|
||||
*
|
||||
* Accepts both uppercase (Figma) and lowercase values.
|
||||
* Size of the chip:
|
||||
* - "s"
|
||||
* - "m"
|
||||
*/
|
||||
size?: ChipSizeValue;
|
||||
className?: string;
|
||||
/**
|
||||
* Whether the chip should be non-interactive. Defaults to `true` when
|
||||
* `state === "disabled"` to preserve historical behavior. Pass
|
||||
* `disabled={false}` alongside `state="Disabled"` to render the dimmed
|
||||
* `disabled={false}` alongside `state="disabled"` to render the dimmed
|
||||
* "disabled" visual while keeping the chip clickable — useful for toggle
|
||||
* groups where the unselected state is the disabled Figma visual.
|
||||
* groups where the unselected state is the disabled visual.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { InputWithCounterView } from "./InputWithCounter.view";
|
||||
import type { InputWithCounterProps } from "./InputWithCounter.types";
|
||||
|
||||
/**
|
||||
* Figma: "Control / InputWithCounter" (TODO(figma)).
|
||||
* Single-line text input with a label, optional help glyph, and a live
|
||||
* `value.length / maxLength` counter underneath.
|
||||
*/
|
||||
const InputWithCounterContainer = memo<InputWithCounterProps>((props) => {
|
||||
return <InputWithCounterView {...props} />;
|
||||
});
|
||||
|
||||
InputWithCounterContainer.displayName = "InputWithCounter";
|
||||
|
||||
export default InputWithCounterContainer;
|
||||
@@ -1,2 +1,2 @@
|
||||
export { InputWithCounterView as default } from "./InputWithCounter.view";
|
||||
export { default } from "./InputWithCounter.container";
|
||||
export type { InputWithCounterProps } from "./InputWithCounter.types";
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
import { memo } from "react";
|
||||
import MultiSelectView from "./MultiSelect.view";
|
||||
import type { MultiSelectProps } from "./MultiSelect.types";
|
||||
import {
|
||||
normalizeMultiSelectSize,
|
||||
normalizeChipPalette,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
/**
|
||||
* Figma: "Control / MultiSelect" (TODO(figma)). Labelled set of chips for
|
||||
* picking multiple values, with an optional add button for custom entries.
|
||||
*/
|
||||
const MultiSelectContainer = memo<MultiSelectProps>(
|
||||
({
|
||||
label,
|
||||
showHelpIcon = true,
|
||||
size: sizeProp = "M",
|
||||
palette: paletteProp = "Default",
|
||||
size: sizeProp = "m",
|
||||
palette: paletteProp = "default",
|
||||
options,
|
||||
onChipClick,
|
||||
onAddClick,
|
||||
@@ -24,8 +24,8 @@ const MultiSelectContainer = memo<MultiSelectProps>(
|
||||
onCustomChipClose,
|
||||
className = "",
|
||||
}) => {
|
||||
const size = normalizeMultiSelectSize(sizeProp);
|
||||
const palette = normalizeChipPalette(paletteProp);
|
||||
const size = sizeProp;
|
||||
const palette = paletteProp;
|
||||
|
||||
return (
|
||||
<MultiSelectView
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface ChipOption {
|
||||
state?: ChipStateValue;
|
||||
}
|
||||
|
||||
export type MultiSelectSizeValue = "S" | "M" | "s" | "m";
|
||||
export type MultiSelectSizeValue = "s" | "m";
|
||||
|
||||
export interface MultiSelectProps {
|
||||
/**
|
||||
@@ -21,13 +21,11 @@ export interface MultiSelectProps {
|
||||
*/
|
||||
showHelpIcon?: boolean;
|
||||
/**
|
||||
* Size variant: "S" (small) or "M" (medium)
|
||||
* Accepts both uppercase (Figma) and lowercase values.
|
||||
* Size variant: "s" (small) or "m" (medium)
|
||||
*/
|
||||
size?: MultiSelectSizeValue;
|
||||
/**
|
||||
* Palette for chips: "Default" or "Inverse"
|
||||
* Accepts both PascalCase (Figma) and lowercase values.
|
||||
* Palette for chips: "default" or "inverse"
|
||||
*/
|
||||
palette?: ChipPaletteValue;
|
||||
/**
|
||||
|
||||
@@ -28,7 +28,7 @@ function MultiSelectView({
|
||||
? "gap-[var(--measures-spacing-200,8px)]"
|
||||
: "gap-[var(--measures-spacing-300,12px)]";
|
||||
|
||||
const chipSize = isSmall ? "S" : "M";
|
||||
const chipSize = size;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -41,8 +41,8 @@ function MultiSelectView({
|
||||
helpIcon={showHelpIcon}
|
||||
asterisk={false}
|
||||
helperText={false}
|
||||
size={size === "s" ? "S" : "M"}
|
||||
palette={palette === "inverse" ? "Inverse" : "Default"}
|
||||
size={size}
|
||||
palette={palette}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -53,13 +53,12 @@ function MultiSelectView({
|
||||
{options.map((option) => (
|
||||
<Chip
|
||||
key={option.id}
|
||||
label={option.state === "Custom" ? "" : option.label}
|
||||
state={option.state || "Unselected"}
|
||||
palette={palette === "inverse" ? "Inverse" : "Default"}
|
||||
label={option.state === "custom" ? "" : option.label}
|
||||
state={option.state || "unselected"}
|
||||
palette={palette}
|
||||
size={chipSize}
|
||||
onClick={() => {
|
||||
// Only toggle if not in Custom state
|
||||
if (option.state !== "Custom" && onChipClick) {
|
||||
if (option.state !== "custom" && onChipClick) {
|
||||
onChipClick(option.id);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
import { memo, useCallback, useId } from "react";
|
||||
import { RadioButtonView } from "./RadioButton.view";
|
||||
import type { RadioButtonProps } from "./RadioButton.types";
|
||||
import {
|
||||
normalizeMode,
|
||||
normalizeState,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
const RadioButtonContainer = ({
|
||||
checked = false,
|
||||
@@ -22,9 +18,8 @@ const RadioButtonContainer = ({
|
||||
ariaLabel,
|
||||
className = "",
|
||||
}: RadioButtonProps) => {
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const mode = normalizeMode(modeProp);
|
||||
const state = normalizeState(stateProp);
|
||||
const mode = modeProp;
|
||||
const state = stateProp;
|
||||
|
||||
// If state is "selected", it means checked in Figma terms
|
||||
const normalizedState = state === "selected" || checked ? "selected" : state;
|
||||
|
||||
@@ -3,14 +3,12 @@ import type { ModeValue, StateValue } from "../../../../lib/propNormalization";
|
||||
export interface RadioButtonProps {
|
||||
checked?: boolean;
|
||||
/**
|
||||
* Mode variant. Accepts both "standard"/"Standard" and "inverse"/"Inverse" (case-insensitive).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
* Mode variant.
|
||||
*/
|
||||
mode?: ModeValue;
|
||||
/**
|
||||
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus", "selected"/"Selected" (case-insensitive).
|
||||
* Visual state.
|
||||
* Note: "selected" state is represented by the `checked` prop in practice.
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
*/
|
||||
state?: StateValue;
|
||||
/**
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import { memo, useCallback, useId } from "react";
|
||||
import { RadioGroupView } from "./RadioGroup.view";
|
||||
import type { RadioGroupProps } from "./RadioGroup.types";
|
||||
import {
|
||||
normalizeMode,
|
||||
normalizeState,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
/**
|
||||
* Figma: "Control / RadioGroup" (TODO(figma)). Group of radio buttons sharing
|
||||
* a name that emits the single currently selected value.
|
||||
*/
|
||||
const RadioGroupContainer = ({
|
||||
name,
|
||||
value,
|
||||
@@ -19,14 +19,11 @@ const RadioGroupContainer = ({
|
||||
className = "",
|
||||
...props
|
||||
}: RadioGroupProps) => {
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const mode = normalizeMode(modeProp);
|
||||
// Normalize state, but handle "With Subtext" separately (it's represented by options with subtext)
|
||||
const state =
|
||||
typeof stateProp === "string" &&
|
||||
(stateProp.toLowerCase() === "with subtext" || stateProp === "With Subtext")
|
||||
? "default" // "With Subtext" is handled via RadioOption.subtext, use default state
|
||||
: normalizeState(stateProp);
|
||||
const mode = modeProp;
|
||||
const state: "default" | "hover" | "focus" | "selected" =
|
||||
stateProp === "With Subtext" || stateProp === "with subtext"
|
||||
? "default"
|
||||
: stateProp;
|
||||
// Generate unique ID for accessibility if not provided
|
||||
const generatedId = useId();
|
||||
const groupId = name || `radio-group-${generatedId}`;
|
||||
|
||||
@@ -12,14 +12,12 @@ export interface RadioGroupProps {
|
||||
value?: string;
|
||||
onChange?: (_data: { value: string }) => void;
|
||||
/**
|
||||
* Mode variant. Accepts both "standard"/"Standard" and "inverse"/"Inverse" (case-insensitive).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
* Mode variant.
|
||||
*/
|
||||
mode?: ModeValue;
|
||||
/**
|
||||
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
|
||||
* Visual state.
|
||||
* Figma also supports "With Subtext" state, which is handled via RadioOption.subtext.
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
*/
|
||||
state?: StateValue | "With Subtext" | "with subtext";
|
||||
disabled?: boolean;
|
||||
|
||||
@@ -16,12 +16,11 @@ import React, {
|
||||
import { useClickOutside } from "../../../hooks";
|
||||
import { SelectInputView } from "./SelectInput.view";
|
||||
import type { SelectInputProps } from "./SelectInput.types";
|
||||
import {
|
||||
normalizeState,
|
||||
normalizeSmallMediumLargeSize,
|
||||
normalizeLabelVariant,
|
||||
} from "../../../../lib/propNormalization";
|
||||
|
||||
/**
|
||||
* Figma: "Control / SelectInput" (TODO(figma)). Custom-styled select dropdown
|
||||
* with a labelled trigger button and floating option menu.
|
||||
*/
|
||||
const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
|
||||
(
|
||||
{
|
||||
@@ -53,22 +52,14 @@ const SelectInputContainer = forwardRef<HTMLButtonElement, SelectInputProps>(
|
||||
const shouldShowLabel =
|
||||
showLabel !== undefined ? showLabel : labelText !== undefined;
|
||||
|
||||
// Normalize state - handle "state5" as disabled
|
||||
let normalizedState = externalStateProp;
|
||||
if (normalizedState === "state5" || normalizedState === "State5") {
|
||||
normalizedState = "default"; // Map to default, disabled prop handles the disabled state
|
||||
normalizedState = "default";
|
||||
}
|
||||
const externalState = normalizeState(normalizedState);
|
||||
const externalState = normalizedState;
|
||||
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
// Note: labelVariant and size are normalized for future use but not yet implemented in the view
|
||||
const _labelVariant = labelVariantProp
|
||||
? normalizeLabelVariant(labelVariantProp)
|
||||
: undefined;
|
||||
const _size = sizeProp
|
||||
? normalizeSmallMediumLargeSize(sizeProp)
|
||||
: undefined;
|
||||
// Mark as intentionally unused for future implementation
|
||||
const _labelVariant = labelVariantProp;
|
||||
const _size = sizeProp;
|
||||
void _labelVariant;
|
||||
void _size;
|
||||
|
||||
|
||||
@@ -7,18 +7,8 @@ export interface SelectOptionData {
|
||||
|
||||
import type { StateValue } from "../../../../lib/propNormalization";
|
||||
|
||||
export type SelectInputLabelVariantValue =
|
||||
| "default"
|
||||
| "horizontal"
|
||||
| "Default"
|
||||
| "Horizontal";
|
||||
export type SelectInputSizeValue =
|
||||
| "small"
|
||||
| "medium"
|
||||
| "large"
|
||||
| "Small"
|
||||
| "Medium"
|
||||
| "Large";
|
||||
export type SelectInputLabelVariantValue = "default" | "horizontal";
|
||||
export type SelectInputSizeValue = "small" | "medium" | "large";
|
||||
|
||||
export interface SelectInputProps {
|
||||
id?: string;
|
||||
@@ -33,18 +23,15 @@ export interface SelectInputProps {
|
||||
*/
|
||||
showLabel?: boolean;
|
||||
/**
|
||||
* Label variant. Accepts both lowercase and PascalCase (case-insensitive).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
* Label variant.
|
||||
*/
|
||||
labelVariant?: SelectInputLabelVariantValue;
|
||||
/**
|
||||
* Select input size. Accepts both lowercase and PascalCase (case-insensitive).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
* Select input size.
|
||||
*/
|
||||
size?: SelectInputSizeValue;
|
||||
/**
|
||||
* Visual state. Accepts "default"/"Default", "active"/"Active", "focus"/"Focus", "error"/"Error", "state5"/"State5" (State5 = Disabled).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
* Visual state. "state5" maps to disabled.
|
||||
*/
|
||||
state?: StateValue | "state5" | "State5";
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { Children, type ReactNode } from "react";
|
||||
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
|
||||
import SelectDropdown from "./SelectDropdown";
|
||||
import SelectOption from "./SelectOption";
|
||||
import SelectOption from "../SelectOption";
|
||||
import type { SelectOptionData } from "./SelectInput.types";
|
||||
|
||||
export interface SelectInputViewProps {
|
||||
|
||||
+5
-3
@@ -3,8 +3,11 @@
|
||||
import { forwardRef, memo, useCallback } from "react";
|
||||
import { SelectOptionView } from "./SelectOption.view";
|
||||
import type { SelectOptionProps } from "./SelectOption.types";
|
||||
import { normalizeContextMenuItemSize } from "../../../../../lib/propNormalization";
|
||||
|
||||
/**
|
||||
* Figma: "Control / SelectOption" (TODO(figma)). Single option row rendered
|
||||
* inside `SelectInput`'s dropdown menu.
|
||||
*/
|
||||
const SelectOptionContainer = forwardRef<HTMLDivElement, SelectOptionProps>(
|
||||
(
|
||||
{
|
||||
@@ -18,8 +21,7 @@ const SelectOptionContainer = forwardRef<HTMLDivElement, SelectOptionProps>(
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const size = normalizeContextMenuItemSize(sizeProp);
|
||||
const size = sizeProp;
|
||||
const getTextSize = (): string => {
|
||||
switch (size) {
|
||||
case "small":
|
||||
+2
-9
@@ -1,10 +1,4 @@
|
||||
export type SelectOptionSizeValue =
|
||||
| "small"
|
||||
| "medium"
|
||||
| "large"
|
||||
| "Small"
|
||||
| "Medium"
|
||||
| "Large";
|
||||
export type SelectOptionSizeValue = "small" | "medium" | "large";
|
||||
|
||||
export interface SelectOptionProps {
|
||||
children?: React.ReactNode;
|
||||
@@ -15,8 +9,7 @@ export interface SelectOptionProps {
|
||||
_e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>,
|
||||
) => void;
|
||||
/**
|
||||
* Select option size. Accepts both lowercase and PascalCase (case-insensitive).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
* Select option size.
|
||||
*/
|
||||
size?: SelectOptionSizeValue;
|
||||
}
|
||||
@@ -3,8 +3,11 @@
|
||||
import { memo, useCallback, useId, forwardRef } from "react";
|
||||
import { SwitchView } from "./Switch.view";
|
||||
import type { SwitchProps } from "./Switch.types";
|
||||
import { normalizeState } from "../../../../lib/propNormalization";
|
||||
|
||||
/**
|
||||
* Figma: "Control / Switch" (TODO(figma)). Animated on/off toggle switch,
|
||||
* optionally paired with a trailing text label.
|
||||
*/
|
||||
const SwitchContainer = memo(
|
||||
forwardRef<HTMLButtonElement, SwitchProps>((props, ref) => {
|
||||
const {
|
||||
@@ -18,8 +21,7 @@ const SwitchContainer = memo(
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
// Normalize props to handle both PascalCase (Figma) and lowercase (codebase)
|
||||
const state = normalizeState(stateProp);
|
||||
const state = stateProp;
|
||||
|
||||
const switchId = useId();
|
||||
|
||||
|
||||
@@ -17,8 +17,7 @@ export interface SwitchProps extends Omit<
|
||||
onFocus?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
|
||||
onBlur?: (_e: React.FocusEvent<HTMLButtonElement>) => void;
|
||||
/**
|
||||
* Visual state. Accepts "default"/"Default", "hover"/"Hover", "focus"/"Focus" (case-insensitive).
|
||||
* Figma uses PascalCase, codebase uses lowercase - both are supported.
|
||||
* Visual state.
|
||||
*/
|
||||
state?: StateValue;
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user