@godxjp/ui 30.4.1 → 30.5.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.
Files changed (66) hide show
  1. package/agent/START-HERE.md +6 -6
  2. package/agent/components/Carousel.json +4 -1
  3. package/agent/components/RecordPicker.json +103 -0
  4. package/agent/components/ResponsiveGrid.json +3 -1
  5. package/agent/components/Reveal.json +6 -1
  6. package/agent/components/SpaceCompact.json +1 -1
  7. package/agent/components/Table.json +6 -0
  8. package/agent/components/Text.json +8 -0
  9. package/agent/components-index.json +5 -0
  10. package/agent/components.json +131 -4
  11. package/agent/index.json +7 -7
  12. package/agent/llms.txt +7 -7
  13. package/agent/patterns/brand-theme-block.json +33 -0
  14. package/agent/patterns-index.json +32 -0
  15. package/agent/patterns.json +33 -0
  16. package/agent/tokens.json +12 -0
  17. package/dist/components/data-display/table.d.ts +30 -1
  18. package/dist/components/data-display/table.js +2 -1
  19. package/dist/components/data-entry/index.d.ts +2 -0
  20. package/dist/components/data-entry/index.js +2 -0
  21. package/dist/components/data-entry/record-picker.d.ts +62 -0
  22. package/dist/components/data-entry/record-picker.js +296 -0
  23. package/dist/components/general/typography.d.ts +1 -0
  24. package/dist/components/general/typography.js +10 -0
  25. package/dist/contracts/measurement.json +1 -1
  26. package/dist/i18n/messages/en.json +18 -0
  27. package/dist/i18n/messages/ja.json +18 -0
  28. package/dist/i18n/messages/vi.json +18 -0
  29. package/dist/props/components/data-entry.prop.d.ts +58 -0
  30. package/dist/props/components/general.prop.d.ts +15 -1
  31. package/dist/props/registry.d.ts +17 -0
  32. package/dist/props/registry.js +17 -0
  33. package/dist/props/vocabulary/index.d.ts +1 -1
  34. package/dist/props/vocabulary/interaction.prop.d.ts +13 -0
  35. package/dist/styles/data-display-layout.css +6 -3
  36. package/dist/styles/data-entry-layout.css +71 -0
  37. package/dist/styles/dialog-layout.css +8 -0
  38. package/dist/styles/focus-ring.css +2 -0
  39. package/dist/styles/layout.css +4 -0
  40. package/dist/styles/table-layout.css +13 -0
  41. package/dist/styles/text-layout.css +8 -0
  42. package/dist/tokens/components/data-display.css +2 -0
  43. package/dist/tokens/components/data-entry.css +3 -0
  44. package/docs/FRAME-COVERAGE-REPORT.md +3 -2
  45. package/docs/assets/tcgm/card-1.svg +1 -0
  46. package/docs/assets/tcgm/card-2.svg +1 -0
  47. package/docs/assets/tcgm/card-3.svg +1 -0
  48. package/docs/assets/tcgm/card-4.svg +1 -0
  49. package/docs/assets/tcgm/card-5.svg +1 -0
  50. package/docs/assets/tcgm/card-6.svg +1 -0
  51. package/docs/assets/tcgm/hero-prism.svg +1 -0
  52. package/docs/assets/tcgm/scene-1.svg +1 -0
  53. package/docs/assets/tcgm/scene-2.svg +1 -0
  54. package/docs/assets/tcgm/scene-3.svg +1 -0
  55. package/docs/assets/tcgm/scene-4.svg +1 -0
  56. package/docs/assets/tcgm/scene-5.svg +1 -0
  57. package/docs/assets/tcgm/tcgm-lockup-color.svg +1 -0
  58. package/docs/assets/tcgm/tcgm-symbol-color.svg +1 -0
  59. package/docs/data-entry/record-picker.tsx +208 -0
  60. package/docs/general/typography.tsx +28 -0
  61. package/docs/i18n/messages/en.json +185 -1
  62. package/docs/i18n/messages/ja.json +185 -1
  63. package/docs/i18n/messages/vi.json +185 -1
  64. package/docs/showcase/tcgm-website.tsx +1047 -0
  65. package/package.json +3 -3
  66. package/scripts/ui-audit.mjs +23 -0
@@ -3,7 +3,7 @@
3
3
  You are about to write code against a design system you did not author. This file is the whole
4
4
  contract. Read it before you write JSX.
5
5
 
6
- **This catalog describes `@godxjp/ui` 30.4.1.** If the project you are editing has a different
6
+ **This catalog describes `@godxjp/ui` 30.5.2.** If the project you are editing has a different
7
7
  version in its `package.json`, read the pinned catalog for THAT version instead
8
8
  (`…/v<their-version>/agent/…`). A catalog newer than the installed package describes props that do
9
9
  not exist yet; older, and it hides props that do. Neither failure announces itself.
@@ -43,11 +43,11 @@ Then ask it: `search_components`, `get_component`, `get_tokens`, `get_rule`, `li
43
43
  **You cannot run a process** (ChatGPT web · Claude.ai · anything fetching URLs)
44
44
  → These files are for you. Fetch in this order:
45
45
 
46
- 0. `patterns-index.json` — 20 whole-task patterns as name + tagline + tags. **If your
46
+ 0. `patterns-index.json` — 21 whole-task patterns as name + tagline + tags. **If your
47
47
  task is a task** — "build a settings page", "confirm a destructive delete", "a list page with
48
48
  filters" — start HERE, not at the components. Then fetch `patterns/<name>.json` for complete,
49
49
  copy-paste-ready code. A component index answers "does X exist"; it cannot answer "build Y".
50
- 1. `components-index.json` — 45 KB, all 172 components as name + group +
50
+ 1. `components-index.json` — 46 KB, all 173 components as name + group +
51
51
  tagline. Read this when you already know the SHAPE you need. Each entry may carry `absorbed`:
52
52
  names that **do not exist** and map to it — `Combobox`, `Autocomplete`, `CountrySelect` and
53
53
  `SearchSelect` are all `Select`. If you are about to hand-roll something, search this field
@@ -56,10 +56,10 @@ Then ask it: `search_components`, `get_component`, `get_tokens`, `get_rule`, `li
56
56
  its `importPath`, and its examples. Fetch only the handful you picked in step 1.
57
57
  3. `rules.json` — 50 cardinal rules. The ones about raw HTML and hardcoded colour are not
58
58
  style advice.
59
- 4. `tokens.json` — 2071 design tokens, each tagged with its `tier`. **If you were handed a
59
+ 4. `tokens.json` — 2073 design tokens, each tagged with its `tier`. **If you were handed a
60
60
  brand, read the 211 `foundation` entries first** — `--primary`, `--background`,
61
61
  `--radius`, `--font-size-base` are the handful everything else derives from. The
62
- 1757 `component` entries are per-part knobs; reach for one only when a role is
62
+ 1759 `component` entries are per-part knobs; reach for one only when a role is
63
63
  right everywhere except one component.
64
64
  5. `anti-ai-tells.json` — 26 shapes that make generated UI look generated, each with the
65
65
  fix. Read before you reach for a gradient hero or a wall of coloured chips.
@@ -145,7 +145,7 @@ has stopped following the brand.
145
145
  |---|---|---|---|
146
146
  | `foundation` | 211 | the seeds — `--primary`, `--background`, `--foreground`, `--radius`, `--font-size-base`, `--shadow-color`. Everything below is derived from these | **yes — this is the main road.** Handed a brand colour, this is where it goes: `:root { --primary: <H> <S>% <L>%; }` (HSL components, no `hsl()` wrapper) |
