@godxjp/ui 32.12.1 → 32.14.0

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/DESIGN.md CHANGED
@@ -202,7 +202,7 @@ A list whose rows are decisions. Show requester, target, reason, submitted time;
202
202
 
203
203
  1. Pick the archetype, call `get_exemplar` and build from that screen, not from memory.
204
204
  2. `npx @godxjp/ui audit <files>` — zero errors.
205
- 3. `npx @godxjp/ui visual-audit <url> <routes>` — runs every route at 320, 390, 768, 1024, 1280 and 844×390 (landscape phone) and measures what a screenshot glance misses: clipped or squeezed controls (WCAG 1.4.10 has two halves — no two-way scrolling AND no lost information; page overflow alone checks only the first), compact search, avatar-only identity, editor inset, action count and target size, rail and search keylines, tiny tracked capitals. Zero machine failures at every width; an "unmapped" result is not a pass. Signed-in routes: `--storage-state state.json` (Playwright storageState). WCAG 1.4.12 / 1.4.4 cells: `--text-spacing` / `--text-zoom 200` (add `--screenshots DIR`) — each reports its geometry delta against the base render, and a zero delta is "override not applied", not a pass.
205
+ 3. `npx @godxjp/ui visual-audit <url> <routes>` — runs every route at 320, 390, 768, 1024, 1280 and 844×390 (landscape phone) and measures what a screenshot glance misses: clipped or squeezed controls (WCAG 1.4.10 has two halves — no two-way scrolling AND no lost information; page overflow alone checks only the first), compact search, avatar-only identity, editor inset, action count and target size, rail and search keylines, tiny tracked capitals. Zero machine failures at every width; an "unmapped" result is not a pass. Signed-in routes: `--storage-state state.json` (Playwright storageState). WCAG 1.4.12 / 1.4.4 cells: `--text-spacing` / `--text-zoom 200` (add `--screenshots DIR`) — each reports its geometry delta against the base render, and a zero delta is "override not applied", not a pass. Visible text that did not take the override is listed in `notTaken` (selector + text) and fails the cell as "override partially applied".
206
206
  4. Inspect every screenshot at every width, region by region — top bar, page header, body, aside or rail, menus and drawers — against the checklist the report prints. A region you were told not to change is still inspected; a defect there becomes an issue, not a silent pass.
207
207
  5. No top-N and no round cap. Stop only when every width has zero machine failures and every checklist row is pass or a written N/A.
208
208
  6. The builder does not accept its own screen. Report "probes passed; independent acceptance pending" with the report JSON, the screenshots per width and the checklist; acceptance comes from someone who did not build it (a person, or a separate agent given only the build and the artifacts). Never call a screen done on self-review.
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 — 162 components, 2204 tokens, 43 cardinal rules — is published in two forms from one
21
+ The catalog — 162 components, 2210 tokens, 43 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
 
@@ -56,7 +56,7 @@ static files straight from this public repo — no hosting, no deploy step:
56
56
  | [`agent/components-index.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json) | 48 KB | all 162 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
58
  | [`agent/components.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json) | 1.2 MB | all 162 entries in one file — most URL fetchers truncate this silently; prefer the per-component files |
59
- | [`agent/tokens.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json) | 849 KB | all 2204 design tokens, their values and why they exist |
59
+ | [`agent/tokens.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json) | 851 KB | all 2210 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) | 21 KB | the 43 cardinal rules |
62
62
  <!-- prettier-ignore-end -->
package/agent/DESIGN.md CHANGED
@@ -202,7 +202,7 @@ A list whose rows are decisions. Show requester, target, reason, submitted time;
202
202
 
203
203
  1. Pick the archetype, call `get_exemplar` and build from that screen, not from memory.
204
204
  2. `npx @godxjp/ui audit <files>` — zero errors.
205
- 3. `npx @godxjp/ui visual-audit <url> <routes>` — runs every route at 320, 390, 768, 1024, 1280 and 844×390 (landscape phone) and measures what a screenshot glance misses: clipped or squeezed controls (WCAG 1.4.10 has two halves — no two-way scrolling AND no lost information; page overflow alone checks only the first), compact search, avatar-only identity, editor inset, action count and target size, rail and search keylines, tiny tracked capitals. Zero machine failures at every width; an "unmapped" result is not a pass. Signed-in routes: `--storage-state state.json` (Playwright storageState). WCAG 1.4.12 / 1.4.4 cells: `--text-spacing` / `--text-zoom 200` (add `--screenshots DIR`) — each reports its geometry delta against the base render, and a zero delta is "override not applied", not a pass.
205
+ 3. `npx @godxjp/ui visual-audit <url> <routes>` — runs every route at 320, 390, 768, 1024, 1280 and 844×390 (landscape phone) and measures what a screenshot glance misses: clipped or squeezed controls (WCAG 1.4.10 has two halves — no two-way scrolling AND no lost information; page overflow alone checks only the first), compact search, avatar-only identity, editor inset, action count and target size, rail and search keylines, tiny tracked capitals. Zero machine failures at every width; an "unmapped" result is not a pass. Signed-in routes: `--storage-state state.json` (Playwright storageState). WCAG 1.4.12 / 1.4.4 cells: `--text-spacing` / `--text-zoom 200` (add `--screenshots DIR`) — each reports its geometry delta against the base render, and a zero delta is "override not applied", not a pass. Visible text that did not take the override is listed in `notTaken` (selector + text) and fails the cell as "override partially applied".
206
206
  4. Inspect every screenshot at every width, region by region — top bar, page header, body, aside or rail, menus and drawers — against the checklist the report prints. A region you were told not to change is still inspected; a defect there becomes an issue, not a silent pass.
207
207
  5. No top-N and no round cap. Stop only when every width has zero machine failures and every checklist row is pass or a written N/A.
208
208
  6. The builder does not accept its own screen. Report "probes passed; independent acceptance pending" with the report JSON, the screenshots per width and the checklist; acceptance comes from someone who did not build it (a person, or a separate agent given only the build and the artifacts). Never call a screen done on self-review.
@@ -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` 32.12.1.** If the project you are editing has a different
6
+ **This catalog describes `@godxjp/ui` 32.14.0.** 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.
@@ -61,10 +61,10 @@ screen anatomy, the visual target the rules below do not give you. Then fetch in
61
61
  its `importPath`, and its examples. Fetch only the handful you picked in step 1.
62
62
  3. `rules.json` — 43 cardinal rules. The ones about raw HTML and hardcoded colour are not
63
63
  style advice.
64
- 4. `tokens.json` — 2204 design tokens, each tagged with its `tier`. **If you were handed a
64
+ 4. `tokens.json` — 2210 design tokens, each tagged with its `tier`. **If you were handed a
65
65
  brand, read the 211 `foundation` entries first** — `--primary`, `--background`,
66
66
  `--radius`, `--font-size-base` are the handful everything else derives from. The
67
- 1880 `component` entries are per-part knobs; reach for one only when a role is
67
+ 1886 `component` entries are per-part knobs; reach for one only when a role is
68
68
  right everywhere except one component.
69
69
  5. `anti-ai-tells.json` — 35 shapes that make generated UI look generated, each with the
70
70
  fix. Read before you reach for a gradient hero or a wall of coloured chips.
@@ -150,7 +150,7 @@ has stopped following the brand.
150
150
  |---|---|---|---|
151
151
  | `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.** The default `--primary` is a neutral placeholder: set it (`:root { --primary: <H> <S>% <L>%; }`, HSL components, no `hsl()` wrapper) or import a theme preset such as `@godxjp/ui/themes/godx.css` |
152
152
  | `semantic` | 113 | 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 |
153
- | `component` | 1880 | 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` |
153
+ | `component` | 1886 | 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` |
154
154
 
155
155
  A token whose `value` is `initial` is not empty and not broken: `initial` is the guaranteed-invalid
156
156
  value, so the real default is computed where the element paints it. Set it and yours wins.
@@ -83,7 +83,7 @@
83
83
  "DO use `unread` for a notification list — the dot is a SHAPE with localized `sr-only` text (\"Unread\"/\"Read\"), so it never reads as colour alone, and the row surface reads `--list-row-unread-background` (default `hsl(var(--muted))` — chosen so the xs muted description line stays WCAG AA on the emphasized surface; `--accent` would drop it to 4.23:1). DON'T substitute a `Badge` — that renders a labelled pill, not a compact status dot.",
