@godxjp/ui 32.15.0 → 33.1.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 +2 -2
- package/README.md +4 -4
- package/agent/DESIGN.md +2 -2
- package/agent/START-HERE.md +15 -14
- package/agent/components/Card.json +1 -1
- package/agent/components/CardBar.json +4 -2
- package/agent/components/ScrollArea.json +1 -0
- package/agent/components/Tabs.json +2 -2
- package/agent/components/ToggleGroup.json +3 -1
- package/agent/components/Toolbar.json +7 -12
- package/agent/components-index.json +2 -2
- package/agent/components.json +18 -18
- package/agent/index.json +7 -7
- package/agent/llms.txt +8 -8
- package/agent/patterns/data-table-page.json +1 -1
- package/agent/patterns/inertia-list-page.json +1 -1
- package/agent/patterns/list-filters.json +24 -0
- package/agent/patterns-index.json +23 -0
- package/agent/patterns.json +26 -2
- package/agent/rules.json +5 -0
- package/agent/tokens.json +0 -6
- package/dist/components/navigation/index.d.ts +1 -1
- package/dist/components/navigation/toolbar.d.ts +2 -2
- package/dist/components/navigation/toolbar.js +0 -3
- package/dist/contracts/measurement.json +1 -1
- package/dist/props/components/navigation.prop.d.ts +10 -12
- package/dist/props/registry.d.ts +0 -5
- package/dist/props/registry.js +0 -5
- package/dist/styles/layers.json +1 -1
- package/dist/styles/layout.css +0 -33
- package/dist/styles.css +1 -1
- package/dist/tokens/components/navigation.css +0 -2
- package/docs/CONSUMER-RULES.md +1 -1
- package/docs/migrations/v33.md +60 -0
- package/package.json +2 -2
- package/scripts/consumer-rule.md +3 -0
- package/scripts/ui-audit.mjs +61 -1
package/docs/CONSUMER-RULES.md
CHANGED
|
@@ -5,7 +5,7 @@ should look like) is in the package's `DESIGN.md`. Everything else in `docs/` is
|
|
|
5
5
|
|
|
6
6
|
1. Load styles with `import "@godxjp/ui/styles.css"` (precompiled, no Tailwind needed) or `@import "@godxjp/ui/styles"` (Tailwind v4). Neither ships font files: fonts are opt-in since v32 (`pnpm add @fontsource/noto-sans-jp @fontsource/m-plus-2`, then `@import "@godxjp/ui/styles/fonts.css"` after the styles import; the GoDX preset does this for you). Never cherry-pick `*-layout.css`.
|
|
7
7
|
2. **Mount `<AppProvider defaultLocale="…">` once, at the root.** Without it the kit's OWN strings (dialog close/cancel/delete, counters, picker labels) fall to the resting locale (`<html lang>` when it names a registered locale, else `en`) and pickers to UTC (gh#1005). A development build now warns once when a kit component renders outside it; `syncI18nLocale(locale, fallback)` is the escape for an app that truly cannot mount the provider. Every page is `<PageContainer title extra footer>` (a `subtitle` only when it adds a fact the title does not); its sections are spaced by the page. Group items inside a section with `<Flex direction="col" gap>` or `<ResponsiveGrid>`.
|
|
8
|
-
3. No Tailwind layout on your own elements: no `flex`, `grid`, `gap-*`, `p-*`, `m-*`, `space-*`. Rows are `<Flex>` (default row), stacks are `<Flex direction="col">`, grids are `<ResponsiveGrid>`.
|
|
8
|
+
3. No Tailwind layout on your own elements: no `flex`, `grid`, `gap-*`, `p-*`, `m-*`, `space-*`. Rows are `<Flex>` (default row), stacks are `<Flex direction="col">`, grids are `<ResponsiveGrid>`. A list's filters are ONE `<FilterBar>` that wraps at every width — never a sideways-scrolling strip of filters or filter chips; past 2–4 filters the rest go behind one "More filters" `Sheet` (cardinal rule #51, pattern `list-filters`, audit rules `filter-bar-no-overflow` and `filter-strip-no-scroll`).
|
|
9
9
|
4. No hand-rolled surfaces: no `rounded-* border bg-*` divs. A box is `Card`, a pill is `Badge`, a person is `Avatar`, a row is `ListRow`, a label/value pair is `Descriptions`, an empty area is `EmptyState`, a read-only sample of a colour the USER chose is `Swatch`. A LIST of those rows is `<Flex as="ul" marker="none" direction="col" gap="none">` with `<ListRow as="li">` children — `marker="none"` keeps the `<ul>`, the `<li>` semantics and the gap token while dropping the bullet and the indent. Never a raw `<ul>`/`<ol>` (it carries no gap token), never `<div role="list">` + `<div role="listitem">`, and never a wrapper around each row: the divider is `:not(:last-child)` among siblings, so a row alone in its own wrapper is always the last one and EVERY divider disappears silently. A bulleted prose list is the same `<Flex as="ul">` without `marker`.
|
|
10
10
|
5. Real controls only: `Button`, `Input`, `Select`, `Textarea`, `Checkbox`… never raw `<button>`/`<input>`; a labelled control lives in `<FormField label>`. A Select outside a form takes `width="auto"`. **A disabled control's reason is visible text, never a tooltip** — see below. **Fields live in a `<Form layout="horizontal" labelWidth controlWidth>`** — never a group of `FormField`s without one, never a hand-rolled `<Flex>` row of fields (a row that belongs together is `<Flex attached>` or `<Form columns>`); size each control for its content with `controlWidth` (GOV.UK text-input width: a port ~7rem, a short enum ~10rem); three or more fields is a page, not a Dialog. The audit rules are `formfield-needs-form`, `dialog-form-too-big` and `select-width-hint`. **A case the kit cannot express is an issue, not a hand-roll:** open it on godx-jp/godxjp-ui, use the nearest valid composition, and mark it `// TODO(godxjp-ui#<n>)` (gh#998). **One action row, one Button size:** every `Button` under one parent takes the same `size` — a missing size is `default`, and `icon-sm` pairs with `sm`, `icon-xs` with `xs`, `icon` with default. Never `size="sm"` beside a default Button. The audit rules are `mixed-button-size` (static) and `mixed-button-height` (visual-audit, within 0.5px) (gh#1084).
|
|
11
11
|
6. Text is `<Text>` / `<Heading>` with `tone`, `size`, `weight`, `truncate`, `mono` — not `className="text-muted-foreground font-semibold"`. A STANDALONE glyph is `<Icon as={Lock} size="sm" tone="muted" />` from `@godxjp/ui/general`: a bare lucide icon carries its own `width="24" height="24"` and only four places re-size it for you (`Button`, a `DropdownMenuItem`, a `TopbarItem`, a `ListRow` leading slot), so everywhere else — in a `Text`, a table cell, an `<a>` — it draws at 24px beside 14px type. `size-4` and `w-[16px]` are closed to you by rules 3 and 8, and `size={16}` hard-codes a number the theme owns; `Icon` puts the glyph on the `--icon-size-*` scale (`2xs…4xl`) instead, and is `aria-hidden` unless you give it a `label` (then it is `role="img"` with that name). The audit rule is `lucide-icon-needs-size`.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Migrating to @godxjp/ui v33
|
|
2
|
+
|
|
3
|
+
Off-cycle approved: Pham Thai Duong 2026-10-15 — FilterBar overflow="scroll" taught consumers to build sideways-scrolling filter bars; removing it cannot wait for the next scheduled major.
|
|
4
|
+
|
|
5
|
+
v33 removes one thing: the horizontally scrolling filter bar. `FilterBar` (and its alias
|
|
6
|
+
`Toolbar`) no longer accepts an `overflow` prop. A filter bar now always wraps. All six packages
|
|
7
|
+
(`@godxjp/ui`, `@godxjp/ui-mcp`, `@godxjp/markdown`, `@godxjp/editor`, `@godxjp/block-editor` and
|
|
8
|
+
`@godxjp/chat`) move to 33.0.0 together.
|
|
9
|
+
|
|
10
|
+
## Why
|
|
11
|
+
|
|
12
|
+
The library recommended `overflow="scroll"` for list pages with many filters and long Japanese,
|
|
13
|
+
English or Vietnamese labels. A consumer list page followed that advice, and the result was
|
|
14
|
+
measured at 1240px in Vietnamese: the filter strip was 1945px wide inside a 921px frame, the first
|
|
15
|
+
filter label was clipped at the inline-start edge, and the user had to scroll sideways to reach
|
|
16
|
+
filters they could not see. Leading products do not do this. GitHub, Linear, Gmail, Jira and
|
|
17
|
+
Shopify Polaris IndexFilters never scroll a filter bar sideways; they let it wrap, or they collapse
|
|
18
|
+
the rarely used filters behind a single "More filters" or "Add filter" control.
|
|
19
|
+
|
|
20
|
+
## Breaking changes
|
|
21
|
+
|
|
22
|
+
### FilterBar / Toolbar `overflow` is removed
|
|
23
|
+
|
|
24
|
+
- The `overflow` prop is gone from `FilterBar` and `Toolbar`, and the `FilterBarOverflowProp` type
|
|
25
|
+
is no longer exported. Passing `overflow` is now a TypeScript error.
|
|
26
|
+
- The strip no longer carries a `data-overflow` attribute, and the
|
|
27
|
+
`.ui-toolbar[data-overflow="scroll"]` rules are removed from the stylesheet.
|
|
28
|
+
- The bar always wraps: below 640px the filters stack in one column, from 640px up they flow onto
|
|
29
|
+
as many rows as they need. This is the behaviour the old default `overflow="wrap"` already had,
|
|
30
|
+
so a bar that never passed `overflow`, or passed `overflow="wrap"`, renders exactly as before.
|
|
31
|
+
|
|
32
|
+
### Token `--filter-bar-scroll-padding-y` is removed
|
|
33
|
+
|
|
34
|
+
It only reserved the scrollbar gutter under the removed scroll strip. A theme that sets it can
|
|
35
|
+
delete the line; nothing reads it any more.
|
|
36
|
+
|
|
37
|
+
`Tabs overflow`, `Topbar overflow` and the chat attachments `overflow` are different components
|
|
38
|
+
and are unchanged.
|
|
39
|
+
|
|
40
|
+
## How to upgrade
|
|
41
|
+
|
|
42
|
+
1. Upgrade all six packages to 33.0.x together.
|
|
43
|
+
2. Run `npx godxjp-ui audit <src>`. The new
|
|
44
|
+
`filter-bar-no-overflow` rule reports every `<FilterBar>` or `<Toolbar>` that still passes
|
|
45
|
+
`overflow`. The TypeScript build reports the same places.
|
|
46
|
+
3. Delete the `overflow` prop at each place it reports. If a page used `overflow="scroll"` because
|
|
47
|
+
it has many filters, check how tall the wrapped bar is. When it takes more than two rows, keep
|
|
48
|
+
the few filters people use most in the bar and move the rest behind one "More filters" Button
|
|
49
|
+
that opens a Sheet holding them as `FilterBarGroup`s. The library has no dedicated component for
|
|
50
|
+
this; it is a composition of Button, Sheet and FilterBarGroup, and the applied filters can still
|
|
51
|
+
show as `chips` on the bar.
|
|
52
|
+
4. Remove any page CSS that targeted `[data-overflow]` on the filter strip, and any theme line that
|
|
53
|
+
set `--filter-bar-scroll-padding-y`.
|
|
54
|
+
5. Run `npx godxjp-ui sync-rules` and restart your coding agent so it stops recommending the
|
|
55
|
+
removed prop.
|
|
56
|
+
|
|
57
|
+
## Support
|
|
58
|
+
|
|
59
|
+
v32 receives no further releases. A security fix for v32 is an owner exception, decided case by
|
|
60
|
+
case.
|
package/package.json
CHANGED
package/scripts/consumer-rule.md
CHANGED
|
@@ -21,6 +21,9 @@ your theme, or import a preset (`@godxjp/ui/themes/godx.css`). Details: `docs/CU
|
|
|
21
21
|
surface (a table, a form block), never a frame around every section.
|
|
22
22
|
- No `subtitle` that restates the `title`. Leave it out unless it adds a fact.
|
|
23
23
|
- Filters sit ABOVE the table they filter, never after it.
|
|
24
|
+
- A filter bar (`FilterBar`) WRAPS and never scrolls sideways — no horizontal strip of filters
|
|
25
|
+
or filter chips. 2–4 filters inline; the rest behind ONE "More filters" `Sheet`. `get_rule 51`,
|
|
26
|
+
`get_pattern list-filters`.
|
|
24
27
|
- KPI tiles (`StatCard`) only on dashboards, 3–4 at most. A list page opens with its list.
|
|
25
28
|
- `EmptyState` = one sentence + one action. Inside a Card it has no icon.
|
|
26
29
|
- `Alert` only for something the user must act on now; not a page decoration.
|
package/scripts/ui-audit.mjs
CHANGED
|
@@ -477,6 +477,19 @@ const RULES = [
|
|
|
477
477
|
message:
|
|
478
478
|
"Abstract value components use onValueChange, not onChange. Reserve onChange for DOM events.",
|
|
479
479
|
},
|
|
480
|
+
{
|
|
481
|
+
id: "filter-bar-no-overflow",
|
|
482
|
+
replacement: "the default wrapping FilterBar",
|
|
483
|
+
severity: "error",
|
|
484
|
+
spansElement: true,
|
|
485
|
+
// `overflow` was removed from FilterBar/Toolbar in 33.0.0. Its `scroll` value made the bar one
|
|
486
|
+
// horizontally scrolling strip — measured in a consumer at 1240px (vi): 1945px of filters in a
|
|
487
|
+
// 921px frame, the first label clipped. Tabs and Topbar keep their own `overflow`; only the
|
|
488
|
+
// filter bar's opening tag is matched (`<FilterBarGroup` and `<DataTable.Toolbar` are not).
|
|
489
|
+
test: new RegExp(`<(?:FilterBar|Toolbar)\\b${ATTRS}\\boverflow=`, "g"),
|
|
490
|
+
message:
|
|
491
|
+
"FilterBar/Toolbar has no `overflow` prop (removed in 33.0.0) — a filter bar wraps and never scrolls sideways. Delete the prop; with many filters keep the common ones in the bar and put the rest behind one 'More filters' Button that opens a Sheet.",
|
|
492
|
+
},
|
|
480
493
|
{
|
|
481
494
|
id: "no-arbitrary-hex",
|
|
482
495
|
replacement: "a semantic token",
|
|
@@ -912,7 +925,7 @@ const RULES = [
|
|
|
912
925
|
standard:
|
|
913
926
|
"WCAG 2.2 SC 2.1.1 (Keyboard) · WAI-ARIA 1.2 (group) · Deque axe-core scrollable-region-focusable",
|
|
914
927
|
message:
|
|
915
|
-
'Hand-rolled scrollport (overflow-auto / -scroll on your own element) — use <ScrollArea label={t("…")} orientation> so the box that scrolls is a tab stop with a role and a localized name, and stops being one when there is nothing to scroll (gh#821). Without it, anything below the fold is reachable by mouse and by nothing else (WCAG 2.2 SC 2.1.1). axe only catches the instances whose content happens to have no focusable child, so this checks the markup rather than one render (gh#825). overflow-hidden is a clipping box, not a scrollport, and is not flagged.',
|
|
928
|
+
'Hand-rolled scrollport (overflow-auto / -scroll on your own element) — use <ScrollArea label={t("…")} orientation> so the box that scrolls is a tab stop with a role and a localized name, and stops being one when there is nothing to scroll (gh#821). Without it, anything below the fold is reachable by mouse and by nothing else (WCAG 2.2 SC 2.1.1). axe only catches the instances whose content happens to have no focusable child, so this checks the markup rather than one render (gh#825). overflow-hidden is a clipping box, not a scrollport, and is not flagged. A strip of FILTERS or form controls is not a scrollport at all — it wraps (`filter-strip-no-scroll`, cardinal rule #51); do not move it into a horizontal ScrollArea.',
|
|
916
929
|
},
|
|
917
930
|
{
|
|
918
931
|
id: "no-hand-rolled-break-anywhere",
|
|
@@ -2499,6 +2512,53 @@ for (const dir of SCAN_DIRS) {
|
|
|
2499
2512
|
}
|
|
2500
2513
|
}
|
|
2501
2514
|
|
|
2515
|
+
/*
|
|
2516
|
+
* A HAND-ROLLED sideways filter strip (cardinal rule #51). `filter-bar-no-overflow` only sees
|
|
2517
|
+
* the removed prop; the same strip rebuilt as `<Flex className="overflow-x-auto">` or
|
|
2518
|
+
* `<ScrollArea orientation="horizontal">` around FilterBar/FormField/Select/SearchInput is
|
|
2519
|
+
* the defect 33.0.0 removed (1240px vi: 1945px of filters in a 921px frame, the first label
|
|
2520
|
+
* clipped). WCAG 2.2 SC 1.4.10 excepts two-dimensional content, not search fields or controls,
|
|
2521
|
+
* so a box whose body holds a table (an editable grid) or Cards (a kanban board, a carousel of
|
|
2522
|
+
* records, each with its own controls) is left alone. Only the
|
|
2523
|
+
* INLINE axis is matched: `overflow-auto` / `overflow-y-auto` are vertical panes, where a
|
|
2524
|
+
* Sheet of stacked filters legitimately scrolls. Chip rows (ToggleGroup/Segmented) are not
|
|
2525
|
+
* matched: the same component is an editor toolbar, which SC 1.4.10 does except.
|
|
2526
|
+
*/
|
|
2527
|
+
const FILTER_CONTROL =
|
|
2528
|
+
/<(?:FilterBar|Toolbar|FilterBarGroup|ToolbarGroup|FormField|SearchInput|Select|Input|DatePicker|DateRangePicker|TimePicker|NumberInput|Cascader|TreeSelect)(?=[\s/>])/;
|
|
2529
|
+
const HOLDS_2D_CONTENT = /<(?:Table|DataTable|table|Card)(?=[\s/>])/;
|
|
2530
|
+
const scrollOpeners = new Set();
|
|
2531
|
+
for (const m of scanContent.matchAll(/(?<![\w-])overflow-x-(?:auto|scroll)(?![\w-])/g)) {
|
|
2532
|
+
const at = scanContent.lastIndexOf("<", m.index);
|
|
2533
|
+
if (at >= 0 && jsxOpeningEnd(scanContent, at) > m.index) scrollOpeners.add(at);
|
|
2534
|
+
}
|
|
2535
|
+
for (const m of scanContent.matchAll(/<ScrollArea(?=[\s>])/g)) {
|
|
2536
|
+
const open = scanContent.slice(m.index, jsxOpeningEnd(scanContent, m.index) + 1);
|
|
2537
|
+
if (/\borientation\s*=\s*\{?\s*["'](?:horizontal|both)["']/.test(open)) {
|
|
2538
|
+
scrollOpeners.add(m.index);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
for (const at of scrollOpeners) {
|
|
2542
|
+
const tag = /^<([A-Za-z][\w.]*)/.exec(scanContent.slice(at))?.[1];
|
|
2543
|
+
if (!tag) continue;
|
|
2544
|
+
const openEnd = jsxOpeningEnd(scanContent, at);
|
|
2545
|
+
if (/\/\s*$/.test(scanContent.slice(at, openEnd))) continue;
|
|
2546
|
+
const close = closingTagRange(scanContent, tag, openEnd + 1);
|
|
2547
|
+
const body = scanContent.slice(openEnd + 1, close ? close.start : scanContent.length);
|
|
2548
|
+
if (!FILTER_CONTROL.test(body) || HOLDS_2D_CONTENT.test(body)) continue;
|
|
2549
|
+
const lineNo = lineAt(at);
|
|
2550
|
+
if (suppressed("filter-strip-no-scroll", lineNo - 1)) continue;
|
|
2551
|
+
findings.push({
|
|
2552
|
+
file: rel,
|
|
2553
|
+
line: lineNo,
|
|
2554
|
+
rule: "filter-strip-no-scroll",
|
|
2555
|
+
severity: "error",
|
|
2556
|
+
standard: "WCAG 2.2 SC 1.4.10 (Reflow) · @godxjp/ui cardinal rule #51",
|
|
2557
|
+
message: `A sideways-scrolling strip of filters/controls (<${tag}> scrolls on the inline axis around FilterBar/FormField/Select/SearchInput). A filter bar WRAPS at every width and never scrolls sideways: drop the scroller and let <FilterBar> wrap; keep the 2–4 most-used filters inline and put the rest behind ONE "More filters" Button that opens a Sheet (MCP: get_rule 51, get_pattern list-filters).`,
|
|
2558
|
+
snippet: scanContent.slice(at, at + 120).replace(/\s+/g, " "),
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
|
|
2502
2562
|
for (const m of scanContent.matchAll(/<(Dialog\.Body|DialogBody)(?=[\s>])[\s\S]*?<\/\1>/g)) {
|
|
2503
2563
|
const count = (m[0].match(/<FormField(?=[\s>/])/g) ?? []).length;
|
|
2504
2564
|
if (count < 3) continue;
|