147
147
  | `semantic` | 103 | named roles that follow the seeds — `--ring`, `--text-link`, `--primary-hover`, `--overlay-background` | only when the seed is right and ONE role must differ. That role then stops following a later brand change |
148
- | `component` | 1757 | per-part knobs, `--{component}-{part}-{property}` | rarely. Most are declared `initial` with the real default at the call site — deliberate, so a scoped override re-resolves instead of freezing at `:root` |
148
+ | `component` | 1759 | per-part knobs, `--{component}-{part}-{property}` | rarely. Most are declared `initial` with the real default at the call site — deliberate, so a scoped override re-resolves instead of freezing at `:root` |
149
149
 
150
150
  A token whose `value` is `initial` is not empty and not broken: `initial` is the guaranteed-invalid
151
151
  value, so the real default is computed where the element paints it. Set it and yours wins.
@@ -39,7 +39,10 @@
39
39
  "DON'T use a Carousel where ALL items must be seen/compared at once or be keyboard-reachable in reading order (e.g. a list of selectable options, a data table, primary navigation) — hiding content behind a swipe is an anti-pattern there; use a Grid/`ResponsiveGrid`, `ScrollArea`, or `Tabs`.",
40
40
  "DON'T autoplay without a pause-on-hover/focus control and reduced-motion respect — pass the Embla autoplay plugin via `plugins` only for non-essential decorative content, never for content the user must read.",
41
41
  "DO set `opts={{ loop: true }}` for galleries that wrap, and rely on the built-in disabling: `CarouselPrevious`/`CarouselNext` auto-disable at the ends (via `canScrollPrev`/`canScrollNext`) — don't hide them, let them grey out.",
42
- "DO give each `<CarouselItem>` real, meaningful content; the component already injects an 'N of M' slide label for screen readers, so don't add a redundant one (a consumer `aria-label` on the item overrides the default)."
42
+ "DO give each `<CarouselItem>` real, meaningful content; the component already injects an 'N of M' slide label for screen readers, so don't add a redundant one (a consumer `aria-label` on the item overrides the default).",
43
+ "DO size a MULTI-ITEM rail with `basis-*` on the item: `<CarouselItem className=\"basis-full sm:basis-1/2 lg:basis-1/3\">` is 1/2/3 per view, and `basis-4/5` leaves the \"peek\" that tells a phone user the rail scrolls. Use the FRACTIONAL ladder, never an arbitrary `basis-[82%]` (the audit rejects it). Fixed in gh#930 — before that the item carried `min-width: 100%`, which silently beat every `basis-*`: the documented 3-across composition computed `flex-basis: 33.3333%` and still painted one full-width slide 32px WIDER than its own rail. If a rail renders 1-up on an older version, that is the bug, not your className.",
44
+ "DON'T wrap each `<CarouselItem>` in `<Reveal on=\"view\">`. The rail clips its slides, so the observer on a slide outside the window has an empty intersection rect and the slide never becomes visible — measured at 3 of 4 slides stuck at `opacity: 0`. Put ONE `<Reveal on=\"view\">` around the whole `<Carousel>`.",
45
+ "DO expect the prev/next arrows to OVERLAY the rail edges — they are `position: absolute` inside the Carousel, so placing them in a caption row is not something a prop offers. Two knobs retune them: `--carousel-arrow-inset-block-start` for the vertical placement, and `--carousel-arrow-size` for the BUTTON box (default 2rem/32px, which clears WCAG 2.2 SC 2.5.8's 24px floor). A brand whose own standard is a 44px target sets `--carousel-arrow-size: 2.75rem` in its theme — the box and the glyph are separate decisions, so the mark stays on the icon scale via `--carousel-arrow-icon-size`. The box token is gh#931; before it, those were two bare literals and the target was the one dimension a theme could not reach."
43
46
  ],