84
84
  "DO pass `density=\"compact\"` for the canonical invitation / history row — an Avatar, a title (+ description) and one or two small trailing Buttons that must read as ONE line inside a narrow card (≈326px content at 390px), or a history row whose status Badge + ISO-8601 date belongs beside the title. Measured at 390px: 62px tall vs 126px at the default density (where the actions wrapped), history row 41px vs 114px. DON'T reach for it just to \"make things tighter\" on a roomy page — the default density is the entity-row measure.",
85
85
  "DON'T add one-off `min-width`/wrapping CSS in the consumer app for a long title + two trailing Buttons. The row already shrinks and WRAPS: the content column keeps only `min(var(--list-row-body-min-width), 100%)` and the trailing actions drop onto their own line below the threshold. Retune the threshold with `--list-row-body-min-width` (default 12rem) and the action gap with `--list-row-trailing-gap`; pass `overflow=\"wrap\"` (usually with `align=\"start\"`) when the title must stay fully readable at 390px instead of truncating.",
86
- "DO expect rows placed DIRECTLY in a padded (non-flush) `CardContent` — or in a `Flex as=\"ul\"` there — to drop their own inline padding: the card already holds the inset, so the row text lines up with the CardTitle instead of sitting 16px inside it. Inside `CardContent flush` (the documented home, with edge-to-edge dividers) the row keeps `--list-row-padding-x`."
86
+ "DO expect rows ANYWHERE inside a padded (non-flush) `CardContent` — directly, in a `Flex as=\"ul\"`, or wrapped (`DataState` children in a div > `Flex as=\"ul\"` > `ListRow as=\"li\" asChild` > link, gh#1325) — to drop their own inline padding: the card already holds the inset, so the row text lines up with the CardTitle instead of sitting 16px inside it. A row keeps `--list-row-padding-x` inside `CardContent flush` (the documented home, with edge-to-edge dividers) — including a flush CardContent nested in a padded one — and inside a table cell."
87
87
  ],
88
88
  "useCases": [
89
89
  "Account security page — a list of active sessions (device + last-seen as title/description, a destructive 'Revoke' Button in trailing).",
@@ -40,7 +40,7 @@
40
40
  "type": "ReactNode | { start?: ReactNode; end?: ReactNode }"
41
41
  },
42
42
  {
43
- "description": "FIXED chrome band between the page header and the body — a filter strip, a status bar, a \"channel workflow\" rail. It is a SIBLING of the body, not content inside it: with `fill` the body is the scroll viewport, so the band is `flex: none` OUTSIDE the scroller and never scrolls away or gets slid under. Shares the page gutters and the `measure` cap with the header and the body (the three bands line up on both edges), goes full-bleed under variant=\"flush\" (wrap padded strips in PageContainer.Inset), and renders NOTHING when omitted — no wrapper, no gap. It sits FLUSH against the header above and the body below: chrome is attached, not a third page section floating between two voids, so the band cancels the container gap from itself and its ONLY breathing room is its own inset.",
43
+ "description": "FIXED chrome band between the page header and the body — a filter strip, a status bar, a \"channel workflow\" rail. It is a SIBLING of the body, not content inside it: with `fill` the body is the scroll viewport, so the band is `flex: none` OUTSIDE the scroller and never scrolls away or gets slid under. Shares the page gutters and the `measure` cap with the header and the body (the three bands line up on both edges), goes full-bleed under variant=\"flush\" (wrap padded strips in PageContainer.Inset), and renders NOTHING when omitted — no wrapper, no gap. Its BOX sits FLUSH against the header above and the body below (chrome is attached, not a third page section floating between two voids, so the band cancels the container gap from itself), and by default its inset gives the band's CONTENT that gap back: a plain filter strip (a Segmented, a row of filters) sits one band gap from the header and from the body (gh#1323 — it used to touch both, 0px). A theme that paints or rules the band sets a tighter inset with `--page-toolbar-pad-block`.",
44
44
  "name": "toolbar",
45
45
  "type": "ReactNode"
46
46
  },
@@ -164,7 +164,7 @@
164
164
  "DO: Use `toolbar` for any FIXED strip that belongs between the page header and the page content — a filter/segment bar, a status or connection band, a list's bulk-action rail. DON'T put it in `children` (with `fill` the body is the scroller, so it scrolls out of sight) and NEVER hand-lay it with `position: sticky` / a `top-0 z-10` wrapper at the call site: page chrome is the shell's job, and a sticky strip still lets content flow underneath it — the half-sliced row every hand-rolled version produces. It stays out of the scroll viewport, inherits the page gutters and the `measure` cap so it lines up with the title and the body, and is entirely absent from the DOM when the prop is omitted.",
165
165
  "DO: Know the `toolbar` band draws NO bottom rule by default — `--page-toolbar-divider` is unset and falls back to `--page-header-divider` (itself `none`), so a service that opts into the page-header divider gets a consistent band rule in ONE declaration, and `--page-toolbar-divider: none` silences just the band. `variant='ghost'` keeps both quiet unless a theme opts one in explicitly, which it then lets through.",
166
166
  "DO: Give the `toolbar` band its own SURFACE from the theme when the design separates it from the page ground — `--page-toolbar-background: hsl(var(--card));` declared once (`:root` or a scoped `[data-tenant]`) paints the whole band, page gutters and `measure` cap included, and stays re-themeable per tenant. It is the `background` shorthand, so a gradient works too. The default is `transparent`, i.e. the band looks exactly as it did before the knob existed (rule #44).",
167
- "DO: Set `--page-toolbar-pad-block` in the SAME theme declaration that paints or rules the band. It is the band's ONLY breathing room: the band sits FLUSH against the header and the body (chrome is attached — a ruled, painted band adrift in two 16px voids divides nothing), so there is no outside space to tune. The default is `0` and stays `0`: a transparent band is not a surface and has no inside for an inset to breathe, and under `fill` every pixel of band height comes straight off the scroll viewport the slot exists to protect. Once the band is painted or ruled it DOES have an inside, and `--page-toolbar-pad-block: var(--space-2)` is where that inset belongs. The CALL SITE never sets it — a strip padded at the call site pads only the strip, not the band.",
167
+ "DO: Set `--page-toolbar-pad-block` in the SAME theme declaration that paints or rules the band. The band's box sits FLUSH against the header and the body (chrome is attached — a ruled, painted band adrift in two 16px voids divides nothing); the inset is its breathing room. Unset, it defaults to the page's band gap, so a transparent filter strip sits exactly one band gap from the header and from the body, like any other band (gh#1323). Once the band is painted or ruled it has an inside of its own, and `--page-toolbar-pad-block: var(--space-2)` is where that tighter inset belongs; `0px` restores a strip flush to both edges. The CALL SITE never sets it — a strip padded at the call site pads only the strip, not the band.",
168
168
  "DO: Silence the `footer` band's top rule with `--page-footer-divider: none` when the footer content already carries its own frame — a chat composer is a bordered Card, and the shell's full-width rule then lands directly above it as a SECOND line (a pixel diff against a consumer chat design caught a 100%-wide rule at y=701 the design does not have). This is the ONE page-chrome divider whose default is a RULE rather than silence, deliberately: `footer` is the shared slot a form's Save/Cancel bar lands in, where that line separates the actions from the content. Unset is byte-identical to the literal the rule used to hard-code. All three page bands are now one contract: `--page-header-divider` / `--page-toolbar-divider` / `--page-footer-divider`, each read at the CALL SITE with a fallback, none of them a `border-*` utility at the call site.",
169
169
  "DON'T: Style the `toolbar` band from the call site. `toolbar={<div className='bg-card py-1.5'>…</div>}` is hand-laid page chrome: the utility paints the STRIP, not the band, so it stops at the content box instead of running the full page width (and full-bleed under `variant='flush'`); it is invisible to per-tenant theming; and it puts geometry the shell owns back into the app. The band's ground, inset and rule are `--page-toolbar-background` / `--page-toolbar-pad-block` / `--page-toolbar-divider` — three theme declarations, zero call-site classes.",
