@godxjp/ui 33.8.0 → 33.9.1

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
@@ -18,7 +18,7 @@ npm i @godxjp/ui
18
18
 
19
19
  <!-- agent-catalog-counts:start -->
20
20
 
21
- The catalog — 166 components, 2235 tokens, 44 cardinal rules — is published in two forms from one
21
+ The catalog — 169 components, 2235 tokens, 44 cardinal rules — is published in two forms from one
22
22
  source. **Entry point for either: [`AGENTS.md`](AGENTS.md).**
23
23
  <!-- agent-catalog-counts:end -->
24
24
 
@@ -53,9 +53,9 @@ static files straight from this public repo — no hosting, no deploy step:
53
53
  | [`agent/llms.txt`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/llms.txt) | 3 KB | the [llms.txt](https://llmstxt.org/) entry point |
54
54
  | [`agent/DESIGN.md`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/DESIGN.md) | 17 KB | the visual spec: tokens, colour scarcity, admin screen anatomy, do/don't |
55
55
  | [`agent/index.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json) | 3 KB | manifest: version, counts, every file's URL |
56
- | [`agent/components-index.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json) | 50 KB | all 166 as name + group + tagline — **fetch this first** |
56
+ | [`agent/components-index.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json) | 52 KB | all 169 as name + group + tagline — **fetch this first** |
57
57
  | [`agent/components/<Name>.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json) | 1 KB–37 KB | one file per component, each with its `importPath` — **this is the route to take** |
58
- | [`agent/components.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json) | 1.3 MB | all 166 entries in one file — most URL fetchers truncate this silently; prefer the per-component files |
58
+ | [`agent/components.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json) | 1.3 MB | all 169 entries in one file — most URL fetchers truncate this silently; prefer the per-component files |
59
59
  | [`agent/tokens.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json) | 859 KB | all 2235 design tokens, their values and why they exist |
60
60
  | [`agent/vocabulary.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/vocabulary.json) | 7 KB | the controlled prop vocabulary |
61
61
  | [`agent/rules.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json) | 23 KB | the 44 cardinal rules |
@@ -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` 33.8.0.** If the project you are editing has a different
6
+ **This catalog describes `@godxjp/ui` 33.9.1.** 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.
@@ -52,7 +52,7 @@ screen anatomy, the visual target the rules below do not give you. Then fetch in
52
52
  task is a task** — "build a settings page", "confirm a destructive delete", "a list page with
53
53
  filters" — start HERE, not at the components. Then fetch `patterns/<name>.json` for complete,
54
54
  copy-paste-ready code. A component index answers "does X exist"; it cannot answer "build Y".
55
- 1. `components-index.json` — 50 KB, all 166 components as name + group +
55
+ 1. `components-index.json` — 52 KB, all 169 components as name + group +
56
56
  tagline. Read this when you already know the SHAPE you need. Each entry may carry `absorbed`:
57
57
  names that **do not exist** and map to it — `Combobox`, `Autocomplete`, `CountrySelect` and
58
58
  `SearchSelect` are all `Select`. If you are about to hand-roll something, search this field
@@ -0,0 +1,73 @@
1
+ {
2
+ "docPath": "docs/data-entry/rich-text-editor.tsx",
3
+ "example": "import { RichTextEditor } from \"@godxjp/block-editor\";\nimport { FormField } from \"@godxjp/ui/data-entry\";\n\n<FormField label=\"説明\" required error={errors.description}>\n <RichTextEditor value={description} onValueChange={setDescription} />\n</FormField>",
4
+ "group": "data-entry",
5
+ "importPath": "@godxjp/block-editor",
6
+ "name": "RichTextEditor",
7
+ "props": [
8
+ {
9
+ "description": "Controlled HTML body (with `onValueChange`). Read through the schema: script, links, images, attributes, styles, h4–h6, tables are dropped to their text.",
10
+ "name": "value",
11
+ "type": "string"
12
+ },
13
+ {
14
+ "description": "Uncontrolled initial HTML.",
15
+ "name": "defaultValue",
16
+ "type": "string"
17
+ },
18
+ {
19
+ "description": "The schema's HTML after each edit — `\"\"` for an empty body, never `<p></p>`. A new `value` from outside (a reset, a locale tab) replaces the document WITHOUT calling it, and never during an IME composition.",
20
+ "name": "onValueChange",
21
+ "type": "(html: string) => void"
22
+ },
23
+ {
24
+ "description": "Shown while the body is empty.",
25
+ "name": "placeholder",
26
+ "type": "string"
27
+ },
28
+ {
29
+ "description": "Validation state the frame paints, as on Input; FormField's error arrives as `aria-invalid`.",
30
+ "name": "status",
31
+ "type": "\"error\" | \"warning\""
32
+ },
33
+ {
34
+ "description": "BCP-47 tag of the body's language, on the editable.",
35
+ "name": "lang",
36
+ "type": "string"
37
+ },
38
+ {
39
+ "description": "Override any toolbar string (defaults: kit ja / en / vi, `blockEditor.*`).",
40
+ "name": "labels",
41
+ "type": "Partial<RichTextEditorLabels>"
42
+ },
43
+ {
44
+ "description": "Not editable. readOnly drops the toolbar; disabled keeps it, disabled.",
45
+ "name": "disabled / readOnly",
46
+ "type": "boolean"
47
+ },
48
+ {
49
+ "description": "Land on the editable `textbox` — FormField's id, label, helper, required and error wiring reach it directly.",
50
+ "name": "id / aria-*",
51
+ "type": "string"
52
+ }
53
+ ],
54
+ "related": [
55
+ "TranslatableRichText — the same editor holding one HTML value per locale.",
56
+ "BlockEditor — the Markdown block editor in the same package.",
57
+ "MarkdownEditor — Markdown in a Textarea."
58
+ ],
59
+ "rules": [],
60
+ "storyPath": "data-entry/rich-text-editor.tsx",
61
+ "tagline": "A form field that edits HTML (sibling package `@godxjp/block-editor`, gh#1365): a fixed toolbar — bold, italic, strike, inline code · H1–H3, bulleted / numbered list, quote · undo, redo — over a Tiptap surface whose SCHEMA is the sanitiser. `value` / `onValueChange` carry HTML holding only p, br, h1–h3, ul, ol, li, blockquote, strong, em, s, code (`RICH_TEXT_ALLOWED_TAGS`), with one attribute in all (an integer `start` on `<ol>`); anything else in `value` or a paste is kept as its text. For content already stored as HTML; new content belongs in Markdown (BlockEditor / MarkdownEditor).",
62
+ "usage": [
63
+ "DO wrap it in FormField, like any field: the label names the editable, `required` and the error reach it as `aria-required` / `aria-invalid` + `aria-errormessage`.",
64
+ "DO store what onValueChange returns: it is already sanitised by the schema, and render it as HTML. Sanitise again on the server if other clients can write the same column.",
65
+ "DO use TranslatableRichText for a per-locale `{ ja, en, vi }` map instead of wrapping Tabs around it.",
66
+ "DON'T pick it for NEW content: Markdown (BlockEditor, MarkdownEditor) is the kit's storage format; this exists for columns that already hold HTML.",
67
+ "Keyboard: each toolbar group is one tab stop with arrow keys; ⌘/Ctrl+B / I / Z / Shift+Z and the Markdown input rules (`# `, `- `, `1. `, `> `) work in the body."
68
+ ],
69
+ "useCases": [
70
+ "Product, material or recipe description stored as HTML and rendered by a storefront",
71
+ "Notice or email body column that already holds HTML"
72
+ ]
73
+ }
@@ -5,9 +5,9 @@
5
5
  "name": "Tabs",
6
6
  "props": [
7
7
  {
8
- "description": "Optional data-driven tab list. When provided, Tabs renders all triggers and content panels. When Tabs owns the initial selection (no `value`, and no `defaultValue` naming an existing ENABLED item), it falls back to the first item that is NOT `disabled` — never a disabled one — and selects nothing if every item is disabled. `icon` is a leading glyph in the trigger (Ant Design `Tab.icon`). `closable` / `closeIcon` only apply under `variant=\"editable-card\"`; `closable: false` opts one tab out of removal (Ant Design `getRemovable`). `forceRender` (Ant Design `Tab.forceRender`) mounts THAT panel up front and keeps it mounted while another tab is selected, without switching the whole strip over with `destroyOnHidden={false}` — antd's per-item `destroyOnHidden` is deliberately not offered, because on this component \"kept mounted\" is one state and it would be a second spelling of `forceRender`. `count` (+ `overflowCount`, default 99; `showZero`, default true; `countLabel`) draws the counter pill beside the label — 「未対応 12」 — in the SAME vocabulary Button and Toggle publish and through the same helper, formatted with `Intl.NumberFormat` in the active locale. Pass `countLabel` to say what the number is: the pill is `aria-hidden` and an `sr-only` clause carries the digits, so the tab announces \"未対応, 12 件の課題\" and never the concatenated \"未対応12\". Prefer this over antd's answer, which is to put a Badge inside `label` — with the number inside the label the package owns neither its size nor its tone as the tab moves between selected / unselected / disabled, so every consumer aligns it differently.",
8
+ "description": "Optional data-driven tab list. When provided, Tabs renders all triggers and content panels. When Tabs owns the initial selection (no `value`, and no `defaultValue` naming an existing ENABLED item), it falls back to the first item that is NOT `disabled` — never a disabled one — and selects nothing if every item is disabled. `icon` is a leading glyph in the trigger (Ant Design `Tab.icon`). `closable` / `closeIcon` only apply under `variant=\"editable-card\"`; `closable: false` opts one tab out of removal (Ant Design `getRemovable`). `forceRender` (Ant Design `Tab.forceRender`) mounts THAT panel up front and keeps it mounted while another tab is selected, without switching the whole strip over with `destroyOnHidden={false}` — antd's per-item `destroyOnHidden` is deliberately not offered, because on this component \"kept mounted\" is one state and it would be a second spelling of `forceRender`. `count` (+ `overflowCount`, default 99; `showZero`, default true; `countLabel`) draws the counter pill beside the label — 「未対応 12」 — in the SAME vocabulary Button and Toggle publish and through the same helper, formatted with `Intl.NumberFormat` in the active locale. Pass `countLabel` to say what the number is: the pill is `aria-hidden` and an `sr-only` clause carries the digits, so the tab announces \"未対応, 12 件の課題\" and never the concatenated \"未対応12\". Prefer this over antd's answer, which is to put a Badge inside `label` — with the number inside the label the package owns neither its size nor its tone as the tab moves between selected / unselected / disabled, so every consumer aligns it differently. `href` makes the strip a ROUTE strip (gh#1364) — see `linkComponent`.",
9
9
  "name": "items",
10
- "type": "{ value: string; label: React.ReactNode; content: React.ReactNode; disabled?: boolean; icon?: React.ReactNode; closable?: boolean; closeIcon?: React.ReactNode; forceRender?: boolean; count?: number; overflowCount?: number; showZero?: boolean; countLabel?: string }[]"
10
+ "type": "{ value: string; label: React.ReactNode; content?: React.ReactNode; href?: string; disabled?: boolean; icon?: React.ReactNode; closable?: boolean; closeIcon?: React.ReactNode; forceRender?: boolean; count?: number; overflowCount?: number; showZero?: boolean; countLabel?: string }[]"
11
11
  },
12
12
  {
13
13
  "description": "Controlled active tab key.",
@@ -122,6 +122,11 @@
122
122
  "name": "onTabScroll",
123
123
  "type": "(info: { direction: \"start\" | \"end\" }) => void"
124
124
  },
125
+ {
126
+ "description": "ROUTE STRIP (gh#1364). When ANY item carries `href`, Tabs renders a `<nav>` landmark of real links instead of a tablist: each tab is an `<a href>` (middle-click, Ctrl/⌘-click, \"open in new tab\" and the context menu all work — no click is ever `preventDefault`ed), the item whose `value` equals `value` gets `aria-current=\"page\"`, and no tabpanel is rendered (`content` is ignored; the route renders its own page). Pass the CURRENT ROUTE as `value` (derive it from the pathname). `linkComponent` is the SAME contract and the same value as `Sidebar.linkComponent` / `NavList.linkComponent` — Inertia `<Link>`, or `createSidebarLink(Link, \"to\")` for React Router / TanStack — and must forward every prop to its `<a>`. Without it each tab is a plain `<a href>`. Name the nav with `aria-label`. Same variants, sizes and `--tabs-*` tokens as the tablist, and the strip scrolls its overflow exactly like one (`overflow=\"menu\"` additionally exposes hidden routes as native links). Keyboard is the browser's: Tab walks the links, Enter follows one. WHY NOT React Aria link tabs: `<Tab href>` renders `<a role=\"tab\">` (the link role is hidden) and React Aria preventDefaults every real click on it to replay a synthetic one.",
127
+ "name": "linkComponent",
128
+ "type": "SidebarLinkComponentProp"
129
+ },
125
130
  {
126
131
  "description": "Class on the TRIGGER STRIP (`TabsList`) under the `items` API — the handle that composing the tree manually gives you as `<TabsList className>`. `className` reaches only the root, which holds the strip AND the panels, so anything meant for the bar alone belongs here. Almost always unnecessary: placement, size, centring and the card rail are props and `--tabs-*` tokens.",
127
132
  "name": "listClassName",
@@ -162,6 +167,7 @@
162
167
  "DO put a tab's count in `count` (+ `countLabel`), not inside `label`. A number concatenated into the label loses the pill's size, its tone as the tab is selected/deselected, and — measured on Button in gh#734 — the accessible name: the digits run straight onto the label («未対応12»). With the slot the tab announces \"未対応, 12 件の課題\". DON'T drop a `<Badge>` into `label` to get the same look; there is one counting API and Button, Toggle and Tabs all draw it.",
163
168
  "DON'T go looking for antd's `tabBarGutter`, `tabBarStyle`, `renderTabBar`, `classNames`/`styles` or `more.popupRender` — each is declined on the record, not missing. The gutter between triggers is a theme knob (`--tabs-list-line-space-gap`, `--tabs-card-list-space-gap`; the pill strip has no gutter by design) because a px number is a constant, not a semantic axis; the other four exist upstream to let a consumer replace the rendered markup, and this library answers that layer with tokens (docs/DESIGN-AUTHORITY.md refuses them by name).",
164
169
  "DON'T re-centre the strip with a `justify-center` utility. `TabsList` aligns with `safe center` on purpose: plain centring splits the overflow across BOTH edges while `scrollLeft` only ever covers the trailing one, so the leading tab ends up permanently outside the scrollport and no gesture reaches it. `safe` keeps the centred look while the tabs fit and falls back to start alignment the moment they don't.",
170
+ "DO give a strip of sibling ROUTES (a settings area: /settings/cart-timeout, /settings/customer, …) `items[].href` + `linkComponent` and pass the current route as `value`. DON'T hand-roll it from `Link` pills in an `inline-flex rounded-lg bg-muted p-1` div (no `aria-current`, flagged by ui-audit), and DON'T drive a button tablist with `onValueChange` + `router.push` — that loses open-in-new-tab and middle-click (gh#1364).",
165
171
  "DON'T draw a list status view as Tabs. A list status view (All / Active / Closed, each with its count) that shows the SAME table — same columns, same row actions, a different query — is a filter, so it is `Segmented` with `count`, placed in the list's `Toolbar`; past 4 statuses it is a `Select`. `Tabs` only when each status is a genuinely different panel (its own columns, actions or empty state), declared as `TabsContent` or pointed at with `controls` (WAI-ARIA APG: a tab selects a panel, a radiogroup selects a value) (gh#1324)."
166
172
  ],
167
173
  "useCases": [
@@ -169,6 +175,7 @@
169
175
  "Controlled tabs driven by URL search params (e.g. `?tab=history`) where the parent reads/writes the active key and passes it to `value` / `onValueChange`.",
170
176
  "Vertical side-rail navigation inside a `SplitPane` or settings layout where `orientation=\"vertical\"` on the root and `variant=\"line\"` on `TabsList` combine to produce a sidebar-style tab strip.",
171
177
  "Lightweight widget tabs on a dashboard card — e.g. a 'Pending' panel (due date, remind action) and a 'Paid' panel (paid date, receipt link), each its own `DataTable` with its own columns — where an uncontrolled `defaultValue` is sufficient and no URL state is needed. Two views of the SAME table are a filter: `Segmented`.",
178
+ "A settings area with ~14 sibling routes shown as a horizontal strip above the page body — `items[].href` + `linkComponent`, `value` = the current route; it scrolls at phone width (gh#1364).",
172
179
  "Admin entity profile pages (company, partner, employee) where each `TabsContent` wraps an Inertia deferred prop panel, lazy-loading expensive data only when the tab is first activated."
173
180
  ]
174
181
  }
@@ -0,0 +1,69 @@
1
+ {
2
+ "example": "import { FormField, TranslatableField, Textarea } from \"@godxjp/ui/data-entry\";\n\n<FormField label=\"説明\">\n <TranslatableField\n locales={[\"ja\", \"en\", \"vi\"]}\n value={description}\n onValueChange={setDescription}\n renderControl={(control) => <Textarea {...control} rows={4} />}\n />\n</FormField>",
3
+ "group": "data-entry",
4
+ "importPath": "@godxjp/ui/data-entry",
5
+ "name": "TranslatableField",
6
+ "props": [
7
+ {
8
+ "description": "Renders the one control in the panel. Spread `control` onto it: `lang`, `value` / `onValueChange` (the active locale's text), the resolved `placeholder`, `status`, `size`, and the field-a11y props (`aria-labelledby` with the locale's name, `aria-invalid`, `aria-errormessage`) plus every prop the field does not own (`id`, `disabled`, …).",
9
+ "name": "renderControl",
10
+ "type": "(control: TranslatableFieldControlProp) => ReactNode"
11
+ },
12
+ {
13
+ "description": "Turns the fallback locale's stored text into the plain placeholder an empty locale shows (HTML → text for a rich-text control). Default: the text as stored.",
14
+ "name": "toPlaceholder",
15
+ "type": "(fallbackText: string) => string"
16
+ },
17
+ {
18
+ "description": "The per-locale map; onValueChange gets the whole map, only the active key changed.",
19
+ "name": "value / defaultValue / onValueChange",
20
+ "type": "TranslatableValue"
21
+ },
22
+ {
23
+ "description": "Exactly as on TranslatableInput.",
24
+ "name": "locales / defaultLocale / fallbackLocale / errors",
25
+ "type": "—"
26
+ },
27
+ {
28
+ "description": "Passed to the control; the tab strip uses Tabs `sm` (`md` for `lg`).",
29
+ "name": "size",
30
+ "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
31
+ },
32
+ {
33
+ "description": "Shown when there is no fallback text.",
34
+ "name": "placeholder",
35
+ "type": "string"
36
+ },
37
+ {
38
+ "description": "Passed to the control; the active locale's error turns it into `error`.",
39
+ "name": "status",
40
+ "type": "\"error\" | \"warning\""
41
+ },
42
+ {
43
+ "description": "Passed to the control, so a FormField label click focuses it.",
44
+ "name": "id",
45
+ "type": "string"
46
+ },
47
+ {
48
+ "description": "Root (strip + panel) class override.",
49
+ "name": "className",
50
+ "type": "string"
51
+ }
52
+ ],
53
+ "related": [
54
+ "TranslatableInput — the engine around an Input.",
55
+ "TranslatableTextarea — the engine around a Textarea.",
56
+ "TranslatableRichText (@godxjp/block-editor) — the engine around a RichTextEditor."
57
+ ],
58
+ "rules": [],
59
+ "storyPath": "data-entry/translatable-input.tsx",
60
+ "tagline": "The locale-tab engine behind TranslatableInput, TranslatableTextarea and `@godxjp/block-editor`'s TranslatableRichText (gh#1365): the locale list and first tab (AppProvider `contentLocales`), the per-locale map, the fallback placeholder, the error marks and the FormField error report — around ONE control you render with `renderControl`. Use it only to make another control translatable; for text use TranslatableInput / TranslatableTextarea.",
61
+ "usage": [
62
+ "DO render ONE control and spread `control` onto it (`<X {...control} ref={ref} />`): the engine swaps its value per tab, so the ref, FormField id and IME composition stay on one node.",
63
+ "DO make the control accept `lang`, `status`, `placeholder` and the field-a11y props on its focus target — that is how the locale, the error and the label reach it.",
64
+ "DON'T use it for a plain text field: TranslatableInput / TranslatableTextarea are this engine with an Input / Textarea already."
65
+ ],
66
+ "useCases": [
67
+ "Making a rich-text, date or custom control hold one value per locale with the kit's tab / error / fallback contract"
68
+ ]
69
+ }
@@ -0,0 +1,70 @@
1
+ {
2
+ "docPath": "docs/data-entry/rich-text-editor.tsx",
3
+ "example": "import { TranslatableRichText } from \"@godxjp/block-editor\";\nimport { FormField, type TranslatableValue } from \"@godxjp/ui/data-entry\";\n\nconst [description, setDescription] = React.useState<TranslatableValue>({ ja: \"<p>醤油ラーメン</p>\" });\n\n<FormField label=\"説明\" required>\n <TranslatableRichText\n locales={[\"ja\", \"en\", \"vi\"]}\n fallbackLocale=\"ja\"\n value={description}\n onValueChange={setDescription}\n errors={{ vi: \"ベトナム語の説明を入力してください\" }}\n />\n</FormField>",
4
+ "group": "data-entry",
5
+ "importPath": "@godxjp/block-editor",
6
+ "name": "TranslatableRichText",
7
+ "props": [
8
+ {
9
+ "description": "Controlled per-locale HTML, keyed by BCP-47 tag. A missing key or `\"\"` is an empty locale.",
10
+ "name": "value",
11
+ "type": "TranslatableValue"
12
+ },
13
+ {
14
+ "description": "Uncontrolled initial per-locale HTML.",
15
+ "name": "defaultValue",
16
+ "type": "TranslatableValue"
17
+ },
18
+ {
19
+ "description": "The WHOLE map after an edit; only the active locale's HTML changes.",
20
+ "name": "onValueChange",
21
+ "type": "(value: TranslatableValue) => void"
22
+ },
23
+ {
24
+ "description": "Tab order. Default: AppProvider `contentLocales` when set, else the registered locales.",
25
+ "name": "locales",
26
+ "type": "readonly string[]"
27
+ },
28
+ {
29
+ "description": "First tab. Default: the first of `contentLocales` when in `locales`, else the UI locale.",
30
+ "name": "defaultLocale",
31
+ "type": "string"
32
+ },
33
+ {
34
+ "description": "Whose text an empty locale shows as its placeholder. Default: AppProvider `fallbackLocale`.",
35
+ "name": "fallbackLocale",
36
+ "type": "string"
37
+ },
38
+ {
39
+ "description": "Per-locale messages: tab marks for each (glyph + screen-reader text), `aria-invalid` and FormField's error line for the active one.",
40
+ "name": "errors",
41
+ "type": "Partial<Record<string, ReactNode>>"
42
+ },
43
+ {
44
+ "description": "The tab strip's tier, as on TranslatableTextarea.",
45
+ "name": "size",
46
+ "type": "\"sm\" | \"md\" | \"lg\""
47
+ },
48
+ {
49
+ "description": "As on RichTextEditor; `ref` is the editor's handle.",
50
+ "name": "placeholder / disabled / readOnly / labels / id",
51
+ "type": "—"
52
+ }
53
+ ],
54
+ "related": [
55
+ "RichTextEditor — the single-locale editor it composes.",
56
+ "TranslatableTextarea — the same contract over plain text.",
57
+ "TranslatableField — the engine, for another control."
58
+ ],
59
+ "rules": [],
60
+ "storyPath": "data-entry/rich-text-editor.tsx",
61
+ "tagline": "ONE RichTextEditor holding an HTML value per locale (`{ ja, en, vi }`), switched by locale tabs (gh#1365). The kit's TranslatableField engine — the same as TranslatableInput / TranslatableTextarea: locales and first tab from AppProvider `contentLocales`, fallback from its `fallbackLocale`, per-locale error marks, the active locale's error in FormField. An empty locale shows the fallback locale's TEXT (not its HTML) as its placeholder; switching tabs swaps the HTML without calling onValueChange.",
62
+ "usage": [
63
+ "DO set `<AppProvider contentLocales={[\"ja\", \"en\", \"vi\"]}>` once; the field then needs no `locales` and opens on the source language.",
64
+ "DO wrap it in FormField and pass your validator's per-locale messages as `errors` (Laravel `description.vi` → `errors.vi`).",
65
+ "DON'T build Tabs + RichTextEditor yourself: the tab↔panel ARIA, lang, error marks and FormField report are the TranslatableField engine's."
66
+ ],
67
+ "useCases": [
68
+ "Product / material / recipe description stored as per-locale HTML (Astrotomic / Spatie translatable)"
69
+ ]
70
+ }
@@ -313,6 +313,16 @@
313
313
  "name": "BlockEditor",
314
314
  "tagline": "Notion / note.com style block editor (sibling package `@godxjp/block-editor`, versioned with the kit) that reads and writes MARKDOWN through `@godxjp/markdown/codec`: what onValueChange returns is byte-for-byte normalize(body). `/` block menu at the caret, ⋮⋮ handle (drag, turn into, duplicate, move, delete), floating format toolbar, `[[` wikilink suggestions, paste / drop uploads through the host, IME-safe, ja / en / vi (gh#1156)."
315
315
  },
316
+ {
317
+ "group": "data-entry",
318
+ "name": "RichTextEditor",
319
+ "tagline": "A form field that edits HTML (sibling package `@godxjp/block-editor`, gh#1365): a fixed toolbar — bold, italic, strike, inline code · H1–H3, bulleted / numbered list, quote · undo, redo — over a Tiptap surface whose SCHEMA is the sanitiser. `value` / `onValueChange` carry HTML holding only p, br, h1–h3, ul, ol, li, blockquote, strong, em, s, code (`RICH_TEXT_ALLOWED_TAGS`), with one attribute in all (an integer `start` on `<ol>`); anything else in `value` or a paste is kept as its text. For content already stored as HTML; new content belongs in Markdown (BlockEditor / MarkdownEditor)."
320
+ },
321
+ {
322
+ "group": "data-entry",
323
+ "name": "TranslatableRichText",
324
+ "tagline": "ONE RichTextEditor holding an HTML value per locale (`{ ja, en, vi }`), switched by locale tabs (gh#1365). The kit's TranslatableField engine — the same as TranslatableInput / TranslatableTextarea: locales and first tab from AppProvider `contentLocales`, fallback from its `fallbackLocale`, per-locale error marks, the active locale's error in FormField. An empty locale shows the fallback locale's TEXT (not its HTML) as its placeholder; switching tabs swaps the HTML without calling onValueChange."
325
+ },
316
326
  {
317
327
  "group": "data-display",
318
328
  "name": "Prose",
@@ -418,6 +428,11 @@
418
428
  "name": "TranslatableTextarea",
419
429
  "tagline": "TranslatableInput over a Textarea (gh#1336): one multi-line field holding a value per locale, switched by locale tabs. Same locale, fallback-placeholder, error-mark and FormField contract; every Textarea prop (rows, autoGrow, count…) applies to the one control."
420
430
  },
431
+ {
432
+ "group": "data-entry",
433
+ "name": "TranslatableField",
434
+ "tagline": "The locale-tab engine behind TranslatableInput, TranslatableTextarea and `@godxjp/block-editor`'s TranslatableRichText (gh#1365): the locale list and first tab (AppProvider `contentLocales`), the per-locale map, the fallback placeholder, the error marks and the FormField error report — around ONE control you render with `renderControl`. Use it only to make another control translatable; for text use TranslatableInput / TranslatableTextarea."
435
+ },
421
436
  {
422
437
  "group": "data-entry",
423
438
  "name": "Label",
@@ -5173,6 +5173,149 @@
5173
5173
  "Keyboard: `/` menu (↑↓ Enter Esc), Alt+Shift+↑/↓ moves a block, Mod+/ opens the block menu, Markdown shortcuts (`# `, `- `, `> `, ```` ``` ````, `---`)."
5174
5174
  ]
5175
5175
  },