44
47
  "useCases": [
45
48
  "Feature / onboarding highlight cards on a dashboard or landing surface, with CarouselDots showing position.",
@@ -0,0 +1,103 @@
1
+ {
2
+ "example": "import { RecordPicker } from \"@godxjp/ui/data-entry\";\n\n<RecordPicker\n mode=\"multiple\"\n count={totalApprovers} // the WHOLE set, from the server\n loadOptions={async ({ query, filters }) =>\n api.approvers({ q: query, role: filters.role })\n }\n filters={[\n { name: \"role\", label: \"Role\", options: roleOptions },\n ]}\n selectedOptions={form.approvers} // labels the form already has\n value={form.approverIds}\n onValueChange={(ids) => form.setApproverIds(ids as string[])}\n/>",
3
+ "group": "data-entry",
4
+ "importPath": "@godxjp/ui/data-entry",
5
+ "name": "RecordPicker",
6
+ "props": [
7
+ {
8
+ "defaultValue": "\"single\"",
9
+ "description": "Decides the VALUE SHAPE and the dialog's commit model. `single` fires onValueChange with a single value (or null) and closes on the pick; `multiple` fires an array and commits only on Confirm, so a mis-click in a list of ten thousand is undone by Cancel rather than by re-finding the row.",
10
+ "name": "mode",
11
+ "type": "\"single\" | \"multiple\""
12
+ },
13
+ {
14
+ "defaultValue": "10",
15
+ "description": "The dropdown/dialog switch. A dropdown is the right control for eight people and the wrong one for eight hundred; this is where that line is drawn, once, by a service rather than per screen.",
16
+ "name": "threshold",
17
+ "type": "number"
18
+ },
19
+ {
20
+ "description": "Size of the WHOLE set, which only a server knows — a page of results does not, so never pass `rows.length` from a fetch. Omitted with `options` it is their length; omitted with `loadOptions` the dialog shape is assumed (you cannot count what you have not fetched).",
21
+ "name": "count",
22
+ "type": "number"
23
+ },
24
+ {
25
+ "description": "Static rows, filtered client-side. Provide this OR `loadOptions`. Same row shape Select takes, `group` included — a mixed 'users and roles' picker is two groups, not two components.",
26
+ "name": "options",
27
+ "type": "(SearchSelectOptionProp | SelectOptionGroupProp)[]"
28
+ },
29
+ {
30
+ "description": "Server fetcher, debounced. `filters` carries YOUR OWN vocabulary back (the `name` of each declared filter), so the server reads the keys it already understands instead of a shape this component invented.",
31
+ "name": "loadOptions",
32
+ "type": "(params: { query: string; filters: Record<string, string>; cursor?: string }) => Promise<{ options: SearchSelectOptionProp[]; count?: number; nextCursor?: string }>"
33
+ },
34
+ {
35
+ "description": "Filter controls rendered above the dialog list; their values reach `loadOptions({ filters })`. This is the 'search condition' half of the report — picking an issue by key alone fails, picking it by status + type + assignee works.",
36
+ "name": "filters",
37
+ "type": "{ name: string; label: string; options: { value: string; label: string }[] }[]"
38
+ },
39
+ {
40
+ "description": "Labels for values the picker HOLDS but has not loaded. On first render there is no result page at all, so without this a chip renders a raw id; it is merged ahead of anything that loads later, so the chip also survives a query that does not return its row.",
41
+ "name": "selectedOptions",
42
+ "type": "SearchSelectOptionProp[]"
43
+ },
44
+ {
45
+ "description": "A real row meaning \"none\" (an unassigned owner), pinned first — NOT a cleared field. The distinction matters: \"no assignee\" is a value the record holds and the server stores, while an empty field is the absence of an answer.",
46
+ "name": "emptyOption",
47
+ "type": "{ value: string; label: string }"
48
+ },
49
+ {
50
+ "description": "Controlled value. Array for `multiple`, single value (or null) otherwise.",
51
+ "name": "value",
52
+ "type": "string | string[] | null"
53
+ },
54
+ {
55
+ "description": "Receives the SHAPE you passed in — never a one-element array for a single picker, which the consumer would have to unwrap at every call site.",
56
+ "name": "onValueChange",
57
+ "type": "(value: string | string[] | null) => void"
58
+ },
59
+ {
60
+ "description": "Uncontrolled initial value, same shape as `value`. Use `value` + `onValueChange` when a form owns the state.",
61
+ "name": "defaultValue",
62
+ "type": "string | string[] | null"
63
+ },
64
+ {
65
+ "description": "Trigger text while nothing is chosen. Defaults to the localized `dataEntry.recordPicker.placeholder`; once a value exists the trigger shows chips instead.",
66
+ "name": "placeholder",
67
+ "type": "string"
68
+ },
69
+ {
70
+ "description": "Dialog heading. Defaults to the localized `dataEntry.recordPicker.dialogTitle`.",
71
+ "name": "dialogTitle",
72
+ "type": "string"
73
+ },
74
+ {
75
+ "description": "Control height of the trigger, on the shared control ladder.",
76
+ "name": "size",
77
+ "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
78
+ }
79
+ ],
80
+ "related": [
81
+ "Select — what this renders at or under the threshold. Use Select directly when the set is small and fixed.",
82
+ "Command — the dialog list is a Command with `shouldFilter={false}`; the query is answered by your server, not by cmdk over a page it cannot see.",
83
+ "DataTable — for displaying and acting on records rather than picking one.",
84
+ "Dialog — the over-threshold branch; `RecordPicker` opens one for you."
85
+ ],
86
+ "rules": [],
87
+ "storyPath": "data-entry/RecordPicker.stories.tsx",
88
+ "tagline": "ONE picker whose SHAPE follows the size of the set behind it: at or under `threshold` it IS a Select (dropdown with search); over it, a Dialog with search + consumer-declared filters, chips, and a confirm step for `multiple`. Reach for it when the same field may hold eight records on one tenant and eight thousand on another.",
89
+ "usage": [
90
+ "DO pass `count` from the server whenever the list is server-backed. It is the only honest input to the threshold decision — `rows.length` describes the page you fetched, not the set behind it, so a picker over 9000 records would render as a dropdown showing 20.",
91
+ "DO declare `filters` in your own vocabulary (`{ name: 'status', … }`) — the names travel to `loadOptions({ filters })` unchanged, so the server reads the keys it already has.",
92
+ "DO pass `selectedOptions` for any value the form arrives holding. On first render there is no result page, so a chip without it shows a raw id; with it the chip is correct before anything loads and survives a query that excludes its row.",
93
+ "DO use `emptyOption` for \"unassigned\" rather than leaving the field empty — a record that HOLDS \"no owner\" is a different state from a field nobody answered, and only the first round-trips.",
94
+ "DON'T reach for this when the set is small and fixed (a status, a priority, a country): that is `Select`, and RecordPicker just renders Select for you with an extra layer of props in between.",
95
+ "DON'T use it as a table replacement. It picks records; it does not display, sort or edit them — a screen that needs columns and bulk actions wants DataTable.",
96
+ "DON'T nest it inside a Popover. It opens a Dialog, and a dialog inside a popover loses focus containment; it is designed to be opened from a field inside a Dialog or Sheet, which is where the consumer reports put it."
97
+ ],
98
+ "useCases": [
99
+ "Assigning an owner on a busy admin screen where one tenant has 8 people and another has 800.",
100
+ "An approval step that mixes USERS and ROLES in one field — two `group`s in one list, chips distinguishable by the option's `icon`.",
101
+ "Picking an issue/record by key OR title with status/type/assignee filters, replacing a free-text key field that only failed validation after save."
102
+ ]
103
+ }
@@ -64,7 +64,9 @@
64
64
  "DO NOT place a DataTable inside a ResponsiveGrid column beside a card or chart. DataTable must occupy its own full-width row in a Card with CardContent flush. Nesting a multi-column table in a grid column squeezes CJK text to one character per line (see rule 37).",
65
65
  "DO use ResponsiveGrid for page-level spacing — it applies the correct gap token (--space-stack-md) automatically. Never add raw gap-* / p-* / space-* utilities to the page layout around tiles; compose spacing through this component instead (rule 40).",
66
66
  "DO render SkeletonStat children in place of StatCard tiles while KPIs are loading — same columns prop, same count as the real tiles. Switch to real StatCard once data resolves.",
67
- "The grid uses CSS container queries, not viewport media queries — it responds to its containing block width, not the window. Ensure the container is not artificially constrained (e.g. inside a narrow SplitPane column) or column expansion will never trigger."
67
+ "The grid uses CSS container queries, not viewport media queries — it responds to its containing block width, not the window. Ensure the container is not artificially constrained (e.g. inside a narrow SplitPane column) or column expansion will never trigger.",
68
+ "THE STEP NAMES ARE CONTAINER WIDTHS, AND THEY DO NOT LINE UP WITH VIEWPORT NAMES — `sm` is 40rem, `md` 48rem, `lg` 64rem OF THE GRID'S OWN BOX. A page gutter eats the difference: inside a 1280px shell with 32px gutters a 768px tablet hands the grid 704px, so `columns={{ base: 2, md: 3 }}` stays 2-up there — measured. Key the tablet column to `sm` instead (`{ base: 2, sm: 3, lg: 4 }`) and verify by measuring the GRID, not the window. Never reach for a viewport media query to 'correct' it.",
69
+ "An UNNAMED `@container` in your own CSS resolves against the nearest ANCESTOR container, which inside a grid is the grid — not the card. A rule meant to fire per card measured against the 358px grid instead of the 163px cell. Either make the card its own container (`container-type: inline-size`) or use a plain media query when the viewport is genuinely what you meant."
68
70
  ],
69
71
  "useCases": [
70
72
  "Dashboard KPI row: rendering 3–4 StatCard tiles (revenue, member count, active invoices, overdue amount) that reflow to a 2-column stacked grid on tablet and a single column on mobile.",
@@ -44,7 +44,9 @@
44
44
  "related": [
45
45
  "AuthShell — pairs with Reveal for the auth card entrance; AuthShell delegates all motion to Reveal.",
46
46
  "Card — the most common thing to wrap in <Reveal> (dashboard cards, auth card, settings sections).",
47
- "ResponsiveGrid — combine with `<Reveal delay={n}>` (or `asChild`) per grid item for a staggered grid reveal."
47
+ "ResponsiveGrid — combine with `<Reveal delay={n}>` (or `asChild`) per grid item for a staggered grid reveal.",
48
+ "Carousel — wrap the WHOLE Carousel in one `<Reveal on=\"view\">`, never each CarouselItem: the rail clips its slides, so a per-slide observer never fires for the ones outside the window.",
49
+ "ScrollArea — same rule as Carousel: reveal the scroller, not the children it clips."
48
50
  ],
49
51
  "rules": [],
50
52
  "storyPath": "general/Reveal.stories.tsx",
@@ -57,6 +59,9 @@
57
59
  "DO stagger a list/column by passing an increasing `delay` (1, 2, 3…) to successive siblings — the ordinal maps to `--reveal-stagger-step`, so a service retunes the cascade rhythm from one token.",
58
60
  "DO pass `asChild` when wrapping an element that must keep its own box in a grid/flex row (the reveal merges onto that element instead of adding a <div>).",
59
61
  "DO rely on the built-in reduced-motion behaviour — under `prefers-reduced-motion: reduce` the animation is dropped and content renders in its final, fully-visible position with no layout shift. Never gate visibility on the animation.",
62
+ "DON'T put `on=\"view\"` on an element a scroller CLIPS — a Carousel slide, a horizontal ScrollArea child, anything inside `overflow: hidden`/`clip`. The observer's root is the VIEWPORT, and a clipped box has an EMPTY intersection rect, so an item parked outside its rail never leaves `data-reveal-state=\"out\"` and stays at `opacity: 0` forever. Measured on a 4-slide rail at 1440: 3 of the 4 slides sat invisible after scrolling the whole document, and stayed invisible. This is the one failure this component may not have. Reveal the RAIL (one `<Reveal on=\"view\">` around the whole `<Carousel>`/`<ScrollArea>`), never the slides.",
63
+ "DON'T budget a reveal per card inside a rail even when it does work — one region is the entrance, the cascade belongs to a grid whose items all share the page's scroll. `delay` caps at 6, so a long list should pass `Math.min(i, 5) + 1` rather than the raw index.",
64
+ "DO expect a re-filtered list to re-enter: changing a filter unmounts and remounts the wrappers, so each new item gets a fresh observer and animates again. Items already scrolled past are re-revealed on their way back, which is correct — they are never left hidden.",
60
65
  "DO NOT set a raw ms delay or a literal translate distance — the whole point is that `delay` is a controlled ordinal and the distance/duration come from tokens."
61
66
  ],