170
170
  "DO: Set `headerScale='chrome'` when the page's top row is CHROME rather than a document title — a chat channel header, a mail thread's subject line, an IDE tab, a conversation view. The `<h1>` drops to the body type step (--page-title-font-size-chrome) at every width, so the header band stops eating the height the content needs: a consumer chat page measured a 61px band with a 24px channel name where the design asked for ~40px at the `sm` step. Pair it with `variant='ghost'` for the full quiet chrome header — ghost drops the header's bottom pad and lets no divider inherit in — and with `fill` + `toolbar` + `footer`/`stickyFooter` for the canonical chat surface. The heading stays an `<h1>`: this is a type step, never a heading-level downgrade. The same attribute also drops the page's top padding to `--page-pad-block-start-chrome` (0), so the band sits ON the frame instead of floating in a document's top margin — four consequences of ONE fact (this row is furniture), not four props a call site has to keep in lockstep: the subtitle drops to `--page-subtitle-font-size-chrome` (~11px) so the caption under a channel name stops matching the name's own size, and the `extra` cluster centres on the bar instead of top-packing against a heading that is no longer tall. If a design wants its chrome inset or a different caption step, the theme retunes those tokens once; never pad, negative-margin or `self-center` the page at the call site.",
@@ -78,6 +78,7 @@
78
78
  "tagline": "One-of-N from a small, closed, always-visible set — the enterprise Segmented / filter bar drawn on react-aria-components' RadioGroup. A track with the chosen item as a lifted slab. Reach for it INSTEAD OF a Select when there are 2-4 options and all of them fit on screen, and instead of ToggleGroup when exactly one must always be chosen.",
79
79
  "usage": [
80
80
  "DO use it for a closed set of 2-4 peer choices that are cheap to show — theme, view mode, a date range preset.",
81
+ "DO use it for a list status view. 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).",
81
82
  "DO give it an aria-label (or aria-labelledby) — the group needs a name, and each item takes its own from its label.",
82
83
  "DON'T use ToggleGroup for a one-of-N choice unless you also pass `disallowEmptySelection`: without it, type=single is a row of aria-pressed toggle buttons that can end up with nothing selected, which a setting can never be (gh#744). Segmented is the one-of-N control and needs no such switch.",
83
84
  "DON'T use it past ~4 options — that is a Select. Up to four, a horizontal track WRAPS to a second row when its options cannot share one (a phone-width status filter with counts), so no label or count is truncated while its item fits on a row. `block` is the exception: it promises EQUAL widths, so it still truncates — don't use `block` for four labelled options at phone width.",
@@ -161,13 +161,14 @@
161
161
  "DO add `bodied` whenever a `card`/`editable-card` strip stands on its own (a saved-views ribbon over a list). DON'T wrap the panel in your own `<Card>` to give it a surface — that is the measured defect `bodied` exists for: it adds a SECOND border at the Card radius over the strip↔panel gap and the pair reads as two stacked boxes. The one place you do not need it is inside `<Card tabList>`, where the card's own border already wraps strip and body.",
162
162
  "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
163
  "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
- "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."
164
+ "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.",
165
+ "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)."
165
166
  ],
166
167
  "useCases": [
167
168
  "Detail drawers or pages that need full per-panel control — e.g. an accounting journal-entry sheet where one panel has `forceMount` to keep a live chart mounted, requiring custom `TabsContent` props that `Tabs` cannot pass.",
168
169
  "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`.",
169
170
  "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.",
170
- "Lightweight widget tabs on a dashboard card — e.g. switching a `DataTable` between 'Pending' and 'Paid' invoice views — where an uncontrolled `defaultValue` is sufficient and no URL state is needed.",
171
+ "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`.",
171
172
  "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."
172
173
  ]
173
174
  }
@@ -685,7 +685,7 @@
685
685
  "type": "ReactNode | { start?: ReactNode; end?: ReactNode }"
686
686
  },
687
687
  {
688
- "description": "FIXED chrome band between the page header and the body — a filter strip, a status bar, a \"channel workflow\" rail. It is a SIBLING of the body, not content inside it: with `fill` the body is the scroll viewport, so the band is `flex: none` OUTSIDE the scroller and never scrolls away or gets slid under. Shares the page gutters and the `measure` cap with the header and the body (the three bands line up on both edges), goes full-bleed under variant=\"flush\" (wrap padded strips in PageContainer.Inset), and renders NOTHING when omitted — no wrapper, no gap. It sits FLUSH against the header above and the body below: chrome is attached, not a third page section floating between two voids, so the band cancels the container gap from itself and its ONLY breathing room is its own inset.",
688
+ "description": "FIXED chrome band between the page header and the body — a filter strip, a status bar, a \"channel workflow\" rail. It is a SIBLING of the body, not content inside it: with `fill` the body is the scroll viewport, so the band is `flex: none` OUTSIDE the scroller and never scrolls away or gets slid under. Shares the page gutters and the `measure` cap with the header and the body (the three bands line up on both edges), goes full-bleed under variant=\"flush\" (wrap padded strips in PageContainer.Inset), and renders NOTHING when omitted — no wrapper, no gap. Its BOX sits FLUSH against the header above and the body below (chrome is attached, not a third page section floating between two voids, so the band cancels the container gap from itself), and by default its inset gives the band's CONTENT that gap back: a plain filter strip (a Segmented, a row of filters) sits one band gap from the header and from the body (gh#1323 — it used to touch both, 0px). A theme that paints or rules the band sets a tighter inset with `--page-toolbar-pad-block`.",
689
689
  "name": "toolbar",
690
690
  "type": "ReactNode"
691
691
  },
@@ -809,7 +809,7 @@
809
809
  "DO: Use `toolbar` for any FIXED strip that belongs between the page header and the page content — a filter/segment bar, a status or connection band, a list's bulk-action rail. DON'T put it in `children` (with `fill` the body is the scroller, so it scrolls out of sight) and NEVER hand-lay it with `position: sticky` / a `top-0 z-10` wrapper at the call site: page chrome is the shell's job, and a sticky strip still lets content flow underneath it — the half-sliced row every hand-rolled version produces. It stays out of the scroll viewport, inherits the page gutters and the `measure` cap so it lines up with the title and the body, and is entirely absent from the DOM when the prop is omitted.",
810
810
  "DO: Know the `toolbar` band draws NO bottom rule by default — `--page-toolbar-divider` is unset and falls back to `--page-header-divider` (itself `none`), so a service that opts into the page-header divider gets a consistent band rule in ONE declaration, and `--page-toolbar-divider: none` silences just the band. `variant='ghost'` keeps both quiet unless a theme opts one in explicitly, which it then lets through.",
811
811
  "DO: Give the `toolbar` band its own SURFACE from the theme when the design separates it from the page ground — `--page-toolbar-background: hsl(var(--card));` declared once (`:root` or a scoped `[data-tenant]`) paints the whole band, page gutters and `measure` cap included, and stays re-themeable per tenant. It is the `background` shorthand, so a gradient works too. The default is `transparent`, i.e. the band looks exactly as it did before the knob existed (rule #44).",
812
- "DO: Set `--page-toolbar-pad-block` in the SAME theme declaration that paints or rules the band. It is the band's ONLY breathing room: the band sits FLUSH against the header and the body (chrome is attached — a ruled, painted band adrift in two 16px voids divides nothing), so there is no outside space to tune. The default is `0` and stays `0`: a transparent band is not a surface and has no inside for an inset to breathe, and under `fill` every pixel of band height comes straight off the scroll viewport the slot exists to protect. Once the band is painted or ruled it DOES have an inside, and `--page-toolbar-pad-block: var(--space-2)` is where that inset belongs. The CALL SITE never sets it — a strip padded at the call site pads only the strip, not the band.",
812
+ "DO: Set `--page-toolbar-pad-block` in the SAME theme declaration that paints or rules the band. The band's box sits FLUSH against the header and the body (chrome is attached — a ruled, painted band adrift in two 16px voids divides nothing); the inset is its breathing room. Unset, it defaults to the page's band gap, so a transparent filter strip sits exactly one band gap from the header and from the body, like any other band (gh#1323). Once the band is painted or ruled it has an inside of its own, and `--page-toolbar-pad-block: var(--space-2)` is where that tighter inset belongs; `0px` restores a strip flush to both edges. The CALL SITE never sets it — a strip padded at the call site pads only the strip, not the band.",
813
813
  "DO: Silence the `footer` band's top rule with `--page-footer-divider: none` when the footer content already carries its own frame — a chat composer is a bordered Card, and the shell's full-width rule then lands directly above it as a SECOND line (a pixel diff against a consumer chat design caught a 100%-wide rule at y=701 the design does not have). This is the ONE page-chrome divider whose default is a RULE rather than silence, deliberately: `footer` is the shared slot a form's Save/Cancel bar lands in, where that line separates the actions from the content. Unset is byte-identical to the literal the rule used to hard-code. All three page bands are now one contract: `--page-header-divider` / `--page-toolbar-divider` / `--page-footer-divider`, each read at the CALL SITE with a fallback, none of them a `border-*` utility at the call site.",
