@godxjp/ui 33.0.0 → 33.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +2 -2
- package/README.md +3 -3
- package/agent/DESIGN.md +2 -2
- package/agent/START-HERE.md +12 -11
- 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 +1 -1
- package/agent/components/ToggleGroup.json +3 -1
- package/agent/components/Toolbar.json +6 -4
- package/agent/components-index.json +2 -2
- package/agent/components.json +16 -9
- package/agent/index.json +5 -5
- package/agent/llms.txt +6 -6
- 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/dist/contracts/measurement.json +1 -1
- package/dist/styles/layers.json +1 -1
- package/docs/CONSUMER-RULES.md +1 -1
- package/package.json +2 -2
- package/scripts/consumer-rule.md +3 -0
- package/scripts/ui-audit.mjs +48 -1
package/DESIGN.md
CHANGED
|
@@ -125,7 +125,7 @@ Colour is scarce. Most of the screen is canvas, ink and hairline.
|
|
|
125
125
|
|
|
126
126
|
- **PageContainer** is the page shell: `title`, optional `subtitle`, `breadcrumb`, `extra` (page actions, primary last), `toolbar`, `footer`. Omit `title` only when the content draws the page's one `<h1>` (a document reader); PageContainer then renders no heading.
|
|
127
127
|
- **Card** only for a real boundary: a table with its own toolbar and pagination, a list of setting rows, a dashboard panel, a danger zone (`accent="destructive"`), a selectable item. Not around every section, never nested. `CardContent flush` for tables and row lists.
|
|
128
|
-
- **Toolbar** (+ `ToolbarGroup`) is the filter strip: search, labelled filters, clear-all, result count. It sits above the table, never in the page header.
|
|
128
|
+
- **Toolbar** (+ `ToolbarGroup`) is the filter strip: search, labelled filters, clear-all, result count. It sits above the table, never in the page header. It wraps and never scrolls sideways; the 2–4 most-used filters stay inline and the rest go behind one "More filters" Button that opens a `Sheet` (cardinal rule #51, pattern `list-filters`).
|
|
129
129
|
- **DataTable** for lists: sort, selection with `DataTable.BulkActions`, density, empty/loading/error states. Row actions in one kebab `DropdownMenu`.
|
|
130
130
|
- **Descriptions** for record metadata. **ListRow** for setting rows (`trailing` holds the control).
|
|
131
131
|
- **Form + FormField** for every input; actions at the end of the form or in a sticky `PageContainer footer`. Create/edit is a page, not a dialog.
|
|
@@ -192,7 +192,7 @@ A list whose rows are decisions. Show requester, target, reason, submitted time;
|
|
|
192
192
|
- Check 390, 768 and 1280 wide. Nothing scrolls horizontally except a wide table inside its own container.
|
|
193
193
|
- `DataTable` hides `hiddenOnMobile` columns and can stack rows (`collapseBelow`); `Table preset="action-collection"` keeps the actions column visible at 390.
|
|
194
194
|
- `ResponsiveGrid` columns follow its container. `AppShell` turns the sidebar into a drawer on narrow screens.
|
|
195
|
-
- Header `extra` wraps under the title; a toolbar wraps its filters. Touch targets 44px at `comfortable`.
|
|
195
|
+
- Header `extra` wraps under the title; a toolbar wraps its filters at every width, phone included: no sideways-scrolling strip of filters or filter chips (rule #51). Touch targets 44px at `comfortable`.
|
|
196
196
|
- Below 640px: at most 2 direct actions in `extra` plus one overflow `DropdownMenu` for the rest; it wraps but never prioritises for you.
|
|
197
197
|
- Bar search is `TopbarSearch`, never a bare `SearchInput`: it folds to a named search cell below 640px. To share the content keyline, keep the default `topbarSpan="content"` and put it first in `Topbar start`.
|
|
198
198
|
- Account cell: `TopbarItem icon={<Avatar/>} labelHideBelow="sm"` with the name repeated in the menu's `DropdownMenuLabel`, so a phone shows the avatar only.
|
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 — 163 components, 2228 tokens,
|
|
21
|
+
The catalog — 163 components, 2228 tokens, 44 cardinal rules — is published in two forms from one
|
|
22
22
|
source. **Entry point for either: [`AGENTS.md`](AGENTS.md).**
|
|
23
23
|
<!-- agent-catalog-counts:end -->
|
|
24
24
|
|
|
@@ -53,12 +53,12 @@ static files straight from this public repo — no hosting, no deploy step:
|
|
|
53
53
|
| [`agent/llms.txt`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/llms.txt) | 3 KB | the [llms.txt](https://llmstxt.org/) entry point |
|
|
54
54
|
| [`agent/DESIGN.md`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/DESIGN.md) | 17 KB | the visual spec: tokens, colour scarcity, admin screen anatomy, do/don't |
|
|
55
55
|
| [`agent/index.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json) | 3 KB | manifest: version, counts, every file's URL |
|
|
56
|
-
| [`agent/components-index.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json) |
|
|
56
|
+
| [`agent/components-index.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json) | 49 KB | all 163 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 163 entries in one file — most URL fetchers truncate this silently; prefer the per-component files |
|
|
59
59
|
| [`agent/tokens.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json) | 857 KB | all 2228 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
|
-
| [`agent/rules.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json) |
|
|
61
|
+
| [`agent/rules.json`](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json) | 22 KB | the 44 cardinal rules |
|
|
62
62
|
<!-- prettier-ignore-end -->
|
|
63
63
|
<!-- agent-catalog-files:end -->
|
|
64
64
|
|
package/agent/DESIGN.md
CHANGED
|
@@ -125,7 +125,7 @@ Colour is scarce. Most of the screen is canvas, ink and hairline.
|
|
|
125
125
|
|
|
126
126
|
- **PageContainer** is the page shell: `title`, optional `subtitle`, `breadcrumb`, `extra` (page actions, primary last), `toolbar`, `footer`. Omit `title` only when the content draws the page's one `<h1>` (a document reader); PageContainer then renders no heading.
|
|
127
127
|
- **Card** only for a real boundary: a table with its own toolbar and pagination, a list of setting rows, a dashboard panel, a danger zone (`accent="destructive"`), a selectable item. Not around every section, never nested. `CardContent flush` for tables and row lists.
|
|
128
|
-
- **Toolbar** (+ `ToolbarGroup`) is the filter strip: search, labelled filters, clear-all, result count. It sits above the table, never in the page header.
|
|
128
|
+
- **Toolbar** (+ `ToolbarGroup`) is the filter strip: search, labelled filters, clear-all, result count. It sits above the table, never in the page header. It wraps and never scrolls sideways; the 2–4 most-used filters stay inline and the rest go behind one "More filters" Button that opens a `Sheet` (cardinal rule #51, pattern `list-filters`).
|
|
129
129
|
- **DataTable** for lists: sort, selection with `DataTable.BulkActions`, density, empty/loading/error states. Row actions in one kebab `DropdownMenu`.
|
|
130
130
|
- **Descriptions** for record metadata. **ListRow** for setting rows (`trailing` holds the control).
|
|
131
131
|
- **Form + FormField** for every input; actions at the end of the form or in a sticky `PageContainer footer`. Create/edit is a page, not a dialog.
|
|
@@ -192,7 +192,7 @@ A list whose rows are decisions. Show requester, target, reason, submitted time;
|
|
|
192
192
|
- Check 390, 768 and 1280 wide. Nothing scrolls horizontally except a wide table inside its own container.
|
|
193
193
|
- `DataTable` hides `hiddenOnMobile` columns and can stack rows (`collapseBelow`); `Table preset="action-collection"` keeps the actions column visible at 390.
|
|
194
194
|
- `ResponsiveGrid` columns follow its container. `AppShell` turns the sidebar into a drawer on narrow screens.
|
|
195
|
-
- Header `extra` wraps under the title; a toolbar wraps its filters. Touch targets 44px at `comfortable`.
|
|
195
|
+
- Header `extra` wraps under the title; a toolbar wraps its filters at every width, phone included: no sideways-scrolling strip of filters or filter chips (rule #51). Touch targets 44px at `comfortable`.
|
|
196
196
|
- Below 640px: at most 2 direct actions in `extra` plus one overflow `DropdownMenu` for the rest; it wraps but never prioritises for you.
|
|
197
197
|
- Bar search is `TopbarSearch`, never a bare `SearchInput`: it folds to a named search cell below 640px. To share the content keyline, keep the default `topbarSpan="content"` and put it first in `Topbar start`.
|
|
198
198
|
- Account cell: `TopbarItem icon={<Avatar/>} labelHideBelow="sm"` with the name repeated in the menu's `DropdownMenuLabel`, so a phone shows the avatar only.
|
package/agent/START-HERE.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
You are about to write code against a design system you did not author. This file is the whole
|
|
4
4
|
contract. Read it before you write JSX.
|
|
5
5
|
|
|
6
|
-
**This catalog describes `@godxjp/ui` 33.
|
|
6
|
+
**This catalog describes `@godxjp/ui` 33.1.1.** If the project you are editing has a different
|
|
7
7
|
version in its `package.json`, read the pinned catalog for THAT version instead
|
|
8
8
|
(`…/v<their-version>/agent/…`). A catalog newer than the installed package describes props that do
|
|
9
9
|
not exist yet; older, and it hides props that do. Neither failure announces itself.
|
|
@@ -48,18 +48,18 @@ inspect every screenshot at every width region by region (no top-N cap), and han
|
|
|
48
48
|
→ These files are for you. Read `DESIGN.md` (in this folder) first: tokens, scarcity rules and
|
|
49
49
|
screen anatomy, the visual target the rules below do not give you. Then fetch in this order:
|
|
50
50
|
|
|
51
|
-
0. `patterns-index.json` —
|
|
51
|
+
0. `patterns-index.json` — 25 whole-task patterns as name + tagline + tags. **If your
|
|
52
52
|
task is a task** — "build a settings page", "confirm a destructive delete", "a list page with
|
|
53
53
|
filters" — start HERE, not at the components. Then fetch `patterns/<name>.json` for complete,
|
|
54
54
|
copy-paste-ready code. A component index answers "does X exist"; it cannot answer "build Y".
|
|
55
|
-
1. `components-index.json` —
|
|
55
|
+
1. `components-index.json` — 49 KB, all 163 components as name + group +
|
|
56
56
|
tagline. Read this when you already know the SHAPE you need. Each entry may carry `absorbed`:
|
|
57
57
|
names that **do not exist** and map to it — `Combobox`, `Autocomplete`, `CountrySelect` and
|
|
58
58
|
`SearchSelect` are all `Select`. If you are about to hand-roll something, search this field
|
|
59
59
|
first; it exists because that is the mistake.
|
|
60
60
|
2. `components/<Name>.json` — one file per component (1 KB–37 KB, median 6 KB), carrying its props,
|
|
61
61
|
its `importPath`, and its examples. Fetch only the handful you picked in step 1.
|
|
62
|
-
3. `rules.json` —
|
|
62
|
+
3. `rules.json` — 44 cardinal rules. The ones about raw HTML and hardcoded colour are not
|
|
63
63
|
style advice.
|
|
64
64
|
4. `tokens.json` — 2228 design tokens, each tagged with its `tier`. **If you were handed a
|
|
65
65
|
brand, read the 216 `foundation` entries first** — `--primary`, `--background`,
|
|
@@ -167,18 +167,19 @@ theme, every tenant scope and every audit in this package.
|
|
|
167
167
|
import { PageContainer, Flex } from "@godxjp/ui/layout";
|
|
168
168
|
import { Card, CardContent, DataTable } from "@godxjp/ui/data-display";
|
|
169
169
|
import { Button } from "@godxjp/ui/general";
|
|
170
|
-
import {
|
|
170
|
+
import { FilterBar } from "@godxjp/ui/navigation";
|
|
171
171
|
|
|
172
172
|
export default function InvoicesPage() {
|
|
173
173
|
return (
|
|
174
174
|
<PageContainer title="請求一覧" extra={<Button>請求を作成</Button>}>
|
|
175
175
|
<Flex direction="col" gap="sm">
|
|
176
|
-
{/* filters sit ABOVE the table they filter
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
176
|
+
{/* filters sit ABOVE the table they filter, in ONE FilterBar that wraps — never a
|
|
177
|
+
sideways-scrolling strip (rule #51); many filters → patterns/list-filters.json */}
|
|
178
|
+
<FilterBar
|
|
179
|
+
search={{ ariaLabel: "請求を検索", placeholder: "請求を検索" }}
|
|
180
|
+
filters={[{ value: "status", label: "状態", options: statuses, defaultSelected: "unpaid" }]}
|
|
181
|
+
actions={<Button variant="outline">CSV</Button>}
|
|
182
|
+
/>
|
|
182
183
|
|
|
183
184
|
{/* `flush` because DataTable draws its own edges — a padded body double-insets it */}
|
|
184
185
|
<Card>
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"CardHeader",
|
|
85
85
|
"CardTitle"
|
|
86
86
|
],
|
|
87
|
-
"tagline": "Surface container with optional accent stripe, variant fill (including the Ant Design borderless edge), hoverable lift, and density. ⚠️ The bare <Card> has NO inner padding — body content MUST be wrapped in <CardContent> (titles in <CardHeader>), or it sits FLUSH against the card edges. Never hand-roll padding with className=\"p-4\"; use <CardContent>. Compose with CardHeader/CardTitle/CardContent/CardFooter. For Ant Design's card-head TAB STRIP (tabs under the title, inside the card border) use the `tabList`/`activeTabKey`/`defaultActiveTabKey`/`onTabChange`/`extra`/`tabProps` props — the Card renders the strip and the children become the selected tab's body. For a non-tab
|
|
87
|
+
"tagline": "Surface container with optional accent stripe, variant fill (including the Ant Design borderless edge), hoverable lift, and density. ⚠️ The bare <Card> has NO inner padding — body content MUST be wrapped in <CardContent> (titles in <CardHeader>), or it sits FLUSH against the card edges. Never hand-roll padding with className=\"p-4\"; use <CardContent>. Compose with CardHeader/CardTitle/CardContent/CardFooter. For Ant Design's card-head TAB STRIP (tabs under the title, inside the card border) use the `tabList`/`activeTabKey`/`defaultActiveTabKey`/`onTabChange`/`extra`/`tabProps` props — the Card renders the strip and the children become the selected tab's body. For a non-tab TOOL strip inside the card (composer or editor tools) use <CardBar extra={…}> — a positionable bar that auto-draws its separator from its position (top→bottom border, bottom→top border, middle→both) and pins `extra` content to the inline-end edge; place it as first/last child of the Card. List FILTERS (search, filter selects, filter chips) are NOT a CardBar: CardBar's main slot scrolls sideways, and a filter strip never does — they go in a FilterBar ABOVE the card, which wraps (cardinal rule #51, pattern `list-filters`).",
|
|
88
88
|
"usage": [
|
|
89
89
|
"DO always wrap body content in <CardContent> — the bare <Card> div has zero inner padding; content renders flush against card edges without it. Never add className=\"p-4\" directly on <Card> as a substitute.",
|
|
90
90
|
"DO put titles/descriptions in <CardHeader>/<CardTitle>/<CardDescription>. Use <CardHeader banded> for a visually separated muted-background header band (mirrors <CardFooter separated>). Pair with <CardAction> inside a flex-row CardHeader for header-level action buttons.",
|
|
@@ -33,14 +33,16 @@
|
|
|
33
33
|
],
|
|
34
34
|
"related": [
|
|
35
35
|
"Card",
|
|
36
|
-
"Flex"
|
|
36
|
+
"Flex",
|
|
37
|
+
"Toolbar — the list-page filter strip (alias FilterBar); it wraps, so filters go there rather than in a CardBar."
|
|
37
38
|
],
|
|
38
39
|
"rules": [
|
|
39
40
|
9
|
|
40
41
|
],
|
|
41
42
|
"tagline": "Inline card toolbar with scoped inset and divider edges.",
|
|
42
43
|
"usage": [
|
|
43
|
-
"Compose inside Card. Unset border follows its position; explicit border prevents double rules in stacked bars."
|
|
44
|
+
"Compose inside Card. Unset border follows its position; explicit border prevents double rules in stacked bars.",
|
|
45
|
+
"DON'T put list filters (search, filter selects, filter chips) in a CardBar: its main slot scrolls sideways when crowded, and a filter strip wraps (cardinal rule #51). Filters are a Toolbar/FilterBar above the card."
|
|
44
46
|
],
|
|
45
47
|
"useCases": [
|
|
46
48
|
"Composer tool strips and view tabs."
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"DO reach for ScrollArea rather than your own `overflow-auto` div: it is the same native scrolling, plus the token-styled scrollbar, the keyboard tab stop the axe rule wants, and bottom anchoring. A bare overflow div gets none of that and each call site re-decides the scrollbar's look.",
|
|
85
85
|
"DON'T put ScrollArea inside a flex parent without giving it a `flex-1` or fixed size — it will collapse to zero height and appear broken.",
|
|
86
86
|
"For horizontal-only scrolling use `orientation=\"horizontal\"` with a width constraint; the content is allowed to grow past the box on that axis, so a row of non-shrinking columns overflows instead of squashing.",
|
|
87
|
+
"DON'T use a horizontal ScrollArea for a strip of filters, search fields, form controls or filter chips — a filter strip WRAPS (cardinal rule #51; ui-audit `filter-strip-no-scroll`). Use FilterBar, and past 2–4 filters the `list-filters` pattern's 'More filters' Sheet.",
|
|
87
88
|
"DO use `viewportRef` (or plain `ref`) when you need the scrolling element, never a `querySelector` for an internal attribute — `[data-radix-scroll-area-viewport]` is gone with Radix, and any selector matches the wrong node the moment two ScrollAreas nest.",
|
|
88
89
|
"DO build a live stream (chat, log tail, streaming response, activity feed) with `anchor=\"bottom\"` instead of writing `viewport.scrollTop = viewport.scrollHeight` in an effect. That naive version is the BUG, not the feature: it yanks a reader who has scrolled up back to the bottom on every arriving message (WCAG 3.2.5).",
|
|
89
90
|
"DO pair `anchor=\"bottom\"` with `onAnchoredChange` and a real Button (\"jump to newest\"). A keyboard user who has scrolled up needs a focusable route back to new content — the anchor alone is a pointer affordance.",
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
],
|
|
136
136
|
"related": [
|
|
137
137
|
"Steps (@godxjp/ui/navigation) — sequential wizard/progress indicator. Use Steps when order and completion state matter (multi-step forms, onboarding flows); use Tabs when panels are non-sequential and any tab can be visited freely.",
|
|
138
|
-
"Toolbar / ToolbarGroup (@godxjp/ui/navigation) —
|
|
138
|
+
"Toolbar / ToolbarGroup (@godxjp/ui/navigation) — the list-page filter strip; it WRAPS onto extra rows and never scrolls sideways the way a tab strip may (rule #51). Visually resembles `line`-variant tabs but is semantically different: Toolbar filters a dataset, it does not switch content panels. Never use Tabs as a filter control.",
|
|
139
139
|
"DropdownMenu (@godxjp/ui/navigation) — use for space-constrained contexts where showing all tab triggers at once is impractical (e.g. mobile overflow menu). If only 2-3 options exist and screen space is tight, a Segmented control is a lighter alternative to a full tab strip."
|
|
140
140
|
],
|
|
141
141
|
"rules": [],
|
|
@@ -73,7 +73,8 @@
|
|
|
73
73
|
],
|
|
74
74
|
"rules": [
|
|
75
75
|
3,
|
|
76
|
-
13
|
|
76
|
+
13,
|
|
77
|
+
51
|
|
77
78
|
],
|
|
78
79
|
"storyPath": "data-entry/toggle-group.tsx",
|
|
79
80
|
"subParts": [
|
|
@@ -89,6 +90,7 @@
|
|
|
89
90
|
"DO build a tag-filter panel as `<ToggleGroup type=\"multiple\" variant=\"soft\" shape=\"pill\" size=\"xs\">` with one counted `ToggleGroupItem` per tag — that is the whole antd `Tag.CheckableTag` row, one tab stop per chip, no `Tag` component needed (gh#734).",
|
|
90
91
|
"DO set `size`/`variant` on an individual ToggleGroupItem only when that ONE item must differ from the group.",
|
|
91
92
|
"DO add `wrap` to a TAG FILTER ROW — `<ToggleGroup type=\"multiple\" variant=\"soft\" shape=\"pill\" size=\"xs\" wrap>` is the whole folder-tag panel however many tags the folder has. This is what replaces the hand-built `<ul>` of individual `<Toggle>`s a row wider than its rail used to force (gh#741): that list loses exactly what the group owns — the shared `variant`/`size`/`shape` context, ONE `value`/`onValueChange`, and the group's arrow-key traversal, which keeps walking the chips in DOM order across the wrapped lines.",
|
|
93
|
+
"DON'T put a filter-chip row in a horizontal scroller (`overflow-x-auto`, `ScrollArea orientation=\"horizontal\"`) with `shrink-0 whitespace-nowrap` chips — at every width, phone included, a chip row that filters a list takes `wrap` (cardinal rule #51). A sideways chip row hides chips past the edge with no affordance, and this kit ships no primitive that guarantees the first chip unclipped and a visible scroll cue, so the scroller is a hand-roll.",
|
|
92
94
|
"DON'T set `wrap` on a 3–4 item segmented group. Measured at a 320px rail it is one 32px line either way, so the prop buys nothing and only adds a way for a toolbar to reflow.",
|
|
93
95
|
"DON'T pass size='default' — it is not a member of the `xs | sm | md | lg` union. Omit `size` for the md default.",
|
|
94
96
|
"DO give the group an accessible name (`aria-label`) — it renders a `group` of toggle buttons (single, the default), a `radiogroup` (single + `disallowEmptySelection`) or a `toolbar` of toggle buttons (multiple), and all three need a name."
|
|
@@ -100,13 +100,15 @@
|
|
|
100
100
|
"ToolbarGroup — the labelled wrapper for each individual filter control inside a Toolbar (SearchInput is placed directly, without a group).",
|
|
101
101
|
"DataTable.Toolbar — the in-table strip for column/density/bulk-action controls; Toolbar (this) is the page-level filter strip that sits ABOVE the table.",
|
|
102
102
|
"SearchInput — the debounced free-text control placed as the first child of Toolbar.",
|
|
103
|
-
"Badge — compose Badge + a sibling icon Button to render each active-filter chip; Badge itself is a non-interactive leaf."
|
|
103
|
+
"Badge — compose Badge + a sibling icon Button to render each active-filter chip; Badge itself is a non-interactive leaf.",
|
|
104
|
+
"Sheet — holds the filters beyond the 2–4 inline ones, opened by ONE 'More filters' Button inside the bar (pattern `list-filters`, rule #51). Not a ScrollArea: a filter strip never scrolls sideways."
|
|
104
105
|
],
|
|
105
106
|
"rules": [
|
|
106
107
|
23,
|
|
107
108
|
44,
|
|
108
109
|
45,
|
|
109
|
-
46
|
|
110
|
+
46,
|
|
111
|
+
51
|
|
110
112
|
],
|
|
111
113
|
"storyPath": "navigation/toolbar.tsx",
|
|
112
114
|
"subParts": [
|
|
@@ -114,14 +116,14 @@
|
|
|
114
116
|
"FilterBar",
|
|
115
117
|
"FilterBarGroup"
|
|
116
118
|
],
|
|
117
|
-
"tagline": "List-page filter strip — SearchInput + labelled ToolbarGroup filter slots (or the typed `search`/`filters`/`chips` model) + a clear-all affordance, optionally sticky. `FilterBar`/`FilterBarGroup` are the same components under their alias names.",
|
|
119
|
+
"tagline": "List-page filter strip — SearchInput + labelled ToolbarGroup filter slots (or the typed `search`/`filters`/`chips` model) + a clear-all affordance, optionally sticky. It WRAPS and never scrolls sideways; many filters → 2–4 inline + ONE 'More filters' Sheet (rule #51, pattern `list-filters`). `FilterBar`/`FilterBarGroup` are the same components under their alias names.",
|
|
118
120
|
"usage": [
|
|
119
121
|
"DO prefer the typed model on a canonical list page — `search`/`filters`/`chips`/`onChipRemove`/`actions`/`resultCount` (+ `loading`/`disabled`/`error`) make the bar own layout, widths, chip lifecycle and keyboard order through --filter-bar-* tokens while ALL state stays consumer data. Children composition remains fully supported and unchanged when no model prop is passed.",
|
|
120
122
|
"DO place Toolbar ABOVE the table Card, never inside a `CardContent flush`. Put SearchInput as a direct child (it self-labels) and wrap every other control in a ToolbarGroup with a `label`.",
|
|
121
123
|
"DO drive the clear-all button with `hasActiveFilters` + `onClear` — it only renders when both are truthy. The strip collapses to a single stacked column below 640px automatically.",
|
|
122
124
|
"DO set `sticky` for long list pages so the filters stay reachable while scrolling; if a topbar sits above the list, raise `--filter-bar-sticky-offset` so the strip parks below it.",
|
|
123
125
|
"DO pass `controlId` on a ToolbarGroup that wraps ONE control and give that control the same `id` — the visible caption then becomes the control's real `<label htmlFor>` (WCAG 2.5.3 label-in-name). Without it the caption only names the group wrapper and the control itself is NAMELESS (axe: select-name / button-name), so you must fall back to an `aria-label` that repeats the caption.",
|
|
124
|
-
"DO let the strip WRAP —
|
|
126
|
+
"DO let the strip WRAP — cardinal rule #51 (`get_rule 51`). It stacks below 640px and wraps onto extra rows above; it never scrolls sideways, and neither may anything you wrap it in (`ScrollArea orientation=\"horizontal\"`, an `overflow-x-auto` box): the `overflow` prop and its `scroll` strategy were removed in 33.0.0 because a sideways strip hid filters off-screen and clipped the first label. When the filters are many, keep the 2–4 most-used ones inline and put the rest behind ONE 'More filters' Button (`count` = hidden filters applied) that opens a Sheet holding them as FilterBarGroup + control; every applied filter, inline or hidden, is a chip in `chips` (the chips row wraps too). Copy-paste composition: pattern `list-filters` (`get_pattern list-filters`); there is no dedicated component. Never re-implement the geometry in the page: no page-local flex/grid/width/overflow rules on a filter strip.",
|
|
125
127
|
"DON'T build active-filter chips by nesting a Button inside a Badge (invalid markup + broken focus). Render each chip as a Badge label with a SIBLING icon Button (`aria-label` = 'clear <filter>'); a ghost `size='sm'` Button clears all.",
|
|
126
128
|
"DON'T hand-roll a debounced search box or a raw `<select>` — compose SearchInput and Select. Toolbar is layout + clear-all only; the controls own their own state and a11y.",
|
|
127
129
|
"Prefer the typed model for a canonical list page: pass `search` + `filters` + `chips` + `resultCount` and the bar owns the layout, widths, chip lifecycle, keyboard order (search → filters → children → reset → actions → chip removes) and responsive stacking through --filter-bar-* tokens. Everything stays consumer data — the bar renders state, never owns it.",
|
|
@@ -215,7 +215,7 @@
|
|
|
215
215
|
{
|
|
216
216
|
"group": "data-display",
|
|
217
217
|
"name": "Card",
|
|
218
|
-
"tagline": "Surface container with optional accent stripe, variant fill (including the Ant Design borderless edge), hoverable lift, and density. ⚠️ The bare <Card> has NO inner padding — body content MUST be wrapped in <CardContent> (titles in <CardHeader>), or it sits FLUSH against the card edges. Never hand-roll padding with className=\"p-4\"; use <CardContent>. Compose with CardHeader/CardTitle/CardContent/CardFooter. For Ant Design's card-head TAB STRIP (tabs under the title, inside the card border) use the `tabList`/`activeTabKey`/`defaultActiveTabKey`/`onTabChange`/`extra`/`tabProps` props — the Card renders the strip and the children become the selected tab's body. For a non-tab
|
|
218
|
+
"tagline": "Surface container with optional accent stripe, variant fill (including the Ant Design borderless edge), hoverable lift, and density. ⚠️ The bare <Card> has NO inner padding — body content MUST be wrapped in <CardContent> (titles in <CardHeader>), or it sits FLUSH against the card edges. Never hand-roll padding with className=\"p-4\"; use <CardContent>. Compose with CardHeader/CardTitle/CardContent/CardFooter. For Ant Design's card-head TAB STRIP (tabs under the title, inside the card border) use the `tabList`/`activeTabKey`/`defaultActiveTabKey`/`onTabChange`/`extra`/`tabProps` props — the Card renders the strip and the children become the selected tab's body. For a non-tab TOOL strip inside the card (composer or editor tools) use <CardBar extra={…}> — a positionable bar that auto-draws its separator from its position (top→bottom border, bottom→top border, middle→both) and pins `extra` content to the inline-end edge; place it as first/last child of the Card. List FILTERS (search, filter selects, filter chips) are NOT a CardBar: CardBar's main slot scrolls sideways, and a filter strip never does — they go in a FilterBar ABOVE the card, which wraps (cardinal rule #51, pattern `list-filters`)."
|
|
219
219
|
},
|
|
220
220
|
{
|
|
221
221
|
"group": "data-display",
|
|
@@ -483,7 +483,7 @@
|
|
|
483
483
|
{
|
|
484
484
|
"group": "navigation",
|
|
485
485
|
"name": "Toolbar",
|
|
486
|
-
"tagline": "List-page filter strip — SearchInput + labelled ToolbarGroup filter slots (or the typed `search`/`filters`/`chips` model) + a clear-all affordance, optionally sticky. `FilterBar`/`FilterBarGroup` are the same components under their alias names."
|
|
486
|
+
"tagline": "List-page filter strip — SearchInput + labelled ToolbarGroup filter slots (or the typed `search`/`filters`/`chips` model) + a clear-all affordance, optionally sticky. It WRAPS and never scrolls sideways; many filters → 2–4 inline + ONE 'More filters' Sheet (rule #51, pattern `list-filters`). `FilterBar`/`FilterBarGroup` are the same components under their alias names."
|
|
487
487
|
},
|
|
488
488
|
{
|
|
489
489
|
"group": "providers",
|
package/agent/components.json
CHANGED
|
@@ -391,14 +391,16 @@
|
|
|
391
391
|
],
|
|
392
392
|
"related": [
|
|
393
393
|
"Card",
|
|
394
|
-
"Flex"
|
|
394
|
+
"Flex",
|
|
395
|
+
"Toolbar — the list-page filter strip (alias FilterBar); it wraps, so filters go there rather than in a CardBar."
|
|
395
396
|
],
|
|
396
397
|
"rules": [
|
|
397
398
|
9
|
|
398
399
|
],
|
|
399
400
|
"tagline": "Inline card toolbar with scoped inset and divider edges.",
|
|
400
401
|
"usage": [
|
|
401
|
-
"Compose inside Card. Unset border follows its position; explicit border prevents double rules in stacked bars."
|
|
402
|
+
"Compose inside Card. Unset border follows its position; explicit border prevents double rules in stacked bars.",
|
|
403
|
+
"DON'T put list filters (search, filter selects, filter chips) in a CardBar: its main slot scrolls sideways when crowded, and a filter strip wraps (cardinal rule #51). Filters are a Toolbar/FilterBar above the card."
|
|
402
404
|
],
|
|
403
405
|
"useCases": [
|
|
404
406
|
"Composer tool strips and view tabs."
|
|
@@ -3727,7 +3729,7 @@
|
|
|
3727
3729
|
"CardHeader",
|
|
3728
3730
|
"CardTitle"
|
|
3729
3731
|
],
|
|
3730
|
-
"tagline": "Surface container with optional accent stripe, variant fill (including the Ant Design borderless edge), hoverable lift, and density. ⚠️ The bare <Card> has NO inner padding — body content MUST be wrapped in <CardContent> (titles in <CardHeader>), or it sits FLUSH against the card edges. Never hand-roll padding with className=\"p-4\"; use <CardContent>. Compose with CardHeader/CardTitle/CardContent/CardFooter. For Ant Design's card-head TAB STRIP (tabs under the title, inside the card border) use the `tabList`/`activeTabKey`/`defaultActiveTabKey`/`onTabChange`/`extra`/`tabProps` props — the Card renders the strip and the children become the selected tab's body. For a non-tab
|
|
3732
|
+
"tagline": "Surface container with optional accent stripe, variant fill (including the Ant Design borderless edge), hoverable lift, and density. ⚠️ The bare <Card> has NO inner padding — body content MUST be wrapped in <CardContent> (titles in <CardHeader>), or it sits FLUSH against the card edges. Never hand-roll padding with className=\"p-4\"; use <CardContent>. Compose with CardHeader/CardTitle/CardContent/CardFooter. For Ant Design's card-head TAB STRIP (tabs under the title, inside the card border) use the `tabList`/`activeTabKey`/`defaultActiveTabKey`/`onTabChange`/`extra`/`tabProps` props — the Card renders the strip and the children become the selected tab's body. For a non-tab TOOL strip inside the card (composer or editor tools) use <CardBar extra={…}> — a positionable bar that auto-draws its separator from its position (top→bottom border, bottom→top border, middle→both) and pins `extra` content to the inline-end edge; place it as first/last child of the Card. List FILTERS (search, filter selects, filter chips) are NOT a CardBar: CardBar's main slot scrolls sideways, and a filter strip never does — they go in a FilterBar ABOVE the card, which wraps (cardinal rule #51, pattern `list-filters`).",
|
|
3731
3733
|
"usage": [
|
|
3732
3734
|
"DO always wrap body content in <CardContent> — the bare <Card> div has zero inner padding; content renders flush against card edges without it. Never add className=\"p-4\" directly on <Card> as a substitute.",
|
|
3733
3735
|
"DO put titles/descriptions in <CardHeader>/<CardTitle>/<CardDescription>. Use <CardHeader banded> for a visually separated muted-background header band (mirrors <CardFooter separated>). Pair with <CardAction> inside a flex-row CardHeader for header-level action buttons.",
|
|
@@ -8178,7 +8180,7 @@
|
|
|
8178
8180
|
],
|
|
8179
8181
|
"related": [
|
|
8180
8182
|
"Steps (@godxjp/ui/navigation) — sequential wizard/progress indicator. Use Steps when order and completion state matter (multi-step forms, onboarding flows); use Tabs when panels are non-sequential and any tab can be visited freely.",
|
|
8181
|
-
"Toolbar / ToolbarGroup (@godxjp/ui/navigation) —
|
|
8183
|
+
"Toolbar / ToolbarGroup (@godxjp/ui/navigation) — the list-page filter strip; it WRAPS onto extra rows and never scrolls sideways the way a tab strip may (rule #51). Visually resembles `line`-variant tabs but is semantically different: Toolbar filters a dataset, it does not switch content panels. Never use Tabs as a filter control.",
|
|
8182
8184
|
"DropdownMenu (@godxjp/ui/navigation) — use for space-constrained contexts where showing all tab triggers at once is impractical (e.g. mobile overflow menu). If only 2-3 options exist and screen space is tight, a Segmented control is a lighter alternative to a full tab strip."
|
|
8183
8185
|
],
|
|
8184
8186
|
"rules": [],
|
|
@@ -8656,13 +8658,15 @@
|
|
|
8656
8658
|
"ToolbarGroup — the labelled wrapper for each individual filter control inside a Toolbar (SearchInput is placed directly, without a group).",
|
|
8657
8659
|
"DataTable.Toolbar — the in-table strip for column/density/bulk-action controls; Toolbar (this) is the page-level filter strip that sits ABOVE the table.",
|
|
8658
8660
|
"SearchInput — the debounced free-text control placed as the first child of Toolbar.",
|
|
8659
|
-
"Badge — compose Badge + a sibling icon Button to render each active-filter chip; Badge itself is a non-interactive leaf."
|
|
8661
|
+
"Badge — compose Badge + a sibling icon Button to render each active-filter chip; Badge itself is a non-interactive leaf.",
|
|
8662
|
+
"Sheet — holds the filters beyond the 2–4 inline ones, opened by ONE 'More filters' Button inside the bar (pattern `list-filters`, rule #51). Not a ScrollArea: a filter strip never scrolls sideways."
|
|
8660
8663
|
],
|
|
8661
8664
|
"rules": [
|
|
8662
8665
|
23,
|
|
8663
8666
|
44,
|
|
8664
8667
|
45,
|
|
8665
|
-
46
|
|
8668
|
+
46,
|
|
8669
|
+
51
|
|
8666
8670
|
],
|
|
8667
8671
|
"storyPath": "navigation/toolbar.tsx",
|
|
8668
8672
|
"subParts": [
|
|
@@ -8670,14 +8674,14 @@
|
|
|
8670
8674
|
"FilterBar",
|
|
8671
8675
|
"FilterBarGroup"
|
|
8672
8676
|
],
|
|
8673
|
-
"tagline": "List-page filter strip — SearchInput + labelled ToolbarGroup filter slots (or the typed `search`/`filters`/`chips` model) + a clear-all affordance, optionally sticky. `FilterBar`/`FilterBarGroup` are the same components under their alias names.",
|
|
8677
|
+
"tagline": "List-page filter strip — SearchInput + labelled ToolbarGroup filter slots (or the typed `search`/`filters`/`chips` model) + a clear-all affordance, optionally sticky. It WRAPS and never scrolls sideways; many filters → 2–4 inline + ONE 'More filters' Sheet (rule #51, pattern `list-filters`). `FilterBar`/`FilterBarGroup` are the same components under their alias names.",
|
|
8674
8678
|
"usage": [
|
|
8675
8679
|
"DO prefer the typed model on a canonical list page — `search`/`filters`/`chips`/`onChipRemove`/`actions`/`resultCount` (+ `loading`/`disabled`/`error`) make the bar own layout, widths, chip lifecycle and keyboard order through --filter-bar-* tokens while ALL state stays consumer data. Children composition remains fully supported and unchanged when no model prop is passed.",
|
|
8676
8680
|
"DO place Toolbar ABOVE the table Card, never inside a `CardContent flush`. Put SearchInput as a direct child (it self-labels) and wrap every other control in a ToolbarGroup with a `label`.",
|
|
8677
8681
|
"DO drive the clear-all button with `hasActiveFilters` + `onClear` — it only renders when both are truthy. The strip collapses to a single stacked column below 640px automatically.",
|
|
8678
8682
|
"DO set `sticky` for long list pages so the filters stay reachable while scrolling; if a topbar sits above the list, raise `--filter-bar-sticky-offset` so the strip parks below it.",
|
|
8679
8683
|
"DO pass `controlId` on a ToolbarGroup that wraps ONE control and give that control the same `id` — the visible caption then becomes the control's real `<label htmlFor>` (WCAG 2.5.3 label-in-name). Without it the caption only names the group wrapper and the control itself is NAMELESS (axe: select-name / button-name), so you must fall back to an `aria-label` that repeats the caption.",
|
|
8680
|
-
"DO let the strip WRAP —
|
|
8684
|
+
"DO let the strip WRAP — cardinal rule #51 (`get_rule 51`). It stacks below 640px and wraps onto extra rows above; it never scrolls sideways, and neither may anything you wrap it in (`ScrollArea orientation=\"horizontal\"`, an `overflow-x-auto` box): the `overflow` prop and its `scroll` strategy were removed in 33.0.0 because a sideways strip hid filters off-screen and clipped the first label. When the filters are many, keep the 2–4 most-used ones inline and put the rest behind ONE 'More filters' Button (`count` = hidden filters applied) that opens a Sheet holding them as FilterBarGroup + control; every applied filter, inline or hidden, is a chip in `chips` (the chips row wraps too). Copy-paste composition: pattern `list-filters` (`get_pattern list-filters`); there is no dedicated component. Never re-implement the geometry in the page: no page-local flex/grid/width/overflow rules on a filter strip.",
|
|
8681
8685
|
"DON'T build active-filter chips by nesting a Button inside a Badge (invalid markup + broken focus). Render each chip as a Badge label with a SIBLING icon Button (`aria-label` = 'clear <filter>'); a ghost `size='sm'` Button clears all.",
|
|
8682
8686
|
"DON'T hand-roll a debounced search box or a raw `<select>` — compose SearchInput and Select. Toolbar is layout + clear-all only; the controls own their own state and a11y.",
|
|
8683
8687
|
"Prefer the typed model for a canonical list page: pass `search` + `filters` + `chips` + `resultCount` and the bar owns the layout, widths, chip lifecycle, keyboard order (search → filters → children → reset → actions → chip removes) and responsive stacking through --filter-bar-* tokens. Everything stays consumer data — the bar renders state, never owns it.",
|
|
@@ -11124,6 +11128,7 @@
|
|
|
11124
11128
|
"DO reach for ScrollArea rather than your own `overflow-auto` div: it is the same native scrolling, plus the token-styled scrollbar, the keyboard tab stop the axe rule wants, and bottom anchoring. A bare overflow div gets none of that and each call site re-decides the scrollbar's look.",
|
|
11125
11129
|
"DON'T put ScrollArea inside a flex parent without giving it a `flex-1` or fixed size — it will collapse to zero height and appear broken.",
|
|
11126
11130
|
"For horizontal-only scrolling use `orientation=\"horizontal\"` with a width constraint; the content is allowed to grow past the box on that axis, so a row of non-shrinking columns overflows instead of squashing.",
|
|
11131
|
+
"DON'T use a horizontal ScrollArea for a strip of filters, search fields, form controls or filter chips — a filter strip WRAPS (cardinal rule #51; ui-audit `filter-strip-no-scroll`). Use FilterBar, and past 2–4 filters the `list-filters` pattern's 'More filters' Sheet.",
|
|
11127
11132
|
"DO use `viewportRef` (or plain `ref`) when you need the scrolling element, never a `querySelector` for an internal attribute — `[data-radix-scroll-area-viewport]` is gone with Radix, and any selector matches the wrong node the moment two ScrollAreas nest.",
|
|
11128
11133
|
"DO build a live stream (chat, log tail, streaming response, activity feed) with `anchor=\"bottom\"` instead of writing `viewport.scrollTop = viewport.scrollHeight` in an effect. That naive version is the BUG, not the feature: it yanks a reader who has scrolled up back to the bottom on every arriving message (WCAG 3.2.5).",
|
|
11129
11134
|
"DO pair `anchor=\"bottom\"` with `onAnchoredChange` and a real Button (\"jump to newest\"). A keyboard user who has scrolled up needs a focusable route back to new content — the anchor alone is a pointer affordance.",
|
|
@@ -12021,7 +12026,8 @@
|
|
|
12021
12026
|
],
|
|
12022
12027
|
"rules": [
|
|
12023
12028
|
3,
|
|
12024
|
-
13
|
|
12029
|
+
13,
|
|
12030
|
+
51
|
|
12025
12031
|
],
|
|
12026
12032
|
"storyPath": "data-entry/toggle-group.tsx",
|
|
12027
12033
|
"subParts": [
|
|
@@ -12037,6 +12043,7 @@
|
|
|
12037
12043
|
"DO build a tag-filter panel as `<ToggleGroup type=\"multiple\" variant=\"soft\" shape=\"pill\" size=\"xs\">` with one counted `ToggleGroupItem` per tag — that is the whole antd `Tag.CheckableTag` row, one tab stop per chip, no `Tag` component needed (gh#734).",
|
|
12038
12044
|
"DO set `size`/`variant` on an individual ToggleGroupItem only when that ONE item must differ from the group.",
|
|
12039
12045
|
"DO add `wrap` to a TAG FILTER ROW — `<ToggleGroup type=\"multiple\" variant=\"soft\" shape=\"pill\" size=\"xs\" wrap>` is the whole folder-tag panel however many tags the folder has. This is what replaces the hand-built `<ul>` of individual `<Toggle>`s a row wider than its rail used to force (gh#741): that list loses exactly what the group owns — the shared `variant`/`size`/`shape` context, ONE `value`/`onValueChange`, and the group's arrow-key traversal, which keeps walking the chips in DOM order across the wrapped lines.",
|
|
12046
|
+
"DON'T put a filter-chip row in a horizontal scroller (`overflow-x-auto`, `ScrollArea orientation=\"horizontal\"`) with `shrink-0 whitespace-nowrap` chips — at every width, phone included, a chip row that filters a list takes `wrap` (cardinal rule #51). A sideways chip row hides chips past the edge with no affordance, and this kit ships no primitive that guarantees the first chip unclipped and a visible scroll cue, so the scroller is a hand-roll.",
|
|
12040
12047
|
"DON'T set `wrap` on a 3–4 item segmented group. Measured at a 320px rail it is one 32px line either way, so the prop buys nothing and only adds a way for a toolbar to reflow.",
|
|
12041
12048
|
"DON'T pass size='default' — it is not a member of the `xs | sm | md | lg` union. Omit `size` for the md default.",
|
|
12042
12049
|
"DO give the group an accessible name (`aria-label`) — it renders a `group` of toggle buttons (single, the default), a `radiogroup` (single + `disallowEmptySelection`) or a `toolbar` of toggle buttons (multiple), and all three need a name."
|
package/agent/index.json
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"counts": {
|
|
3
3
|
"anti-ai-tells": 35,
|
|
4
4
|
"components": 163,
|
|
5
|
-
"patterns":
|
|
6
|
-
"rules":
|
|
5
|
+
"patterns": 25,
|
|
6
|
+
"rules": 44,
|
|
7
7
|
"tokens": 2228,
|
|
8
8
|
"vocabulary": 14
|
|
9
9
|
},
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"file": "components-index.json",
|
|
19
|
-
"note": "
|
|
19
|
+
"note": "49 KB — name + group + tagline for all 163. FETCH THIS FIRST, then fetch only the components you chose.",
|
|
20
20
|
"url": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
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/v33.
|
|
57
|
+
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v33.1.1/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",
|
|
@@ -68,5 +68,5 @@
|
|
|
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": "33.
|
|
71
|
+
"version": "33.1.1"
|
|
72
72
|
}
|
package/agent/llms.txt
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @godxjp/ui
|
|
2
2
|
|
|
3
3
|
> A Japanese-enterprise React design system: 163 components, 2228 design tokens,
|
|
4
|
-
>
|
|
4
|
+
> 44 cardinal rules. This file is the entry point for AI agents. Catalog version 33.1.1.
|
|
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@33.
|
|
7
|
+
(`npx @godxjp/ui-mcp@33.1.1`). It is searchable and version-locked. These files exist for agents
|
|
8
8
|
that can only fetch URLs.
|
|
9
9
|
|
|
10
10
|
## Start
|
|
@@ -15,19 +15,19 @@ that can only fetch URLs.
|
|
|
15
15
|
|
|
16
16
|
## Catalog
|
|
17
17
|
|
|
18
|
-
- [patterns-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/patterns-index.json):
|
|
19
|
-
- [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json):
|
|
18
|
+
- [patterns-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/patterns-index.json): 25 whole-task patterns (name, tagline, tags). Start here when the task is a TASK — "build a settings page" — then fetch `patterns/<name>.json` for complete code.
|
|
19
|
+
- [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 49 KB — all 163 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
|
|
20
20
|
- [components/<Name>.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
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` — 216 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 113 `semantic` roles, 1899 `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
|
-
- [rules.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json):
|
|
24
|
+
- [rules.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json): 44 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.
|
|
26
26
|
|
|
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/v33.
|
|
30
|
+
the tag: `.../godx-jp/godxjp-ui/v33.1.1/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.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"table-state",
|
|
5
5
|
"list-page"
|
|
6
6
|
],
|
|
7
|
-
"code": "import { useState } from \"react\";\nimport { DataState } from \"@godxjp/ui/query\";\nimport { Card, CardContent, DataTable, EmptyState, type ColumnDef } from \"@godxjp/ui/data-display\";\nimport { SkeletonTable } from \"@godxjp/ui/feedback\";\nimport { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\";\nimport { Toolbar, ToolbarGroup, Pagination } from \"@godxjp/ui/navigation\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { useQuery, keepPreviousData } from \"@tanstack/react-query\";\n\ntype Row = { id: string; name: string; status: string };\n\nexport function MembersTablePage({ orgId }: { orgId?: string }) {\n const [q, setQ] = useState(\"\");\n const [status, setStatus] = useState(\"all\");\n const [page, setPage] = useState(1);\n const pageSize = 20;\n\n // placeholderData: keepPreviousData → on page/filter change the PREVIOUS page stays visible\n // (a stale refresh) instead of flashing a skeleton. enabled gates on the prerequisite (orgId).\n const query = useQuery({\n queryKey: [\"members\", orgId, q, status, page],\n queryFn: () => fetchMembers({ orgId: orgId!, q, status, page, pageSize }),\n enabled: Boolean(orgId),\n placeholderData: keepPreviousData,\n });\n\n const columns: ColumnDef<Row>[] = [\n { key: \"name\", header: \"氏名\", render: (r) => <span className=\"font-medium\">{r.name}</span> },\n { key: \"status\", header: \"ステータス\" },\n ];\n\n return (\n <Flex direction=\"col\" gap=\"lg\">\n {/* Toolbar (filter bar) is STANDALONE and always visible so the user can change filters even\n while a query is idle/loading/empty. Changing a filter resets to page 1. */}\n <Toolbar hasActiveFilters={q !== \"\" || status !== \"all\"}\n onClear={() => { setQ(\"\"); setStatus(\"all\"); setPage(1); }}>\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=\"active\">有効</SelectItem>\n <SelectItem value=\"invited\">招待中</SelectItem>\n </SelectContent>\n </Select>\n </ToolbarGroup>\n </Toolbar>\n\n {/* The table surface owns exactly ONE card. Pagination lives INSIDE the success branch, so it\n is hidden during prerequisite / loading / empty / error and for single-page results. */}\n <DataState\n query={query}\n prerequisite={<EmptyState variant=\"section\" title=\"組織を選択してください\" />}\n skeleton={<Card><CardContent flush><SkeletonTable rows={8} columns={2} /></CardContent></Card>}\n empty={<EmptyState variant=\"section\" title=\"該当するメンバーがいません\" description=\"検索条件を変更してください。\" />}\n isEmpty={(d) => d.items.length === 0}\n >\n {(d) => {\n const totalPages = Math.ceil(d.total / pageSize);\n return (\n <>\n <Card><CardContent flush><DataTable data={d.items} columns={columns} getRowId={(r) => r.id} /></CardContent></Card>\n {totalPages > 1 && (\n <Pagination value={page} total={d.total} pageSize={pageSize} showTotal onValueChange={setPage} />\n )}\n </>\n );\n }}\n </DataState>\n </Flex>\n );\n}\n// Responsive: Pagination's showTotal range + controls stack at narrow widths automatically.\n// A transient retry keeps q/status/page (they live in component state, not the query) — the\n// same page reloads in place; the user never loses their filters.",
|
|
7
|
+
"code": "import { useState } from \"react\";\nimport { DataState } from \"@godxjp/ui/query\";\nimport { Card, CardContent, DataTable, EmptyState, type ColumnDef } from \"@godxjp/ui/data-display\";\nimport { SkeletonTable } from \"@godxjp/ui/feedback\";\nimport { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\";\nimport { Toolbar, ToolbarGroup, Pagination } from \"@godxjp/ui/navigation\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { useQuery, keepPreviousData } from \"@tanstack/react-query\";\n\ntype Row = { id: string; name: string; status: string };\n\nexport function MembersTablePage({ orgId }: { orgId?: string }) {\n const [q, setQ] = useState(\"\");\n const [status, setStatus] = useState(\"all\");\n const [page, setPage] = useState(1);\n const pageSize = 20;\n\n // placeholderData: keepPreviousData → on page/filter change the PREVIOUS page stays visible\n // (a stale refresh) instead of flashing a skeleton. enabled gates on the prerequisite (orgId).\n const query = useQuery({\n queryKey: [\"members\", orgId, q, status, page],\n queryFn: () => fetchMembers({ orgId: orgId!, q, status, page, pageSize }),\n enabled: Boolean(orgId),\n placeholderData: keepPreviousData,\n });\n\n const columns: ColumnDef<Row>[] = [\n { key: \"name\", header: \"氏名\", render: (r) => <span className=\"font-medium\">{r.name}</span> },\n { key: \"status\", header: \"ステータス\" },\n ];\n\n return (\n <Flex direction=\"col\" gap=\"lg\">\n {/* Toolbar (filter bar) is STANDALONE and always visible so the user can change filters even\n while a query is idle/loading/empty. Changing a filter resets to page 1. It WRAPS, never\n scrolls sideways (rule #51); more than 2–4 filters → the \"list-filters\" pattern. */}\n <Toolbar hasActiveFilters={q !== \"\" || status !== \"all\"}\n onClear={() => { setQ(\"\"); setStatus(\"all\"); setPage(1); }}>\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=\"active\">有効</SelectItem>\n <SelectItem value=\"invited\">招待中</SelectItem>\n </SelectContent>\n </Select>\n </ToolbarGroup>\n </Toolbar>\n\n {/* The table surface owns exactly ONE card. Pagination lives INSIDE the success branch, so it\n is hidden during prerequisite / loading / empty / error and for single-page results. */}\n <DataState\n query={query}\n prerequisite={<EmptyState variant=\"section\" title=\"組織を選択してください\" />}\n skeleton={<Card><CardContent flush><SkeletonTable rows={8} columns={2} /></CardContent></Card>}\n empty={<EmptyState variant=\"section\" title=\"該当するメンバーがいません\" description=\"検索条件を変更してください。\" />}\n isEmpty={(d) => d.items.length === 0}\n >\n {(d) => {\n const totalPages = Math.ceil(d.total / pageSize);\n return (\n <>\n <Card><CardContent flush><DataTable data={d.items} columns={columns} getRowId={(r) => r.id} /></CardContent></Card>\n {totalPages > 1 && (\n <Pagination value={page} total={d.total} pageSize={pageSize} showTotal onValueChange={setPage} />\n )}\n </>\n );\n }}\n </DataState>\n </Flex>\n );\n}\n// Responsive: Pagination's showTotal range + controls stack at narrow widths automatically.\n// A transient retry keeps q/status/page (they live in component state, not the query) — the\n// same page reloads in place; the user never loses their filters.",
|
|
8
8
|
"name": "data-table-page",
|
|
9
9
|
"tagline": "Filter (Toolbar) + table surface + bottom pagination driven by a query state machine — pagination shows ONLY for successful multi-page data; filters/page survive a transient retry.",
|
|
10
10
|
"tags": [
|
|
@@ -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 {/* 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",
|
|
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). It WRAPS and\n never scrolls sideways (rule #51); past 2–4 filters, see the \"list-filters\" pattern. */}\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": [
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"aliases": [
|
|
3
|
+
"more-filters",
|
|
4
|
+
"many-filters",
|
|
5
|
+
"filter-sheet",
|
|
6
|
+
"advanced-filters",
|
|
7
|
+
"add-filter"
|
|
8
|
+
],
|
|
9
|
+
"code": "// Research basis: Shopify Polaris Filters (\"include no more than 2 or 3 promoted filters\"; a\n// More button opens a sheet with the rest), Linear (one Filter menu), IBM Carbon filtering\n// (filters hidden in a drawer/menu show how many are applied), WCAG 2.2 SC 1.4.10 Reflow\n// (search fields and controls are not in the 2-D exception: they reflow, they do not scroll).\n//\n// RULE #51: a filter bar WRAPS — stacked below 640px, extra rows above — at every width.\n// ❌ FilterBar overflow=\"scroll\" (removed in 33.0.0), <ScrollArea orientation=\"horizontal\"> or an\n// overflow-x-auto box around filters: the strip runs off-screen and clips its first label.\n// ✅ the common filters inline + ONE \"More filters\" Sheet for the rest + wrapping chips.\nimport { useState } from \"react\";\nimport { SlidersHorizontal } from \"lucide-react\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\";\nimport {\n Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader,\n SheetTitle, SheetTrigger,\n} from \"@godxjp/ui/feedback\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { FilterBar, FilterBarGroup } from \"@godxjp/ui/navigation\";\n\ntype Option = { value: string; label: string };\ntype Filters = Record<\"q\" | \"status\" | \"owner\" | \"region\" | \"channel\" | \"period\", string>;\nexport const NO_FILTERS: Filters = { q: \"\", status: \"\", owner: \"\", region: \"\", channel: \"\", period: \"\" };\n\nconst STATUS: Option[] = [{ value: \"open\", label: \"Open\" }, { value: \"paid\", label: \"Paid\" }, { value: \"void\", label: \"Void\" }];\nconst OWNER: Option[] = [{ value: \"me\", label: \"Me\" }, { value: \"team\", label: \"My team\" }];\n// Everything people reach for less often. Order = the order of the sheet.\nconst MORE: { key: \"region\" | \"channel\" | \"period\"; label: string; options: Option[] }[] = [\n { key: \"region\", label: \"Region\", options: [{ value: \"kanto\", label: \"Kanto\" }, { value: \"kansai\", label: \"Kansai\" }] },\n { key: \"channel\", label: \"Sales channel\", options: [{ value: \"web\", label: \"Web\" }, { value: \"store\", label: \"Store\" }] },\n { key: \"period\", label: \"Period\", options: [{ value: \"month\", label: \"This month\" }, { value: \"quarter\", label: \"This quarter\" }] },\n];\nconst LABELS: Record<string, { label: string; options: Option[] }> = {\n status: { label: \"Status\", options: STATUS },\n owner: { label: \"Owner\", options: OWNER },\n ...Object.fromEntries(MORE.map((f) => [f.key, f])),\n};\n\nexport function OrderFilters({ value, onValueChange, resultCount }: {\n value: Filters; onValueChange: (next: Filters) => void; resultCount: number;\n}) {\n const [moreOpen, setMoreOpen] = useState(false);\n const set = (key: keyof Filters, v: string) => onValueChange({ ...value, [key]: v });\n\n // ONE chip per applied filter — inline AND hidden ones, so nothing applied is out of sight.\n const chips = Object.entries(LABELS)\n .filter(([key]) => value[key as keyof Filters] !== \"\")\n .map(([key, f]) => ({\n value: key,\n label: `${f.label}: ${f.options.find((o) => o.value === value[key as keyof Filters])?.label ?? \"\"}`,\n }));\n if (value.q) chips.unshift({ value: \"q\", label: `Search: ${value.q}` });\n const hiddenApplied = MORE.filter((f) => value[f.key] !== \"\").length;\n\n return (\n <FilterBar\n search={{ value: value.q, onValueChange: (v) => set(\"q\", v), placeholder: \"Search orders\", ariaLabel: \"Search orders\" }}\n // The 2–4 most-used filters, INLINE. Typed filters wire each caption as the real <label>.\n filters={[\n { value: \"status\", label: \"Status\", options: STATUS, selected: value.status, onSelectedChange: (v) => set(\"status\", v), placeholder: \"All\" },\n { value: \"owner\", label: \"Owner\", options: OWNER, selected: value.owner, onSelectedChange: (v) => set(\"owner\", v), placeholder: \"Anyone\" },\n ]}\n chips={chips}\n onChipRemove={(key) => set(key as keyof Filters, \"\")}\n hasActiveFilters={chips.length > 0}\n onClear={() => onValueChange(NO_FILTERS)}\n resultCount={resultCount}\n >\n {/* Children render after the typed filters, before the reset: ONE \"More filters\" control.\n count = hidden filters applied, so the closed sheet still says so. Button paints a 0 pill by\n default (showZero = true) — showZero={false} hides it when nothing hidden is applied (#1333). */}\n <Sheet open={moreOpen} onOpenChange={setMoreOpen}>\n <SheetTrigger asChild>\n <Button variant=\"outline\" count={hiddenApplied} showZero={false} countLabel=\"applied\">\n <SlidersHorizontal aria-hidden=\"true\" />\n More filters\n </Button>\n </SheetTrigger>\n <SheetContent side=\"right\">\n <SheetHeader>\n <SheetTitle>More filters</SheetTitle>\n <SheetDescription>Applied filters also show as chips above the list.</SheetDescription>\n </SheetHeader>\n <SheetBody>\n {/* A Sheet scrolls VERTICALLY; the fields stack. The same FilterBarGroup labels them. */}\n <Flex direction=\"col\" gap=\"md\">\n {MORE.map((f) => (\n <FilterBarGroup key={f.key} label={f.label} controlId={`more-${f.key}`}>\n <Select value={value[f.key]} onValueChange={(v: string) => set(f.key, v)}>\n <SelectTrigger id={`more-${f.key}`}><SelectValue placeholder=\"All\" /></SelectTrigger>\n <SelectContent>\n {f.options.map((o) => <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>)}\n </SelectContent>\n </Select>\n </FilterBarGroup>\n ))}\n </Flex>\n </SheetBody>\n <SheetFooter>\n <SheetClose asChild><Button>Show results</Button></SheetClose>\n </SheetFooter>\n </SheetContent>\n </Sheet>\n </FilterBar>\n );\n}\n// Phone width: the bar stacks (search, each filter, More filters, reset) and the chips wrap.\n// A row of filter CHIPS (ToggleGroup) takes `wrap` — it does not scroll sideways either.\n// Few filters (≤ 4)? Drop the Sheet: the plain bar in the data-table-page pattern is enough.",
|
|
10
|
+
"name": "list-filters",
|
|
11
|
+
"tagline": "A list page with MANY filters (cardinal rule #51) — FilterBar WRAPS; the 2–4 most-used filters stay inline, the rest sit behind ONE 'More filters' Button (count = hidden filters applied) that opens a Sheet; every applied filter is a removable chip. Never a sideways-scrolling strip.",
|
|
12
|
+
"tags": [
|
|
13
|
+
"filter",
|
|
14
|
+
"filters",
|
|
15
|
+
"list",
|
|
16
|
+
"toolbar",
|
|
17
|
+
"filter-bar",
|
|
18
|
+
"sheet",
|
|
19
|
+
"chips",
|
|
20
|
+
"search",
|
|
21
|
+
"responsive",
|
|
22
|
+
"datatable"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -256,6 +256,29 @@
|
|
|
256
256
|
"datatable"
|
|
257
257
|
]
|
|
258
258
|
},
|
|
259
|
+
{
|
|
260
|
+
"aliases": [
|
|
261
|
+
"more-filters",
|
|
262
|
+
"many-filters",
|
|
263
|
+
"filter-sheet",
|
|
264
|
+
"advanced-filters",
|
|
265
|
+
"add-filter"
|
|
266
|
+
],
|
|
267
|
+
"name": "list-filters",
|
|
268
|
+
"tagline": "A list page with MANY filters (cardinal rule #51) — FilterBar WRAPS; the 2–4 most-used filters stay inline, the rest sit behind ONE 'More filters' Button (count = hidden filters applied) that opens a Sheet; every applied filter is a removable chip. Never a sideways-scrolling strip.",
|
|
269
|
+
"tags": [
|
|
270
|
+
"filter",
|
|
271
|
+
"filters",
|
|
272
|
+
"list",
|
|
273
|
+
"toolbar",
|
|
274
|
+
"filter-bar",
|
|
275
|
+
"sheet",
|
|
276
|
+
"chips",
|
|
277
|
+
"search",
|
|
278
|
+
"responsive",
|
|
279
|
+
"datatable"
|
|
280
|
+
]
|
|
281
|
+
},
|
|
259
282
|
{
|
|
260
283
|
"aliases": [
|
|
261
284
|
"organization-switcher",
|
package/agent/patterns.json
CHANGED
|
@@ -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 {/* 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",
|
|
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). It WRAPS and\n never scrolls sideways (rule #51); past 2–4 filters, see the \"list-filters\" pattern. */}\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": [
|
|
@@ -260,7 +260,7 @@
|
|
|
260
260
|
"table-state",
|
|
261
261
|
"list-page"
|
|
262
262
|
],
|
|
263
|
-
"code": "import { useState } from \"react\";\nimport { DataState } from \"@godxjp/ui/query\";\nimport { Card, CardContent, DataTable, EmptyState, type ColumnDef } from \"@godxjp/ui/data-display\";\nimport { SkeletonTable } from \"@godxjp/ui/feedback\";\nimport { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\";\nimport { Toolbar, ToolbarGroup, Pagination } from \"@godxjp/ui/navigation\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { useQuery, keepPreviousData } from \"@tanstack/react-query\";\n\ntype Row = { id: string; name: string; status: string };\n\nexport function MembersTablePage({ orgId }: { orgId?: string }) {\n const [q, setQ] = useState(\"\");\n const [status, setStatus] = useState(\"all\");\n const [page, setPage] = useState(1);\n const pageSize = 20;\n\n // placeholderData: keepPreviousData → on page/filter change the PREVIOUS page stays visible\n // (a stale refresh) instead of flashing a skeleton. enabled gates on the prerequisite (orgId).\n const query = useQuery({\n queryKey: [\"members\", orgId, q, status, page],\n queryFn: () => fetchMembers({ orgId: orgId!, q, status, page, pageSize }),\n enabled: Boolean(orgId),\n placeholderData: keepPreviousData,\n });\n\n const columns: ColumnDef<Row>[] = [\n { key: \"name\", header: \"氏名\", render: (r) => <span className=\"font-medium\">{r.name}</span> },\n { key: \"status\", header: \"ステータス\" },\n ];\n\n return (\n <Flex direction=\"col\" gap=\"lg\">\n {/* Toolbar (filter bar) is STANDALONE and always visible so the user can change filters even\n while a query is idle/loading/empty. Changing a filter resets to page 1. */}\n <Toolbar hasActiveFilters={q !== \"\" || status !== \"all\"}\n onClear={() => { setQ(\"\"); setStatus(\"all\"); setPage(1); }}>\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=\"active\">有効</SelectItem>\n <SelectItem value=\"invited\">招待中</SelectItem>\n </SelectContent>\n </Select>\n </ToolbarGroup>\n </Toolbar>\n\n {/* The table surface owns exactly ONE card. Pagination lives INSIDE the success branch, so it\n is hidden during prerequisite / loading / empty / error and for single-page results. */}\n <DataState\n query={query}\n prerequisite={<EmptyState variant=\"section\" title=\"組織を選択してください\" />}\n skeleton={<Card><CardContent flush><SkeletonTable rows={8} columns={2} /></CardContent></Card>}\n empty={<EmptyState variant=\"section\" title=\"該当するメンバーがいません\" description=\"検索条件を変更してください。\" />}\n isEmpty={(d) => d.items.length === 0}\n >\n {(d) => {\n const totalPages = Math.ceil(d.total / pageSize);\n return (\n <>\n <Card><CardContent flush><DataTable data={d.items} columns={columns} getRowId={(r) => r.id} /></CardContent></Card>\n {totalPages > 1 && (\n <Pagination value={page} total={d.total} pageSize={pageSize} showTotal onValueChange={setPage} />\n )}\n </>\n );\n }}\n </DataState>\n </Flex>\n );\n}\n// Responsive: Pagination's showTotal range + controls stack at narrow widths automatically.\n// A transient retry keeps q/status/page (they live in component state, not the query) — the\n// same page reloads in place; the user never loses their filters.",
|
|
263
|
+
"code": "import { useState } from \"react\";\nimport { DataState } from \"@godxjp/ui/query\";\nimport { Card, CardContent, DataTable, EmptyState, type ColumnDef } from \"@godxjp/ui/data-display\";\nimport { SkeletonTable } from \"@godxjp/ui/feedback\";\nimport { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\";\nimport { Toolbar, ToolbarGroup, Pagination } from \"@godxjp/ui/navigation\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { useQuery, keepPreviousData } from \"@tanstack/react-query\";\n\ntype Row = { id: string; name: string; status: string };\n\nexport function MembersTablePage({ orgId }: { orgId?: string }) {\n const [q, setQ] = useState(\"\");\n const [status, setStatus] = useState(\"all\");\n const [page, setPage] = useState(1);\n const pageSize = 20;\n\n // placeholderData: keepPreviousData → on page/filter change the PREVIOUS page stays visible\n // (a stale refresh) instead of flashing a skeleton. enabled gates on the prerequisite (orgId).\n const query = useQuery({\n queryKey: [\"members\", orgId, q, status, page],\n queryFn: () => fetchMembers({ orgId: orgId!, q, status, page, pageSize }),\n enabled: Boolean(orgId),\n placeholderData: keepPreviousData,\n });\n\n const columns: ColumnDef<Row>[] = [\n { key: \"name\", header: \"氏名\", render: (r) => <span className=\"font-medium\">{r.name}</span> },\n { key: \"status\", header: \"ステータス\" },\n ];\n\n return (\n <Flex direction=\"col\" gap=\"lg\">\n {/* Toolbar (filter bar) is STANDALONE and always visible so the user can change filters even\n while a query is idle/loading/empty. Changing a filter resets to page 1. It WRAPS, never\n scrolls sideways (rule #51); more than 2–4 filters → the \"list-filters\" pattern. */}\n <Toolbar hasActiveFilters={q !== \"\" || status !== \"all\"}\n onClear={() => { setQ(\"\"); setStatus(\"all\"); setPage(1); }}>\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=\"active\">有効</SelectItem>\n <SelectItem value=\"invited\">招待中</SelectItem>\n </SelectContent>\n </Select>\n </ToolbarGroup>\n </Toolbar>\n\n {/* The table surface owns exactly ONE card. Pagination lives INSIDE the success branch, so it\n is hidden during prerequisite / loading / empty / error and for single-page results. */}\n <DataState\n query={query}\n prerequisite={<EmptyState variant=\"section\" title=\"組織を選択してください\" />}\n skeleton={<Card><CardContent flush><SkeletonTable rows={8} columns={2} /></CardContent></Card>}\n empty={<EmptyState variant=\"section\" title=\"該当するメンバーがいません\" description=\"検索条件を変更してください。\" />}\n isEmpty={(d) => d.items.length === 0}\n >\n {(d) => {\n const totalPages = Math.ceil(d.total / pageSize);\n return (\n <>\n <Card><CardContent flush><DataTable data={d.items} columns={columns} getRowId={(r) => r.id} /></CardContent></Card>\n {totalPages > 1 && (\n <Pagination value={page} total={d.total} pageSize={pageSize} showTotal onValueChange={setPage} />\n )}\n </>\n );\n }}\n </DataState>\n </Flex>\n );\n}\n// Responsive: Pagination's showTotal range + controls stack at narrow widths automatically.\n// A transient retry keeps q/status/page (they live in component state, not the query) — the\n// same page reloads in place; the user never loses their filters.",
|
|
264
264
|
"name": "data-table-page",
|
|
265
265
|
"tagline": "Filter (Toolbar) + table surface + bottom pagination driven by a query state machine — pagination shows ONLY for successful multi-page data; filters/page survive a transient retry.",
|
|
266
266
|
"tags": [
|
|
@@ -272,6 +272,30 @@
|
|
|
272
272
|
"datatable"
|
|
273
273
|
]
|
|
274
274
|
},
|
|
275
|
+
{
|
|
276
|
+
"aliases": [
|
|
277
|
+
"more-filters",
|
|
278
|
+
"many-filters",
|
|
279
|
+
"filter-sheet",
|
|
280
|
+
"advanced-filters",
|
|
281
|
+
"add-filter"
|
|
282
|
+
],
|
|
283
|
+
"code": "// Research basis: Shopify Polaris Filters (\"include no more than 2 or 3 promoted filters\"; a\n// More button opens a sheet with the rest), Linear (one Filter menu), IBM Carbon filtering\n// (filters hidden in a drawer/menu show how many are applied), WCAG 2.2 SC 1.4.10 Reflow\n// (search fields and controls are not in the 2-D exception: they reflow, they do not scroll).\n//\n// RULE #51: a filter bar WRAPS — stacked below 640px, extra rows above — at every width.\n// ❌ FilterBar overflow=\"scroll\" (removed in 33.0.0), <ScrollArea orientation=\"horizontal\"> or an\n// overflow-x-auto box around filters: the strip runs off-screen and clips its first label.\n// ✅ the common filters inline + ONE \"More filters\" Sheet for the rest + wrapping chips.\nimport { useState } from \"react\";\nimport { SlidersHorizontal } from \"lucide-react\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\";\nimport {\n Sheet, SheetBody, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader,\n SheetTitle, SheetTrigger,\n} from \"@godxjp/ui/feedback\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { FilterBar, FilterBarGroup } from \"@godxjp/ui/navigation\";\n\ntype Option = { value: string; label: string };\ntype Filters = Record<\"q\" | \"status\" | \"owner\" | \"region\" | \"channel\" | \"period\", string>;\nexport const NO_FILTERS: Filters = { q: \"\", status: \"\", owner: \"\", region: \"\", channel: \"\", period: \"\" };\n\nconst STATUS: Option[] = [{ value: \"open\", label: \"Open\" }, { value: \"paid\", label: \"Paid\" }, { value: \"void\", label: \"Void\" }];\nconst OWNER: Option[] = [{ value: \"me\", label: \"Me\" }, { value: \"team\", label: \"My team\" }];\n// Everything people reach for less often. Order = the order of the sheet.\nconst MORE: { key: \"region\" | \"channel\" | \"period\"; label: string; options: Option[] }[] = [\n { key: \"region\", label: \"Region\", options: [{ value: \"kanto\", label: \"Kanto\" }, { value: \"kansai\", label: \"Kansai\" }] },\n { key: \"channel\", label: \"Sales channel\", options: [{ value: \"web\", label: \"Web\" }, { value: \"store\", label: \"Store\" }] },\n { key: \"period\", label: \"Period\", options: [{ value: \"month\", label: \"This month\" }, { value: \"quarter\", label: \"This quarter\" }] },\n];\nconst LABELS: Record<string, { label: string; options: Option[] }> = {\n status: { label: \"Status\", options: STATUS },\n owner: { label: \"Owner\", options: OWNER },\n ...Object.fromEntries(MORE.map((f) => [f.key, f])),\n};\n\nexport function OrderFilters({ value, onValueChange, resultCount }: {\n value: Filters; onValueChange: (next: Filters) => void; resultCount: number;\n}) {\n const [moreOpen, setMoreOpen] = useState(false);\n const set = (key: keyof Filters, v: string) => onValueChange({ ...value, [key]: v });\n\n // ONE chip per applied filter — inline AND hidden ones, so nothing applied is out of sight.\n const chips = Object.entries(LABELS)\n .filter(([key]) => value[key as keyof Filters] !== \"\")\n .map(([key, f]) => ({\n value: key,\n label: `${f.label}: ${f.options.find((o) => o.value === value[key as keyof Filters])?.label ?? \"\"}`,\n }));\n if (value.q) chips.unshift({ value: \"q\", label: `Search: ${value.q}` });\n const hiddenApplied = MORE.filter((f) => value[f.key] !== \"\").length;\n\n return (\n <FilterBar\n search={{ value: value.q, onValueChange: (v) => set(\"q\", v), placeholder: \"Search orders\", ariaLabel: \"Search orders\" }}\n // The 2–4 most-used filters, INLINE. Typed filters wire each caption as the real <label>.\n filters={[\n { value: \"status\", label: \"Status\", options: STATUS, selected: value.status, onSelectedChange: (v) => set(\"status\", v), placeholder: \"All\" },\n { value: \"owner\", label: \"Owner\", options: OWNER, selected: value.owner, onSelectedChange: (v) => set(\"owner\", v), placeholder: \"Anyone\" },\n ]}\n chips={chips}\n onChipRemove={(key) => set(key as keyof Filters, \"\")}\n hasActiveFilters={chips.length > 0}\n onClear={() => onValueChange(NO_FILTERS)}\n resultCount={resultCount}\n >\n {/* Children render after the typed filters, before the reset: ONE \"More filters\" control.\n count = hidden filters applied, so the closed sheet still says so. Button paints a 0 pill by\n default (showZero = true) — showZero={false} hides it when nothing hidden is applied (#1333). */}\n <Sheet open={moreOpen} onOpenChange={setMoreOpen}>\n <SheetTrigger asChild>\n <Button variant=\"outline\" count={hiddenApplied} showZero={false} countLabel=\"applied\">\n <SlidersHorizontal aria-hidden=\"true\" />\n More filters\n </Button>\n </SheetTrigger>\n <SheetContent side=\"right\">\n <SheetHeader>\n <SheetTitle>More filters</SheetTitle>\n <SheetDescription>Applied filters also show as chips above the list.</SheetDescription>\n </SheetHeader>\n <SheetBody>\n {/* A Sheet scrolls VERTICALLY; the fields stack. The same FilterBarGroup labels them. */}\n <Flex direction=\"col\" gap=\"md\">\n {MORE.map((f) => (\n <FilterBarGroup key={f.key} label={f.label} controlId={`more-${f.key}`}>\n <Select value={value[f.key]} onValueChange={(v: string) => set(f.key, v)}>\n <SelectTrigger id={`more-${f.key}`}><SelectValue placeholder=\"All\" /></SelectTrigger>\n <SelectContent>\n {f.options.map((o) => <SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>)}\n </SelectContent>\n </Select>\n </FilterBarGroup>\n ))}\n </Flex>\n </SheetBody>\n <SheetFooter>\n <SheetClose asChild><Button>Show results</Button></SheetClose>\n </SheetFooter>\n </SheetContent>\n </Sheet>\n </FilterBar>\n );\n}\n// Phone width: the bar stacks (search, each filter, More filters, reset) and the chips wrap.\n// A row of filter CHIPS (ToggleGroup) takes `wrap` — it does not scroll sideways either.\n// Few filters (≤ 4)? Drop the Sheet: the plain bar in the data-table-page pattern is enough.",
|
|
284
|
+
"name": "list-filters",
|
|
285
|
+
"tagline": "A list page with MANY filters (cardinal rule #51) — FilterBar WRAPS; the 2–4 most-used filters stay inline, the rest sit behind ONE 'More filters' Button (count = hidden filters applied) that opens a Sheet; every applied filter is a removable chip. Never a sideways-scrolling strip.",
|
|
286
|
+
"tags": [
|
|
287
|
+
"filter",
|
|
288
|
+
"filters",
|
|
289
|
+
"list",
|
|
290
|
+
"toolbar",
|
|
291
|
+
"filter-bar",
|
|
292
|
+
"sheet",
|
|
293
|
+
"chips",
|
|
294
|
+
"search",
|
|
295
|
+
"responsive",
|
|
296
|
+
"datatable"
|
|
297
|
+
]
|
|
298
|
+
},
|
|
275
299
|
{
|
|
276
300
|
"aliases": [
|
|
277
301
|
"organization-switcher",
|
package/agent/rules.json
CHANGED
|
@@ -213,5 +213,10 @@
|
|
|
213
213
|
"body": "`src/styles/base.css` sets `color: hsl(var(--foreground))` on `body`, which is above every scope a consumer can make, so that `var()` substitutes against the ROOT's `--foreground` exactly once and every element below inherits the already-resolved colour. A plain `<div>` carrying a theme's tokens therefore retints every SURFACE and no TEXT: measured, a title whose own computed `--foreground` was the theme's near-white still painted `rgb(36, 35, 30)`, 1.34:1 on real pixels, with every element reporting it was inside the scope (gh#881). It is the §3 freeze rule applied to a PROPERTY instead of a token, and worse there — a token can be given a knob and `color` on `body` cannot. FIX, React: wrap the region in `ThemeScope` (`@godxjp/ui/app`), which re-states `color` on its own element (`src/lib/overlay-portal.tsx`, `display: contents` so the box goes and the inheritance stays) AND on the body-level host it creates for portalled overlays — the other half, since a Dialog portals to `document.body` and would otherwise wear the root's theme. FIX, stylesheet-only scope: declare `color: hsl(var(--foreground))` on your own selector alongside the tokens; that is your element, not a selector into this package, so cardinal rule \"a consumer sets tokens, never selectors\" permits it — but it does NOT reach portalled overlays. docs/TOKEN-RESOLUTION.md §5 rule 7.",
|
|
214
214
|
"number": 50,
|
|
215
215
|
"title": "A scoped theme must state its own `color`"
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
"body": "A list page's filters live in `FilterBar` (alias `Toolbar`), and the bar WRAPS: one stacked column below 640px, extra rows above. Nothing in it scrolls sideways at any width — no horizontally scrolling strip of filters, search fields or labelled controls, whether built with a prop, a `ScrollArea`, or an `overflow-x-*` box (WCAG 2.2 SC 1.4.10 Reflow: search fields and controls are not in the two-dimensional exception; a sideways strip hides filters off-screen and clips the first label). Keep the 2–4 most-used filters inline. Put every other filter behind ONE 'More filters' `Button` — its `count` = how many of those hidden filters are applied — that opens a `Sheet` holding them as `FilterBarGroup` + control. Every applied filter, inline or hidden, shows as a removable chip in the bar's `chips` row, and that row wraps too. A row of filter chips (`ToggleGroup`) takes `wrap`; it does not scroll either. Copy-paste composition: pattern `list-filters`. Enforced by ui-audit `filter-bar-no-overflow` and `filter-strip-no-scroll`. Prior art: Shopify Polaris Filters (2–3 promoted filters, a More button opening a sheet), Linear (one Filter menu), IBM Carbon (hidden filters show an applied count).",
|
|
219
|
+
"number": 51,
|
|
220
|
+
"title": "A filter bar wraps — many filters go behind ONE 'More filters' control"
|
|
216
221
|
}
|
|
217
222
|
]
|
|
@@ -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": "33.
|
|
3
|
+
"version": "33.1.1",
|
|
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,
|
package/dist/styles/layers.json
CHANGED
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`.
|
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
|
@@ -925,7 +925,7 @@ const RULES = [
|
|
|
925
925
|
standard:
|
|
926
926
|
"WCAG 2.2 SC 2.1.1 (Keyboard) · WAI-ARIA 1.2 (group) · Deque axe-core scrollable-region-focusable",
|
|
927
927
|
message:
|
|
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.',
|
|
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.',
|
|
929
929
|
},
|
|
930
930
|
{
|
|
931
931
|
id: "no-hand-rolled-break-anywhere",
|
|
@@ -2512,6 +2512,53 @@ for (const dir of SCAN_DIRS) {
|
|
|
2512
2512
|
}
|
|
2513
2513
|
}
|
|
2514
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
|
+
|
|
2515
2562
|
for (const m of scanContent.matchAll(/<(Dialog\.Body|DialogBody)(?=[\s>])[\s\S]*?<\/\1>/g)) {
|
|
2516
2563
|
const count = (m[0].match(/<FormField(?=[\s>/])/g) ?? []).length;
|
|
2517
2564
|
if (count < 3) continue;
|