5176
+ {
5177
+ "docPath": "docs/data-entry/rich-text-editor.tsx",
5178
+ "example": "import { RichTextEditor } from \"@godxjp/block-editor\";\nimport { FormField } from \"@godxjp/ui/data-entry\";\n\n<FormField label=\"説明\" required error={errors.description}>\n <RichTextEditor value={description} onValueChange={setDescription} />\n</FormField>",
5179
+ "group": "data-entry",
5180
+ "importPath": "@godxjp/block-editor",
5181
+ "name": "RichTextEditor",
5182
+ "props": [
5183
+ {
5184
+ "description": "Controlled HTML body (with `onValueChange`). Read through the schema: script, links, images, attributes, styles, h4–h6, tables are dropped to their text.",
5185
+ "name": "value",
5186
+ "type": "string"
5187
+ },
5188
+ {
5189
+ "description": "Uncontrolled initial HTML.",
5190
+ "name": "defaultValue",
5191
+ "type": "string"
5192
+ },
5193
+ {
5194
+ "description": "The schema's HTML after each edit — `\"\"` for an empty body, never `<p></p>`. A new `value` from outside (a reset, a locale tab) replaces the document WITHOUT calling it, and never during an IME composition.",
5195
+ "name": "onValueChange",
5196
+ "type": "(html: string) => void"
5197
+ },
5198
+ {
5199
+ "description": "Shown while the body is empty.",
5200
+ "name": "placeholder",
5201
+ "type": "string"
5202
+ },
5203
+ {
5204
+ "description": "Validation state the frame paints, as on Input; FormField's error arrives as `aria-invalid`.",
5205
+ "name": "status",
5206
+ "type": "\"error\" | \"warning\""
5207
+ },
5208
+ {
5209
+ "description": "BCP-47 tag of the body's language, on the editable.",
5210
+ "name": "lang",
5211
+ "type": "string"
5212
+ },
5213
+ {
5214
+ "description": "Override any toolbar string (defaults: kit ja / en / vi, `blockEditor.*`).",
5215
+ "name": "labels",
5216
+ "type": "Partial<RichTextEditorLabels>"
5217
+ },
5218
+ {
5219
+ "description": "Not editable. readOnly drops the toolbar; disabled keeps it, disabled.",
5220
+ "name": "disabled / readOnly",
5221
+ "type": "boolean"
5222
+ },
5223
+ {
5224
+ "description": "Land on the editable `textbox` — FormField's id, label, helper, required and error wiring reach it directly.",
5225
+ "name": "id / aria-*",
5226
+ "type": "string"
5227
+ }
5228
+ ],
5229
+ "related": [
5230
+ "TranslatableRichText — the same editor holding one HTML value per locale.",
5231
+ "BlockEditor — the Markdown block editor in the same package.",
5232
+ "MarkdownEditor — Markdown in a Textarea."
5233
+ ],
5234
+ "rules": [],
5235
+ "storyPath": "data-entry/rich-text-editor.tsx",
5236
+ "tagline": "A form field that edits HTML (sibling package `@godxjp/block-editor`, gh#1365): a fixed toolbar — bold, italic, strike, inline code · H1–H3, bulleted / numbered list, quote · undo, redo — over a Tiptap surface whose SCHEMA is the sanitiser. `value` / `onValueChange` carry HTML holding only p, br, h1–h3, ul, ol, li, blockquote, strong, em, s, code (`RICH_TEXT_ALLOWED_TAGS`), with one attribute in all (an integer `start` on `<ol>`); anything else in `value` or a paste is kept as its text. For content already stored as HTML; new content belongs in Markdown (BlockEditor / MarkdownEditor).",
5237
+ "usage": [
5238
+ "DO wrap it in FormField, like any field: the label names the editable, `required` and the error reach it as `aria-required` / `aria-invalid` + `aria-errormessage`.",
5239
+ "DO store what onValueChange returns: it is already sanitised by the schema, and render it as HTML. Sanitise again on the server if other clients can write the same column.",
5240
+ "DO use TranslatableRichText for a per-locale `{ ja, en, vi }` map instead of wrapping Tabs around it.",
5241
+ "DON'T pick it for NEW content: Markdown (BlockEditor, MarkdownEditor) is the kit's storage format; this exists for columns that already hold HTML.",
5242
+ "Keyboard: each toolbar group is one tab stop with arrow keys; ⌘/Ctrl+B / I / Z / Shift+Z and the Markdown input rules (`# `, `- `, `1. `, `> `) work in the body."
5243
+ ],
5244
+ "useCases": [
5245
+ "Product, material or recipe description stored as HTML and rendered by a storefront",
5246
+ "Notice or email body column that already holds HTML"
5247
+ ]
5248
+ },
5249
+ {
5250
+ "docPath": "docs/data-entry/rich-text-editor.tsx",
5251
+ "example": "import { TranslatableRichText } from \"@godxjp/block-editor\";\nimport { FormField, type TranslatableValue } from \"@godxjp/ui/data-entry\";\n\nconst [description, setDescription] = React.useState<TranslatableValue>({ ja: \"<p>醤油ラーメン</p>\" });\n\n<FormField label=\"説明\" required>\n <TranslatableRichText\n locales={[\"ja\", \"en\", \"vi\"]}\n fallbackLocale=\"ja\"\n value={description}\n onValueChange={setDescription}\n errors={{ vi: \"ベトナム語の説明を入力してください\" }}\n />\n</FormField>",
5252
+ "group": "data-entry",
5253
+ "importPath": "@godxjp/block-editor",
5254
+ "name": "TranslatableRichText",
5255
+ "props": [
5256
+ {
5257
+ "description": "Controlled per-locale HTML, keyed by BCP-47 tag. A missing key or `\"\"` is an empty locale.",
5258
+ "name": "value",
5259
+ "type": "TranslatableValue"
5260
+ },
5261
+ {
5262
+ "description": "Uncontrolled initial per-locale HTML.",
5263
+ "name": "defaultValue",
5264
+ "type": "TranslatableValue"
5265
+ },
5266
+ {
5267
+ "description": "The WHOLE map after an edit; only the active locale's HTML changes.",
5268
+ "name": "onValueChange",
5269
+ "type": "(value: TranslatableValue) => void"
5270
+ },
5271
+ {
5272
+ "description": "Tab order. Default: AppProvider `contentLocales` when set, else the registered locales.",
5273
+ "name": "locales",
5274
+ "type": "readonly string[]"
5275
+ },
5276
+ {
5277
+ "description": "First tab. Default: the first of `contentLocales` when in `locales`, else the UI locale.",
5278
+ "name": "defaultLocale",
5279
+ "type": "string"
5280
+ },
5281
+ {
5282
+ "description": "Whose text an empty locale shows as its placeholder. Default: AppProvider `fallbackLocale`.",
5283
+ "name": "fallbackLocale",
5284
+ "type": "string"
5285
+ },
5286
+ {
5287
+ "description": "Per-locale messages: tab marks for each (glyph + screen-reader text), `aria-invalid` and FormField's error line for the active one.",
5288
+ "name": "errors",
5289
+ "type": "Partial<Record<string, ReactNode>>"
5290
+ },
5291
+ {
5292
+ "description": "The tab strip's tier, as on TranslatableTextarea.",
5293
+ "name": "size",
5294
+ "type": "\"sm\" | \"md\" | \"lg\""
5295
+ },
5296
+ {
5297
+ "description": "As on RichTextEditor; `ref` is the editor's handle.",
5298
+ "name": "placeholder / disabled / readOnly / labels / id",
5299
+ "type": "—"
5300
+ }
5301
+ ],
5302
+ "related": [
5303
+ "RichTextEditor — the single-locale editor it composes.",
5304
+ "TranslatableTextarea — the same contract over plain text.",
5305
+ "TranslatableField — the engine, for another control."
5306
+ ],
5307
+ "rules": [],
5308
+ "storyPath": "data-entry/rich-text-editor.tsx",
5309
+ "tagline": "ONE RichTextEditor holding an HTML value per locale (`{ ja, en, vi }`), switched by locale tabs (gh#1365). The kit's TranslatableField engine — the same as TranslatableInput / TranslatableTextarea: locales and first tab from AppProvider `contentLocales`, fallback from its `fallbackLocale`, per-locale error marks, the active locale's error in FormField. An empty locale shows the fallback locale's TEXT (not its HTML) as its placeholder; switching tabs swaps the HTML without calling onValueChange.",
5310
+ "usage": [
5311
+ "DO set `<AppProvider contentLocales={[\"ja\", \"en\", \"vi\"]}>` once; the field then needs no `locales` and opens on the source language.",
5312
+ "DO wrap it in FormField and pass your validator's per-locale messages as `errors` (Laravel `description.vi` → `errors.vi`).",
5313
+ "DON'T build Tabs + RichTextEditor yourself: the tab↔panel ARIA, lang, error marks and FormField report are the TranslatableField engine's."
5314
+ ],
5315
+ "useCases": [
5316
+ "Product / material / recipe description stored as per-locale HTML (Astrotomic / Spatie translatable)"
5317
+ ]
5318
+ },
5176
5319
  {
5177
5320
  "example": "import { Prose } from \"@godxjp/ui/data-display\";\nimport Markdown from \"react-markdown\";\nimport remarkGfm from \"remark-gfm\";\n\n<Prose size=\"sm\">\n <Markdown remarkPlugins={[remarkGfm]}>{issue.description}</Markdown>\n</Prose>",
5178
5321
  "group": "data-display",
@@ -7303,6 +7446,75 @@
7303
7446
  "Recipe or notice body stored per locale"
7304
7447
  ]