814
814
  "DON'T: Style the `toolbar` band from the call site. `toolbar={<div className='bg-card py-1.5'>…</div>}` is hand-laid page chrome: the utility paints the STRIP, not the band, so it stops at the content box instead of running the full page width (and full-bleed under `variant='flush'`); it is invisible to per-tenant theming; and it puts geometry the shell owns back into the app. The band's ground, inset and rule are `--page-toolbar-background` / `--page-toolbar-pad-block` / `--page-toolbar-divider` — three theme declarations, zero call-site classes.",
815
815
  "DO: Set `headerScale='chrome'` when the page's top row is CHROME rather than a document title — a chat channel header, a mail thread's subject line, an IDE tab, a conversation view. The `<h1>` drops to the body type step (--page-title-font-size-chrome) at every width, so the header band stops eating the height the content needs: a consumer chat page measured a 61px band with a 24px channel name where the design asked for ~40px at the `sm` step. Pair it with `variant='ghost'` for the full quiet chrome header — ghost drops the header's bottom pad and lets no divider inherit in — and with `fill` + `toolbar` + `footer`/`stickyFooter` for the canonical chat surface. The heading stays an `<h1>`: this is a type step, never a heading-level downgrade. The same attribute also drops the page's top padding to `--page-pad-block-start-chrome` (0), so the band sits ON the frame instead of floating in a document's top margin — four consequences of ONE fact (this row is furniture), not four props a call site has to keep in lockstep: the subtitle drops to `--page-subtitle-font-size-chrome` (~11px) so the caption under a channel name stops matching the name's own size, and the `extra` cluster centres on the bar instead of top-packing against a heading that is no longer tall. If a design wants its chrome inset or a different caption step, the theme retunes those tokens once; never pad, negative-margin or `self-center` the page at the call site.",
@@ -4418,7 +4418,7 @@
4418
4418
  "DO use `unread` for a notification list — the dot is a SHAPE with localized `sr-only` text (\"Unread\"/\"Read\"), so it never reads as colour alone, and the row surface reads `--list-row-unread-background` (default `hsl(var(--muted))` — chosen so the xs muted description line stays WCAG AA on the emphasized surface; `--accent` would drop it to 4.23:1). DON'T substitute a `Badge` — that renders a labelled pill, not a compact status dot.",
4419
4419
  "DO pass `density=\"compact\"` for the canonical invitation / history row — an Avatar, a title (+ description) and one or two small trailing Buttons that must read as ONE line inside a narrow card (≈326px content at 390px), or a history row whose status Badge + ISO-8601 date belongs beside the title. Measured at 390px: 62px tall vs 126px at the default density (where the actions wrapped), history row 41px vs 114px. DON'T reach for it just to \"make things tighter\" on a roomy page — the default density is the entity-row measure.",
4420
4420
  "DON'T add one-off `min-width`/wrapping CSS in the consumer app for a long title + two trailing Buttons. The row already shrinks and WRAPS: the content column keeps only `min(var(--list-row-body-min-width), 100%)` and the trailing actions drop onto their own line below the threshold. Retune the threshold with `--list-row-body-min-width` (default 12rem) and the action gap with `--list-row-trailing-gap`; pass `overflow=\"wrap\"` (usually with `align=\"start\"`) when the title must stay fully readable at 390px instead of truncating.",
4421
- "DO expect rows placed DIRECTLY in a padded (non-flush) `CardContent` — or in a `Flex as=\"ul\"` there — to drop their own inline padding: the card already holds the inset, so the row text lines up with the CardTitle instead of sitting 16px inside it. Inside `CardContent flush` (the documented home, with edge-to-edge dividers) the row keeps `--list-row-padding-x`."
4421
+ "DO expect rows ANYWHERE inside a padded (non-flush) `CardContent` — directly, in a `Flex as=\"ul\"`, or wrapped (`DataState` children in a div > `Flex as=\"ul\"` > `ListRow as=\"li\" asChild` > link, gh#1325) — to drop their own inline padding: the card already holds the inset, so the row text lines up with the CardTitle instead of sitting 16px inside it. A row keeps `--list-row-padding-x` inside `CardContent flush` (the documented home, with edge-to-edge dividers) — including a flush CardContent nested in a padded one — and inside a table cell."
4422
4422
  ],
4423
4423
  "useCases": [
4424
4424
  "Account security page — a list of active sessions (device + last-seen as title/description, a destructive 'Revoke' Button in trailing).",
@@ -8204,13 +8204,14 @@
8204
8204
  "DO add `bodied` whenever a `card`/`editable-card` strip stands on its own (a saved-views ribbon over a list). DON'T wrap the panel in your own `<Card>` to give it a surface — that is the measured defect `bodied` exists for: it adds a SECOND border at the Card radius over the strip↔panel gap and the pair reads as two stacked boxes. The one place you do not need it is inside `<Card tabList>`, where the card's own border already wraps strip and body.",
8205
8205
  "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.",
8206
8206
  "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).",
8207
- "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."
8207
+ "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.",
8208
+ "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)."
8208
8209
  ],
8209
8210
  "useCases": [
8210
8211
  "Detail drawers or pages that need full per-panel control — e.g. an accounting journal-entry sheet where one panel has `forceMount` to keep a live chart mounted, requiring custom `TabsContent` props that `Tabs` cannot pass.",
8211
8212
  "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`.",
8212
8213
  "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.",
8213
- "Lightweight widget tabs on a dashboard card — e.g. switching a `DataTable` between 'Pending' and 'Paid' invoice views — where an uncontrolled `defaultValue` is sufficient and no URL state is needed.",
8214
+ "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`.",
8214
8215
  "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."
8215
8216
  ]
8216
8217
  },
@@ -12601,6 +12602,7 @@
12601
12602
  "tagline": "One-of-N from a small, closed, always-visible set — the enterprise Segmented / filter bar drawn on react-aria-components' RadioGroup. A track with the chosen item as a lifted slab. Reach for it INSTEAD OF a Select when there are 2-4 options and all of them fit on screen, and instead of ToggleGroup when exactly one must always be chosen.",
12602
12603
  "usage": [
12603
12604
  "DO use it for a closed set of 2-4 peer choices that are cheap to show — theme, view mode, a date range preset.",
12605
+ "DO use it for a list status view. 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).",
12604
12606
  "DO give it an aria-label (or aria-labelledby) — the group needs a name, and each item takes its own from its label.",
12605
12607
  "DON'T use ToggleGroup for a one-of-N choice unless you also pass `disallowEmptySelection`: without it, type=single is a row of aria-pressed toggle buttons that can end up with nothing selected, which a setting can never be (gh#744). Segmented is the one-of-N control and needs no such switch.",
12606
12608
  "DON'T use it past ~4 options — that is a Select. Up to four, a horizontal track WRAPS to a second row when its options cannot share one (a phone-width status filter with counts), so no label or count is truncated while its item fits on a row. `block` is the exception: it promises EQUAL widths, so it still truncates — don't use `block` for four labelled options at phone width.",
package/agent/index.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "components": 162,
5
5
  "patterns": 24,
6
6
  "rules": 43,
7
- "tokens": 2204,
7
+ "tokens": 2210,
8
8
  "vocabulary": 14
9
9
  },
10
10
  "design": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/DESIGN.md",
@@ -54,19 +54,19 @@
54
54
  "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.",
55
55
  "read": {
56
56
  "live": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json",
57
- "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v32.12.1/agent/index.json"
57
+ "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v32.14.0/agent/index.json"
58
58
  },
59
59
  "source": "mcp/src/data — the same data @godxjp/ui-mcp serves — plus the foundation and semantic token tiers, read from src/tokens/*.css",
60
60
  "start": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md",
61
61
  "tokenTiers": {
62
62
  "component": "per-part knobs, --{component}-{part}-{property}; usually leave these alone",
63
63
  "counts": {
64
- "component": 1880,
64
+ "component": 1886,
65
65
  "foundation": 211,
66
66
  "semantic": 113
67
67
  },
68
68
  "foundation": "the seeds a consumer is invited to set — --primary, --background, --radius",
69
69
  "semantic": "named roles that follow the seeds — --ring, --text-link, --overlay-background"
70
70
  },
