@godxjp/ui 32.5.0 → 32.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +201 -0
- package/README.md +1 -0
- package/agent/DESIGN.md +201 -0
- package/agent/START-HERE.md +22 -26
- package/agent/anti-ai-tells.json +56 -2
- package/agent/index.json +9 -3
- package/agent/llms.txt +5 -4
- package/agent/patterns/badge-coloring.json +2 -2
- package/agent/patterns/common-fixes.json +1 -1
- package/agent/patterns/confirm-destructive.json +2 -2
- package/agent/patterns/inertia-detail-page.json +2 -2
- package/agent/patterns/inertia-list-page.json +2 -2
- package/agent/patterns/page-sections.json +5 -3
- package/agent/patterns/settings-section-rows.json +2 -2
- package/agent/patterns-index.json +9 -7
- package/agent/patterns.json +16 -14
- package/dist/components/data-entry/select.js +5 -4
- package/dist/contracts/measurement.json +1 -1
- package/dist/styles/layers.json +1 -1
- package/dist/styles.css +1 -1
- package/docs/CONSUMER-RULES.md +4 -3
- package/docs/README.md +1 -1
- package/package.json +3 -2
- package/scripts/_agent-setup.mjs +49 -76
- package/scripts/consumer-rule.md +32 -68
- package/scripts/ui-audit.mjs +152 -22
package/docs/CONSUMER-RULES.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
# @godxjp/ui — the
|
|
1
|
+
# @godxjp/ui — the consumer rules
|
|
2
2
|
|
|
3
|
-
Read this once; the audit enforces it.
|
|
3
|
+
Read this once; the audit enforces it. What the audit cannot see (how a screen is composed, what it
|
|
4
|
+
should look like) is in the package's `DESIGN.md`. Everything else in `docs/` is for contributors.
|
|
4
5
|
|
|
5
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`.
|
|
6
|
-
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 `vi` and pickers to UTC — measured in a Japanese app as Vietnamese buttons, silently (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
|
|
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 `vi` and pickers to UTC — measured in a Japanese app as Vietnamese buttons, silently (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>`.
|
|
7
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
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`.
|
|
9
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 `<SpaceCompact>` 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).
|
package/docs/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Purpose |
|
|
4
4
|
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
5
|
-
| [CONSUMER-RULES.md](./CONSUMER-RULES.md) | **Start here if you build an app with @godxjp/ui** — the
|
|
5
|
+
| [CONSUMER-RULES.md](./CONSUMER-RULES.md) | **Start here if you build an app with @godxjp/ui** — the rules the audit enforces (styles entry, layout owners, real primitives, tokens) |
|
|
6
6
|
| [DEVELOPMENT.md](./DEVELOPMENT.md) | **Contributor guideline** — role/boundary, architecture, how to add/extend, verify |
|
|
7
7
|
| [COMPONENTS.md](./COMPONENTS.md) | Component catalog by taxonomy group |
|
|
8
8
|
| [PROPS-VOCABULARY.md](./PROPS-VOCABULARY.md) | Atomic prop types (`*Prop` suffix) |
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/ui",
|
|
3
|
-
"version": "32.
|
|
4
|
-
"godxUiMcp": "32.
|
|
3
|
+
"version": "32.6.0",
|
|
4
|
+
"godxUiMcp": "32.6.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"description": "React UI framework built on design tokens: accessible components, a controlled prop vocabulary, and a machine-readable catalog for coding agents.",
|
|
29
29
|
"files": [
|
|
30
30
|
"LICENSE",
|
|
31
|
+
"DESIGN.md",
|
|
31
32
|
"agent",
|
|
32
33
|
"dist",
|
|
33
34
|
"scripts/ui-audit.mjs",
|
package/scripts/_agent-setup.mjs
CHANGED
|
@@ -167,87 +167,60 @@ function resyncVersionStamp(text) {
|
|
|
167
167
|
* appended repo-specific sections keeps them.
|
|
168
168
|
*/
|
|
169
169
|
export const WORKFLOW_MD = `${STAMP(KIT_VERSION)}
|
|
170
|
-
# @godxjp/ui —
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
170
|
+
# @godxjp/ui — workflow (read every session)
|
|
171
|
+
|
|
172
|
+
Follow this every time you create or change a page, component or form.
|
|
173
|
+
|
|
174
|
+
## Before JSX
|
|
175
|
+
1. **Design target.** Read \`node_modules/@godxjp/ui/DESIGN.md\` (MCP \`get_design\`): tokens,
|
|
176
|
+
scarcity rules, screen anatomy, do/don't.
|
|
177
|
+
2. **Route.** \`route_consumer_task "<task>"\` → \`compose-a-screen\` for app screens. The
|
|
178
|
+
marketing skills (soft, minimalist, brutalist, gpt-tasteskill, imagegen-*) are for landing
|
|
179
|
+
pages only.
|
|
180
|
+
3. **Exemplar.** \`get_exemplar\` with archetype list | detail | form | settings | dashboard |
|
|
181
|
+
approval. Match its structure; it is the visual target.
|
|
182
|
+
4. **Discover, then look up.** \`search_components "<area>"\` first, \`get_component <Name>\`
|
|
183
|
+
second — \`get_component\` cannot find a name you do not already have (a consumer hand-rolled
|
|
184
|
+
the shipped \`OrgSwitcher\` and \`TopbarItem\` this way). Code already in this repo is not the
|
|
185
|
+
authority: search even when a neighbouring file seems to answer. Never guess a prop.
|
|
186
|
+
|
|
187
|
+
## Contract (ui-audit enforces it; full text in node_modules/@godxjp/ui/docs/CONSUMER-RULES.md)
|
|
188
|
+
- Styles: \`@import "@godxjp/ui/styles"\` (or \`import "@godxjp/ui/styles.css"\`); fonts are opt-in
|
|
189
|
+
via \`@godxjp/ui/styles/fonts.css\`. Never cherry-pick layers.
|
|
190
|
+
- Layout: \`PageContainer\` spaces sections; siblings go in \`<Flex direction="col" gap>\` /
|
|
178
191
|
\`<ResponsiveGrid>\`. No \`flex\` / \`grid\` / \`gap-*\` / \`p-*\` / \`m-*\` on your own elements.
|
|
179
|
-
- Surfaces: \`Card\` / \`Badge\` / \`Avatar\` / \`ListRow\` / \`Descriptions\` / \`EmptyState
|
|
180
|
-
\`rounded border\` div. Text is \`<Text tone size weight
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
is the most common way this rule gets skipped — measured: of four fresh agents given a vague UI
|
|
193
|
-
task, all four answered correctly but two never called the MCP at all, saying the answer was
|
|
194
|
-
already in the code. They were only right because that code had just been fixed; a week earlier
|
|
195
|
-
the same files hand-rolled a switcher and put Buttons in topbar slots, and both agents would
|
|
196
|
-
have copied it. Search the catalog even when a local file looks like it already answers you.
|
|
197
|
-
Also \`list_audit_rules\`, \`list_visual_checks\`. Never guess a prop; never hand-roll what a
|
|
198
|
-
primitive already does.
|
|
199
|
-
2. **Real primitives only.** No raw \`<input>/<select>/<button>/<textarea>/<table>\`, no
|
|
200
|
-
styled-div fakes. A labelled control ALWAYS goes in \`<FormField label=…>\` (it owns the
|
|
201
|
-
label↔control wiring, error/aria, AND the field rhythm). Never pair a bare \`<Label>\`
|
|
202
|
-
with an \`<Input>\`.
|
|
203
|
-
3. **Tokens, not literals.** Semantic colour tokens (bg-primary, text-muted-foreground),
|
|
204
|
-
token spacing/size/radius/type — no raw palette (bg-blue-600), no hex, no arbitrary
|
|
205
|
-
\`[13px]\`. No emoji in product UI. Logical CSS (ms-/me-/ps-/pe-) for RTL.
|
|
206
|
-
|
|
207
|
-
## After writing UI
|
|
208
|
-
4. **Audit (runs automatically too).** \`node node_modules/@godxjp/ui/scripts/ui-audit.mjs\`
|
|
209
|
-
— fix every finding. A PostToolUse hook also runs it on each edit and feeds findings back.
|
|
210
|
-
5. **Before a visual review**, run the runtime audit against the running app:
|
|
211
|
-
\`node node_modules/@godxjp/ui/scripts/visual-audit.mjs <url>\` (axe + contrast + layout).
|
|
192
|
+
- Surfaces: \`Card\` / \`Badge\` / \`Avatar\` / \`ListRow\` / \`Descriptions\` / \`EmptyState\`, never a
|
|
193
|
+
\`rounded border\` div. Text is \`<Text tone size weight>\`.
|
|
194
|
+
- Real controls only; a labelled control sits in \`<FormField label>\` inside a \`<Form>\`.
|
|
195
|
+
- Semantic tokens only: no hex, palette names, arbitrary \`[13px]\`, emoji. Logical CSS (ms-/me-).
|
|
196
|
+
- Screen composition (what the audit cannot see) is in \`.ai/rules/godxjp-ui.md\`.
|
|
197
|
+
|
|
198
|
+
## After JSX
|
|
199
|
+
5. \`node node_modules/@godxjp/ui/scripts/ui-audit.mjs <files>\` — zero errors (a PostToolUse hook
|
|
200
|
+
also runs it on each edit).
|
|
201
|
+
6. Screenshot at 390 / 768 / 1280 next to the exemplar, list the top 3 differences, fix them,
|
|
202
|
+
repeat — at most 3 rounds.
|
|
203
|
+
7. Before a visual review: \`node node_modules/@godxjp/ui/scripts/visual-audit.mjs <url>\`
|
|
204
|
+
(axe + contrast + layout).
|
|
212
205
|
`;
|
|
213
206
|
|
|
214
207
|
/** Delimited block appended to the consumer's CLAUDE.md — loaded into the agent's context
|
|
215
|
-
* every turn
|
|
216
|
-
|
|
208
|
+
* every turn, so it carries only the default path and points at the workflow for the rest.
|
|
209
|
+
* Markers keep it idempotent. */
|
|
210
|
+
export const CLAUDE_MD_BLOCK = `<!-- godxjp-ui:start (managed by @godxjp/ui — rewritten by npx godxjp-ui sync-rules) -->
|
|
217
211
|
${STAMP(KIT_VERSION)}
|
|
218
|
-
## @godxjp/ui —
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
2. \`get_component <Name>\` second, once you know the name.
|
|
231
|
-
\`get_component\` answers "how do I use X" and is USELESS for finding X — you can only look up a
|
|
232
|
-
name you already have. Skipping step 1 is how a consumer hand-rolled an organization switcher
|
|
233
|
-
the package ships (\`OrgSwitcher\`) and built three topbar cells out of Buttons instead of
|
|
234
|
-
\`TopbarItem\`: both were one \`search_components\` call away, and neither was ever searched for.
|
|
235
|
-
**Existing code in this repo is NOT the authority.** Reading a neighbouring file and copying it
|
|
236
|
-
is the most common way this rule gets skipped — measured: of four fresh agents given a vague UI
|
|
237
|
-
task, all four answered correctly but two never called the MCP at all, saying the answer was
|
|
238
|
-
already in the code. They were only right because that code had just been fixed; a week earlier
|
|
239
|
-
the same files hand-rolled a switcher and put Buttons in topbar slots, and both agents would
|
|
240
|
-
have copied it. Search the catalog even when a local file looks like it already answers you.
|
|
241
|
-
Also \`list_audit_rules\`, \`list_visual_checks\`. Never guess a prop; never hand-roll what a
|
|
242
|
-
primitive already does.
|
|
243
|
-
2. **Real primitives only.** No raw \`<input>/<select>/<button>/<textarea>/<table>\`. A labelled
|
|
244
|
-
control ALWAYS goes in \`<FormField label=…>\` — never a bare \`<Label>\`+\`<Input>\`. Semantic
|
|
245
|
-
tokens, not raw palette/hex/arbitrary values. No emoji in product UI. Logical CSS for RTL.
|
|
246
|
-
3. **Scan after writing.** \`node node_modules/@godxjp/ui/scripts/ui-audit.mjs <files>\` and fix
|
|
247
|
-
every finding (a PostToolUse hook runs this automatically and feeds findings back). Before a
|
|
248
|
-
visual review: \`node node_modules/@godxjp/ui/scripts/visual-audit.mjs <url>\` (axe + layout).
|
|
249
|
-
|
|
250
|
-
Full guide: \`.claude/godxjp-ui-workflow.md\`.
|
|
212
|
+
## @godxjp/ui — UI workflow
|
|
213
|
+
|
|
214
|
+
Before any JSX for a screen:
|
|
215
|
+
1. Read \`node_modules/@godxjp/ui/DESIGN.md\` (MCP \`get_design\`).
|
|
216
|
+
2. \`route_consumer_task\` → \`compose-a-screen\`; \`search_components\` before hand-rolling anything.
|
|
217
|
+
3. \`get_exemplar\` (list | detail | form | settings | dashboard | approval) — match its structure.
|
|
218
|
+
|
|
219
|
+
After building:
|
|
220
|
+
4. \`node node_modules/@godxjp/ui/scripts/ui-audit.mjs <files>\` — zero errors.
|
|
221
|
+
5. Screenshot at 390 / 768 / 1280 beside the exemplar; fix the top 3 differences; max 3 rounds.
|
|
222
|
+
|
|
223
|
+
Rules: \`node_modules/@godxjp/ui/docs/CONSUMER-RULES.md\`. Details: \`.claude/godxjp-ui-workflow.md\`.
|
|
251
224
|
<!-- godxjp-ui:end -->
|
|
252
225
|
`;
|
|
253
226
|
|
package/scripts/consumer-rule.md
CHANGED
|
@@ -1,39 +1,33 @@
|
|
|
1
1
|
# @godxjp/ui
|
|
2
2
|
|
|
3
|
-
> **Tệp
|
|
4
|
-
>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
> trở đi là của kho và được giữ lại.)
|
|
10
|
-
|
|
11
|
-
Đây là **danh sách kiểm** bắn mỗi lần chạm một tệp UI. Lý do đầy đủ nằm ở
|
|
12
|
-
`docs/CONSUMER-RULES.md` (10 luật) và, với kho chuột bạch, ở
|
|
13
|
-
`.claude/skills/godx-ui-guinea-pig/SKILL.md`.
|
|
14
|
-
|
|
15
|
-
## Loading styles: one entry, fonts are opt-in
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
import "@godxjp/ui/styles.css"; // precompiled, no Tailwind needed
|
|
19
|
-
```
|
|
3
|
+
> **Tệp do gói `@godxjp/ui` sở hữu**, ghi lại mỗi lần chạy `npx godxjp-ui sync-rules`. Đừng sửa ở
|
|
4
|
+
> đây — luật riêng của kho thuộc về một tệp khác trong `.ai/rules/`.
|
|
5
|
+
|
|
6
|
+
Danh sách kiểm bắn mỗi lần chạm một tệp UI. Luật đầy đủ: `docs/CONSUMER-RULES.md`.
|
|
7
|
+
|
|
8
|
+
## Styles và theme
|
|
20
9
|
|
|
21
10
|
```css
|
|
22
|
-
@import "@godxjp/ui/styles"; /*
|
|
23
|
-
@import "@godxjp/ui/styles/fonts.css"; /* optional, after the styles import: the bundled faces */
|
|
11
|
+
@import "@godxjp/ui/styles"; /* or: import "@godxjp/ui/styles.css" — no Tailwind needed */
|
|
24
12
|
```
|
|
25
13
|
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
Fonts are opt-in (`@godxjp/ui/styles/fonts.css`, after the styles import). Never cherry-pick
|
|
15
|
+
`*-layout.css` layers. The default `--primary` is a neutral placeholder: set `--primary` once in
|
|
16
|
+
your theme, or import a preset (`@godxjp/ui/themes/godx.css`). Details: `docs/CUSTOMER-THEMING.md`.
|
|
28
17
|
|
|
29
|
-
|
|
30
|
-
pnpm add @fontsource/noto-sans-jp @fontsource/m-plus-2
|
|
31
|
-
```
|
|
18
|
+
## Screen composition (ui-audit không thấy những lỗi này)
|
|
32
19
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
20
|
+
- Most sections are a `Heading` + content, NOT a `Card`. A Card is for a genuinely separate
|
|
21
|
+
surface (a table, a form block), never a frame around every section.
|
|
22
|
+
- No `subtitle` that restates the `title`. Leave it out unless it adds a fact.
|
|
23
|
+
- Filters sit ABOVE the table they filter, never after it.
|
|
24
|
+
- KPI tiles (`StatCard`) only on dashboards, 3–4 at most. A list page opens with its list.
|
|
25
|
+
- `EmptyState` = one sentence + one action. Inside a Card it has no icon.
|
|
26
|
+
- `Alert` only for something the user must act on now; not a page decoration.
|
|
27
|
+
- Tiers / categories / roles render in ONE neutral tone (`<Badge variant="outline">`); colour
|
|
28
|
+
is for status.
|
|
29
|
+
- Vary spacing by level: `xs` inside a group, `sm` within a row, `md` between items, `lg`
|
|
30
|
+
between blocks. One gap everywhere reads as generated.
|
|
37
31
|
|
|
38
32
|
## Bố cục chuẩn của platform: BA CỘT, và ba cột là BA PHẠM VI
|
|
39
33
|
|
|
@@ -85,11 +79,10 @@ ngày: năm thứ cần đều ĐÃ CÓ và vẫn bị dựng lại bằng thứ
|
|
|
85
79
|
| dải giữa hai vùng, tự kẻ theo VỊ TRÍ | `<CardBar>` (`border` để ép khi xếp chồng) |
|
|
86
80
|
| chip "điều kiện đang bật" có dấu × để bỏ | `<Badge onRemove>` — KHÔNG phải `TagInput` |
|
|
87
81
|
| dải tab nằm TRONG đầu Card | `<Card tabList activeTabKey onTabChange>` |
|
|
88
|
-
| tiêu đề / đoạn văn / liên kết trong văn bản | `<
|
|
82
|
+
| tiêu đề / đoạn văn / liên kết trong văn bản | `<Heading level>` · `<Text as="p">` · `<Link>` |
|
|
89
83
|
| nút nổi góc màn (quay lên đầu, hành động nhanh) | `<FloatButton>` |
|
|
90
84
|
| khung xương của một FORM khi đang tải | `<SkeletonForm>` |
|
|
91
85
|
| ô màu chỉ để xem, màu do NGƯỜI DÙNG chọn | `<Swatch>` |
|
|
92
|
-
| ảnh đại diện của một tệp / bản xem trước | `<Thumbnail>` |
|
|
93
86
|
| danh sách "có gì trong gói này" | `<FeatureList>` |
|
|
94
87
|
| panel kéo giãn được (chia đôi màn) | `<DraggablePanel>` |
|
|
95
88
|
| màn hội thoại AI | `<Welcome>` · `<Conversations>` · `<Attachments>` · `<ThoughtChain>` · `<Actions>` |
|
|
@@ -120,48 +113,19 @@ godx-mailer dính bốn lỗi form trong một ngày, cả bốn đều là kit
|
|
|
120
113
|
Ca kit không diễn đạt được thì **mở issue ở godx-jp/godxjp-ui**, tạm dùng cách
|
|
121
114
|
hợp lệ gần nhất kèm `// TODO(godxjp-ui#<n>)` — không tự chế.
|
|
122
115
|
|
|
123
|
-
##
|
|
124
|
-
|
|
125
|
-
Đừng với tay sang 12 export `AlertDialog*` nữa. Chúng **vẫn chạy y như cũ** (gỡ
|
|
126
|
-
là breaking change) nhưng là **LỐI CŨ**: đo được 26 export với **12 cặp trùng
|
|
127
|
-
tên** và **0** phần chỉ `AlertDialog` mới có. antd — thẩm quyền bề mặt prop của
|
|
128
|
-
gói này — chỉ có MỘT `Modal`, và mức nguy hiểm ở đó là một PROP.
|
|
116
|
+
## Xác nhận nguy hiểm: `Dialog variant="destructive"`
|
|
129
117
|
|
|
130
118
|
```tsx
|
|
131
|
-
<DialogContent variant="destructive"> {/*
|
|
119
|
+
<DialogContent variant="destructive"> {/* không dùng họ AlertDialog* — lối cũ */}
|
|
132
120
|
```
|
|
133
121
|
|
|
134
|
-
Một prop
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
`docs/DESIGN-AUTHORITY.md` của gói: **nơi antd đặt tên cho một năng lực, gói này
|
|
142
|
-
lấy nguyên tên và nguyên ngữ nghĩa của antd.** Một năng lực còn thiếu được port
|
|
143
|
-
từ antd **100% TRƯỚC** — tên, prop, ngữ nghĩa — rồi mới cải tiến. Không thiết kế
|
|
144
|
-
lại trước, không port một nửa.
|
|
145
|
-
|
|
146
|
-
Nghĩa là với consumer: thấy thiếu prop thì **mở issue kèm tên antd của nó**
|
|
147
|
-
(`sorter`, `closable`, `okType`…), đừng đề xuất một cái tên mới và đừng tự vẽ
|
|
148
|
-
lại bằng class tiện ích. Ba trục cố ý lệch khỏi antd đều đã ghi lý do trong
|
|
149
|
-
DESIGN-AUTHORITY (logical thay cho `left/right`, từ vựng giá trị của gói này,
|
|
150
|
-
`density` thay cho `size`) — lệch thêm thì phải viết ra ở đó.
|
|
151
|
-
|
|
152
|
-
## Catalog giờ chở CẢ luật bố cục — nhưng phải hỏi mới có
|
|
153
|
-
|
|
154
|
-
Mục này từng nói "catalog chở PROP, không chở LUẬT BỐ CỤC", với `CardBar` làm
|
|
155
|
-
bằng chứng: một prop (`extra`), không dòng nào nói nó tự kẻ theo vị trí. **Bằng
|
|
156
|
-
chứng ấy đã hết đúng.** `CardBar` nay có 6 prop, trong đó `border` (`"none" |
|
|
157
|
-
"block-start" | "block-end" | "both"`) ép được đường kẻ khi xếp chồng, và cả
|
|
158
|
-
`Card` lẫn `CardBar` trong catalog đều nói ra luật vị trí (đầu: kẻ dưới · cuối:
|
|
159
|
-
kẻ trên · giữa: cả hai).
|
|
160
|
-
|
|
161
|
-
Nên luật hiện hành là: **hỏi `get_component` trước** — nay nó thường trả lời cả
|
|
162
|
-
hình dạng lẫn luật. Còn khi `usage`/`description` im lặng về bố cục thì mới mở
|
|
163
|
-
`node_modules/@godxjp/ui/src/styles/*-layout.css` của component ấy ra đọc; chú
|
|
164
|
-
thích trong đó vẫn là bản đầy đủ nhất.
|
|
122
|
+
Một prop: `role="alertdialog"`, click ra ngoài không đóng, nút chính nhấn mạnh destructive.
|
|
123
|
+
Thiếu prop thì mở issue kèm tên antd của nó (`docs/DESIGN-AUTHORITY.md`), đừng tự đặt tên.
|
|
124
|
+
|
|
125
|
+
## Luật bố cục: hỏi `get_component` trước
|
|
126
|
+
|
|
127
|
+
`usage`/`description` thường nói cả luật vị trí (vd. `CardBar` tự kẻ theo vị trí, `border` để
|
|
128
|
+
ép). Khi catalog im lặng thì mới đọc `node_modules/@godxjp/ui/src/styles/*-layout.css`.
|
|
165
129
|
|
|
166
130
|
## Card không lồng Card
|
|
167
131
|
|
package/scripts/ui-audit.mjs
CHANGED
|
@@ -295,6 +295,22 @@ const isAsChildSlot = (content, index) => {
|
|
|
295
295
|
const rawTagOutsideAsChildSlot = (tag) => (content) =>
|
|
296
296
|
[...content.matchAll(rawTag(tag))].filter((match) => !isAsChildSlot(content, match.index));
|
|
297
297
|
|
|
298
|
+
/*
|
|
299
|
+
* ADMIN-SCREEN TELLS (gh#1294). Consumer screens built only from correct primitives still read as
|
|
300
|
+
* generated, through structure: a Card per section, a Card in a Card, a KPI tile row above a
|
|
301
|
+
* list, an icon EmptyState inside a Card, a wall of Badges or Alerts. Each is sometimes right, so
|
|
302
|
+
* these are WARNINGS. The per-file limits were set from four consumer repos (ql, platform,
|
|
303
|
+
* godx-task, godx-content: 511 files importing @godxjp/ui, 2026-10-14) to flag the densest
|
|
304
|
+
* ~10–15% of the files that use the tag at all:
|
|
305
|
+
* Card ≥5 → 43 of 336 files (12.8%)
|
|
306
|
+
* Badge ≥5 → 28 of 211 (13.3%)
|
|
307
|
+
* Alert ≥4 → 27 of 200 (13.5%)
|
|
308
|
+
*/
|
|
309
|
+
const CARD_LIMIT = 5;
|
|
310
|
+
const BADGE_LIMIT = 5;
|
|
311
|
+
const ALERT_LIMIT = 4;
|
|
312
|
+
const STATCARD_ROW = 3;
|
|
313
|
+
|
|
298
314
|
/**
|
|
299
315
|
* @type {{id:string, severity:'error'|'warn', test:RegExp, message:string, standard?:string,
|
|
300
316
|
* exempt?:RegExp, classOnly?:boolean}[]}
|
|
@@ -915,6 +931,64 @@ const RULES = [
|
|
|
915
931
|
message:
|
|
916
932
|
"Buttons in one row mix sizes — every Button under one parent takes the same `size` (a missing size is `default`; `icon-sm` pairs with `sm`, `icon-xs` with `xs`, `icon` with default). Pick the row's size and apply it to every button in it (docs/CONSUMER-RULES.md §5).",
|
|
917
933
|
},
|
|
934
|
+
// Admin-screen tells (gh#1294) — structure, not decoration; limits measured, see CARD_LIMIT.
|
|
935
|
+
{
|
|
936
|
+
id: "nested-card",
|
|
937
|
+
replacement: "Heading + Flex inside the one Card",
|
|
938
|
+
scope: "consumer",
|
|
939
|
+
severity: "warn",
|
|
940
|
+
spansElement: true,
|
|
941
|
+
matches: nestedCardMatches,
|
|
942
|
+
message:
|
|
943
|
+
"A Card (or StatCard) inside a Card — two borders for one boundary. Group inside the outer Card with a Heading and Flex/Separator; keep Card for a real surface boundary.",
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
id: "card-per-section",
|
|
947
|
+
replacement: "Heading + Flex direction=col per section",
|
|
948
|
+
scope: "consumer",
|
|
949
|
+
severity: "warn",
|
|
950
|
+
spansElement: true,
|
|
951
|
+
matches: (source) => densityMatches(source, ["Card"], CARD_LIMIT),
|
|
952
|
+
message: `${CARD_LIMIT}+ Cards in one file — a Card per section. Mark sections with a Heading and section spacing (PageContainer children, Flex gap="lg"); keep Card for a real boundary (a table surface, a selectable item).`,
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
id: "kpi-row-off-dashboard",
|
|
956
|
+
replacement: "drop the StatCard row; put counts in the table header or Pagination total",
|
|
957
|
+
scope: "consumer",
|
|
958
|
+
severity: "warn",
|
|
959
|
+
spansElement: true,
|
|
960
|
+
matches: kpiRowMatches,
|
|
961
|
+
message: `${STATCARD_ROW}+ StatCards outside a dashboard — a row count is not a KPI. Keep StatCard rows for a dashboard with comparison or trend (delta); a list shows its total in Pagination showTotal.`,
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
id: "empty-state-icon-in-card",
|
|
965
|
+
replacement: 'EmptyState title + action, no icon (or variant="compact")',
|
|
966
|
+
scope: "consumer",
|
|
967
|
+
severity: "warn",
|
|
968
|
+
spansElement: true,
|
|
969
|
+
matches: emptyStateIconInCardMatches,
|
|
970
|
+
message:
|
|
971
|
+
'An icon EmptyState inside a Card. In an admin surface an empty state is one sentence and one action: drop `icon`, or use variant="compact".',
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
id: "badge-wall",
|
|
975
|
+
replacement: "plain text for categories; Badge only for status",
|
|
976
|
+
scope: "consumer",
|
|
977
|
+
severity: "warn",
|
|
978
|
+
spansElement: true,
|
|
979
|
+
matches: (source) => densityMatches(source, ["Badge", "Tag"], BADGE_LIMIT),
|
|
980
|
+
message: `${BADGE_LIMIT}+ Badge/Tag in one file. Badge is for status; render categories, tiers and scopes as plain text, and keep one status column per table.`,
|
|
981
|
+
},
|
|
982
|
+
{
|
|
983
|
+
id: "alert-per-page",
|
|
984
|
+
replacement:
|
|
985
|
+
"one Alert for the page's blocking state; inline FormField errors and toasts for the rest",
|
|
986
|
+
scope: "consumer",
|
|
987
|
+
severity: "warn",
|
|
988
|
+
spansElement: true,
|
|
989
|
+
matches: (source) => densityMatches(source, ["Alert"], ALERT_LIMIT),
|
|
990
|
+
message: `${ALERT_LIMIT}+ Alerts in one file. An Alert is for a state that blocks or changes the task; field errors belong in FormField, results in a toast, and standing info in plain text.`,
|
|
991
|
+
},
|
|
918
992
|
];
|
|
919
993
|
|
|
920
994
|
/**
|
|
@@ -1554,9 +1628,11 @@ const BUTTON_ROW_TRANSPARENT = new Set([
|
|
|
1554
1628
|
"TooltipTrigger",
|
|
1555
1629
|
]);
|
|
1556
1630
|
|
|
1557
|
-
/**
|
|
1558
|
-
|
|
1559
|
-
|
|
1631
|
+
/**
|
|
1632
|
+
* An opening tag with every `{…}` body blanked (the `{` kept), so `icon={<Plus size="sm" />}`
|
|
1633
|
+
* cannot answer for the element that owns the attribute.
|
|
1634
|
+
*/
|
|
1635
|
+
function topLevelAttrs(openTag) {
|
|
1560
1636
|
let top = "";
|
|
1561
1637
|
let depth = 0;
|
|
1562
1638
|
let quote = "";
|
|
@@ -1573,7 +1649,12 @@ function buttonSizeOf(openTag) {
|
|
|
1573
1649
|
top += depth ? (ch === "{" && depth === 1 ? "{" : " ") : ch;
|
|
1574
1650
|
if (ch === "}") depth--;
|
|
1575
1651
|
}
|
|
1576
|
-
|
|
1652
|
+
return top;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
/** A Button's `size` literal from its OPENING tag's own attributes (not a nested element's). */
|
|
1656
|
+
function buttonSizeOf(openTag) {
|
|
1657
|
+
const at = /\bsize\s*=\s*/.exec(topLevelAttrs(openTag));
|
|
1577
1658
|
if (!at) return "default";
|
|
1578
1659
|
const rest = openTag.slice(at.index + at[0].length);
|
|
1579
1660
|
const lit = /^(?:"([^"]*)"|'([^']*)'|\{\s*(?:"([^"]*)"|'([^']*)'|`([^`$]*)`)\s*\})/.exec(rest);
|
|
@@ -1587,15 +1668,14 @@ const buttonSizeFamily = (size) => {
|
|
|
1587
1668
|
};
|
|
1588
1669
|
|
|
1589
1670
|
/**
|
|
1590
|
-
*
|
|
1591
|
-
*
|
|
1592
|
-
*
|
|
1593
|
-
*
|
|
1671
|
+
* Every JSX opening tag in the file, with the stack of elements open around it. A tag stack over
|
|
1672
|
+
* the whole file; an element in an attribute (`actions={<>…</>}`) gets the attribute's owner as an
|
|
1673
|
+
* `attr: true` frame, kept apart from the owner's children. A `<` glued to an identifier outside
|
|
1674
|
+
* JSX children is a TS generic. `stack` is live: read it before asking for the next element.
|
|
1594
1675
|
*/
|
|
1595
|
-
function*
|
|
1676
|
+
function* jsxElements(source) {
|
|
1596
1677
|
const stack = []; // { tag, id, attr }
|
|
1597
1678
|
const pending = []; // opening tags whose attributes are still being scanned
|
|
1598
|
-
const groups = new Map();
|
|
1599
1679
|
let nextId = 0;
|
|
1600
1680
|
const settle = (upTo) => {
|
|
1601
1681
|
while (pending.length && pending.at(-1).end < upTo) {
|
|
@@ -1625,21 +1705,31 @@ function* mixedButtonSizeMatches(source) {
|
|
|
1625
1705
|
const end = jsxOpeningEnd(source, m.index);
|
|
1626
1706
|
if (end >= source.length) continue;
|
|
1627
1707
|
const openTag = source.slice(m.index, end + 1);
|
|
1628
|
-
|
|
1629
|
-
const parent = stack.findLast((f) => f.attr || !BUTTON_ROW_TRANSPARENT.has(f.tag));
|
|
1630
|
-
const size = buttonSizeOf(openTag);
|
|
1631
|
-
if (parent && size !== null) {
|
|
1632
|
-
(groups.get(parent.id) ?? groups.set(parent.id, []).get(parent.id)).push({
|
|
1633
|
-
size,
|
|
1634
|
-
index: m.index,
|
|
1635
|
-
openTag,
|
|
1636
|
-
});
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1708
|
+
yield { tag, index: m.index, openTag, stack };
|
|
1639
1709
|
const frame = { tag, id: nextId++, attr: true };
|
|
1640
1710
|
stack.push(frame);
|
|
1641
1711
|
pending.push({ end, frame, selfClose: /\/\s*>$/.test(openTag) });
|
|
1642
1712
|
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/**
|
|
1716
|
+
* `mixed-button-size`: group every <Button> by its nearest non-transparent JSX parent and yield
|
|
1717
|
+
* one match per group whose size families differ.
|
|
1718
|
+
*/
|
|
1719
|
+
function* mixedButtonSizeMatches(source) {
|
|
1720
|
+
const groups = new Map();
|
|
1721
|
+
for (const { tag, index, openTag, stack } of jsxElements(source)) {
|
|
1722
|
+
if (tag !== "Button") continue;
|
|
1723
|
+
const parent = stack.findLast((f) => f.attr || !BUTTON_ROW_TRANSPARENT.has(f.tag));
|
|
1724
|
+
const size = buttonSizeOf(openTag);
|
|
1725
|
+
if (parent && size !== null) {
|
|
1726
|
+
(groups.get(parent.id) ?? groups.set(parent.id, []).get(parent.id)).push({
|
|
1727
|
+
size,
|
|
1728
|
+
index,
|
|
1729
|
+
openTag,
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1643
1733
|
for (const group of groups.values()) {
|
|
1644
1734
|
const families = new Set(group.map((b) => buttonSizeFamily(b.size)));
|
|
1645
1735
|
if (group.length < 2 || families.size < 2) continue;
|
|
@@ -1650,6 +1740,46 @@ function* mixedButtonSizeMatches(source) {
|
|
|
1650
1740
|
}
|
|
1651
1741
|
}
|
|
1652
1742
|
|
|
1743
|
+
/** One finding per file, at the `limit`-th occurrence of any tag in `tags`, naming the count. */
|
|
1744
|
+
function* densityMatches(source, tags, limit) {
|
|
1745
|
+
const hits = [];
|
|
1746
|
+
for (const { tag, index } of jsxElements(source)) if (tags.includes(tag)) hits.push(index);
|
|
1747
|
+
if (hits.length >= limit) yield { 0: `${hits.length} in this file`, index: hits[limit - 1] };
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/** A Card or StatCard opened while another Card is open, in children or in an attribute. */
|
|
1751
|
+
function* nestedCardMatches(source) {
|
|
1752
|
+
for (const { tag, index, openTag, stack } of jsxElements(source)) {
|
|
1753
|
+
if ((tag === "Card" || tag === "StatCard") && stack.some((f) => f.tag === "Card")) {
|
|
1754
|
+
yield { 0: openTag, index };
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
/** ≥3 StatCards in a file whose path does not say it is a dashboard. */
|
|
1760
|
+
function* kpiRowMatches(source, rel = "") {
|
|
1761
|
+
if (/dashboard/i.test(rel)) return;
|
|
1762
|
+
yield* densityMatches(source, ["StatCard"], STATCARD_ROW);
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/** An EmptyState that draws an icon (not `icon={null}`, not compact) inside a Card. */
|
|
1766
|
+
function* emptyStateIconInCardMatches(source) {
|
|
1767
|
+
for (const { tag, index, openTag, stack } of jsxElements(source)) {
|
|
1768
|
+
if (tag !== "EmptyState" || !stack.some((f) => f.tag === "Card" || f.tag === "CardContent")) {
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
const top = topLevelAttrs(openTag);
|
|
1772
|
+
const icon = /\bicon\s*=\s*/.exec(top);
|
|
1773
|
+
if (!icon || /^\{\s*(?:null|undefined)\s*\}/.test(openTag.slice(icon.index + icon[0].length))) {
|
|
1774
|
+
continue;
|
|
1775
|
+
}
|
|
1776
|
+
const variant = /\bvariant\s*=\s*/.exec(top);
|
|
1777
|
+
const rest = variant ? openTag.slice(variant.index + variant[0].length) : "";
|
|
1778
|
+
if (/^(?:"compact"|'compact'|\{\s*["'`]compact["'`]\s*\})/.test(rest)) continue;
|
|
1779
|
+
yield { 0: openTag, index };
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1653
1783
|
// Structural: a <Card> (without p-0) whose first child is body content rather than a Card
|
|
1654
1784
|
// sub-component sits FLUSH (no padding). Per-line regexes can't see across lines, so this is a
|
|
1655
1785
|
// whole-file pass. The body must be wrapped in <CardContent> (titles in <CardHeader>).
|
|
@@ -2065,7 +2195,7 @@ for (const dir of SCAN_DIRS) {
|
|
|
2065
2195
|
for (const rule of fileRules) {
|
|
2066
2196
|
if (!rule.spansElement || !isJsx) continue;
|
|
2067
2197
|
for (const match of rule.matches
|
|
2068
|
-
? rule.matches(scanContent)
|
|
2198
|
+
? rule.matches(scanContent, rel)
|
|
2069
2199
|
: scanContent.matchAll(rule.test)) {
|
|
2070
2200
|
// A comparison `>` inside a JSX prop is not the end of its opening tag.
|
|
2071
2201
|
if (
|