7305
7448
  },
7449
+ {
7450
+ "example": "import { FormField, TranslatableField, Textarea } from \"@godxjp/ui/data-entry\";\n\n<FormField label=\"説明\">\n <TranslatableField\n locales={[\"ja\", \"en\", \"vi\"]}\n value={description}\n onValueChange={setDescription}\n renderControl={(control) => <Textarea {...control} rows={4} />}\n />\n</FormField>",
7451
+ "group": "data-entry",
7452
+ "importPath": "@godxjp/ui/data-entry",
7453
+ "name": "TranslatableField",
7454
+ "props": [
7455
+ {
7456
+ "description": "Renders the one control in the panel. Spread `control` onto it: `lang`, `value` / `onValueChange` (the active locale's text), the resolved `placeholder`, `status`, `size`, and the field-a11y props (`aria-labelledby` with the locale's name, `aria-invalid`, `aria-errormessage`) plus every prop the field does not own (`id`, `disabled`, …).",
7457
+ "name": "renderControl",
7458
+ "type": "(control: TranslatableFieldControlProp) => ReactNode"
7459
+ },
7460
+ {
7461
+ "description": "Turns the fallback locale's stored text into the plain placeholder an empty locale shows (HTML → text for a rich-text control). Default: the text as stored.",
7462
+ "name": "toPlaceholder",
7463
+ "type": "(fallbackText: string) => string"
7464
+ },
7465
+ {
7466
+ "description": "The per-locale map; onValueChange gets the whole map, only the active key changed.",
7467
+ "name": "value / defaultValue / onValueChange",
7468
+ "type": "TranslatableValue"
7469
+ },
7470
+ {
7471
+ "description": "Exactly as on TranslatableInput.",
7472
+ "name": "locales / defaultLocale / fallbackLocale / errors",
7473
+ "type": "—"
7474
+ },
7475
+ {
7476
+ "description": "Passed to the control; the tab strip uses Tabs `sm` (`md` for `lg`).",
7477
+ "name": "size",
7478
+ "type": "\"xs\" | \"sm\" | \"md\" | \"lg\""
7479
+ },
7480
+ {
7481
+ "description": "Shown when there is no fallback text.",
7482
+ "name": "placeholder",
7483
+ "type": "string"
7484
+ },
7485
+ {
7486
+ "description": "Passed to the control; the active locale's error turns it into `error`.",
7487
+ "name": "status",
7488
+ "type": "\"error\" | \"warning\""
7489
+ },
7490
+ {
7491
+ "description": "Passed to the control, so a FormField label click focuses it.",
7492
+ "name": "id",
7493
+ "type": "string"
7494
+ },
7495
+ {
7496
+ "description": "Root (strip + panel) class override.",
7497
+ "name": "className",
7498
+ "type": "string"
7499
+ }
7500
+ ],
7501
+ "related": [
7502
+ "TranslatableInput — the engine around an Input.",
7503
+ "TranslatableTextarea — the engine around a Textarea.",
7504
+ "TranslatableRichText (@godxjp/block-editor) — the engine around a RichTextEditor."
7505
+ ],
7506
+ "rules": [],
7507
+ "storyPath": "data-entry/translatable-input.tsx",
7508
+ "tagline": "The locale-tab engine behind TranslatableInput, TranslatableTextarea and `@godxjp/block-editor`'s TranslatableRichText (gh#1365): the locale list and first tab (AppProvider `contentLocales`), the per-locale map, the fallback placeholder, the error marks and the FormField error report — around ONE control you render with `renderControl`. Use it only to make another control translatable; for text use TranslatableInput / TranslatableTextarea.",
7509
+ "usage": [
7510
+ "DO render ONE control and spread `control` onto it (`<X {...control} ref={ref} />`): the engine swaps its value per tab, so the ref, FormField id and IME composition stay on one node.",
7511
+ "DO make the control accept `lang`, `status`, `placeholder` and the field-a11y props on its focus target — that is how the locale, the error and the label reach it.",
7512
+ "DON'T use it for a plain text field: TranslatableInput / TranslatableTextarea are this engine with an Input / Textarea already."
7513
+ ],
7514
+ "useCases": [
7515
+ "Making a rich-text, date or custom control hold one value per locale with the kit's tab / error / fallback contract"
7516
+ ]
7517
+ },
7306
7518
  {
7307
7519
  "example": "import { Label } from \"@godxjp/ui/data-entry\";\n\n<Label htmlFor=\"stackable\">併用を許可</Label>",
7308
7520
  "group": "data-entry",
@@ -8233,9 +8445,9 @@
8233
8445
  "name": "Tabs",
8234
8446
  "props": [
8235
8447
  {
8236
- "description": "Optional data-driven tab list. When provided, Tabs renders all triggers and content panels. When Tabs owns the initial selection (no `value`, and no `defaultValue` naming an existing ENABLED item), it falls back to the first item that is NOT `disabled` — never a disabled one — and selects nothing if every item is disabled. `icon` is a leading glyph in the trigger (Ant Design `Tab.icon`). `closable` / `closeIcon` only apply under `variant=\"editable-card\"`; `closable: false` opts one tab out of removal (Ant Design `getRemovable`). `forceRender` (Ant Design `Tab.forceRender`) mounts THAT panel up front and keeps it mounted while another tab is selected, without switching the whole strip over with `destroyOnHidden={false}` — antd's per-item `destroyOnHidden` is deliberately not offered, because on this component \"kept mounted\" is one state and it would be a second spelling of `forceRender`. `count` (+ `overflowCount`, default 99; `showZero`, default true; `countLabel`) draws the counter pill beside the label — 「未対応 12」 — in the SAME vocabulary Button and Toggle publish and through the same helper, formatted with `Intl.NumberFormat` in the active locale. Pass `countLabel` to say what the number is: the pill is `aria-hidden` and an `sr-only` clause carries the digits, so the tab announces \"未対応, 12 件の課題\" and never the concatenated \"未対応12\". Prefer this over antd's answer, which is to put a Badge inside `label` — with the number inside the label the package owns neither its size nor its tone as the tab moves between selected / unselected / disabled, so every consumer aligns it differently.",
8448
+ "description": "Optional data-driven tab list. When provided, Tabs renders all triggers and content panels. When Tabs owns the initial selection (no `value`, and no `defaultValue` naming an existing ENABLED item), it falls back to the first item that is NOT `disabled` — never a disabled one — and selects nothing if every item is disabled. `icon` is a leading glyph in the trigger (Ant Design `Tab.icon`). `closable` / `closeIcon` only apply under `variant=\"editable-card\"`; `closable: false` opts one tab out of removal (Ant Design `getRemovable`). `forceRender` (Ant Design `Tab.forceRender`) mounts THAT panel up front and keeps it mounted while another tab is selected, without switching the whole strip over with `destroyOnHidden={false}` — antd's per-item `destroyOnHidden` is deliberately not offered, because on this component \"kept mounted\" is one state and it would be a second spelling of `forceRender`. `count` (+ `overflowCount`, default 99; `showZero`, default true; `countLabel`) draws the counter pill beside the label — 「未対応 12」 — in the SAME vocabulary Button and Toggle publish and through the same helper, formatted with `Intl.NumberFormat` in the active locale. Pass `countLabel` to say what the number is: the pill is `aria-hidden` and an `sr-only` clause carries the digits, so the tab announces \"未対応, 12 件の課題\" and never the concatenated \"未対応12\". Prefer this over antd's answer, which is to put a Badge inside `label` — with the number inside the label the package owns neither its size nor its tone as the tab moves between selected / unselected / disabled, so every consumer aligns it differently. `href` makes the strip a ROUTE strip (gh#1364) — see `linkComponent`.",
8237
8449
  "name": "items",
8238
- "type": "{ value: string; label: React.ReactNode; content: React.ReactNode; disabled?: boolean; icon?: React.ReactNode; closable?: boolean; closeIcon?: React.ReactNode; forceRender?: boolean; count?: number; overflowCount?: number; showZero?: boolean; countLabel?: string }[]"
8450
+ "type": "{ value: string; label: React.ReactNode; content?: React.ReactNode; href?: string; disabled?: boolean; icon?: React.ReactNode; closable?: boolean; closeIcon?: React.ReactNode; forceRender?: boolean; count?: number; overflowCount?: number; showZero?: boolean; countLabel?: string }[]"
8239
8451
  },
8240
8452
  {
8241
8453
  "description": "Controlled active tab key.",
@@ -8350,6 +8562,11 @@
8350
8562
  "name": "onTabScroll",
8351
8563
  "type": "(info: { direction: \"start\" | \"end\" }) => void"
8352
8564
  },
8565
+ {
8566
+ "description": "ROUTE STRIP (gh#1364). When ANY item carries `href`, Tabs renders a `<nav>` landmark of real links instead of a tablist: each tab is an `<a href>` (middle-click, Ctrl/⌘-click, \"open in new tab\" and the context menu all work — no click is ever `preventDefault`ed), the item whose `value` equals `value` gets `aria-current=\"page\"`, and no tabpanel is rendered (`content` is ignored; the route renders its own page). Pass the CURRENT ROUTE as `value` (derive it from the pathname). `linkComponent` is the SAME contract and the same value as `Sidebar.linkComponent` / `NavList.linkComponent` — Inertia `<Link>`, or `createSidebarLink(Link, \"to\")` for React Router / TanStack — and must forward every prop to its `<a>`. Without it each tab is a plain `<a href>`. Name the nav with `aria-label`. Same variants, sizes and `--tabs-*` tokens as the tablist, and the strip scrolls its overflow exactly like one (`overflow=\"menu\"` additionally exposes hidden routes as native links). Keyboard is the browser's: Tab walks the links, Enter follows one. WHY NOT React Aria link tabs: `<Tab href>` renders `<a role=\"tab\">` (the link role is hidden) and React Aria preventDefaults every real click on it to replay a synthetic one.",
8567
+ "name": "linkComponent",
8568
+ "type": "SidebarLinkComponentProp"
8569
+ },
8353
8570
  {
8354
8571
  "description": "Class on the TRIGGER STRIP (`TabsList`) under the `items` API — the handle that composing the tree manually gives you as `<TabsList className>`. `className` reaches only the root, which holds the strip AND the panels, so anything meant for the bar alone belongs here. Almost always unnecessary: placement, size, centring and the card rail are props and `--tabs-*` tokens.",
8355
8572
  "name": "listClassName",
@@ -8390,6 +8607,7 @@
8390
8607
  "DO put a tab's count in `count` (+ `countLabel`), not inside `label`. A number concatenated into the label loses the pill's size, its tone as the tab is selected/deselected, and — measured on Button in gh#734 — the accessible name: the digits run straight onto the label («未対応12»). With the slot the tab announces \"未対応, 12 件の課題\". DON'T drop a `<Badge>` into `label` to get the same look; there is one counting API and Button, Toggle and Tabs all draw it.",
8391
8608
  "DON'T go looking for antd's `tabBarGutter`, `tabBarStyle`, `renderTabBar`, `classNames`/`styles` or `more.popupRender` — each is declined on the record, not missing. The gutter between triggers is a theme knob (`--tabs-list-line-space-gap`, `--tabs-card-list-space-gap`; the pill strip has no gutter by design) because a px number is a constant, not a semantic axis; the other four exist upstream to let a consumer replace the rendered markup, and this library answers that layer with tokens (docs/DESIGN-AUTHORITY.md refuses them by name).",
8392
8609
  "DON'T re-centre the strip with a `justify-center` utility. `TabsList` aligns with `safe center` on purpose: plain centring splits the overflow across BOTH edges while `scrollLeft` only ever covers the trailing one, so the leading tab ends up permanently outside the scrollport and no gesture reaches it. `safe` keeps the centred look while the tabs fit and falls back to start alignment the moment they don't.",
8610
+ "DO give a strip of sibling ROUTES (a settings area: /settings/cart-timeout, /settings/customer, …) `items[].href` + `linkComponent` and pass the current route as `value`. DON'T hand-roll it from `Link` pills in an `inline-flex rounded-lg bg-muted p-1` div (no `aria-current`, flagged by ui-audit), and DON'T drive a button tablist with `onValueChange` + `router.push` — that loses open-in-new-tab and middle-click (gh#1364).",
8393
8611
  "DON'T draw a list status view as Tabs. A list status view (All / Active / Closed, each with its count) that shows the SAME table — same columns, same row actions, a different query — is a filter, so it is `Segmented` with `count`, placed in the list's `Toolbar`; past 4 statuses it is a `Select`. `Tabs` only when each status is a genuinely different panel (its own columns, actions or empty state), declared as `TabsContent` or pointed at with `controls` (WAI-ARIA APG: a tab selects a panel, a radiogroup selects a value) (gh#1324)."
8394
8612
  ],
8395
8613
  "useCases": [
@@ -8397,6 +8615,7 @@
8397
8615
  "Controlled tabs driven by URL search params (e.g. `?tab=history`) where the parent reads/writes the active key and passes it to `value` / `onValueChange`.",
8398
8616
  "Vertical side-rail navigation inside a `SplitPane` or settings layout where `orientation=\"vertical\"` on the root and `variant=\"line\"` on `TabsList` combine to produce a sidebar-style tab strip.",
8399
8617
  "Lightweight widget tabs on a dashboard card — e.g. a 'Pending' panel (due date, remind action) and a 'Paid' panel (paid date, receipt link), each its own `DataTable` with its own columns — where an uncontrolled `defaultValue` is sufficient and no URL state is needed. Two views of the SAME table are a filter: `Segmented`.",
8618
+ "A settings area with ~14 sibling routes shown as a horizontal strip above the page body — `items[].href` + `linkComponent`, `value` = the current route; it scrolls at phone width (gh#1364).",
8400
8619
  "Admin entity profile pages (company, partner, employee) where each `TabsContent` wraps an Inertia deferred prop panel, lazy-loading expensive data only when the tab is first activated."
8401
8620
  ]
8402
8621
  },