71
- "version": "32.12.1"
71
+ "version": "32.14.0"
72
72
  }
package/agent/llms.txt CHANGED
@@ -1,10 +1,10 @@
1
1
  # @godxjp/ui
2
2
 
3
- > A Japanese-enterprise React design system: 162 components, 2204 design tokens,
4
- > 43 cardinal rules. This file is the entry point for AI agents. Catalog version 32.12.1.
3
+ > A Japanese-enterprise React design system: 162 components, 2210 design tokens,
4
+ > 43 cardinal rules. This file is the entry point for AI agents. Catalog version 32.14.0.
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@32.12.1`). It is searchable and version-locked. These files exist for agents
7
+ (`npx @godxjp/ui-mcp@32.14.0`). It is searchable and version-locked. These files exist for agents
8
8
  that can only fetch URLs.
9
9
 
10
10
  ## Start
@@ -19,7 +19,7 @@ that can only fetch URLs.
19
19
  - [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 48 KB — all 162 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
20
20
  - [components/&lt;Name&gt;.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json): one file per component (1 KB–37 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.
21
21
  - [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.
22
- - [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), 113 `semantic` roles, 1880 `component` knobs.
22
+ - [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), 113 `semantic` roles, 1886 `component` knobs.
23
23
  - [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.
24
24
  - [rules.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json): 43 cardinal rules.
25
25
  - [anti-ai-tells.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/anti-ai-tells.json): 35 shapes that make generated UI look generated, each with its fix.
@@ -27,7 +27,7 @@ that can only fetch URLs.
27
27
  ## Pinning
28
28
 
29
29
  Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
30
- the tag: `.../godx-jp/godxjp-ui/v32.12.1/agent/...`. A catalog that does not match the installed
30
+ the tag: `.../godx-jp/godxjp-ui/v32.14.0/agent/...`. A catalog that does not match the installed
31
31
  package describes props that are absent, or hides props that are present, and says nothing either way.
32
32
 
33
33
  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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "code": "import { Head, router } from \"@inertiajs/react\"\nimport { useMemo, useState } from \"react\"\nimport { PageContainer, Flex } from \"@godxjp/ui/layout\"\nimport { Card, CardContent, DataTable, EmptyState, Badge, type ColumnDef } from \"@godxjp/ui/data-display\"\nimport { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\"\nimport { Toolbar, ToolbarGroup, Pagination } from \"@godxjp/ui/navigation\"\nimport { formatDate } from \"@godxjp/ui/datetime\"\nimport { withCrmLayout } from \"@/layouts/crm-layout\" // see \"inertia-persistent-layout\"\n\ntype Coupon = { id: string; name: string; status: string; scope: string; validFrom: string; validTo: string; usage: number }\nconst PAGE_SIZE = 10\n\nfunction Coupons({ coupons }: { coupons: Coupon[] }) {\n const [q, setQ] = useState(\"\")\n const [status, setStatus] = useState(\"all\")\n const [page, setPage] = useState(1)\n\n const filtered = useMemo(() => coupons.filter((c) => {\n if (q && !c.name.toLowerCase().includes(q.toLowerCase())) return false\n if (status !== \"all\" && c.status !== status) return false\n return true\n }), [coupons, q, status])\n const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)\n\n // ColumnDef = { key, header, render?, align?: \"left\"|\"center\"|\"right\", sortable?, width? }\n const columns: ColumnDef<Coupon>[] = [\n { key: \"name\", header: \"クーポン名\", render: (c) => <span className=\"font-medium\">{c.name}</span> },\n { key: \"scope\", header: \"スコープ\" }, // a category is plain text; Badge is for the status column only\n { key: \"status\", header: \"ステータス\", render: (c) => <Badge status={c.status} /> },\n { key: \"valid\", header: \"有効期間\", render: (c) => `${formatDate(c.validFrom)} 〜 ${formatDate(c.validTo)}` },\n { key: \"usage\", header: \"利用数\", align: \"right\", render: (c) => c.usage.toLocaleString() },\n ]\n\n return (\n <>\n <Head title=\"クーポン管理\" />\n {/* RULE: every page wraps in PageContainer; vertical rhythm via Flex direction=\"col\"/ResponsiveGrid, never p-* or gap-* */}\n {/* No subtitle that restates the title, no StatCard row: the total is Pagination's showTotal. */}\n <PageContainer title=\"クーポン管理\">\n <Flex direction=\"col\" gap=\"lg\">\n {/* Toolbar is the filter-bar primitive (standalone, above the table surface). */}\n <Toolbar hasActiveFilters={q !== \"\" || status !== \"all\"} onClear={() => { setQ(\"\"); setStatus(\"all\"); setPage(1) }}>\n {/* SearchInput is value + onSearch(v) — NOT onChange */}\n <SearchInput placeholder=\"クーポン名で検索\" value={q} onSearch={(v) => { setQ(v); setPage(1) }} />\n <ToolbarGroup label=\"ステータス\">\n <Select value={status} onValueChange={(v) => { setStatus(v); setPage(1) }}>\n <SelectTrigger><SelectValue /></SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">全ステータス</SelectItem>\n <SelectItem value=\"公開中\">公開中</SelectItem>\n <SelectItem value=\"下書き\">下書き</SelectItem>\n </SelectContent>\n </Select>\n </ToolbarGroup>\n </Toolbar>\n\n <Card>\n <CardContent flush>\n {filtered.length === 0\n ? <EmptyState title=\"該当するクーポンがありません\" description=\"検索条件を変更してください。\" /> /* one sentence, no icon */\n : <DataTable data={paged} columns={columns} getRowId={(c) => c.id} onRowClick={(c) => router.visit(`/coupons/${c.id}`)} />}\n </CardContent>\n </Card>\n\n {/* Pagination ONLY for real multi-page data — hide it while empty or single-page. */}\n {filtered.length > PAGE_SIZE && (\n <Pagination value={page} total={filtered.length} pageSize={PAGE_SIZE} showTotal onValueChange={(p) => setPage(p)} />\n )}\n </Flex>\n </PageContainer>\n </>\n )\n}\n\nCoupons.layout = withCrmLayout\nexport default Coupons",
2
+ "code": "import { Head, router } from \"@inertiajs/react\"\nimport { useMemo, useState } from \"react\"\nimport { PageContainer, Flex } from \"@godxjp/ui/layout\"\nimport { Card, CardContent, DataTable, EmptyState, Badge, type ColumnDef } from \"@godxjp/ui/data-display\"\nimport { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\"\nimport { Toolbar, ToolbarGroup, Pagination } from \"@godxjp/ui/navigation\"\nimport { formatDate } from \"@godxjp/ui/datetime\"\nimport { withCrmLayout } from \"@/layouts/crm-layout\" // see \"inertia-persistent-layout\"\n\ntype Coupon = { id: string; name: string; status: string; scope: string; validFrom: string; validTo: string; usage: number }\nconst PAGE_SIZE = 10\n\nfunction Coupons({ coupons }: { coupons: Coupon[] }) {\n const [q, setQ] = useState(\"\")\n const [status, setStatus] = useState(\"all\")\n const [page, setPage] = useState(1)\n\n const filtered = useMemo(() => coupons.filter((c) => {\n if (q && !c.name.toLowerCase().includes(q.toLowerCase())) return false\n if (status !== \"all\" && c.status !== status) return false\n return true\n }), [coupons, q, status])\n const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)\n\n // ColumnDef = { key, header, render?, align?: \"left\"|\"center\"|\"right\", sortable?, width? }\n const columns: ColumnDef<Coupon>[] = [\n { key: \"name\", header: \"クーポン名\", render: (c) => <span className=\"font-medium\">{c.name}</span> },\n { key: \"scope\", header: \"スコープ\" }, // a category is plain text; Badge is for the status column only\n { key: \"status\", header: \"ステータス\", render: (c) => <Badge status={c.status} /> },\n { key: \"valid\", header: \"有効期間\", render: (c) => `${formatDate(c.validFrom)} 〜 ${formatDate(c.validTo)}` },\n { key: \"usage\", header: \"利用数\", align: \"right\", render: (c) => c.usage.toLocaleString() },\n ]\n\n return (\n <>\n <Head title=\"クーポン管理\" />\n {/* RULE: every page wraps in PageContainer; vertical rhythm via Flex direction=\"col\"/ResponsiveGrid, never p-* or gap-* */}\n {/* No subtitle that restates the title, no StatCard row: the total is Pagination's showTotal. */}\n <PageContainer title=\"クーポン管理\">\n <Flex direction=\"col\" gap=\"lg\">\n {/* Toolbar is the filter-bar primitive (standalone, above the table surface). */}\n <Toolbar hasActiveFilters={q !== \"\" || status !== \"all\"} onClear={() => { setQ(\"\"); setStatus(\"all\"); setPage(1) }}>\n {/* SearchInput is value + onSearch(v) — NOT onChange */}\n <SearchInput placeholder=\"クーポン名で検索\" value={q} onSearch={(v) => { setQ(v); setPage(1) }} />\n {/* Status view of the SAME table = a filter: Segmented with count for 2-4 statuses, this\n Select past 4. Never Tabs: Tabs switch panels with their own columns (gh#1324). */}\n <ToolbarGroup label=\"ステータス\">\n <Select value={status} onValueChange={(v) => { setStatus(v); setPage(1) }}>\n <SelectTrigger><SelectValue /></SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">全ステータス</SelectItem>\n <SelectItem value=\"公開中\">公開中</SelectItem>\n <SelectItem value=\"下書き\">下書き</SelectItem>\n </SelectContent>\n </Select>\n </ToolbarGroup>\n </Toolbar>\n\n <Card>\n <CardContent flush>\n {filtered.length === 0\n ? <EmptyState title=\"該当するクーポンがありません\" description=\"検索条件を変更してください。\" /> /* one sentence, no icon */\n : <DataTable data={paged} columns={columns} getRowId={(c) => c.id} onRowClick={(c) => router.visit(`/coupons/${c.id}`)} />}\n </CardContent>\n </Card>\n\n {/* Pagination ONLY for real multi-page data — hide it while empty or single-page. */}\n {filtered.length > PAGE_SIZE && (\n <Pagination value={page} total={filtered.length} pageSize={PAGE_SIZE} showTotal onValueChange={(p) => setPage(p)} />\n )}\n </Flex>\n </PageContainer>\n </>\n )\n}\n\nCoupons.layout = withCrmLayout\nexport default Coupons",
3
3
  "name": "inertia-list-page",
4
4
  "tagline": "Inertia + @godxjp/ui list page — PageContainer + Toolbar (above the table) + DataTable + Badge + Pagination. No KPI row, no restating subtitle.",
5
5
  "tags": [
@@ -151,7 +151,7 @@
151
151
  ]
152
152
  },
153
153
  {
154
- "code": "import { Head, router } from \"@inertiajs/react\"\nimport { useMemo, useState } from \"react\"\nimport { PageContainer, Flex } from \"@godxjp/ui/layout\"\nimport { Card, CardContent, DataTable, EmptyState, Badge, type ColumnDef } from \"@godxjp/ui/data-display\"\nimport { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\"\nimport { Toolbar, ToolbarGroup, Pagination } from \"@godxjp/ui/navigation\"\nimport { formatDate } from \"@godxjp/ui/datetime\"\nimport { withCrmLayout } from \"@/layouts/crm-layout\" // see \"inertia-persistent-layout\"\n\ntype Coupon = { id: string; name: string; status: string; scope: string; validFrom: string; validTo: string; usage: number }\nconst PAGE_SIZE = 10\n\nfunction Coupons({ coupons }: { coupons: Coupon[] }) {\n const [q, setQ] = useState(\"\")\n const [status, setStatus] = useState(\"all\")\n const [page, setPage] = useState(1)\n\n const filtered = useMemo(() => coupons.filter((c) => {\n if (q && !c.name.toLowerCase().includes(q.toLowerCase())) return false\n if (status !== \"all\" && c.status !== status) return false\n return true\n }), [coupons, q, status])\n const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)\n\n // ColumnDef = { key, header, render?, align?: \"left\"|\"center\"|\"right\", sortable?, width? }\n const columns: ColumnDef<Coupon>[] = [\n { key: \"name\", header: \"クーポン名\", render: (c) => <span className=\"font-medium\">{c.name}</span> },\n { key: \"scope\", header: \"スコープ\" }, // a category is plain text; Badge is for the status column only\n { key: \"status\", header: \"ステータス\", render: (c) => <Badge status={c.status} /> },\n { key: \"valid\", header: \"有効期間\", render: (c) => `${formatDate(c.validFrom)} 〜 ${formatDate(c.validTo)}` },\n { key: \"usage\", header: \"利用数\", align: \"right\", render: (c) => c.usage.toLocaleString() },\n ]\n\n return (\n <>\n <Head title=\"クーポン管理\" />\n {/* RULE: every page wraps in PageContainer; vertical rhythm via Flex direction=\"col\"/ResponsiveGrid, never p-* or gap-* */}\n {/* No subtitle that restates the title, no StatCard row: the total is Pagination's showTotal. */}\n <PageContainer title=\"クーポン管理\">\n <Flex direction=\"col\" gap=\"lg\">\n {/* Toolbar is the filter-bar primitive (standalone, above the table surface). */}\n <Toolbar hasActiveFilters={q !== \"\" || status !== \"all\"} onClear={() => { setQ(\"\"); setStatus(\"all\"); setPage(1) }}>\n {/* SearchInput is value + onSearch(v) — NOT onChange */}\n <SearchInput placeholder=\"クーポン名で検索\" value={q} onSearch={(v) => { setQ(v); setPage(1) }} />\n <ToolbarGroup label=\"ステータス\">\n <Select value={status} onValueChange={(v) => { setStatus(v); setPage(1) }}>\n <SelectTrigger><SelectValue /></SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">全ステータス</SelectItem>\n <SelectItem value=\"公開中\">公開中</SelectItem>\n <SelectItem value=\"下書き\">下書き</SelectItem>\n </SelectContent>\n </Select>\n </ToolbarGroup>\n </Toolbar>\n\n <Card>\n <CardContent flush>\n {filtered.length === 0\n ? <EmptyState title=\"該当するクーポンがありません\" description=\"検索条件を変更してください。\" /> /* one sentence, no icon */\n : <DataTable data={paged} columns={columns} getRowId={(c) => c.id} onRowClick={(c) => router.visit(`/coupons/${c.id}`)} />}\n </CardContent>\n </Card>\n\n {/* Pagination ONLY for real multi-page data — hide it while empty or single-page. */}\n {filtered.length > PAGE_SIZE && (\n <Pagination value={page} total={filtered.length} pageSize={PAGE_SIZE} showTotal onValueChange={(p) => setPage(p)} />\n )}\n </Flex>\n </PageContainer>\n </>\n )\n}\n\nCoupons.layout = withCrmLayout\nexport default Coupons",
154
+ "code": "import { Head, router } from \"@inertiajs/react\"\nimport { useMemo, useState } from \"react\"\nimport { PageContainer, Flex } from \"@godxjp/ui/layout\"\nimport { Card, CardContent, DataTable, EmptyState, Badge, type ColumnDef } from \"@godxjp/ui/data-display\"\nimport { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\"\nimport { Toolbar, ToolbarGroup, Pagination } from \"@godxjp/ui/navigation\"\nimport { formatDate } from \"@godxjp/ui/datetime\"\nimport { withCrmLayout } from \"@/layouts/crm-layout\" // see \"inertia-persistent-layout\"\n\ntype Coupon = { id: string; name: string; status: string; scope: string; validFrom: string; validTo: string; usage: number }\nconst PAGE_SIZE = 10\n\nfunction Coupons({ coupons }: { coupons: Coupon[] }) {\n const [q, setQ] = useState(\"\")\n const [status, setStatus] = useState(\"all\")\n const [page, setPage] = useState(1)\n\n const filtered = useMemo(() => coupons.filter((c) => {\n if (q && !c.name.toLowerCase().includes(q.toLowerCase())) return false\n if (status !== \"all\" && c.status !== status) return false\n return true\n }), [coupons, q, status])\n const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)\n\n // ColumnDef = { key, header, render?, align?: \"left\"|\"center\"|\"right\", sortable?, width? }\n const columns: ColumnDef<Coupon>[] = [\n { key: \"name\", header: \"クーポン名\", render: (c) => <span className=\"font-medium\">{c.name}</span> },\n { key: \"scope\", header: \"スコープ\" }, // a category is plain text; Badge is for the status column only\n { key: \"status\", header: \"ステータス\", render: (c) => <Badge status={c.status} /> },\n { key: \"valid\", header: \"有効期間\", render: (c) => `${formatDate(c.validFrom)} 〜 ${formatDate(c.validTo)}` },\n { key: \"usage\", header: \"利用数\", align: \"right\", render: (c) => c.usage.toLocaleString() },\n ]\n\n return (\n <>\n <Head title=\"クーポン管理\" />\n {/* RULE: every page wraps in PageContainer; vertical rhythm via Flex direction=\"col\"/ResponsiveGrid, never p-* or gap-* */}\n {/* No subtitle that restates the title, no StatCard row: the total is Pagination's showTotal. */}\n <PageContainer title=\"クーポン管理\">\n <Flex direction=\"col\" gap=\"lg\">\n {/* Toolbar is the filter-bar primitive (standalone, above the table surface). */}\n <Toolbar hasActiveFilters={q !== \"\" || status !== \"all\"} onClear={() => { setQ(\"\"); setStatus(\"all\"); setPage(1) }}>\n {/* SearchInput is value + onSearch(v) — NOT onChange */}\n <SearchInput placeholder=\"クーポン名で検索\" value={q} onSearch={(v) => { setQ(v); setPage(1) }} />\n {/* Status view of the SAME table = a filter: Segmented with count for 2-4 statuses, this\n Select past 4. Never Tabs: Tabs switch panels with their own columns (gh#1324). */}\n <ToolbarGroup label=\"ステータス\">\n <Select value={status} onValueChange={(v) => { setStatus(v); setPage(1) }}>\n <SelectTrigger><SelectValue /></SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">全ステータス</SelectItem>\n <SelectItem value=\"公開中\">公開中</SelectItem>\n <SelectItem value=\"下書き\">下書き</SelectItem>\n </SelectContent>\n </Select>\n </ToolbarGroup>\n </Toolbar>\n\n <Card>\n <CardContent flush>\n {filtered.length === 0\n ? <EmptyState title=\"該当するクーポンがありません\" description=\"検索条件を変更してください。\" /> /* one sentence, no icon */\n : <DataTable data={paged} columns={columns} getRowId={(c) => c.id} onRowClick={(c) => router.visit(`/coupons/${c.id}`)} />}\n </CardContent>\n </Card>\n\n {/* Pagination ONLY for real multi-page data — hide it while empty or single-page. */}\n {filtered.length > PAGE_SIZE && (\n <Pagination value={page} total={filtered.length} pageSize={PAGE_SIZE} showTotal onValueChange={(p) => setPage(p)} />\n )}\n </Flex>\n </PageContainer>\n </>\n )\n}\n\nCoupons.layout = withCrmLayout\nexport default Coupons",
155
155
  "name": "inertia-list-page",
156
156
  "tagline": "Inertia + @godxjp/ui list page — PageContainer + Toolbar (above the table) + DataTable + Badge + Pagination. No KPI row, no restating subtitle.",
157
157
  "tags": [
package/agent/tokens.json CHANGED
@@ -1638,7 +1638,7 @@
1638
1638
  "value": "initial"
1639
1639
  },
1640
1640
  {
1641
- "description": "PageContainer `toolbar` — the fixed chrome band between the header and the (scrolling) body. ALL THREE knobs are quiet by default (rule #44): background — `transparent`, i.e. the band shows the page ground exactly as it did before the knob existed. It is bound at `:root` and NOT `initial`, unlike the divider below, because the default is a plain CSS keyword rather than another role token: there is nothing for a scoped [data-tenant]/.dark override to re-resolve, so the `initial` + call-site-fallback dance would buy nothing and only hide the default from anyone reading this file. A service that wants the band to read as its own surface — the design that asked for this knob puts a chat channel's workflow rail on the card ground — sets `--page-toolbar-background: hsl(var(--card));` ONCE in its theme. Never `className=\"bg-card\"` at the call site: that is hand-laid page chrome, it paints only under `variant=\"flush\"` gutters correctly by accident, and it cannot be re-themed per tenant. pad-block 0 — the band's ONLY breathing room, and the only one it should ever have. The band is chrome, so it sits FLUSH against the header above and the body below (src/styles/layout.css cancels the container's --page-band-gap from the band itself); there is deliberately no outside space to tune, because a ruled, painted band floating in a void divides nothing. A service that opts into the background or the divider gives the band its inset HERE (`--page-toolbar-pad-block: var(--space-2)`) instead of padding the strip at the call site. Still 0 by default: a transparent band is not a surface and has no inside to breathe, and under `fill` every pixel of it comes straight off the scroll viewport the slot exists to protect. divider — declared `initial` so the CALL SITE can fall back to --page-header-divider (`var(--page-toolbar-divider, var(--page-header-divider))`). One theme declaration therefore rules the whole page chrome consistently, and a scoped [data-tenant]/.dark override of the header divider still reaches the band — a `:root` binding would freeze it at the :root value (docs/TOKENS.md · \"Role-mirror knobs MUST be `initial`\"). Set `--page-toolbar-divider: none` to silence just this band.",
1641
+ "description": "PageContainer `toolbar` — the fixed chrome band between the header and the (scrolling) body. ALL THREE knobs are quiet by default (rule #44): background — `transparent`, i.e. the band shows the page ground exactly as it did before the knob existed. It is bound at `:root` and NOT `initial`, unlike the divider below, because the default is a plain CSS keyword rather than another role token: there is nothing for a scoped [data-tenant]/.dark override to re-resolve, so the `initial` + call-site-fallback dance would buy nothing and only hide the default from anyone reading this file. A service that wants the band to read as its own surface — the design that asked for this knob puts a chat channel's workflow rail on the card ground — sets `--page-toolbar-background: hsl(var(--card));` ONCE in its theme. Never `className=\"bg-card\"` at the call site: that is hand-laid page chrome, it paints only under `variant=\"flush\"` gutters correctly by accident, and it cannot be re-themed per tenant. pad-block — `initial`; the call site falls back to the container's `--page-band-gap` (`var(--page-toolbar-pad-block, var(--page-band-gap))`), which is scoped on `.ui-page-container` and so cannot be bound here. The band's BOX sits FLUSH against the header above and the body below (src/styles/layout.css cancels the container's gap from the band itself — a painted, ruled band floating in a void divides nothing), and this inset gives the band's CONTENT the page's band rhythm back: a plain filter strip sits one band gap from the header and from the body, the same as any other band. At the old `0px` default a transparent strip touched both surfaces, 0px (gh#1323). A service that paints or rules the band sets a tighter inset HERE (`--page-toolbar-pad-block: var(--space-2)`) instead of padding the strip at the call site; `0px` restores the flush strip. divider — declared `initial` so the CALL SITE can fall back to --page-header-divider (`var(--page-toolbar-divider, var(--page-header-divider))`). One theme declaration therefore rules the whole page chrome consistently, and a scoped [data-tenant]/.dark override of the header divider still reaches the band — a `:root` binding would freeze it at the :root value (docs/TOKENS.md · \"Role-mirror knobs MUST be `initial`\"). Set `--page-toolbar-divider: none` to silence just this band.",
1642
1642
  "name": "--page-toolbar-background",