62
67
  "useCases": [
@@ -50,7 +50,7 @@
50
50
  "DON'T reach for `className=\"rounded-none\"` / `[&>*:not(:first-child)]` utilities to join controls yourself — `ui-audit`'s `no-utility-layout` rule blocks exactly that, and it is precisely the corner-radius seam this component owns.",
51
51
  "DO set `size` on each CHILD individually, not on SpaceCompact — the row has no size prop of its own by design (see `density`); every control already owns its own `size` axis (`xs|sm|md|lg`).",
52
52
  "DO use `fullWidth` inside a narrow form card so the joined row spans the field column, exactly like a lone Input would.",
53
- "`fullWidth` sets NO per-child flex ratio — same as antd, whose Space.Compact block style only stretches the ROW (`display: flex; width: 100%`), never the children. Two fields (NumberInput+Select, Input+Select) split the row evenly because both already default to their own full width; a Button beside a field keeps its own content width and the field absorbs the rest (Select+Button, Input+Button) — the same split you get outside SpaceCompact.",
53
+ "`fullWidth` gives EVERY child an equal share of the row — each item carries `flex: 1 1 0%`, so the split is even no matter what the children would size to on their own. Measured at a 1169px row: NumberInput+Select 585/585, and Select+Button ALSO 585/585, i.e. a Button under `fullWidth` is stretched to half the row, not left at its content width. Want the Button at content width and the field absorbing the rest? Omit `fullWidth` — the row is then `inline-flex` and sizes to content (measured 223/40). Do NOT remove the per-child `flex` to chase antd's block style: without it the items collapse to their content and the row stops filling at all (measured 203/73.6 in a 1169px column), which defeats `fullWidth`.",
54
54
  "DON'T expect corner-radius welding on `orientation=\"vertical\"` yet — the shared border still collapses, but each child keeps all four of its own corners rounded until a block-axis radius knob exists on the Input/trigger families (documented gap, not a silent one)."
55
55
  ],
