@motir/design-system 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,8 +19,9 @@ See the keystone decision: [`docs/decisions/design-system-package.md`](../../doc
19
19
 
20
20
  - **`@motir/design-system/theme.css`** — the full token layer as a distributable
21
21
  stylesheet: the Tailwind v4 `@theme` block, `@custom-variant dark`, the Tier-3
22
- `--el-*` element tokens, and every `[data-palette]` / `[data-style]` /
23
- `[data-type]` / `[data-surface]` override + surface-material block.
22
+ `--el-*` element tokens, every `[data-palette]` / `[data-style]` /
23
+ `[data-type]` / `[data-surface]` override + surface-material block, and the
24
+ **motion floor** below.
24
25
  - **`@motir/design-system`** (JS entry) — the axis **registries**
25
26
  (`STYLE_IDS` / `PALETTE_IDS` / `TYPE_IDS`, `resolve*`, `is*Id`,
26
27
  `STYLE_DEFAULT_TYPE`), the **theme-apply API** (`buildThemeInitScript`,
@@ -57,6 +58,34 @@ Use the primitives + provider as normal React components:
57
58
  import { ThemeProvider, Button, Card } from '@motir/design-system';
58
59
  ```
59
60
 
61
+ ## Motion — the reduced-motion floor is the PACKAGE's, not the consumer's
62
+
63
+ **`theme.css` owns the `prefers-reduced-motion` guard for every animation the
64
+ design system ships.** A consumer imports the stylesheet and is done; it does
65
+ not have to know which primitives animate, and it must not have to rediscover
66
+ the guard per surface. Two kinds live here:
67
+
68
+ - **Style-axis motion** — the `aurora` canvas drift, the `3d-immersive` tilt and
69
+ menu unfold — gated inside the `[data-style]` block that introduced it,
70
+ because the motion belongs to that style.
71
+ - **The motion floor** — motion that belongs to no style and renders under every
72
+ axis combination. Today that is `.animate-spin`, which `Spinner` paints and
73
+ `<Button loading>` inherits: under `prefers-reduced-motion: reduce` it sets
74
+ `animation: none` and nothing else (MOTIR-3844). It is unlayered, which is what
75
+ makes it outrank Tailwind's `@layer utilities` copy of the same class whatever
76
+ the specificity or the `@import` order.
77
+
78
+ **Stop the rotation, keep the glyph.** A spinner is the only signal that a
79
+ control is working, so the guard stills it rather than hiding it — the ring keeps
80
+ its size, its border and its `role="status"`. A consumer whose only in-flight cue
81
+ is the rotation should pair it with a **label change**, since with the animation
82
+ stilled the word is the whole of the state.
83
+
84
+ **Adding an animation to this package means adding its guard here**, in the same
85
+ change — `tests/theme/reducedMotionSpinner.test.ts` in `motir-core` reads the
86
+ floor back off the COMPILED stylesheet for both consumer shapes, so a guard that
87
+ does not survive compilation fails there rather than in someone's browser.
88
+
60
89
  ## Boundary notes (how the extraction applied the ADR's rule)
61
90
 
62
91
  The ADR's boundary rule: a file moves in **iff** its only imports are other
@@ -116,11 +145,14 @@ package-scoped git tag. There is no manual `npm publish` in the normal path.
116
145
  **Dry run:** trigger the workflow from the Actions tab via **Run workflow**
117
146
  (`workflow_dispatch`) with **dry run** checked to build + pack without publishing.
118
147
 
119
- **Auth:** the publish step authenticates with the `NPM_TOKEN` repository secret (an
120
- npm automation/granular token with publish rights on the `@motir` scope). npm
121
- provenance is left off by default enabling it needs a `repository` field in this
122
- `package.json` plus `id-token: write` in the workflow (see the workflow header).
123
-
124
- > The **first** publish (`0.1.0`) may instead be cut manually (`npm publish`), or
125
- > by pushing a `design-system-v0.1.0` tag once `NPM_TOKEN` is set — the workflow
126
- > handles both the first and every subsequent release identically.
148
+ **Auth:** the publish step uses **OIDC Trusted Publishing** — no `NPM_TOKEN` secret.
149
+ GitHub mints a short-lived id-token (`id-token: write`) that npm exchanges for a
150
+ one-time publish credential, gated by a Trusted Publisher configured on npmjs.com
151
+ for this package (org `moooon-B-V` / repo `motir-core` / this workflow filename).
152
+ npm provenance is auto-generated on a Trusted Publish (the `repository` field in
153
+ this `package.json` satisfies its requirement).
154
+
155
+ > The **first** publish (`0.1.0`) was cut manually (`npm publish` with an
156
+ > interactive OTP), because OIDC cannot bootstrap a package that does not yet exist
157
+ > — the Trusted Publisher config lives on the package's settings page. Every release
158
+ > from `0.1.1` on flows through the workflow token-free.
@@ -31,13 +31,34 @@ import { TypeId } from '../../theme/typography.js';
31
31
  * apply exactly as they do in the shipped app. NEVER a Tier-0 `--color-*` or a
32
32
  * raw `rounded-*`/`p-*`/`h-*` (the colour + shape token rules in CLAUDE.md).
33
33
  *
34
- * Nesting caveat: a SCOPED base entry (`warm-editorial` / `motir` palette /
35
- * `motir` type) has no globals.css override block, so when it is nested under a
36
- * NON-base `<html>` it inherits that ancestor's axis rather than resetting to
37
- * the base. In the real consumers this never bites onboarding runs against the
38
- * default `<html>`, and the appearance preview uses LIVE mode (inherits the
39
- * active selection it is meant to show). Full nested-base isolation (a reset
40
- * block or an iframe) is a noted follow-up if a future consumer needs it.
34
+ * ⚠️ WHAT A SCOPED VIGNETTE IS AND IS NOT ISOLATED FROM (rewritten, MOTIR-3933).
35
+ * This paragraph used to describe the nested-base leak and then discharge it
36
+ * with "in the real consumers this never bites". That was a claim about the
37
+ * consumer set at the time of writing, recorded as though it were a property of
38
+ * the component and it is what let a public page ship ten identical palette
39
+ * previews. The consumer set grew. Measured, not assumed:
40
+ *
41
+ * • COLOUR (`palette`) — ISOLATED. The wrapper carries `data-appearance-scope`
42
+ * alongside the axis attribute, which re-emits the Tier-3 `--el-*` layer
43
+ * onto this element so it reads the `--color-*` this subtree overrides
44
+ * rather than the ones `:root` declared. theme.css states that requirement
45
+ * at the declaration site; this component did not honour it, and without it
46
+ * a scoped palette changed nothing the vignette paints with. The base
47
+ * `motir` entry now has its own block, and every per-palette dark block has
48
+ * a descendant arm, so a scoped tile is correct in BOTH themes and for the
49
+ * base entry too.
50
+ * • TYPE (`type`) — ISOLATED, same mechanism plus a base `[data-type='motir']`
51
+ * block. That axis ships no descendant rules, so the tokens are all of it.
52
+ * • SHAPE / MATERIAL (`styleId`) — **NOT ISOLATED, and this is open.** The
53
+ * style axis ships ~69 descendant rules (`[data-style='X'] [data-surface=…]`,
54
+ * `[data-style='X'] body`, …) and a descendant combinator does not stop at a
55
+ * nested `data-style`. Under an active `glassmorphism` only 1 of 11 scoped
56
+ * style vignettes wears its own material; under `aurora`, 3 of 11. The
57
+ * shape TOKENS still scope correctly for a non-base style — it is the
58
+ * material rules that cross. MOTIR-3947 owns the fix and the mechanism
59
+ * choice (selector barrier vs an isolated render tree); do not add a
60
+ * `[data-style='warm-editorial']` base block as a partial remedy, because it
61
+ * makes a token assertion pass on a tile that still paints as the ancestor.
41
62
  */
42
63
  interface StyleVignetteProps {
43
64
  /** Scope the vignette to a specific style; omit to inherit the active style. */
@@ -155,6 +155,20 @@ var pillVariants = cva(
155
155
  status: {
156
156
  planned: "bg-(--el-tint-lavender) text-(--el-text-strong) border-transparent",
157
157
  "in-progress": "bg-(--el-tint-sky) text-(--el-text-strong) border-transparent",
158
+ // BUILT, waiting on checks (MOTIR-3103). The first PER-STATUS tone on
159
+ // this axis — the other three are per-CATEGORY, and `implemented` sits in
160
+ // the same `in_progress` category as In Progress, Planning and In Review,
161
+ // so without its own tone four statuses render one chip.
162
+ //
163
+ // It uses the recipe theme.css's own `--el-status-*` header prescribes —
164
+ // "a status CHIP bg = color-mix(var(--el-status-X) 14%, var(--el-surface))
165
+ // with --el-text-strong" — which is the same dilution the `priority`
166
+ // variant below already renders. The hue is not spent by any other
167
+ // status in any palette (measured at ΔE2000 >= 10 across all ten by
168
+ // tests/theme/statusHueSeparation.test.ts), and the chip additionally
169
+ // carries a GLYPH, because a 14% tint is a subtle mark by construction
170
+ // and finding #35 forbids resting a state on colour alone.
171
+ implemented: "bg-[color-mix(in_srgb,var(--el-status-implemented)_14%,var(--el-surface))] text-(--el-text-strong) border-transparent",
158
172
  done: "bg-(--el-tint-mint) text-(--el-text-strong) border-transparent"
159
173
  },
160
174
  severity: {
@@ -183,10 +197,16 @@ var pillVariants = cva(
183
197
  // through the DEDICATED `--el-role-*` tokens (MOTIR-1274 · 1266.3) so a
184
198
  // palette can tune roles apart from the other --el-tint-* meanings; each
185
199
  // defaults to its prior tint (zero visual change).
200
+ // `custom` (MOTIR-2257 · MOTIR-2485) is a project's OWN role, which has no
201
+ // enum value — it takes the next free tint slot (peach) because this
202
+ // surface had already spent lavender / sky / mint on the built-ins, so a
203
+ // custom chip in any of those would read as one of them. The KIND is
204
+ // carried in WORDS by the chip's accessible name; nothing rests on the hue.
186
205
  memberRole: {
187
206
  admin: "bg-(--el-role-admin) text-(--el-text-strong) border-transparent",
188
207
  member: "bg-(--el-role-member) text-(--el-text-strong) border-transparent",
189
- viewer: "bg-(--el-role-viewer) text-(--el-text-strong) border-transparent"
208
+ viewer: "bg-(--el-role-viewer) text-(--el-text-strong) border-transparent",
209
+ custom: "bg-(--el-role-custom) text-(--el-text-strong) border-transparent"
190
210
  },
191
211
  // Organization roles (Story 6.10 · design/org-admin): owner → lavender
192
212
  // (the highest, brand-purple tier), admin → sky, member → mint — the hue
@@ -261,6 +281,7 @@ function StyleVignette({ styleId, palette, type, label, className }) {
261
281
  if (styleId) axisAttrs["data-style"] = styleId;
262
282
  if (palette) axisAttrs["data-palette"] = palette;
263
283
  if (type) axisAttrs["data-type"] = type;
284
+ if (Object.keys(axisAttrs).length > 0) axisAttrs["data-appearance-scope"] = "";
264
285
  return /* @__PURE__ */ jsxs(
265
286
  "div",
266
287
  {
@@ -285,8 +306,8 @@ function StyleVignette({ styleId, palette, type, label, className }) {
285
306
  /* @__PURE__ */ jsx("span", { className: "size-2.5 rounded-full bg-(--el-accent)", "aria-hidden": true }),
286
307
  /* @__PURE__ */ jsx("span", { className: "font-serif text-[12px] font-semibold text-(--el-text)", children: "Acme" }),
287
308
  /* @__PURE__ */ jsx("span", { className: "ml-2 rounded-(--radius-control) bg-(--el-accent) px-2 py-[3px] text-[10px] font-medium text-(--el-accent-text)", children: "Board" }),
288
- /* @__PURE__ */ jsx("span", { className: "text-[10px] text-(--el-text-muted)", children: "Backlog" }),
289
- /* @__PURE__ */ jsx("span", { className: "text-[10px] text-(--el-text-muted)", children: "Reports" }),
309
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-(--el-text-secondary)", children: "Backlog" }),
310
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-(--el-text-secondary)", children: "Reports" }),
290
311
  /* @__PURE__ */ jsx("span", { className: "ml-auto size-5 rounded-full bg-(--el-muted)", "aria-hidden": true })
291
312
  ]
292
313
  }
@@ -356,7 +377,7 @@ function StyleVignette({ styleId, palette, type, label, className }) {
356
377
  "div",
357
378
  {
358
379
  "data-surface": "input",
359
- className: "flex h-(--height-input) items-center gap-2 rounded-(--radius-input) border border-(--el-border-strong) bg-(--el-page-bg) px-(--spacing-input-x) text-[11px] text-(--el-text-muted)",
380
+ className: "flex h-(--height-input) items-center gap-2 rounded-(--radius-input) border border-(--el-border-strong) bg-(--el-page-bg) px-(--spacing-input-x) text-[11px] text-(--el-text-secondary)",
360
381
  children: [
361
382
  /* @__PURE__ */ jsx(Search, { className: "size-3.5", "aria-hidden": true }),
362
383
  /* @__PURE__ */ jsx("span", { children: "Search work items\u2026" })
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/utils/cn.ts","../../../src/components/ui/Spinner.tsx","../../../src/components/ui/Button.tsx","../../../src/components/ui/Card.tsx","../../../src/components/ui/Pill.tsx","../../../src/components/theme/StyleVignette.tsx"],"names":["Spinner","cva","forwardRef","Button","jsx","Card","jsxs","Pill"],"mappings":"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","file":"StyleVignette.js","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/**\n * Merge Tailwind class names without conflicts.\n *\n * `clsx` handles conditional inclusion (booleans, undefineds, arrays);\n * `twMerge` resolves conflicting Tailwind classes (later wins, e.g.\n * `px-2 px-4` → `px-4`). Together they are the de facto standard for\n * composing Tailwind classes in React components.\n *\n * @example\n * cn('px-2 py-1', isActive && 'bg-primary', className)\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","import { forwardRef, type HTMLAttributes } from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../../utils/cn';\n\n/**\n * Spinner — pure CSS rotation indicator.\n *\n * Three sizes; works standalone or inside a Button's loading state.\n *\n * @example\n * <Spinner size=\"md\" />\n * <Spinner size=\"sm\" aria-label=\"Loading\" />\n */\nconst spinnerVariants = cva(\n 'inline-block animate-spin rounded-full border-current border-t-transparent',\n {\n variants: {\n size: {\n sm: 'h-4 w-4 border-[1.5px]',\n md: 'h-5 w-5 border-2',\n lg: 'h-8 w-8 border-[3px]',\n },\n },\n defaultVariants: { size: 'md' },\n },\n);\n\nexport interface SpinnerProps\n extends Omit<HTMLAttributes<HTMLSpanElement>, 'role'>, VariantProps<typeof spinnerVariants> {}\n\nexport const Spinner = forwardRef<HTMLSpanElement, SpinnerProps>(function Spinner(\n { className, size, ...rest },\n ref,\n) {\n return (\n <span\n ref={ref}\n role=\"status\"\n aria-label={rest['aria-label'] ?? 'Loading'}\n className={cn(spinnerVariants({ size }), className)}\n {...rest}\n />\n );\n});\n","import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { Spinner } from './Spinner';\nimport { cn } from '../../utils/cn';\n\n/**\n * Button — primary interactive primitive.\n *\n * Variants × sizes × states. The `loading` state shows an inline Spinner\n * and disables the button. Use semantic Tailwind token classes; never\n * hardcode colors. Shape responds to `data-style` via\n * `--radius-btn` (rectangles in default, pills in `soft`).\n *\n * @example\n * <Button variant=\"primary\" size=\"md\">Save</Button>\n * <Button variant=\"ghost\" leftIcon={<Plus />} loading>Saving…</Button>\n */\n// Exported so a Next `<Link>` can render AS a styled button (an anchor styled\n// with the button variants — keeps real-link semantics for navigation) without\n// nesting a <button> inside an <a>. Used by the /ready empty state's \"View all\n// issues\" link (Subtask 7.0.6).\nexport const buttonVariants = cva(\n cn(\n // Base layout\n 'inline-flex items-center justify-center gap-2',\n // Typography\n 'font-sans text-sm font-medium leading-none',\n // Shape — semantic tokens that flip with the active style\n 'rounded-(--radius-btn)',\n // Interaction\n 'transition-[transform,background-color,border-color,color] duration-(--transition-duration)',\n 'active:scale-(--active-scale)',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--focus-ring-color) focus-visible:ring-offset-2 focus-visible:ring-offset-background',\n 'disabled:pointer-events-none disabled:opacity-50',\n ),\n {\n variants: {\n variant: {\n primary: 'bg-(--el-accent) text-(--el-accent-text) hover:opacity-90',\n secondary:\n 'bg-transparent text-(--el-text) border border-(--el-button-border) hover:bg-(--el-surface)',\n ghost: 'bg-transparent text-(--el-text) hover:bg-(--el-surface)',\n danger: 'bg-(--el-danger) text-(--el-danger-text) hover:opacity-90',\n },\n size: {\n sm: 'h-(--height-btn-sm) px-3 text-xs',\n md: 'h-(--height-btn-md) px-(--spacing-btn-x)',\n lg: 'h-(--height-btn-lg) px-6 text-base',\n },\n },\n defaultVariants: {\n variant: 'primary',\n size: 'md',\n },\n },\n);\n\nexport interface ButtonProps\n extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {\n /** Shows inline Spinner + disables interaction. */\n loading?: boolean;\n /** Icon rendered before the label. */\n leftIcon?: ReactNode;\n /** Icon rendered after the label. */\n rightIcon?: ReactNode;\n}\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n { variant, size, loading, leftIcon, rightIcon, disabled, className, children, ...rest },\n ref,\n) {\n return (\n <button\n ref={ref}\n type=\"button\"\n // `data-variant` is the hook the 3D / Immersive style uses to give FILLED\n // buttons physical thickness (a solid palette-derived base edge) + a\n // press-DOWN (7.3.39). Inert under every other style.\n data-variant={variant ?? 'primary'}\n className={cn(buttonVariants({ variant, size }), className)}\n disabled={disabled || loading}\n aria-busy={loading || undefined}\n {...rest}\n >\n {loading ? (\n <Spinner size={size === 'lg' ? 'md' : 'sm'} aria-hidden />\n ) : leftIcon ? (\n <span aria-hidden className=\"inline-flex\">\n {leftIcon}\n </span>\n ) : null}\n <span>{children}</span>\n {!loading && rightIcon ? (\n <span aria-hidden className=\"inline-flex\">\n {rightIcon}\n </span>\n ) : null}\n </button>\n );\n});\n","import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../../utils/cn';\n\n/**\n * Card — content container with optional header/footer slots and tint variants.\n *\n * Compose with `header` / `footer` props for the common pattern, or just\n * pass children for a bare card. `clickable` makes the whole card a button\n * (use `onClick` + Enter/Space; pass `href` to render as `<a>` instead).\n *\n * Tint variants use the pastel feature tints from the palette\n * (peach/rose/mint/lavender/sky/yellow) — apply sparingly, never to\n * page-level surfaces.\n *\n * @example\n * <Card header={<h3>Title</h3>}>Body content</Card>\n * <Card tint=\"lavender\" clickable onClick={open}>Pastel card</Card>\n * <Card tint=\"mint\"><a href=\"/x\">Link wrapper</a></Card>\n */\nconst cardVariants = cva(\n cn(\n 'rounded-(--radius-card) border border-(--el-border)',\n 'p-(--spacing-card-padding)',\n 'transition-shadow duration-(--transition-duration)',\n ),\n {\n variants: {\n tint: {\n none: 'bg-(--el-card)',\n peach: 'bg-(--el-tint-peach) border-transparent',\n rose: 'bg-(--el-tint-rose) border-transparent',\n mint: 'bg-(--el-tint-mint) border-transparent',\n lavender: 'bg-(--el-tint-lavender) border-transparent',\n sky: 'bg-(--el-tint-sky) border-transparent',\n yellow: 'bg-(--el-tint-yellow) border-transparent',\n },\n clickable: {\n true: 'cursor-pointer hover:shadow-(--shadow-card) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--focus-ring-color) focus-visible:ring-offset-2 focus-visible:ring-offset-background',\n false: '',\n },\n },\n defaultVariants: { tint: 'none', clickable: false },\n },\n);\n\nexport interface CardProps\n extends Omit<HTMLAttributes<HTMLDivElement>, 'children'>, VariantProps<typeof cardVariants> {\n header?: ReactNode;\n footer?: ReactNode;\n children?: ReactNode;\n}\n\nexport const Card = forwardRef<HTMLDivElement, CardProps>(function Card(\n { tint, clickable, header, footer, className, children, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={cn(cardVariants({ tint, clickable }), className)}\n // `data-tilt` is the hook the 3D / Immersive style's pointer-parallax\n // engine (ImmersiveTilt, 7.3.39) uses to tip this tile toward the cursor.\n // Inert for every other style + under reduced motion; harmless otherwise.\n data-tilt=\"\"\n // `data-surface` is the hook a surface-MATERIAL style (e.g. glassmorphism)\n // uses to frost this panel — see globals.css's material layer. Only the\n // default (untinted) card opts in; a pastel-tint feature card keeps its\n // opaque tint, never a frosted overlay that would wash the hue out.\n {...(tint && tint !== 'none' ? {} : { 'data-surface': 'card' })}\n {...(clickable ? { tabIndex: 0, role: 'button' } : {})}\n {...rest}\n >\n {/* `data-tilt-layer` lifts the slot onto a translateZ plane under the 3D /\n Immersive style (7.3.39) so it PARALLAXES above the card face as the\n card tilts — `front` (the header/title pops most), `back` (the footer\n sits nearer the face). Inert under every other style + reduced motion.\n The body stays on the face so arbitrary content never renders on a\n skewed Z-plane. */}\n {header ? (\n <div data-tilt-layer=\"front\" className=\"mb-(--spacing-md)\">\n {header}\n </div>\n ) : null}\n <div>{children}</div>\n {footer ? (\n <div\n data-tilt-layer=\"back\"\n className=\"border-(--el-border) mt-(--spacing-md) border-t pt-(--spacing-md)\"\n >\n {footer}\n </div>\n ) : null}\n </div>\n );\n});\n","import { forwardRef, type HTMLAttributes } from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../../utils/cn';\n\n/**\n * Pill — compact status or severity label. Pure span, no interaction.\n *\n * Variant axes (use exactly one):\n * - `status`: planned | in-progress | done — Motir's Subtask lifecycle states.\n * - `severity`: info | success | warning | danger — semantic UI states.\n * - `priority`: highest | high | medium | low | lowest — work-item priority, a\n * 5-step diverging ramp (`--el-priority-*` hue tinted over the surface, plus a\n * redundant direction icon). The single source of truth is `PRIORITY_META`.\n * - `memberRole`: admin | member | viewer — a project membership role\n * (Story 6.4), one hued tint per role. (Named `memberRole`, not `role`, so\n * it can't collide with the DOM `role` ARIA attribute on the span.)\n * - `orgRole`: owner | admin | member — an ORGANIZATION role (Story 6.10),\n * sitting above the workspace `memberRole`. owner → lavender (the highest,\n * brand-purple tier), admin → sky, member → mint — per the org-admin design.\n * - `tone`: neutral — a non-semantic chip (counts, metadata). Dark text on a\n * neutral surface. `private` — the epic-level-privacy \"Not public\" badge\n * (Story 6.14 · design epic-privacy panels 1/2/6b): lavender tint + charcoal\n * text, the same AA-safe recipe as `status=\"planned\"` / `memberRole=\"admin\"`.\n *\n * All tones use adaptive dark/light text (`--color-charcoal`) on a hued tint,\n * so every variant clears WCAG AA contrast in both light and dark modes\n * (the hue lives in the background, not the text — PRODECT_FINDINGS #35).\n *\n * Always uses `--radius-badge` for fully rounded ends regardless of display style.\n *\n * @example\n * <Pill status=\"in-progress\">In progress</Pill>\n * <Pill severity=\"danger\">Validation failed</Pill>\n * <Pill tone=\"neutral\">3 members</Pill>\n */\nconst pillVariants = cva(\n cn(\n 'inline-flex items-center gap-1',\n 'rounded-(--radius-badge)',\n 'px-(--spacing-chip-x) py-(--spacing-chip-y)',\n 'font-sans text-xs font-medium',\n 'border',\n ),\n {\n variants: {\n // All colored tones carry the semantic hue in the TINT BACKGROUND and use\n // adaptive dark/light TEXT (`--color-charcoal`: #37352f light, #e5e5e5\n // dark) — the same pattern `planned` always used. The previous saturated\n // hue-on-tint TEXT failed WCAG AA (≈2.5–3.8:1 light, 3.2–4.0:1 dark);\n // charcoal-on-tint clears ~10:1 in both modes (PRODECT_FINDINGS #35).\n status: {\n planned: 'bg-(--el-tint-lavender) text-(--el-text-strong) border-transparent',\n 'in-progress': 'bg-(--el-tint-sky) text-(--el-text-strong) border-transparent',\n done: 'bg-(--el-tint-mint) text-(--el-text-strong) border-transparent',\n },\n severity: {\n info: 'bg-(--el-tint-sky) text-(--el-text-strong) border-transparent',\n success: 'bg-(--el-tint-mint) text-(--el-text-strong) border-transparent',\n warning: 'bg-(--el-tint-peach) text-(--el-text-strong) border-transparent',\n danger: 'bg-(--el-tint-rose) text-(--el-text-strong) border-transparent',\n },\n // Priority chips (MOTIR-1273 · 1266.2) — a 5-step diverging ramp keyed off\n // the `--el-priority-*` hues, un-collapsing `medium` and `lowest` (both\n // were `tone=\"neutral\"` grey). Each hue is diluted to a 14% tint over\n // `--el-surface` so charcoal `--el-text-strong` clears AA in both themes\n // (the hue lives in the background — finding #35); the redundant direction\n // icon (PRIORITY_META) is the non-colour cue. `medium` (slate) and\n // `lowest` (stone) resolve to two distinct greys in every palette.\n priority: {\n highest:\n 'bg-[color-mix(in_srgb,var(--el-priority-highest)_14%,var(--el-surface))] text-(--el-text-strong) border-transparent',\n high: 'bg-[color-mix(in_srgb,var(--el-priority-high)_14%,var(--el-surface))] text-(--el-text-strong) border-transparent',\n medium:\n 'bg-[color-mix(in_srgb,var(--el-priority-medium)_14%,var(--el-surface))] text-(--el-text-strong) border-transparent',\n low: 'bg-[color-mix(in_srgb,var(--el-priority-low)_14%,var(--el-surface))] text-(--el-text-strong) border-transparent',\n lowest:\n 'bg-[color-mix(in_srgb,var(--el-priority-lowest)_14%,var(--el-surface))] text-(--el-text-strong) border-transparent',\n },\n // Project membership roles (Story 6.4 · design/projects access-members):\n // admin → lavender, member → sky, viewer → mint — the hue in the tint,\n // charcoal text (AA-safe, finding #35), same recipe as `status`. Routed\n // through the DEDICATED `--el-role-*` tokens (MOTIR-1274 · 1266.3) so a\n // palette can tune roles apart from the other --el-tint-* meanings; each\n // defaults to its prior tint (zero visual change).\n memberRole: {\n admin: 'bg-(--el-role-admin) text-(--el-text-strong) border-transparent',\n member: 'bg-(--el-role-member) text-(--el-text-strong) border-transparent',\n viewer: 'bg-(--el-role-viewer) text-(--el-text-strong) border-transparent',\n },\n // Organization roles (Story 6.10 · design/org-admin): owner → lavender\n // (the highest, brand-purple tier), admin → sky, member → mint — the hue\n // in the tint, charcoal text (AA-safe, finding #35), same recipe as above.\n // Dedicated `--el-org-role-*` tokens (MOTIR-1274 · 1266.3); zero change.\n orgRole: {\n owner: 'bg-(--el-org-role-owner) text-(--el-text-strong) border-transparent',\n admin: 'bg-(--el-org-role-admin) text-(--el-text-strong) border-transparent',\n member: 'bg-(--el-org-role-member) text-(--el-text-strong) border-transparent',\n },\n tone: {\n neutral: 'bg-(--el-chip-bg) text-(--el-text-secondary) border-(--el-chip-border)',\n // Epic-level privacy \"Not public\" badge (Story 6.14 · design\n // epic-privacy panels 1/2/6b) — lavender tint, charcoal text, the same\n // AA-safe recipe as `status=\"planned\"` / `memberRole=\"admin\"` (≈9.7:1).\n // Dedicated `--el-privacy-private` token (MOTIR-1274 · 1266.3); the\n // matching `--el-privacy-public` is defined ahead of a \"public\" badge.\n private: 'bg-(--el-privacy-private) text-(--el-text-strong) border-transparent',\n // Inactive \"Archived\" badge (MOTIR-1276 · 1266.5) — a QUIET muted fill\n // with slate ink, not a hued tint: archived is an inactive state, not a\n // severity. Dedicated `--el-archived-pill-*` tokens so a palette can tune\n // the inactive badge apart from the neutral chip; AA-safe (slate on muted\n // clears AA both themes).\n archived: 'bg-(--el-archived-pill-bg) text-(--el-archived-pill-text) border-transparent',\n },\n },\n defaultVariants: {},\n },\n);\n\nexport interface PillProps\n extends HTMLAttributes<HTMLSpanElement>, VariantProps<typeof pillVariants> {}\n\nexport const Pill = forwardRef<HTMLSpanElement, PillProps>(function Pill(\n { status, severity, priority, memberRole, orgRole, tone, className, children, ...rest },\n ref,\n) {\n return (\n <span\n ref={ref}\n className={cn(\n pillVariants({ status, severity, priority, memberRole, orgRole, tone }),\n className,\n )}\n {...rest}\n >\n {children}\n </span>\n );\n});\n","import type { ReactNode } from 'react';\nimport { BookOpen, Search, SlidersHorizontal } from 'lucide-react';\nimport { cn } from '../../utils/cn';\nimport { buttonVariants } from '../ui/Button';\nimport { Card } from '../ui/Card';\nimport { Pill } from '../ui/Pill';\nimport type { StyleId } from '../../theme/styles';\nimport type { PaletteId } from '../../theme/palettes';\nimport type { TypeId } from '../../theme/typography';\n\n/**\n * StyleVignette — the LIVE preview specimen that makes a design axis's FEEL\n * legible (Subtask 7.3.37 / MOTIR-1050).\n *\n * The fix for \"I only see typography change\": instead of a token-swatch table,\n * this is a composed, realistic MINI-SURFACE — a nav rail, a work-item card, a\n * search input, a button row, and a floating modal — rendered LIVE under a\n * style's shape/feel tokens + component overrides (and, independently, a palette\n * and a type pairing). Switching any axis re-shapes / re-skins / re-types the\n * whole vignette, so the user sees the product's feel, not a colour chip.\n *\n * ── Two modes ────────────────────────────────────────────────────────────\n * • LIVE (no axis props) — the wrapper sets NO `data-*` axis attribute, so it\n * INHERITS the active theme from `<html>` (driven by `theme-context`). Use\n * this for the appearance-settings preview and the design-step showcase: the\n * vignette re-renders automatically as the global selection changes.\n * • SCOPED (any of `styleId` / `palette` / `type`) — the wrapper carries that\n * axis attribute, so the globals.css axis block recomputes the tokens for\n * this subtree ONLY. Use this for the onboarding Style gallery (one vignette\n * per style) and the Palette step (one style re-rendered under each palette).\n *\n * Built on the `app/tokens/page.tsx` specimen pattern: real `--el-*` colour\n * tokens + element-semantic shape tokens + the `data-surface` material hooks, so\n * surface-material styles (glassmorphism's frosted glass, cybercore's glow grid)\n * apply exactly as they do in the shipped app. NEVER a Tier-0 `--color-*` or a\n * raw `rounded-*`/`p-*`/`h-*` (the colour + shape token rules in CLAUDE.md).\n *\n * Nesting caveat: a SCOPED base entry (`warm-editorial` / `motir` palette /\n * `motir` type) has no globals.css override block, so when it is nested under a\n * NON-base `<html>` it inherits that ancestor's axis rather than resetting to\n * the base. In the real consumers this never bites — onboarding runs against the\n * default `<html>`, and the appearance preview uses LIVE mode (inherits the\n * active selection it is meant to show). Full nested-base isolation (a reset\n * block or an iframe) is a noted follow-up if a future consumer needs it.\n */\n\nexport interface StyleVignetteProps {\n /** Scope the vignette to a specific style; omit to inherit the active style. */\n styleId?: StyleId;\n /** Scope the vignette to a specific palette; omit to inherit the active palette. */\n palette?: PaletteId;\n /** Scope the vignette to a specific type pairing; omit to inherit the active type. */\n type?: TypeId;\n /**\n * Accessible name for the preview region. The vignette is decorative chrome,\n * so it is announced as a single labelled image rather than a pile of\n * read-out controls.\n */\n label?: string;\n className?: string;\n}\n\n/** A muted progress bar; `accent` tints it with the active accent. */\nfunction Bar({ accent, widthPct }: { accent?: boolean; widthPct: number }) {\n return (\n <div className=\"h-[6px] w-full overflow-hidden rounded-(--radius-badge) bg-(--el-muted)\">\n <div\n className={cn(\n 'h-full rounded-(--radius-badge)',\n accent ? 'bg-(--el-accent)' : 'bg-(--el-border-strong)',\n )}\n style={{ width: `${widthPct}%` }}\n />\n </div>\n );\n}\n\n/** One row of the work-item card's mini list — a status dot + label + chip. */\nfunction ListRow({ dotVar, label, chip }: { dotVar: string; label: string; chip: string }) {\n return (\n <div className=\"flex items-center gap-2 text-[11px] text-(--el-text-secondary)\">\n <span\n className=\"size-2 shrink-0 rounded-full\"\n style={{ background: `var(${dotVar})` }}\n aria-hidden\n />\n <span className=\"truncate\">{label}</span>\n <span className=\"ml-auto shrink-0 rounded-(--radius-badge) bg-(--el-surface) px-(--spacing-chip-x) py-(--spacing-chip-y) text-[9px] font-medium text-(--el-text-secondary)\">\n {chip}\n </span>\n </div>\n );\n}\n\nexport function StyleVignette({ styleId, palette, type, label, className }: StyleVignetteProps) {\n // Only emit an axis attribute when the caller pins it; an omitted axis stays\n // inherited (LIVE mode), which is how the appearance preview follows the\n // global selection without re-implementing the theme context.\n const axisAttrs: Record<string, string> = {};\n if (styleId) axisAttrs['data-style'] = styleId;\n if (palette) axisAttrs['data-palette'] = palette;\n if (type) axisAttrs['data-type'] = type;\n\n return (\n <div\n {...axisAttrs}\n role=\"img\"\n aria-label={label ?? 'Design style preview'}\n className={cn(\n 'style-vignette relative isolate overflow-hidden',\n 'rounded-(--radius-card) border border-(--el-border) bg-(--el-surface)',\n 'shadow-(--shadow-subtle)',\n className,\n )}\n >\n {/* Material canvas: transparent for flat styles; the glassmorphism gradient\n / cybercore grid is painted here via the descendant rules in\n globals.css (a preview can't lean on the body-scoped canvas). */}\n <span className=\"sv-canvas pointer-events-none absolute inset-0 -z-10\" aria-hidden />\n\n <div className=\"flex flex-col gap-3 p-3\">\n {/* NAV — a compact rail; data-surface lets a material style frost it. */}\n <div\n data-surface=\"sidebar\"\n className=\"flex items-center gap-2 rounded-(--radius-control) bg-(--el-sidebar-bg) px-3 py-2\"\n >\n <span className=\"size-2.5 rounded-full bg-(--el-accent)\" aria-hidden />\n <span className=\"font-serif text-[12px] font-semibold text-(--el-text)\">Acme</span>\n <span className=\"ml-2 rounded-(--radius-control) bg-(--el-accent) px-2 py-[3px] text-[10px] font-medium text-(--el-accent-text)\">\n Board\n </span>\n <span className=\"text-[10px] text-(--el-text-muted)\">Backlog</span>\n <span className=\"text-[10px] text-(--el-text-muted)\">Reports</span>\n <span className=\"ml-auto size-5 rounded-full bg-(--el-muted)\" aria-hidden />\n </div>\n\n <div className=\"grid grid-cols-[1.4fr_1fr] gap-3\">\n {/* WORK-ITEM CARD — the centrepiece: type hue + serif title + status. */}\n <Card className=\"flex flex-col gap-2 !p-3\">\n <div className=\"flex items-center gap-2\">\n {/* A decorative story-type icon in its `--el-type-story` hue. The\n package can't depend on motir-core's domain `IssueTypeIcon`\n (which maps work-item kinds → glyphs), so the vignette inlines\n the same glyph + hue directly — pixel-identical, boundary-clean. */}\n <BookOpen className=\"size-4 text-(--el-type-story)\" aria-hidden />\n <span className=\"truncate font-serif text-[13px] font-semibold text-(--el-text)\">\n Ship the billing flow\n </span>\n <Pill status=\"in-progress\" className=\"ml-auto !text-[9px]\">\n In progress\n </Pill>\n </div>\n <div className=\"flex flex-col gap-1.5\">\n <ListRow dotVar=\"--el-success\" label=\"Invoice schema\" chip=\"Done\" />\n <ListRow dotVar=\"--el-warning\" label=\"Dunning emails\" chip=\"Review\" />\n <ListRow dotVar=\"--el-info\" label=\"Stripe webhook\" chip=\"To do\" />\n </div>\n <div className=\"mt-0.5 flex flex-col gap-1.5\">\n <Bar accent widthPct={66} />\n <Bar widthPct={38} />\n </div>\n </Card>\n\n {/* MODAL — a floating dialog panel; modal radius + elevation + scrim. */}\n <div className=\"relative\">\n <span\n className=\"pointer-events-none absolute -inset-1 rounded-(--radius-card) bg-black/10\"\n aria-hidden\n />\n <div\n data-surface=\"modal\"\n className=\"relative flex flex-col gap-2 rounded-(--radius-modal) border border-(--el-border) bg-(--el-page-bg) p-3 shadow-(--shadow-modal)\"\n >\n <span className=\"font-serif text-[12px] font-semibold text-(--el-text)\">\n New invoice\n </span>\n <Bar widthPct={100} />\n <Bar widthPct={72} />\n <div className=\"mt-1 flex gap-2\">\n <span\n className={cn(\n buttonVariants({ variant: 'primary', size: 'sm' }),\n '!h-7 !px-3 !text-[10px]',\n )}\n >\n Create\n </span>\n <span\n className={cn(\n buttonVariants({ variant: 'ghost', size: 'sm' }),\n '!h-7 !px-3 !text-[10px]',\n )}\n >\n Cancel\n </span>\n </div>\n </div>\n </div>\n </div>\n\n {/* INPUT — a search field; data-surface lets a material style fill it. */}\n <div\n data-surface=\"input\"\n className=\"flex h-(--height-input) items-center gap-2 rounded-(--radius-input) border border-(--el-border-strong) bg-(--el-page-bg) px-(--spacing-input-x) text-[11px] text-(--el-text-muted)\"\n >\n <Search className=\"size-3.5\" aria-hidden />\n <span>Search work items…</span>\n </div>\n\n {/* BUTTON ROW — primary + ghost, exact Button silhouettes (non-interactive). */}\n <div className=\"flex gap-2\">\n <span className={cn(buttonVariants({ variant: 'primary', size: 'sm' }), '!text-[11px]')}>\n New work item\n </span>\n <span className={cn(buttonVariants({ variant: 'ghost', size: 'sm' }), '!text-[11px]')}>\n <SlidersHorizontal className=\"size-3.5\" aria-hidden />\n Filter\n </span>\n </div>\n </div>\n </div>\n );\n}\n\nexport default StyleVignette as (props: StyleVignetteProps) => ReactNode;\n"]}
1
+ {"version":3,"sources":["../../../src/utils/cn.ts","../../../src/components/ui/Spinner.tsx","../../../src/components/ui/Button.tsx","../../../src/components/ui/Card.tsx","../../../src/components/ui/Pill.tsx","../../../src/components/theme/StyleVignette.tsx"],"names":["Spinner","cva","forwardRef","Button","jsx","Card","jsxs","Pill"],"mappings":"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","file":"StyleVignette.js","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/**\n * Merge Tailwind class names without conflicts.\n *\n * `clsx` handles conditional inclusion (booleans, undefineds, arrays);\n * `twMerge` resolves conflicting Tailwind classes (later wins, e.g.\n * `px-2 px-4` → `px-4`). Together they are the de facto standard for\n * composing Tailwind classes in React components.\n *\n * @example\n * cn('px-2 py-1', isActive && 'bg-primary', className)\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","import { forwardRef, type HTMLAttributes } from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../../utils/cn';\n\n/**\n * Spinner — pure CSS rotation indicator.\n *\n * Three sizes; works standalone or inside a Button's loading state.\n *\n * @example\n * <Spinner size=\"md\" />\n * <Spinner size=\"sm\" aria-label=\"Loading\" />\n */\nconst spinnerVariants = cva(\n 'inline-block animate-spin rounded-full border-current border-t-transparent',\n {\n variants: {\n size: {\n sm: 'h-4 w-4 border-[1.5px]',\n md: 'h-5 w-5 border-2',\n lg: 'h-8 w-8 border-[3px]',\n },\n },\n defaultVariants: { size: 'md' },\n },\n);\n\nexport interface SpinnerProps\n extends Omit<HTMLAttributes<HTMLSpanElement>, 'role'>, VariantProps<typeof spinnerVariants> {}\n\nexport const Spinner = forwardRef<HTMLSpanElement, SpinnerProps>(function Spinner(\n { className, size, ...rest },\n ref,\n) {\n return (\n <span\n ref={ref}\n role=\"status\"\n aria-label={rest['aria-label'] ?? 'Loading'}\n className={cn(spinnerVariants({ size }), className)}\n {...rest}\n />\n );\n});\n","import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { Spinner } from './Spinner';\nimport { cn } from '../../utils/cn';\n\n/**\n * Button — primary interactive primitive.\n *\n * Variants × sizes × states. The `loading` state shows an inline Spinner\n * and disables the button. Use semantic Tailwind token classes; never\n * hardcode colors. Shape responds to `data-style` via\n * `--radius-btn` (rectangles in default, pills in `soft`).\n *\n * @example\n * <Button variant=\"primary\" size=\"md\">Save</Button>\n * <Button variant=\"ghost\" leftIcon={<Plus />} loading>Saving…</Button>\n */\n// Exported so a Next `<Link>` can render AS a styled button (an anchor styled\n// with the button variants — keeps real-link semantics for navigation) without\n// nesting a <button> inside an <a>. Used by the /ready empty state's \"View all\n// issues\" link (Subtask 7.0.6).\nexport const buttonVariants = cva(\n cn(\n // Base layout\n 'inline-flex items-center justify-center gap-2',\n // Typography\n 'font-sans text-sm font-medium leading-none',\n // Shape — semantic tokens that flip with the active style\n 'rounded-(--radius-btn)',\n // Interaction\n 'transition-[transform,background-color,border-color,color] duration-(--transition-duration)',\n 'active:scale-(--active-scale)',\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--focus-ring-color) focus-visible:ring-offset-2 focus-visible:ring-offset-background',\n 'disabled:pointer-events-none disabled:opacity-50',\n ),\n {\n variants: {\n variant: {\n primary: 'bg-(--el-accent) text-(--el-accent-text) hover:opacity-90',\n secondary:\n 'bg-transparent text-(--el-text) border border-(--el-button-border) hover:bg-(--el-surface)',\n ghost: 'bg-transparent text-(--el-text) hover:bg-(--el-surface)',\n danger: 'bg-(--el-danger) text-(--el-danger-text) hover:opacity-90',\n },\n size: {\n sm: 'h-(--height-btn-sm) px-3 text-xs',\n md: 'h-(--height-btn-md) px-(--spacing-btn-x)',\n lg: 'h-(--height-btn-lg) px-6 text-base',\n },\n },\n defaultVariants: {\n variant: 'primary',\n size: 'md',\n },\n },\n);\n\nexport interface ButtonProps\n extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {\n /** Shows inline Spinner + disables interaction. */\n loading?: boolean;\n /** Icon rendered before the label. */\n leftIcon?: ReactNode;\n /** Icon rendered after the label. */\n rightIcon?: ReactNode;\n}\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n { variant, size, loading, leftIcon, rightIcon, disabled, className, children, ...rest },\n ref,\n) {\n return (\n <button\n ref={ref}\n type=\"button\"\n // `data-variant` is the hook the 3D / Immersive style uses to give FILLED\n // buttons physical thickness (a solid palette-derived base edge) + a\n // press-DOWN (7.3.39). Inert under every other style.\n data-variant={variant ?? 'primary'}\n className={cn(buttonVariants({ variant, size }), className)}\n disabled={disabled || loading}\n aria-busy={loading || undefined}\n {...rest}\n >\n {loading ? (\n <Spinner size={size === 'lg' ? 'md' : 'sm'} aria-hidden />\n ) : leftIcon ? (\n <span aria-hidden className=\"inline-flex\">\n {leftIcon}\n </span>\n ) : null}\n <span>{children}</span>\n {!loading && rightIcon ? (\n <span aria-hidden className=\"inline-flex\">\n {rightIcon}\n </span>\n ) : null}\n </button>\n );\n});\n","import { forwardRef, type HTMLAttributes, type ReactNode } from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../../utils/cn';\n\n/**\n * Card — content container with optional header/footer slots and tint variants.\n *\n * Compose with `header` / `footer` props for the common pattern, or just\n * pass children for a bare card. `clickable` makes the whole card a button\n * (use `onClick` + Enter/Space; pass `href` to render as `<a>` instead).\n *\n * Tint variants use the pastel feature tints from the palette\n * (peach/rose/mint/lavender/sky/yellow) — apply sparingly, never to\n * page-level surfaces.\n *\n * @example\n * <Card header={<h3>Title</h3>}>Body content</Card>\n * <Card tint=\"lavender\" clickable onClick={open}>Pastel card</Card>\n * <Card tint=\"mint\"><a href=\"/x\">Link wrapper</a></Card>\n */\nconst cardVariants = cva(\n cn(\n 'rounded-(--radius-card) border border-(--el-border)',\n 'p-(--spacing-card-padding)',\n 'transition-shadow duration-(--transition-duration)',\n ),\n {\n variants: {\n tint: {\n none: 'bg-(--el-card)',\n peach: 'bg-(--el-tint-peach) border-transparent',\n rose: 'bg-(--el-tint-rose) border-transparent',\n mint: 'bg-(--el-tint-mint) border-transparent',\n lavender: 'bg-(--el-tint-lavender) border-transparent',\n sky: 'bg-(--el-tint-sky) border-transparent',\n yellow: 'bg-(--el-tint-yellow) border-transparent',\n },\n clickable: {\n true: 'cursor-pointer hover:shadow-(--shadow-card) focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-(--focus-ring-color) focus-visible:ring-offset-2 focus-visible:ring-offset-background',\n false: '',\n },\n },\n defaultVariants: { tint: 'none', clickable: false },\n },\n);\n\nexport interface CardProps\n extends Omit<HTMLAttributes<HTMLDivElement>, 'children'>, VariantProps<typeof cardVariants> {\n header?: ReactNode;\n footer?: ReactNode;\n children?: ReactNode;\n}\n\nexport const Card = forwardRef<HTMLDivElement, CardProps>(function Card(\n { tint, clickable, header, footer, className, children, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={cn(cardVariants({ tint, clickable }), className)}\n // `data-tilt` is the hook the 3D / Immersive style's pointer-parallax\n // engine (ImmersiveTilt, 7.3.39) uses to tip this tile toward the cursor.\n // Inert for every other style + under reduced motion; harmless otherwise.\n data-tilt=\"\"\n // `data-surface` is the hook a surface-MATERIAL style (e.g. glassmorphism)\n // uses to frost this panel — see globals.css's material layer. Only the\n // default (untinted) card opts in; a pastel-tint feature card keeps its\n // opaque tint, never a frosted overlay that would wash the hue out.\n {...(tint && tint !== 'none' ? {} : { 'data-surface': 'card' })}\n {...(clickable ? { tabIndex: 0, role: 'button' } : {})}\n {...rest}\n >\n {/* `data-tilt-layer` lifts the slot onto a translateZ plane under the 3D /\n Immersive style (7.3.39) so it PARALLAXES above the card face as the\n card tilts — `front` (the header/title pops most), `back` (the footer\n sits nearer the face). Inert under every other style + reduced motion.\n The body stays on the face so arbitrary content never renders on a\n skewed Z-plane. */}\n {header ? (\n <div data-tilt-layer=\"front\" className=\"mb-(--spacing-md)\">\n {header}\n </div>\n ) : null}\n <div>{children}</div>\n {footer ? (\n <div\n data-tilt-layer=\"back\"\n className=\"border-(--el-border) mt-(--spacing-md) border-t pt-(--spacing-md)\"\n >\n {footer}\n </div>\n ) : null}\n </div>\n );\n});\n","import { forwardRef, type HTMLAttributes } from 'react';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../../utils/cn';\n\n/**\n * Pill — compact status or severity label. Pure span, no interaction.\n *\n * Variant axes (use exactly one):\n * - `status`: planned | in-progress | implemented | done — Motir's Subtask\n * lifecycle states. The first three are per-CATEGORY tones; `implemented` is\n * the one PER-STATUS tone (MOTIR-3103), because it shares a category with\n * In Progress / Planning / In Review and must not share their chip.\n * - `severity`: info | success | warning | danger — semantic UI states.\n * - `priority`: highest | high | medium | low | lowest — work-item priority, a\n * 5-step diverging ramp (`--el-priority-*` hue tinted over the surface, plus a\n * redundant direction icon). The single source of truth is `PRIORITY_META`.\n * - `memberRole`: admin | member | viewer | custom — a project membership role\n * (Story 6.4), one hued tint per role. (Named `memberRole`, not `role`, so\n * it can't collide with the DOM `role` ARIA attribute on the span.)\n * - `orgRole`: owner | admin | member — an ORGANIZATION role (Story 6.10),\n * sitting above the workspace `memberRole`. owner → lavender (the highest,\n * brand-purple tier), admin → sky, member → mint — per the org-admin design.\n * - `tone`: neutral — a non-semantic chip (counts, metadata). Dark text on a\n * neutral surface. `private` — the epic-level-privacy \"Not public\" badge\n * (Story 6.14 · design epic-privacy panels 1/2/6b): lavender tint + charcoal\n * text, the same AA-safe recipe as `status=\"planned\"` / `memberRole=\"admin\"`.\n *\n * All tones use adaptive dark/light text (`--color-charcoal`) on a hued tint,\n * so every variant clears WCAG AA contrast in both light and dark modes\n * (the hue lives in the background, not the text — PRODECT_FINDINGS #35).\n *\n * Always uses `--radius-badge` for fully rounded ends regardless of display style.\n *\n * @example\n * <Pill status=\"in-progress\">In progress</Pill>\n * <Pill severity=\"danger\">Validation failed</Pill>\n * <Pill tone=\"neutral\">3 members</Pill>\n */\nconst pillVariants = cva(\n cn(\n 'inline-flex items-center gap-1',\n 'rounded-(--radius-badge)',\n 'px-(--spacing-chip-x) py-(--spacing-chip-y)',\n 'font-sans text-xs font-medium',\n 'border',\n ),\n {\n variants: {\n // All colored tones carry the semantic hue in the TINT BACKGROUND and use\n // adaptive dark/light TEXT (`--color-charcoal`: #37352f light, #e5e5e5\n // dark) — the same pattern `planned` always used. The previous saturated\n // hue-on-tint TEXT failed WCAG AA (≈2.5–3.8:1 light, 3.2–4.0:1 dark);\n // charcoal-on-tint clears ~10:1 in both modes (PRODECT_FINDINGS #35).\n status: {\n planned: 'bg-(--el-tint-lavender) text-(--el-text-strong) border-transparent',\n 'in-progress': 'bg-(--el-tint-sky) text-(--el-text-strong) border-transparent',\n // BUILT, waiting on checks (MOTIR-3103). The first PER-STATUS tone on\n // this axis — the other three are per-CATEGORY, and `implemented` sits in\n // the same `in_progress` category as In Progress, Planning and In Review,\n // so without its own tone four statuses render one chip.\n //\n // It uses the recipe theme.css's own `--el-status-*` header prescribes —\n // \"a status CHIP bg = color-mix(var(--el-status-X) 14%, var(--el-surface))\n // with --el-text-strong\" — which is the same dilution the `priority`\n // variant below already renders. The hue is not spent by any other\n // status in any palette (measured at ΔE2000 >= 10 across all ten by\n // tests/theme/statusHueSeparation.test.ts), and the chip additionally\n // carries a GLYPH, because a 14% tint is a subtle mark by construction\n // and finding #35 forbids resting a state on colour alone.\n implemented:\n 'bg-[color-mix(in_srgb,var(--el-status-implemented)_14%,var(--el-surface))] text-(--el-text-strong) border-transparent',\n done: 'bg-(--el-tint-mint) text-(--el-text-strong) border-transparent',\n },\n severity: {\n info: 'bg-(--el-tint-sky) text-(--el-text-strong) border-transparent',\n success: 'bg-(--el-tint-mint) text-(--el-text-strong) border-transparent',\n warning: 'bg-(--el-tint-peach) text-(--el-text-strong) border-transparent',\n danger: 'bg-(--el-tint-rose) text-(--el-text-strong) border-transparent',\n },\n // Priority chips (MOTIR-1273 · 1266.2) — a 5-step diverging ramp keyed off\n // the `--el-priority-*` hues, un-collapsing `medium` and `lowest` (both\n // were `tone=\"neutral\"` grey). Each hue is diluted to a 14% tint over\n // `--el-surface` so charcoal `--el-text-strong` clears AA in both themes\n // (the hue lives in the background — finding #35); the redundant direction\n // icon (PRIORITY_META) is the non-colour cue. `medium` (slate) and\n // `lowest` (stone) resolve to two distinct greys in every palette.\n priority: {\n highest:\n 'bg-[color-mix(in_srgb,var(--el-priority-highest)_14%,var(--el-surface))] text-(--el-text-strong) border-transparent',\n high: 'bg-[color-mix(in_srgb,var(--el-priority-high)_14%,var(--el-surface))] text-(--el-text-strong) border-transparent',\n medium:\n 'bg-[color-mix(in_srgb,var(--el-priority-medium)_14%,var(--el-surface))] text-(--el-text-strong) border-transparent',\n low: 'bg-[color-mix(in_srgb,var(--el-priority-low)_14%,var(--el-surface))] text-(--el-text-strong) border-transparent',\n lowest:\n 'bg-[color-mix(in_srgb,var(--el-priority-lowest)_14%,var(--el-surface))] text-(--el-text-strong) border-transparent',\n },\n // Project membership roles (Story 6.4 · design/projects access-members):\n // admin → lavender, member → sky, viewer → mint — the hue in the tint,\n // charcoal text (AA-safe, finding #35), same recipe as `status`. Routed\n // through the DEDICATED `--el-role-*` tokens (MOTIR-1274 · 1266.3) so a\n // palette can tune roles apart from the other --el-tint-* meanings; each\n // defaults to its prior tint (zero visual change).\n // `custom` (MOTIR-2257 · MOTIR-2485) is a project's OWN role, which has no\n // enum value — it takes the next free tint slot (peach) because this\n // surface had already spent lavender / sky / mint on the built-ins, so a\n // custom chip in any of those would read as one of them. The KIND is\n // carried in WORDS by the chip's accessible name; nothing rests on the hue.\n memberRole: {\n admin: 'bg-(--el-role-admin) text-(--el-text-strong) border-transparent',\n member: 'bg-(--el-role-member) text-(--el-text-strong) border-transparent',\n viewer: 'bg-(--el-role-viewer) text-(--el-text-strong) border-transparent',\n custom: 'bg-(--el-role-custom) text-(--el-text-strong) border-transparent',\n },\n // Organization roles (Story 6.10 · design/org-admin): owner → lavender\n // (the highest, brand-purple tier), admin → sky, member → mint — the hue\n // in the tint, charcoal text (AA-safe, finding #35), same recipe as above.\n // Dedicated `--el-org-role-*` tokens (MOTIR-1274 · 1266.3); zero change.\n orgRole: {\n owner: 'bg-(--el-org-role-owner) text-(--el-text-strong) border-transparent',\n admin: 'bg-(--el-org-role-admin) text-(--el-text-strong) border-transparent',\n member: 'bg-(--el-org-role-member) text-(--el-text-strong) border-transparent',\n },\n tone: {\n neutral: 'bg-(--el-chip-bg) text-(--el-text-secondary) border-(--el-chip-border)',\n // Epic-level privacy \"Not public\" badge (Story 6.14 · design\n // epic-privacy panels 1/2/6b) — lavender tint, charcoal text, the same\n // AA-safe recipe as `status=\"planned\"` / `memberRole=\"admin\"` (≈9.7:1).\n // Dedicated `--el-privacy-private` token (MOTIR-1274 · 1266.3); the\n // matching `--el-privacy-public` is defined ahead of a \"public\" badge.\n private: 'bg-(--el-privacy-private) text-(--el-text-strong) border-transparent',\n // Inactive \"Archived\" badge (MOTIR-1276 · 1266.5) — a QUIET muted fill\n // with slate ink, not a hued tint: archived is an inactive state, not a\n // severity. Dedicated `--el-archived-pill-*` tokens so a palette can tune\n // the inactive badge apart from the neutral chip; AA-safe (slate on muted\n // clears AA both themes).\n archived: 'bg-(--el-archived-pill-bg) text-(--el-archived-pill-text) border-transparent',\n },\n },\n defaultVariants: {},\n },\n);\n\nexport interface PillProps\n extends HTMLAttributes<HTMLSpanElement>, VariantProps<typeof pillVariants> {}\n\nexport const Pill = forwardRef<HTMLSpanElement, PillProps>(function Pill(\n { status, severity, priority, memberRole, orgRole, tone, className, children, ...rest },\n ref,\n) {\n return (\n <span\n ref={ref}\n className={cn(\n pillVariants({ status, severity, priority, memberRole, orgRole, tone }),\n className,\n )}\n {...rest}\n >\n {children}\n </span>\n );\n});\n","import type { ReactNode } from 'react';\nimport { BookOpen, Search, SlidersHorizontal } from 'lucide-react';\nimport { cn } from '../../utils/cn';\nimport { buttonVariants } from '../ui/Button';\nimport { Card } from '../ui/Card';\nimport { Pill } from '../ui/Pill';\nimport type { StyleId } from '../../theme/styles';\nimport type { PaletteId } from '../../theme/palettes';\nimport type { TypeId } from '../../theme/typography';\n\n/**\n * StyleVignette — the LIVE preview specimen that makes a design axis's FEEL\n * legible (Subtask 7.3.37 / MOTIR-1050).\n *\n * The fix for \"I only see typography change\": instead of a token-swatch table,\n * this is a composed, realistic MINI-SURFACE — a nav rail, a work-item card, a\n * search input, a button row, and a floating modal — rendered LIVE under a\n * style's shape/feel tokens + component overrides (and, independently, a palette\n * and a type pairing). Switching any axis re-shapes / re-skins / re-types the\n * whole vignette, so the user sees the product's feel, not a colour chip.\n *\n * ── Two modes ────────────────────────────────────────────────────────────\n * • LIVE (no axis props) — the wrapper sets NO `data-*` axis attribute, so it\n * INHERITS the active theme from `<html>` (driven by `theme-context`). Use\n * this for the appearance-settings preview and the design-step showcase: the\n * vignette re-renders automatically as the global selection changes.\n * • SCOPED (any of `styleId` / `palette` / `type`) — the wrapper carries that\n * axis attribute, so the globals.css axis block recomputes the tokens for\n * this subtree ONLY. Use this for the onboarding Style gallery (one vignette\n * per style) and the Palette step (one style re-rendered under each palette).\n *\n * Built on the `app/tokens/page.tsx` specimen pattern: real `--el-*` colour\n * tokens + element-semantic shape tokens + the `data-surface` material hooks, so\n * surface-material styles (glassmorphism's frosted glass, cybercore's glow grid)\n * apply exactly as they do in the shipped app. NEVER a Tier-0 `--color-*` or a\n * raw `rounded-*`/`p-*`/`h-*` (the colour + shape token rules in CLAUDE.md).\n *\n * ⚠️ WHAT A SCOPED VIGNETTE IS AND IS NOT ISOLATED FROM (rewritten, MOTIR-3933).\n * This paragraph used to describe the nested-base leak and then discharge it\n * with \"in the real consumers this never bites\". That was a claim about the\n * consumer set at the time of writing, recorded as though it were a property of\n * the component — and it is what let a public page ship ten identical palette\n * previews. The consumer set grew. Measured, not assumed:\n *\n * • COLOUR (`palette`) — ISOLATED. The wrapper carries `data-appearance-scope`\n * alongside the axis attribute, which re-emits the Tier-3 `--el-*` layer\n * onto this element so it reads the `--color-*` this subtree overrides\n * rather than the ones `:root` declared. theme.css states that requirement\n * at the declaration site; this component did not honour it, and without it\n * a scoped palette changed nothing the vignette paints with. The base\n * `motir` entry now has its own block, and every per-palette dark block has\n * a descendant arm, so a scoped tile is correct in BOTH themes and for the\n * base entry too.\n * • TYPE (`type`) — ISOLATED, same mechanism plus a base `[data-type='motir']`\n * block. That axis ships no descendant rules, so the tokens are all of it.\n * • SHAPE / MATERIAL (`styleId`) — **NOT ISOLATED, and this is open.** The\n * style axis ships ~69 descendant rules (`[data-style='X'] [data-surface=…]`,\n * `[data-style='X'] body`, …) and a descendant combinator does not stop at a\n * nested `data-style`. Under an active `glassmorphism` only 1 of 11 scoped\n * style vignettes wears its own material; under `aurora`, 3 of 11. The\n * shape TOKENS still scope correctly for a non-base style — it is the\n * material rules that cross. MOTIR-3947 owns the fix and the mechanism\n * choice (selector barrier vs an isolated render tree); do not add a\n * `[data-style='warm-editorial']` base block as a partial remedy, because it\n * makes a token assertion pass on a tile that still paints as the ancestor.\n */\n\nexport interface StyleVignetteProps {\n /** Scope the vignette to a specific style; omit to inherit the active style. */\n styleId?: StyleId;\n /** Scope the vignette to a specific palette; omit to inherit the active palette. */\n palette?: PaletteId;\n /** Scope the vignette to a specific type pairing; omit to inherit the active type. */\n type?: TypeId;\n /**\n * Accessible name for the preview region. The vignette is decorative chrome,\n * so it is announced as a single labelled image rather than a pile of\n * read-out controls.\n */\n label?: string;\n className?: string;\n}\n\n/** A muted progress bar; `accent` tints it with the active accent. */\nfunction Bar({ accent, widthPct }: { accent?: boolean; widthPct: number }) {\n return (\n <div className=\"h-[6px] w-full overflow-hidden rounded-(--radius-badge) bg-(--el-muted)\">\n <div\n className={cn(\n 'h-full rounded-(--radius-badge)',\n accent ? 'bg-(--el-accent)' : 'bg-(--el-border-strong)',\n )}\n style={{ width: `${widthPct}%` }}\n />\n </div>\n );\n}\n\n/** One row of the work-item card's mini list — a status dot + label + chip. */\nfunction ListRow({ dotVar, label, chip }: { dotVar: string; label: string; chip: string }) {\n return (\n <div className=\"flex items-center gap-2 text-[11px] text-(--el-text-secondary)\">\n <span\n className=\"size-2 shrink-0 rounded-full\"\n style={{ background: `var(${dotVar})` }}\n aria-hidden\n />\n <span className=\"truncate\">{label}</span>\n <span className=\"ml-auto shrink-0 rounded-(--radius-badge) bg-(--el-surface) px-(--spacing-chip-x) py-(--spacing-chip-y) text-[9px] font-medium text-(--el-text-secondary)\">\n {chip}\n </span>\n </div>\n );\n}\n\nexport function StyleVignette({ styleId, palette, type, label, className }: StyleVignetteProps) {\n // Only emit an axis attribute when the caller pins it; an omitted axis stays\n // inherited (LIVE mode), which is how the appearance preview follows the\n // global selection without re-implementing the theme context.\n const axisAttrs: Record<string, string> = {};\n if (styleId) axisAttrs['data-style'] = styleId;\n if (palette) axisAttrs['data-palette'] = palette;\n if (type) axisAttrs['data-type'] = type;\n // SCOPED mode only. `data-appearance-scope` re-emits the Tier-3 `--el-*`\n // layer onto this element, which is what makes the axis attributes above\n // actually change what the subtree paints with: `--el-*` reference\n // `--color-*` and `var()` resolves at the DECLARING element, so without it a\n // nested `data-palette` overrides Tier-0 that nothing here reads.\n //\n // ⚠️ NOT in LIVE mode. With no axis prop the vignette is meant to FOLLOW the\n // global selection, and re-emitting the layer locally would pin the subtree\n // to whatever `--color-*` resolved at mount — the exact opposite of what the\n // appearance preview mounts it for.\n if (Object.keys(axisAttrs).length > 0) axisAttrs['data-appearance-scope'] = '';\n\n return (\n <div\n {...axisAttrs}\n role=\"img\"\n aria-label={label ?? 'Design style preview'}\n className={cn(\n 'style-vignette relative isolate overflow-hidden',\n 'rounded-(--radius-card) border border-(--el-border) bg-(--el-surface)',\n 'shadow-(--shadow-subtle)',\n className,\n )}\n >\n {/* Material canvas: transparent for flat styles; the glassmorphism gradient\n / cybercore grid is painted here via the descendant rules in\n globals.css (a preview can't lean on the body-scoped canvas). */}\n <span className=\"sv-canvas pointer-events-none absolute inset-0 -z-10\" aria-hidden />\n\n <div className=\"flex flex-col gap-3 p-3\">\n {/* NAV — a compact rail; data-surface lets a material style frost it. */}\n <div\n data-surface=\"sidebar\"\n className=\"flex items-center gap-2 rounded-(--radius-control) bg-(--el-sidebar-bg) px-3 py-2\"\n >\n <span className=\"size-2.5 rounded-full bg-(--el-accent)\" aria-hidden />\n <span className=\"font-serif text-[12px] font-semibold text-(--el-text)\">Acme</span>\n <span className=\"ml-2 rounded-(--radius-control) bg-(--el-accent) px-2 py-[3px] text-[10px] font-medium text-(--el-accent-text)\">\n Board\n </span>\n <span className=\"text-[10px] text-(--el-text-secondary)\">Backlog</span>\n <span className=\"text-[10px] text-(--el-text-secondary)\">Reports</span>\n <span className=\"ml-auto size-5 rounded-full bg-(--el-muted)\" aria-hidden />\n </div>\n\n <div className=\"grid grid-cols-[1.4fr_1fr] gap-3\">\n {/* WORK-ITEM CARD — the centrepiece: type hue + serif title + status. */}\n <Card className=\"flex flex-col gap-2 !p-3\">\n <div className=\"flex items-center gap-2\">\n {/* A decorative story-type icon in its `--el-type-story` hue. The\n package can't depend on motir-core's domain `IssueTypeIcon`\n (which maps work-item kinds → glyphs), so the vignette inlines\n the same glyph + hue directly — pixel-identical, boundary-clean. */}\n <BookOpen className=\"size-4 text-(--el-type-story)\" aria-hidden />\n <span className=\"truncate font-serif text-[13px] font-semibold text-(--el-text)\">\n Ship the billing flow\n </span>\n <Pill status=\"in-progress\" className=\"ml-auto !text-[9px]\">\n In progress\n </Pill>\n </div>\n <div className=\"flex flex-col gap-1.5\">\n <ListRow dotVar=\"--el-success\" label=\"Invoice schema\" chip=\"Done\" />\n <ListRow dotVar=\"--el-warning\" label=\"Dunning emails\" chip=\"Review\" />\n <ListRow dotVar=\"--el-info\" label=\"Stripe webhook\" chip=\"To do\" />\n </div>\n <div className=\"mt-0.5 flex flex-col gap-1.5\">\n <Bar accent widthPct={66} />\n <Bar widthPct={38} />\n </div>\n </Card>\n\n {/* MODAL — a floating dialog panel; modal radius + elevation + scrim. */}\n <div className=\"relative\">\n <span\n className=\"pointer-events-none absolute -inset-1 rounded-(--radius-card) bg-black/10\"\n aria-hidden\n />\n <div\n data-surface=\"modal\"\n className=\"relative flex flex-col gap-2 rounded-(--radius-modal) border border-(--el-border) bg-(--el-page-bg) p-3 shadow-(--shadow-modal)\"\n >\n <span className=\"font-serif text-[12px] font-semibold text-(--el-text)\">\n New invoice\n </span>\n <Bar widthPct={100} />\n <Bar widthPct={72} />\n <div className=\"mt-1 flex gap-2\">\n <span\n className={cn(\n buttonVariants({ variant: 'primary', size: 'sm' }),\n '!h-7 !px-3 !text-[10px]',\n )}\n >\n Create\n </span>\n <span\n className={cn(\n buttonVariants({ variant: 'ghost', size: 'sm' }),\n '!h-7 !px-3 !text-[10px]',\n )}\n >\n Cancel\n </span>\n </div>\n </div>\n </div>\n </div>\n\n {/* INPUT — a search field; data-surface lets a material style fill it. */}\n <div\n data-surface=\"input\"\n className=\"flex h-(--height-input) items-center gap-2 rounded-(--radius-input) border border-(--el-border-strong) bg-(--el-page-bg) px-(--spacing-input-x) text-[11px] text-(--el-text-secondary)\"\n >\n <Search className=\"size-3.5\" aria-hidden />\n <span>Search work items…</span>\n </div>\n\n {/* BUTTON ROW — primary + ghost, exact Button silhouettes (non-interactive). */}\n <div className=\"flex gap-2\">\n <span className={cn(buttonVariants({ variant: 'primary', size: 'sm' }), '!text-[11px]')}>\n New work item\n </span>\n <span className={cn(buttonVariants({ variant: 'ghost', size: 'sm' }), '!text-[11px]')}>\n <SlidersHorizontal className=\"size-3.5\" aria-hidden />\n Filter\n </span>\n </div>\n </div>\n </div>\n );\n}\n\nexport default StyleVignette as (props: StyleVignetteProps) => ReactNode;\n"]}
@@ -0,0 +1,84 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ /**
4
+ * Checkbox — a tick box for composing a SET (`role="checkbox"`).
5
+ *
6
+ * The design system's one new primitive for Story MOTIR-2257 (subtask
7
+ * MOTIR-2465), named by `design/projects/design-notes.md` § *Primitives
8
+ * composed* — the row **`Permission checkbox` — NEW**.
9
+ *
10
+ * ⚠️ NOT A `Switch`, AND THE DISTINCTION IS THE REASON THIS EXISTS. A switch
11
+ * says *this setting is on now*; a checkbox says *this is part of the set I am
12
+ * composing*. Twenty-eight switches on the role editor would read as
13
+ * twenty-eight independent settings rather than one role being composed, which
14
+ * is precisely the wrong mental model for the feature. The alternative the
15
+ * product had been living with — a bare browser tick box, unstyled, in exactly
16
+ * one place — would have been the first surface in Motir that visibly is not
17
+ * part of the design system.
18
+ *
19
+ * ⚠️ TWO STATES, NOT THREE (Yue, 2026-08-09). An earlier revision carried a
20
+ * `provenance` discriminator so a permission that came WITH the author's chosen
21
+ * base could render a grey fill, distinct from the accent fill of one they added
22
+ * on top. That distinction died with the stored `based_on` column: the editor
23
+ * still lets an author START FROM a built-in, but nothing records it, so on a
24
+ * re-edit there is no base to have come from and the two fills could not be told
25
+ * apart honestly. A permission is held or it is not — and the accessible name
26
+ * says which, so the state never rests on colour alone.
27
+ *
28
+ * Controlled, like `Switch`. `disabled` renders a non-interactive box rather
29
+ * than hiding the row — the story's server-refused, non-`enforced` keys must
30
+ * stay legible (and disabled text is the one place `--el-text-faint` is
31
+ * legitimate, since 1.4.3 exempts it).
32
+ *
33
+ * @example
34
+ * <Checkbox checked={held} onChange={setHeld} label="Add comments" />
35
+ *
36
+ * @example
37
+ * // A box whose meaning is not set-membership passes its own state clause.
38
+ * <Checkbox
39
+ * checked={done}
40
+ * onChange={setDone}
41
+ * label="Open the DNS panel"
42
+ * stateLabels={{ checked: 'Done', unchecked: 'Not done' }}
43
+ * />
44
+ */
45
+ interface CheckboxProps {
46
+ checked: boolean;
47
+ onChange: (next: boolean) => void;
48
+ /**
49
+ * The control's own name — what it is a box FOR. Rendered as the accessible
50
+ * name together with the held/not-held clause; pass `labelVisible` to draw it
51
+ * beside the box as well.
52
+ */
53
+ label: string;
54
+ disabled?: boolean;
55
+ /** Render `label` as visible text beside the box, not only as the a11y name. */
56
+ labelVisible?: boolean;
57
+ id?: string;
58
+ className?: string;
59
+ /**
60
+ * The state clause the accessible name carries, when *held / not held* is the
61
+ * wrong vocabulary for what this box composes.
62
+ *
63
+ * ⚠️ THE DEFAULT IS SET-MEMBERSHIP, AND THAT IS NOT UNIVERSAL. This primitive
64
+ * was built for the role editor, where a box genuinely means *this permission
65
+ * is part of the set I am composing* — so `checkboxStateLabel` says **Held /
66
+ * Not held**, and for that surface it is exactly right. A box that means
67
+ * *this step is finished* (`Done / Not done`) or *this option is on* reads
68
+ * wrongly under a screen reader with that clause, and the wording is the only
69
+ * thing carrying the state: the fill is colour, which 1.4.1 does not let us
70
+ * lean on.
71
+ *
72
+ * Optional and defaulted, so every existing call site keeps the wording it
73
+ * has and only a caller with different semantics has to think about it.
74
+ */
75
+ stateLabels?: {
76
+ checked: string;
77
+ unchecked: string;
78
+ };
79
+ }
80
+ /** The state clause the accessible name carries — never a colour, always words. */
81
+ declare function checkboxStateLabel(checked: boolean): string;
82
+ declare function Checkbox({ checked, onChange, label, disabled, labelVisible, id, className, stateLabels, }: CheckboxProps): react_jsx_runtime.JSX.Element;
83
+
84
+ export { Checkbox, type CheckboxProps, checkboxStateLabel };
@@ -0,0 +1,70 @@
1
+ import { clsx } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+ import { jsxs, jsx } from 'react/jsx-runtime';
4
+
5
+ // src/utils/cn.ts
6
+ function cn(...inputs) {
7
+ return twMerge(clsx(inputs));
8
+ }
9
+ function checkboxStateLabel(checked) {
10
+ return checked ? "Held" : "Not held";
11
+ }
12
+ function Checkbox({
13
+ checked,
14
+ onChange,
15
+ label,
16
+ disabled,
17
+ labelVisible,
18
+ id,
19
+ className,
20
+ stateLabels
21
+ }) {
22
+ const stateClause = stateLabels ? checked ? stateLabels.checked : stateLabels.unchecked : checkboxStateLabel(checked);
23
+ return /* @__PURE__ */ jsxs(
24
+ "button",
25
+ {
26
+ type: "button",
27
+ role: "checkbox",
28
+ id,
29
+ "aria-checked": checked,
30
+ "aria-disabled": disabled || void 0,
31
+ "aria-label": `${label}, ${stateClause}`,
32
+ disabled,
33
+ onClick: () => !disabled && onChange(!checked),
34
+ className: cn(
35
+ "inline-flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-(--focus-ring-color) focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
36
+ className
37
+ ),
38
+ children: [
39
+ /* @__PURE__ */ jsx(
40
+ "span",
41
+ {
42
+ "aria-hidden": true,
43
+ className: cn(
44
+ "inline-flex size-4 shrink-0 items-center justify-center rounded-(--radius-control) border transition-colors",
45
+ checked ? "border-(--el-accent) bg-(--el-accent) text-(--el-accent-text)" : "border-(--el-border-strong) bg-transparent text-transparent"
46
+ ),
47
+ children: /* @__PURE__ */ jsx(
48
+ "svg",
49
+ {
50
+ viewBox: "0 0 24 24",
51
+ className: "size-3",
52
+ fill: "none",
53
+ stroke: "currentColor",
54
+ strokeWidth: "3",
55
+ strokeLinecap: "round",
56
+ strokeLinejoin: "round",
57
+ children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" })
58
+ }
59
+ )
60
+ }
61
+ ),
62
+ labelVisible ? /* @__PURE__ */ jsx("span", { className: "font-sans text-sm text-(--el-text)", children: label }) : null
63
+ ]
64
+ }
65
+ );
66
+ }
67
+
68
+ export { Checkbox, checkboxStateLabel };
69
+ //# sourceMappingURL=Checkbox.js.map
70
+ //# sourceMappingURL=Checkbox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/utils/cn.ts","../../../src/components/ui/Checkbox.tsx"],"names":[],"mappings":";;;;;AAcO,SAAS,MAAM,MAAA,EAA8B;AAClD,EAAA,OAAO,OAAA,CAAQ,IAAA,CAAK,MAAM,CAAC,CAAA;AAC7B;AC8DO,SAAS,mBAAmB,OAAA,EAA0B;AAC3D,EAAA,OAAO,UAAU,MAAA,GAAS,UAAA;AAC5B;AAEO,SAAS,QAAA,CAAS;AAAA,EACvB,OAAA;AAAA,EACA,QAAA;AAAA,EACA,KAAA;AAAA,EACA,QAAA;AAAA,EACA,YAAA;AAAA,EACA,EAAA;AAAA,EACA,SAAA;AAAA,EACA;AACF,CAAA,EAAkB;AAChB,EAAA,MAAM,WAAA,GAAc,cAChB,OAAA,GACE,WAAA,CAAY,UACZ,WAAA,CAAY,SAAA,GACd,mBAAmB,OAAO,CAAA;AAC9B,EAAA,uBACE,IAAA;AAAA,IAAC,QAAA;AAAA,IAAA;AAAA,MACC,IAAA,EAAK,QAAA;AAAA,MACL,IAAA,EAAK,UAAA;AAAA,MACL,EAAA;AAAA,MACA,cAAA,EAAc,OAAA;AAAA,MACd,iBAAe,QAAA,IAAY,MAAA;AAAA,MAC3B,YAAA,EAAY,CAAA,EAAG,KAAK,CAAA,EAAA,EAAK,WAAW,CAAA,CAAA;AAAA,MACpC,QAAA;AAAA,MACA,SAAS,MAAM,CAAC,QAAA,IAAY,QAAA,CAAS,CAAC,OAAO,CAAA;AAAA,MAC7C,SAAA,EAAW,EAAA;AAAA,QACT,wKAAA;AAAA,QACA;AAAA,OACF;AAAA,MAEA,QAAA,EAAA;AAAA,wBAAA,GAAA;AAAA,UAAC,MAAA;AAAA,UAAA;AAAA,YACC,aAAA,EAAW,IAAA;AAAA,YACX,SAAA,EAAW,EAAA;AAAA,cACT,6GAAA;AAAA,cACA,UACI,+DAAA,GACA;AAAA,aACN;AAAA,YAGA,QAAA,kBAAA,GAAA;AAAA,cAAC,KAAA;AAAA,cAAA;AAAA,gBACC,OAAA,EAAQ,WAAA;AAAA,gBACR,SAAA,EAAU,QAAA;AAAA,gBACV,IAAA,EAAK,MAAA;AAAA,gBACL,MAAA,EAAO,cAAA;AAAA,gBACP,WAAA,EAAY,GAAA;AAAA,gBACZ,aAAA,EAAc,OAAA;AAAA,gBACd,cAAA,EAAe,OAAA;AAAA,gBAEf,QAAA,kBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,CAAA,EAAE,iBAAA,EAAkB;AAAA;AAAA;AAC5B;AAAA,SACF;AAAA,QACC,+BAAe,GAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,oCAAA,EAAsC,iBAAM,CAAA,GAAU;AAAA;AAAA;AAAA,GACxF;AAEJ","file":"Checkbox.js","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/**\n * Merge Tailwind class names without conflicts.\n *\n * `clsx` handles conditional inclusion (booleans, undefineds, arrays);\n * `twMerge` resolves conflicting Tailwind classes (later wins, e.g.\n * `px-2 px-4` → `px-4`). Together they are the de facto standard for\n * composing Tailwind classes in React components.\n *\n * @example\n * cn('px-2 py-1', isActive && 'bg-primary', className)\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","import { cn } from '../../utils/cn';\n\n/**\n * Checkbox — a tick box for composing a SET (`role=\"checkbox\"`).\n *\n * The design system's one new primitive for Story MOTIR-2257 (subtask\n * MOTIR-2465), named by `design/projects/design-notes.md` § *Primitives\n * composed* — the row **`Permission checkbox` — NEW**.\n *\n * ⚠️ NOT A `Switch`, AND THE DISTINCTION IS THE REASON THIS EXISTS. A switch\n * says *this setting is on now*; a checkbox says *this is part of the set I am\n * composing*. Twenty-eight switches on the role editor would read as\n * twenty-eight independent settings rather than one role being composed, which\n * is precisely the wrong mental model for the feature. The alternative the\n * product had been living with — a bare browser tick box, unstyled, in exactly\n * one place — would have been the first surface in Motir that visibly is not\n * part of the design system.\n *\n * ⚠️ TWO STATES, NOT THREE (Yue, 2026-08-09). An earlier revision carried a\n * `provenance` discriminator so a permission that came WITH the author's chosen\n * base could render a grey fill, distinct from the accent fill of one they added\n * on top. That distinction died with the stored `based_on` column: the editor\n * still lets an author START FROM a built-in, but nothing records it, so on a\n * re-edit there is no base to have come from and the two fills could not be told\n * apart honestly. A permission is held or it is not — and the accessible name\n * says which, so the state never rests on colour alone.\n *\n * Controlled, like `Switch`. `disabled` renders a non-interactive box rather\n * than hiding the row — the story's server-refused, non-`enforced` keys must\n * stay legible (and disabled text is the one place `--el-text-faint` is\n * legitimate, since 1.4.3 exempts it).\n *\n * @example\n * <Checkbox checked={held} onChange={setHeld} label=\"Add comments\" />\n *\n * @example\n * // A box whose meaning is not set-membership passes its own state clause.\n * <Checkbox\n * checked={done}\n * onChange={setDone}\n * label=\"Open the DNS panel\"\n * stateLabels={{ checked: 'Done', unchecked: 'Not done' }}\n * />\n */\nexport interface CheckboxProps {\n checked: boolean;\n onChange: (next: boolean) => void;\n /**\n * The control's own name — what it is a box FOR. Rendered as the accessible\n * name together with the held/not-held clause; pass `labelVisible` to draw it\n * beside the box as well.\n */\n label: string;\n disabled?: boolean;\n /** Render `label` as visible text beside the box, not only as the a11y name. */\n labelVisible?: boolean;\n id?: string;\n className?: string;\n /**\n * The state clause the accessible name carries, when *held / not held* is the\n * wrong vocabulary for what this box composes.\n *\n * ⚠️ THE DEFAULT IS SET-MEMBERSHIP, AND THAT IS NOT UNIVERSAL. This primitive\n * was built for the role editor, where a box genuinely means *this permission\n * is part of the set I am composing* — so `checkboxStateLabel` says **Held /\n * Not held**, and for that surface it is exactly right. A box that means\n * *this step is finished* (`Done / Not done`) or *this option is on* reads\n * wrongly under a screen reader with that clause, and the wording is the only\n * thing carrying the state: the fill is colour, which 1.4.1 does not let us\n * lean on.\n *\n * Optional and defaulted, so every existing call site keeps the wording it\n * has and only a caller with different semantics has to think about it.\n */\n stateLabels?: { checked: string; unchecked: string };\n}\n\n/** The state clause the accessible name carries — never a colour, always words. */\nexport function checkboxStateLabel(checked: boolean): string {\n return checked ? 'Held' : 'Not held';\n}\n\nexport function Checkbox({\n checked,\n onChange,\n label,\n disabled,\n labelVisible,\n id,\n className,\n stateLabels,\n}: CheckboxProps) {\n const stateClause = stateLabels\n ? checked\n ? stateLabels.checked\n : stateLabels.unchecked\n : checkboxStateLabel(checked);\n return (\n <button\n type=\"button\"\n role=\"checkbox\"\n id={id}\n aria-checked={checked}\n aria-disabled={disabled || undefined}\n aria-label={`${label}, ${stateClause}`}\n disabled={disabled}\n onClick={() => !disabled && onChange(!checked)}\n className={cn(\n 'inline-flex items-center gap-2 focus-visible:ring-2 focus-visible:ring-(--focus-ring-color) focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n >\n <span\n aria-hidden\n className={cn(\n 'inline-flex size-4 shrink-0 items-center justify-center rounded-(--radius-control) border transition-colors',\n checked\n ? 'border-(--el-accent) bg-(--el-accent) text-(--el-accent-text)'\n : 'border-(--el-border-strong) bg-transparent text-transparent',\n )}\n >\n {/* The tick. `currentColor` inherits the ink the fill above picked. */}\n <svg\n viewBox=\"0 0 24 24\"\n className=\"size-3\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"3\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M20 6 9 17l-5-5\" />\n </svg>\n </span>\n {labelVisible ? <span className=\"font-sans text-sm text-(--el-text)\">{label}</span> : null}\n </button>\n );\n}\n"]}
@@ -28,6 +28,10 @@ interface ComboboxOption<T extends string> {
28
28
  icon?: ReactNode;
29
29
  /** Trailing muted text (e.g. the identifier). */
30
30
  secondary?: string;
31
+ /** Trailing rich content pinned to the option row's far end (e.g. a state
32
+ * Pill or a "Linked to …" chip in the PR-link picker, MOTIR-1596).
33
+ * Decorative — the accessible name is `label`; renders after `secondary`. */
34
+ trailing?: ReactNode;
31
35
  /**
32
36
  * Optional section label. When consecutive options carry different `group`
33
37
  * values, a non-interactive header row is rendered at each transition (the
@@ -48,7 +52,9 @@ interface ComboboxProps<T extends string> {
48
52
  placeholder?: string;
49
53
  searchable?: boolean;
50
54
  searchPlaceholder?: string;
51
- emptyText?: string;
55
+ /** Empty-listbox content. A string, or rich nodes (e.g. a "no matches" line
56
+ * plus a hint sub-line — use block `<span>`s, since this renders in a `<p>`). */
57
+ emptyText?: ReactNode;
52
58
  loading?: boolean;
53
59
  loadingText?: string;
54
60
  disabled?: boolean;