1643
1643
  "tier": "semantic",
1644
1644
  "value": "transparent"
@@ -1647,7 +1647,7 @@
1647
1647
  "description": "Layout primitive tokens: page, section, stack, inline.",
1648
1648
  "name": "--page-toolbar-pad-block",
1649
1649
  "tier": "semantic",
1650
- "value": "0px"
1650
+ "value": "initial"
1651
1651
  },
1652
1652
  {
1653
1653
  "description": "Layout primitive tokens: page, section, stack, inline.",
@@ -5375,6 +5375,36 @@
5375
5375
  "tier": "component",
5376
5376
  "value": "calc(1em / var(--font-size-ratio))"
5377
5377
  },
5378
+ {
5379
+ "description": "scale-exempt: a `pre` in prose follows the surrounding text size, so it scales with user text (gh#1320)",
5380
+ "name": "--prose-code-block-font-size",
5381
+ "tier": "component",
5382
+ "value": "1em"
5383
+ },
5384
+ {
5385
+ "description": "Prose heading steps RELATIVE to the prose text (gh#1320): ProseMirror strips per-block attributes, so editor headings only grow with user text size by inheriting — a fixed step would not. Same default sizes as the global type scale at 14px body; `--heading-h*` still overrides.",
5386
+ "name": "--prose-heading-h1-font-size",
5387
+ "tier": "component",
5388
+ "value": "calc( 1em * var(--font-size-ratio) * var(--font-size-ratio) * var(--font-size-ratio) )"
5389
+ },
5390
+ {
5391
+ "description": "Data-display component tokens — small-by-design text knobs (rule #45/#46).",
5392
+ "name": "--prose-heading-h2-font-size",
5393
+ "tier": "component",
5394
+ "value": "calc(1em * var(--font-size-ratio) * var(--font-size-ratio))"
5395
+ },
5396
+ {
5397
+ "description": "Data-display component tokens — small-by-design text knobs (rule #45/#46).",
5398
+ "name": "--prose-heading-h3-font-size",
5399
+ "tier": "component",
5400
+ "value": "calc(1em * var(--font-size-ratio))"
5401
+ },
5402
+ {
5403
+ "description": "scale-exempt: h4–h6 sit at the prose text size, relative so they scale with it (gh#1320)",
5404
+ "name": "--prose-heading-h4-font-size",
5405
+ "tier": "component",
5406
+ "value": "1em"
5407
+ },
5378
5408
  {
5379
5409
  "description": "Data-display component tokens — small-by-design text knobs (rule #45/#46).",
5380
5410
  "name": "--prose-code-space-inline",
@@ -5405,6 +5435,12 @@
5405
5435
  "tier": "component",
5406
5436
  "value": "var(--block-editor-frame-padding)"
5407
5437
  },