56
56
  "useCases": [
@@ -4,6 +4,12 @@
4
4
  "importPath": "@godxjp/ui/data-display",
5
5
  "name": "Table",
6
6
  "props": [
7
+ {
8
+ "defaultValue": "false",
9
+ "description": "TableRow only. The whole ROW is the target: pointer cursor, a hover that means \"clickable\" rather than merely \"hovered\", and (via focus-ring.css) the design-system focus ring for the real <a>/<button> inside a cell. PRESENTATION ONLY — it binds no handler and deliberately adds no tabindex/role: a <tr> is not a control, and the thing a keyboard user reaches must stay a real control inside the row. Pair it with your own onClick on the row and keep a genuine link/button in the first cell. Reach for it when a design calls for \"a list with COLUMNS whose rows are selectable\" — ListRow gets all of this free by wrapping its own <a>, which a <tr> cannot do, and DataTable solves a different problem (it brings a toolbar and pagination many designs do not have). gh#929.",
10
+ "name": "interactive",
11
+ "type": "boolean"
12
+ },
7
13
  {
8
14
  "description": "PER-INSTANCE column measures for preset=\"action-collection\", in place of re-pointing its --table-action-collection-* knobs from a consumer stylesheet. Those knobs are global by design, and that is the problem: two collections on one screen do not share a column budget — a console that widened `actions` globally so a Japanese status badge would stop breaking to one character per line (an SC 1.4.10 reflow failure) collapsed a sibling table's name column to ~15px in the same change. Emitted as inline custom properties, the same contract Flex `width` uses for a call-site measurement, leaving data-column-widths on the DOM so each escape stays countable.",
9
15
  "name": "columnWidths",
@@ -53,6 +53,12 @@
53
53
  "name": "whitespace",
54
54
  "type": "\"normal\" | \"pre-wrap\""
55
55
  },
56
+ {
57
+ "defaultValue": "\"normal\"",
58
+ "description": "Where an over-long unbroken token may break. THE prop for a machine identifier — an email address, a coupon code, a login id, a template key — in a DataTable/TableCell or a Descriptions value: `break=\"anywhere\"` emits `overflow-wrap: anywhere` (and releases a table cell's inherited `nowrap`), so the column shrinks to the viewport instead of scrolling the page. Use it INSTEAD of `className=\"[overflow-wrap:anywhere] break-words whitespace-normal\"`. NOT `whitespace=\"pre-wrap\"`: that is for typed line breaks, and its `break-word` does not lower min-content, so in a table cell it breaks nothing (measured: a 71-char token holds a 320px table at 630px with pre-wrap, 320px with break=\"anywhere\"). Independent of `whitespace` (both may be set); `truncate` wins (dev builds warn); `clamp` composes.",
59
+ "name": "break",
60
+ "type": "\"normal\" | \"anywhere\""
61
+ },
56
62
  {
57
63
  "description": "Tabular figures for aligned numbers.",
58
64
  "name": "tabular",
@@ -185,6 +191,7 @@
185
191
  "usage": [
186
192
  "DO use `<Text>` for ALL body / inline / caption text instead of a styled `<span>`/`<p>`. Pick `size` from the scale; never write `text-[13px]`/`text-[11px]` or `font-semibold` by hand.",
187
193
  "DO use `tone` for colour (`muted`/`primary`/semantic), `tabular` for numbers, `mono` for codes — not `className=\"text-muted-foreground font-mono tabular-nums\"`.",
194
+ "DO use `break=\"anywhere\"` for an email, code or id in a table cell or Descriptions value — never `className=\"[overflow-wrap:anywhere] break-words whitespace-normal\"`, and never `whitespace=\"pre-wrap\"`, which does not shrink a table column.",
188
195
  "DO use `clamp={n}` for a description limited to n lines (card grids) and `truncate` for a one-line ellipsis — never `className=\"line-clamp-2\"` (banned utility). They are mutually exclusive; `clamp` wins.",
189
196
  "For a heading, use `<Heading level>` instead of a large-size `<Text>`.",
190
197
  "DO use `link` for a link inside RUNNING CONTENT — an issue subject in a table cell, a page name in a list, a \"see all\" at the end of a row — and compose it onto your router link with `asChild`. Never `className=\"text-primary hover:underline\"`: that is consumer rules 6 and 7 in one string, and it leaves a keyboard user with no underline because `hover:` cannot fire for them.",
@@ -193,6 +200,7 @@
193
200
  "useCases": [
194
201
  "A muted caption under a value: `<Text size=\"xs\" tone=\"muted\">2026年5月度</Text>`.",
195
202
  "A monospace id in a list row: `<Text size=\"xs\" mono tone=\"muted\">RC-204881</Text>`.",
203
+ "A staff email in a narrow DataTable cell at 320px, wrapping instead of scrolling the page: `<Text size=\"sm\" break=\"anywhere\">{staff.email}</Text>`.",
196
204
  "An emphasized inline figure: `<Text weight=\"medium\" tabular>¥1,240,000</Text>`.",
197
205
  "A service-card description clamped to 2 lines on a narrow (390px) index: `<Text as=\"p\" size=\"sm\" tone=\"muted\" clamp={2}>{description}</Text>`.",
198
206
  "An issue subject linking out of a table cell, wrapping to two lines: `<Text asChild link truncate={false}><Link href={`/view/${key}`}>{subject}</Link></Text>`.",
@@ -356,6 +356,11 @@
356
356
  "name": "SearchInput",
357
357
  "tagline": "Debounced search box with a clear button. Fires onSearch (NOT onChange) after the debounce. Controlled (value) or uncontrolled (defaultValue)."
358
358
  },
359
+ {
360
+ "group": "data-entry",
361
+ "name": "RecordPicker",
362
+ "tagline": "ONE picker whose SHAPE follows the size of the set behind it: at or under `threshold` it IS a Select (dropdown with search); over it, a Dialog with search + consumer-declared filters, chips, and a confirm step for `multiple`. Reach for it when the same field may hold eight records on one tenant and eight thousand on another."
363
+ },
359
364
  {
360
365
  "absorbed": [
361
366
  "Combobox",
@@ -990,7 +990,7 @@
990
990
  "DON'T reach for `className=\"rounded-none\"` / `[&>*:not(:first-child)]` utilities to join controls yourself — `ui-audit`'s `no-utility-layout` rule blocks exactly that, and it is precisely the corner-radius seam this component owns.",
991
991
  "DO set `size` on each CHILD individually, not on SpaceCompact — the row has no size prop of its own by design (see `density`); every control already owns its own `size` axis (`xs|sm|md|lg`).",
992
992
  "DO use `fullWidth` inside a narrow form card so the joined row spans the field column, exactly like a lone Input would.",
993
- "`fullWidth` sets NO per-child flex ratio — same as antd, whose Space.Compact block style only stretches the ROW (`display: flex; width: 100%`), never the children. Two fields (NumberInput+Select, Input+Select) split the row evenly because both already default to their own full width; a Button beside a field keeps its own content width and the field absorbs the rest (Select+Button, Input+Button) — the same split you get outside SpaceCompact.",
993
+ "`fullWidth` gives EVERY child an equal share of the row — each item carries `flex: 1 1 0%`, so the split is even no matter what the children would size to on their own. Measured at a 1169px row: NumberInput+Select 585/585, and Select+Button ALSO 585/585, i.e. a Button under `fullWidth` is stretched to half the row, not left at its content width. Want the Button at content width and the field absorbing the rest? Omit `fullWidth` — the row is then `inline-flex` and sizes to content (measured 223/40). Do NOT remove the per-child `flex` to chase antd's block style: without it the items collapse to their content and the row stops filling at all (measured 203/73.6 in a 1169px column), which defeats `fullWidth`.",
994
994
  "DON'T expect corner-radius welding on `orientation=\"vertical\"` yet — the shared border still collapses, but each child keeps all four of its own corners rounded until a block-axis radius knob exists on the Input/trigger families (documented gap, not a silent one)."
995
995
  ],
996
996
  "useCases": [
@@ -1065,7 +1065,9 @@
1065
1065
  "DO NOT place a DataTable inside a ResponsiveGrid column beside a card or chart. DataTable must occupy its own full-width row in a Card with CardContent flush. Nesting a multi-column table in a grid column squeezes CJK text to one character per line (see rule 37).",
1066
1066
  "DO use ResponsiveGrid for page-level spacing — it applies the correct gap token (--space-stack-md) automatically. Never add raw gap-* / p-* / space-* utilities to the page layout around tiles; compose spacing through this component instead (rule 40).",
1067
1067
  "DO render SkeletonStat children in place of StatCard tiles while KPIs are loading — same columns prop, same count as the real tiles. Switch to real StatCard once data resolves.",
1068
- "The grid uses CSS container queries, not viewport media queries — it responds to its containing block width, not the window. Ensure the container is not artificially constrained (e.g. inside a narrow SplitPane column) or column expansion will never trigger."
1068
+ "The grid uses CSS container queries, not viewport media queries — it responds to its containing block width, not the window. Ensure the container is not artificially constrained (e.g. inside a narrow SplitPane column) or column expansion will never trigger.",
1069
+ "THE STEP NAMES ARE CONTAINER WIDTHS, AND THEY DO NOT LINE UP WITH VIEWPORT NAMES — `sm` is 40rem, `md` 48rem, `lg` 64rem OF THE GRID'S OWN BOX. A page gutter eats the difference: inside a 1280px shell with 32px gutters a 768px tablet hands the grid 704px, so `columns={{ base: 2, md: 3 }}` stays 2-up there — measured. Key the tablet column to `sm` instead (`{ base: 2, sm: 3, lg: 4 }`) and verify by measuring the GRID, not the window. Never reach for a viewport media query to 'correct' it.",
1070
+ "An UNNAMED `@container` in your own CSS resolves against the nearest ANCESTOR container, which inside a grid is the grid — not the card. A rule meant to fire per card measured against the 358px grid instead of the 163px cell. Either make the card its own container (`container-type: inline-size`) or use a plain media query when the viewport is genuinely what you meant."
1069
1071
  ],
1070
1072
  "useCases": [
1071
1073
  "Dashboard KPI row: rendering 3–4 StatCard tiles (revenue, member count, active invoices, overdue amount) that reflow to a 2-column stacked grid on tablet and a single column on mobile.",
@@ -2657,6 +2659,12 @@
2657
2659
  "name": "whitespace",
2658
2660
  "type": "\"normal\" | \"pre-wrap\""
2659
2661
  },
2662
+ {
2663
+ "defaultValue": "\"normal\"",
2664
+ "description": "Where an over-long unbroken token may break. THE prop for a machine identifier — an email address, a coupon code, a login id, a template key — in a DataTable/TableCell or a Descriptions value: `break=\"anywhere\"` emits `overflow-wrap: anywhere` (and releases a table cell's inherited `nowrap`), so the column shrinks to the viewport instead of scrolling the page. Use it INSTEAD of `className=\"[overflow-wrap:anywhere] break-words whitespace-normal\"`. NOT `whitespace=\"pre-wrap\"`: that is for typed line breaks, and its `break-word` does not lower min-content, so in a table cell it breaks nothing (measured: a 71-char token holds a 320px table at 630px with pre-wrap, 320px with break=\"anywhere\"). Independent of `whitespace` (both may be set); `truncate` wins (dev builds warn); `clamp` composes.",
2665
+ "name": "break",
2666
+ "type": "\"normal\" | \"anywhere\""
2667
+ },
2660
2668
  {
2661
2669
  "description": "Tabular figures for aligned numbers.",
2662
2670
  "name": "tabular",
@@ -2789,6 +2797,7 @@
2789
2797
  "usage": [
2790
2798
  "DO use `<Text>` for ALL body / inline / caption text instead of a styled `<span>`/`<p>`. Pick `size` from the scale; never write `text-[13px]`/`text-[11px]` or `font-semibold` by hand.",
2791
2799
  "DO use `tone` for colour (`muted`/`primary`/semantic), `tabular` for numbers, `mono` for codes — not `className=\"text-muted-foreground font-mono tabular-nums\"`.",
2800
+ "DO use `break=\"anywhere\"` for an email, code or id in a table cell or Descriptions value — never `className=\"[overflow-wrap:anywhere] break-words whitespace-normal\"`, and never `whitespace=\"pre-wrap\"`, which does not shrink a table column.",
2792
2801
  "DO use `clamp={n}` for a description limited to n lines (card grids) and `truncate` for a one-line ellipsis — never `className=\"line-clamp-2\"` (banned utility). They are mutually exclusive; `clamp` wins.",
2793
2802
  "For a heading, use `<Heading level>` instead of a large-size `<Text>`.",
2794
2803
  "DO use `link` for a link inside RUNNING CONTENT — an issue subject in a table cell, a page name in a list, a \"see all\" at the end of a row — and compose it onto your router link with `asChild`. Never `className=\"text-primary hover:underline\"`: that is consumer rules 6 and 7 in one string, and it leaves a keyboard user with no underline because `hover:` cannot fire for them.",
@@ -2797,6 +2806,7 @@
2797
2806
  "useCases": [
2798
2807
  "A muted caption under a value: `<Text size=\"xs\" tone=\"muted\">2026年5月度</Text>`.",
2799
2808
  "A monospace id in a list row: `<Text size=\"xs\" mono tone=\"muted\">RC-204881</Text>`.",
2809
+ "A staff email in a narrow DataTable cell at 320px, wrapping instead of scrolling the page: `<Text size=\"sm\" break=\"anywhere\">{staff.email}</Text>`.",
2800
2810
  "An emphasized inline figure: `<Text weight=\"medium\" tabular>¥1,240,000</Text>`.",
2801
2811
  "A service-card description clamped to 2 lines on a narrow (390px) index: `<Text as=\"p\" size=\"sm\" tone=\"muted\" clamp={2}>{description}</Text>`.",
2802
2812
  "An issue subject linking out of a table cell, wrapping to two lines: `<Text asChild link truncate={false}><Link href={`/view/${key}`}>{subject}</Link></Text>`.",
@@ -3185,7 +3195,9 @@
3185
3195
  "related": [
3186
3196
  "AuthShell — pairs with Reveal for the auth card entrance; AuthShell delegates all motion to Reveal.",
3187
3197
  "Card — the most common thing to wrap in <Reveal> (dashboard cards, auth card, settings sections).",
3188
- "ResponsiveGrid — combine with `<Reveal delay={n}>` (or `asChild`) per grid item for a staggered grid reveal."
3198
+ "ResponsiveGrid — combine with `<Reveal delay={n}>` (or `asChild`) per grid item for a staggered grid reveal.",
3199
+ "Carousel — wrap the WHOLE Carousel in one `<Reveal on=\"view\">`, never each CarouselItem: the rail clips its slides, so a per-slide observer never fires for the ones outside the window.",
3200
+ "ScrollArea — same rule as Carousel: reveal the scroller, not the children it clips."
3189
3201
  ],
3190
3202
  "rules": [],
3191
3203
  "storyPath": "general/Reveal.stories.tsx",
@@ -3198,6 +3210,9 @@
3198
3210
  "DO stagger a list/column by passing an increasing `delay` (1, 2, 3…) to successive siblings — the ordinal maps to `--reveal-stagger-step`, so a service retunes the cascade rhythm from one token.",
3199
3211
  "DO pass `asChild` when wrapping an element that must keep its own box in a grid/flex row (the reveal merges onto that element instead of adding a <div>).",
3200
3212
  "DO rely on the built-in reduced-motion behaviour — under `prefers-reduced-motion: reduce` the animation is dropped and content renders in its final, fully-visible position with no layout shift. Never gate visibility on the animation.",
3213
+ "DON'T put `on=\"view\"` on an element a scroller CLIPS — a Carousel slide, a horizontal ScrollArea child, anything inside `overflow: hidden`/`clip`. The observer's root is the VIEWPORT, and a clipped box has an EMPTY intersection rect, so an item parked outside its rail never leaves `data-reveal-state=\"out\"` and stays at `opacity: 0` forever. Measured on a 4-slide rail at 1440: 3 of the 4 slides sat invisible after scrolling the whole document, and stayed invisible. This is the one failure this component may not have. Reveal the RAIL (one `<Reveal on=\"view\">` around the whole `<Carousel>`/`<ScrollArea>`), never the slides.",
3214
+ "DON'T budget a reveal per card inside a rail even when it does work — one region is the entrance, the cascade belongs to a grid whose items all share the page's scroll. `delay` caps at 6, so a long list should pass `Math.min(i, 5) + 1` rather than the raw index.",
3215
+ "DO expect a re-filtered list to re-enter: changing a filter unmounts and remounts the wrappers, so each new item gets a fresh observer and animates again. Items already scrolled past are re-revealed on their way back, which is correct — they are never left hidden.",
3201
3216
  "DO NOT set a raw ms delay or a literal translate distance — the whole point is that `delay` is a controlled ordinal and the distance/duration come from tokens."
3202
3217
  ],
3203
3218
  "useCases": [
@@ -5057,6 +5072,12 @@
5057
5072
  "importPath": "@godxjp/ui/data-display",
5058
5073
  "name": "Table",
5059
5074
  "props": [
5075
+ {
5076
+ "defaultValue": "false",
5077
+ "description": "TableRow only. The whole ROW is the target: pointer cursor, a hover that means \"clickable\" rather than merely \"hovered\", and (via focus-ring.css) the design-system focus ring for the real <a>/<button> inside a cell. PRESENTATION ONLY — it binds no handler and deliberately adds no tabindex/role: a <tr> is not a control, and the thing a keyboard user reaches must stay a real control inside the row. Pair it with your own onClick on the row and keep a genuine link/button in the first cell. Reach for it when a design calls for \"a list with COLUMNS whose rows are selectable\" — ListRow gets all of this free by wrapping its own <a>, which a <tr> cannot do, and DataTable solves a different problem (it brings a toolbar and pagination many designs do not have). gh#929.",
5078
+ "name": "interactive",
5079
+ "type": "boolean"
5080
+ },
5060
5081
  {
5061
5082
  "description": "PER-INSTANCE column measures for preset=\"action-collection\", in place of re-pointing its --table-action-collection-* knobs from a consumer stylesheet. Those knobs are global by design, and that is the problem: two collections on one screen do not share a column budget — a console that widened `actions` globally so a Japanese status badge would stop breaking to one character per line (an SC 1.4.10 reflow failure) collapsed a sibling table's name column to ~15px in the same change. Emitted as inline custom properties, the same contract Flex `width` uses for a call-site measurement, leaving data-column-widths on the DOM so each escape stays countable.",
5062
5083
  "name": "columnWidths",
@@ -5963,6 +5984,109 @@
5963
5984
  "Toolbar search on a data-heavy accounting page (e.g. journal-entry search, partner lookup in a subledger view) where the 250 ms debounce prevents a flood of API calls on every keystroke without requiring the developer to implement debounce logic."
5964
5985
  ]
5965
5986
  },
5987
+ {
5988
+ "example": "import { RecordPicker } from \"@godxjp/ui/data-entry\";\n\n<RecordPicker\n mode=\"multiple\"\n count={totalApprovers} // the WHOLE set, from the server\n loadOptions={async ({ query, filters }) =>\n api.approvers({ q: query, role: filters.role })\n }\n filters={[\n { name: \"role\", label: \"Role\", options: roleOptions },\n ]}\n selectedOptions={form.approvers} // labels the form already has\n value={form.approverIds}\n onValueChange={(ids) => form.setApproverIds(ids as string[])}\n/>",
5989
+ "group": "data-entry",
5990
+ "importPath": "@godxjp/ui/data-entry",
5991
+ "name": "RecordPicker",
5992
+ "props": [
5993
+ {
5994
+ "defaultValue": "\"single\"",
5995
+ "description": "Decides the VALUE SHAPE and the dialog's commit model. `single` fires onValueChange with a single value (or null) and closes on the pick; `multiple` fires an array and commits only on Confirm, so a mis-click in a list of ten thousand is undone by Cancel rather than by re-finding the row.",
5996
+ "name": "mode",
5997
+ "type": "\"single\" | \"multiple\""
5998
+ },
5999
+ {
6000
+ "defaultValue": "10",
6001
+ "description": "The dropdown/dialog switch. A dropdown is the right control for eight people and the wrong one for eight hundred; this is where that line is drawn, once, by a service rather than per screen.",
6002
+ "name": "threshold",
6003
+ "type": "number"
6004
+ },
6005
+ {
6006
+ "description": "Size of the WHOLE set, which only a server knows — a page of results does not, so never pass `rows.length` from a fetch. Omitted with `options` it is their length; omitted with `loadOptions` the dialog shape is assumed (you cannot count what you have not fetched).",
6007
+ "name": "count",
6008
+ "type": "number"
6009
+ },
6010
+ {
6011
+ "description": "Static rows, filtered client-side. Provide this OR `loadOptions`. Same row shape Select takes, `group` included — a mixed 'users and roles' picker is two groups, not two components.",
6012
+ "name": "options",
6013
+ "type": "(SearchSelectOptionProp | SelectOptionGroupProp)[]"
6014
+ },
6015
+ {
6016
+ "description": "Server fetcher, debounced. `filters` carries YOUR OWN vocabulary back (the `name` of each declared filter), so the server reads the keys it already understands instead of a shape this component invented.",
6017
+ "name": "loadOptions",
6018
+ "type": "(params: { query: string; filters: Record<string, string>; cursor?: string }) => Promise<{ options: SearchSelectOptionProp[]; count?: number; nextCursor?: string }>"
6019
+ },
6020
+ {
6021
+ "description": "Filter controls rendered above the dialog list; their values reach `loadOptions({ filters })`. This is the 'search condition' half of the report — picking an issue by key alone fails, picking it by status + type + assignee works.",
6022
+ "name": "filters",
6023
+ "type": "{ name: string; label: string; options: { value: string; label: string }[] }[]"
6024
+ },
6025
+ {
6026
+ "description": "Labels for values the picker HOLDS but has not loaded. On first render there is no result page at all, so without this a chip renders a raw id; it is merged ahead of anything that loads later, so the chip also survives a query that does not return its row.",
6027
+ "name": "selectedOptions",
6028
+ "type": "SearchSelectOptionProp[]"
6029
+ },
6030
+ {
6031
+ "description": "A real row meaning \"none\" (an unassigned owner), pinned first — NOT a cleared field. The distinction matters: \"no assignee\" is a value the record holds and the server stores, while an empty field is the absence of an answer.",
6032
+ "name": "emptyOption",
6033
+ "type": "{ value: string; label: string }"
6034
+ },
6035
+ {
6036
+ "description": "Controlled value. Array for `multiple`, single value (or null) otherwise.",
6037
+ "name": "value",
6038
+ "type": "string | string[] | null"
6039
+ },
6040
+ {
6041
+ "description": "Receives the SHAPE you passed in — never a one-element array for a single picker, which the consumer would have to unwrap at every call site.",
6042
+ "name": "onValueChange",
6043
+ "type": "(value: string | string[] | null) => void"
6044
+ },
6045
+ {
6046
+ "description": "Uncontrolled initial value, same shape as `value`. Use `value` + `onValueChange` when a form owns the state.",
6047
+ "name": "defaultValue",
6048
+ "type": "string | string[] | null"
6049
+ },
6050
+ {
6051
+ "description": "Trigger text while nothing is chosen. Defaults to the localized `dataEntry.recordPicker.placeholder`; once a value exists the trigger shows chips instead.",
6052
+ "name": "placeholder",
6053
+ "type": "string"
6054
+ },
6055
+ {
6056
+ "description": "Dialog heading. Defaults to the localized `dataEntry.recordPicker.dialogTitle`.",
6057
+ "name": "dialogTitle",
6058
+ "type": "string"
6059
+ },
6060
+ {
6061
+ "description": "Control height of the trigger, on the shared control ladder.",
6062
+ "name": "size",
6063
+ "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
6064
+ }
6065
+ ],
6066
+ "related": [
6067
+ "Select — what this renders at or under the threshold. Use Select directly when the set is small and fixed.",
6068
+ "Command — the dialog list is a Command with `shouldFilter={false}`; the query is answered by your server, not by cmdk over a page it cannot see.",
6069
+ "DataTable — for displaying and acting on records rather than picking one.",
6070
+ "Dialog — the over-threshold branch; `RecordPicker` opens one for you."
6071
+ ],
6072
+ "rules": [],
6073
+ "storyPath": "data-entry/RecordPicker.stories.tsx",
6074
+ "tagline": "ONE picker whose SHAPE follows the size of the set behind it: at or under `threshold` it IS a Select (dropdown with search); over it, a Dialog with search + consumer-declared filters, chips, and a confirm step for `multiple`. Reach for it when the same field may hold eight records on one tenant and eight thousand on another.",
6075
+ "usage": [
6076
+ "DO pass `count` from the server whenever the list is server-backed. It is the only honest input to the threshold decision — `rows.length` describes the page you fetched, not the set behind it, so a picker over 9000 records would render as a dropdown showing 20.",
6077
+ "DO declare `filters` in your own vocabulary (`{ name: 'status', … }`) — the names travel to `loadOptions({ filters })` unchanged, so the server reads the keys it already has.",
6078
+ "DO pass `selectedOptions` for any value the form arrives holding. On first render there is no result page, so a chip without it shows a raw id; with it the chip is correct before anything loads and survives a query that excludes its row.",
6079
+ "DO use `emptyOption` for \"unassigned\" rather than leaving the field empty — a record that HOLDS \"no owner\" is a different state from a field nobody answered, and only the first round-trips.",
6080
+ "DON'T reach for this when the set is small and fixed (a status, a priority, a country): that is `Select`, and RecordPicker just renders Select for you with an extra layer of props in between.",
6081
+ "DON'T use it as a table replacement. It picks records; it does not display, sort or edit them — a screen that needs columns and bulk actions wants DataTable.",
6082
+ "DON'T nest it inside a Popover. It opens a Dialog, and a dialog inside a popover loses focus containment; it is designed to be opened from a field inside a Dialog or Sheet, which is where the consumer reports put it."
6083
+ ],
6084
+ "useCases": [
6085
+ "Assigning an owner on a busy admin screen where one tenant has 8 people and another has 800.",
6086
+ "An approval step that mixes USERS and ROLES in one field — two `group`s in one list, chips distinguishable by the option's `icon`.",
6087
+ "Picking an issue/record by key OR title with status/type/assignee filters, replacing a free-text key field that only failed validation after save."
6088
+ ]
6089
+ },
5966
6090
  {
5967
6091
  "absorbed": [
5968
6092
  "Combobox",
@@ -12741,7 +12865,10 @@
12741
12865
  "DON'T use a Carousel where ALL items must be seen/compared at once or be keyboard-reachable in reading order (e.g. a list of selectable options, a data table, primary navigation) — hiding content behind a swipe is an anti-pattern there; use a Grid/`ResponsiveGrid`, `ScrollArea`, or `Tabs`.",
12742
12866
  "DON'T autoplay without a pause-on-hover/focus control and reduced-motion respect — pass the Embla autoplay plugin via `plugins` only for non-essential decorative content, never for content the user must read.",
12743
12867
  "DO set `opts={{ loop: true }}` for galleries that wrap, and rely on the built-in disabling: `CarouselPrevious`/`CarouselNext` auto-disable at the ends (via `canScrollPrev`/`canScrollNext`) — don't hide them, let them grey out.",
12744
- "DO give each `<CarouselItem>` real, meaningful content; the component already injects an 'N of M' slide label for screen readers, so don't add a redundant one (a consumer `aria-label` on the item overrides the default)."
12868
+ "DO give each `<CarouselItem>` real, meaningful content; the component already injects an 'N of M' slide label for screen readers, so don't add a redundant one (a consumer `aria-label` on the item overrides the default).",
12869
+ "DO size a MULTI-ITEM rail with `basis-*` on the item: `<CarouselItem className=\"basis-full sm:basis-1/2 lg:basis-1/3\">` is 1/2/3 per view, and `basis-4/5` leaves the \"peek\" that tells a phone user the rail scrolls. Use the FRACTIONAL ladder, never an arbitrary `basis-[82%]` (the audit rejects it). Fixed in gh#930 — before that the item carried `min-width: 100%`, which silently beat every `basis-*`: the documented 3-across composition computed `flex-basis: 33.3333%` and still painted one full-width slide 32px WIDER than its own rail. If a rail renders 1-up on an older version, that is the bug, not your className.",
12870
+ "DON'T wrap each `<CarouselItem>` in `<Reveal on=\"view\">`. The rail clips its slides, so the observer on a slide outside the window has an empty intersection rect and the slide never becomes visible — measured at 3 of 4 slides stuck at `opacity: 0`. Put ONE `<Reveal on=\"view\">` around the whole `<Carousel>`.",
12871
+ "DO expect the prev/next arrows to OVERLAY the rail edges — they are `position: absolute` inside the Carousel, so placing them in a caption row is not something a prop offers. Two knobs retune them: `--carousel-arrow-inset-block-start` for the vertical placement, and `--carousel-arrow-size` for the BUTTON box (default 2rem/32px, which clears WCAG 2.2 SC 2.5.8's 24px floor). A brand whose own standard is a 44px target sets `--carousel-arrow-size: 2.75rem` in its theme — the box and the glyph are separate decisions, so the mark stays on the icon scale via `--carousel-arrow-icon-size`. The box token is gh#931; before it, those were two bare literals and the target was the one dimension a theme could not reach."
12745
12872
  ],
12746
12873
  "useCases": [
12747
12874
  "Feature / onboarding highlight cards on a dashboard or landing surface, with CarouselDots showing position.",
package/agent/index.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "counts": {
3
3
  "anti-ai-tells": 26,
4
- "components": 172,
5
- "patterns": 20,
4
+ "components": 173,
5
+ "patterns": 21,
6
6
  "rules": 50,
7
- "tokens": 2071,
7
+ "tokens": 2073,
8
8
  "vocabulary": 14
9
9
  },
10
10
  "files": [
11
11
  {
12
12
  "file": "components-index.json",
13
- "note": "45 KB — name + group + tagline for all 172. FETCH THIS FIRST, then fetch only the components you chose.",
13
+ "note": "46 KB — name + group + tagline for all 173. FETCH THIS FIRST, then fetch only the components you chose.",
14
14
  "url": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json"
15
15
  },
16
16
  {
@@ -48,19 +48,19 @@
48
48
  "note": "Pin to the tag that matches the @godxjp/ui version you installed. A catalog newer than your package describes props you do not have; older, and it hides props you do.",
49
49
  "read": {
50
50
  "live": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json",
51
- "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v30.4.1/agent/index.json"
51
+ "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v30.5.2/agent/index.json"
52
52
  },
53
53
  "source": "mcp/src/data — the same data @godxjp/ui-mcp serves — plus the foundation and semantic token tiers, read from src/tokens/*.css",
54
54
  "start": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md",
55
55
  "tokenTiers": {
56
56
  "component": "per-part knobs, --{component}-{part}-{property}; usually leave these alone",
57
57
  "counts": {
58
- "component": 1757,
58
+ "component": 1759,
59
59
  "foundation": 211,
60
60
  "semantic": 103
61
61
  },
62
62
  "foundation": "the seeds a consumer is invited to set — --primary, --background, --radius",
63
63
  "semantic": "named roles that follow the seeds — --ring, --text-link, --overlay-background"
64
64
  },
65
- "version": "30.4.1"
65
+ "version": "30.5.2"
66
66
  }
package/agent/llms.txt CHANGED
@@ -1,10 +1,10 @@
1
1
  # @godxjp/ui
2
2
 
3
- > A Japanese-enterprise React design system: 172 components, 2071 design tokens,
4
- > 50 cardinal rules. This file is the entry point for AI agents. Catalog version 30.4.1.
3
+ > A Japanese-enterprise React design system: 173 components, 2073 design tokens,
4
+ > 50 cardinal rules. This file is the entry point for AI agents. Catalog version 30.5.2.
5
5
 
6
6
  If your client can run a process, do not read these files — run the MCP server instead
7
- (`npx @godxjp/ui-mcp@30.4.1`). It is searchable and version-locked. These files exist for agents
7
+ (`npx @godxjp/ui-mcp@30.5.2`). It is searchable and version-locked. These files exist for agents
8
8
  that can only fetch URLs.
9
9
 
10
10
  ## Start
@@ -14,11 +14,11 @@ that can only fetch URLs.
14
14
 
15
15
  ## Catalog
16
16
 
17
- - [patterns-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/patterns-index.json): 20 whole-task patterns (name, tagline, tags). Start here when the task is a TASK — "build a settings page" — then fetch `patterns/<name>.json` for complete code.
18
- - [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 45 KB — all 172 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
17
+ - [patterns-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/patterns-index.json): 21 whole-task patterns (name, tagline, tags). Start here when the task is a TASK — "build a settings page" — then fetch `patterns/<name>.json` for complete code.
18
+ - [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 46 KB — all 173 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
19
19
  - [components/&lt;Name&gt;.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json): one file per component (1 KB–34 KB, median 6 KB). Read the index, then fetch only the ones you chose — this is the selective route, and the reason you do not need the blob.
20
20
  - [components.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json): 1.2 MB — every entry in one file. Most URL fetchers truncate a response this size without saying so; prefer the per-component files.
21
- - [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 211 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 103 `semantic` roles, 1757 `component` knobs.
21
+ - [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 211 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 103 `semantic` roles, 1759 `component` knobs.
22
22
  - [vocabulary.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/vocabulary.json): the controlled prop vocabulary — which prop name means what, across every component.
23
23
  - [rules.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json): 50 cardinal rules.
24
24
  - [anti-ai-tells.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/anti-ai-tells.json): 26 shapes that make generated UI look generated, each with its fix.
@@ -26,7 +26,7 @@ that can only fetch URLs.
26
26
  ## Pinning
27
27
 
28
28
  Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
29
- the tag: `.../godx-jp/godxjp-ui/v30.4.1/agent/...`. A catalog that does not match the installed
29
+ the tag: `.../godx-jp/godxjp-ui/v30.5.2/agent/...`. A catalog that does not match the installed
30
30
  package describes props that are absent, or hides props that are present, and says nothing either way.
31
31
 
32
32
  Pinned catalogs only exist for releases whose tag actually contains `agent/`. If `…/v<version>/agent/index.json` returns 404, that release predates this catalog: read `…/main/…` instead and compare `index.json` → `version` against the package you have, so you at least know which way it drifted.