5438
+ {
5439
+ "description": "Raw-Markdown block head: one step below the editor text, relative so it scales with it (gh#1320).",
5440
+ "name": "--block-editor-raw-block-head-font-size",
5441
+ "tier": "component",
5442
+ "value": "calc(1em / var(--font-size-ratio))"
5443
+ },
5408
5444
  {
5409
5445
  "description": "Data-display component tokens — small-by-design text knobs (rule #45/#46).",
5410
5446
  "name": "--block-editor-min-height",
@@ -69,6 +69,7 @@ function useCollapsesToMenu(ref, enabled) {
69
69
  if (typeof ResizeObserver === "undefined") return void 0;
70
70
  const observer = new ResizeObserver(update);
71
71
  observer.observe(bar);
72
+ if (!collapsed) for (const cluster of bar.children) observer.observe(cluster);
72
73
  return () => observer.disconnect();
73
74
  }, [ref, enabled, collapsed]);
74
75
  return enabled && collapsed;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
3
- "version": "32.12.1",
3
+ "version": "32.14.0",
4
4
  "targetSize": {
5
5
  "standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
6
6
  "min": 24,
@@ -5,25 +5,6 @@
5
5
 
6
6
  @layer godxjp-ui-responsive;
7
7
 
8
- [lang="ja"] {
9
- --font-family-sans: var(--font-sans-ja, var(--font-sans-base));
10
- }
11
- [lang="ko"] {
12
- --font-family-sans: var(--font-sans-ko, var(--font-sans-base));
13
- }
14
- [lang="vi"] {
15
- --font-family-sans: var(--font-sans-vi, var(--font-sans-base));
16
- }
17
- [lang="zh"],
18
- [lang="zh-Hans"],
19
- [lang="zh-CN"] {
20
- --font-family-sans: var(--font-sans-zh-hans, var(--font-sans-base));
21
- }
22
- [lang="zh-Hant"],
23
- [lang="zh-TW"] {
24
- --font-family-sans: var(--font-sans-zh-hant, var(--font-sans-base));
25
- }
26
-
27
8
  @source inline(
28
9
  "{bg,text,border,ring}-{primary,secondary,muted,accent,destructive,success,warning,info,attention}"
29
10
  );
@@ -45,6 +26,25 @@
45
26
  @import "./focus-ring.css";
46
27
  @import "./density.css";
47
28
 
29
+ [lang="ja"] {
30
+ --font-family-sans: var(--font-sans-ja, var(--font-sans-base));
31
+ }
32
+ [lang="ko"] {
33
+ --font-family-sans: var(--font-sans-ko, var(--font-sans-base));
34
+ }
35
+ [lang="vi"] {
36
+ --font-family-sans: var(--font-sans-vi, var(--font-sans-base));
37
+ }
38
+ [lang="zh"],
39
+ [lang="zh-Hans"],
40
+ [lang="zh-CN"] {
41
+ --font-family-sans: var(--font-sans-zh-hans, var(--font-sans-base));
42
+ }
43
+ [lang="zh-Hant"],
44
+ [lang="zh-TW"] {
45
+ --font-family-sans: var(--font-sans-zh-hant, var(--font-sans-base));
46
+ }
47
+
48
48
  @theme inline {
49
49
  --color-background: hsl(var(--background));
50
50
  --color-foreground: hsl(var(--foreground));
@@ -922,10 +922,10 @@
922
922
  var(--list-row-compact-padding-x, var(--list-row-padding-x));
923
923
  }
924
924
 
925
- [data-slot="card-content"]:not([data-flush]) > [data-slot="list-row"],
926
925
  [data-slot="card-content"]:not([data-flush])
927
- > :is(ul, ol, [role="list"])
928
- > [data-slot="list-row"] {
926
+ [data-slot="list-row"]:not(
927
+ :where([data-slot="card-content"][data-flush], [data-slot="table-cell"], td, th) *
928
+ ) {
929
929
  padding-inline: 0;
930
930
  }
931
931
 
@@ -1171,6 +1171,10 @@
1171
1171
  tab-size: 2;
1172
1172
  }
1173
1173
 
1174
+ .ui-prose pre {
1175
+ font-size: var(--prose-code-block-font-size);
1176
+ }
1177
+
1174
1178
  :is(.ui-code-block, .ui-prose pre) > code {
1175
1179
  font: inherit;
1176
1180
  background: transparent;
@@ -1350,32 +1354,23 @@
1350
1354
  }
1351
1355
 
1352
1356
  .ui-prose h1 {
1353
- font-size: var(
1354
- --heading-h1,
1355
- calc(
1356
- var(--font-size-base) * var(--font-size-ratio) * var(--font-size-ratio) *
1357
- var(--font-size-ratio)
1358
- )
1359
- );
1357
+ font-size: var(--heading-h1, var(--prose-heading-h1-font-size));
1360
1358
  }
1361
1359
 
1362
1360
  .ui-prose h2 {
1363
- font-size: var(
1364
- --heading-h2,
1365
- calc(var(--font-size-base) * var(--font-size-ratio) * var(--font-size-ratio))
1366
- );
1361
+ font-size: var(--heading-h2, var(--prose-heading-h2-font-size));
1367
1362
  }
1368
1363
 
1369
1364
  .ui-prose h3 {
1370
- font-size: var(--heading-h3, calc(var(--prose-font-size) * var(--font-size-ratio)));
1365
+ font-size: var(--heading-h3, var(--prose-heading-h3-font-size));
1371
1366
  }
1372
1367
 
1373
1368
  .ui-prose h4 {
1374
- font-size: var(--heading-h4, var(--prose-font-size));
1369
+ font-size: var(--heading-h4, var(--prose-heading-h4-font-size));
1375
1370
  }
1376
1371
 
1377
1372
  .ui-prose :is(h5, h6) {
1378
- font-size: var(--prose-font-size);
1373
+ font-size: inherit;
1379
1374
  }
1380
1375
 
1381
1376
  .ui-prose :is(h4, h5, h6) {
@@ -1692,6 +1687,10 @@
1692
1687
  gap: var(--space-2);
1693
1688
  }
1694
1689
 
1690
+ .ui-block-editor .ui-block-editor-raw-block-head :is([data-slot="text"], .ui-button) {
1691
+ font-size: var(--block-editor-raw-block-head-font-size);
1692
+ }
1693
+
1695
1694
  .ui-block-editor-raw-block-source {
1696
1695
  margin: 0;
1697
1696
  white-space: pre-wrap;
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "32.12.1",
2
+ "version": "32.14.0",
3
3
  "generatedBy": "scripts/gen-style-layers.mjs — do not edit by hand; run `pnpm gen:style-layers`",
4
4
  "base": "base.css",
5
5
  "fonts": "fonts.css",