@godxjp/ui 19.6.0 → 20.0.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/dist/components/data-display/card.d.ts +2 -2
- package/dist/components/data-display/collapsible.d.ts +23 -4
- package/dist/components/data-display/collapsible.js +119 -4
- package/dist/components/data-display/index.d.ts +3 -1
- package/dist/components/data-display/index.js +2 -0
- package/dist/components/data-display/legend.d.ts +15 -0
- package/dist/components/data-display/legend.js +11 -0
- package/dist/components/data-display/list-row.js +1 -1
- package/dist/components/data-display/popover.d.ts +58 -5
- package/dist/components/data-display/popover.js +206 -27
- package/dist/components/data-display/progress.d.ts +50 -2
- package/dist/components/data-display/progress.js +57 -9
- package/dist/components/data-entry/calendar.d.ts +1 -1
- package/dist/components/data-entry/calendar.js +67 -32
- package/dist/components/data-entry/checkbox.d.ts +19 -6
- package/dist/components/data-entry/checkbox.js +55 -16
- package/dist/components/data-entry/choice-option.d.ts +1 -1
- package/dist/components/data-entry/date-range-picker.d.ts +1 -1
- package/dist/components/data-entry/field.js +0 -1
- package/dist/components/data-entry/form.js +1 -1
- package/dist/components/data-entry/label.d.ts +3 -2
- package/dist/components/data-entry/label.js +23 -10
- package/dist/components/data-entry/month-picker.d.ts +1 -1
- package/dist/components/data-entry/month-range-picker.d.ts +1 -1
- package/dist/components/data-entry/transfer.js +16 -6
- package/dist/components/data-entry/tree-select-strategy.d.ts +1 -1
- package/dist/components/data-entry/tree-select.js +29 -12
- package/dist/components/data-entry/tree-utils.d.ts +1 -1
- package/dist/components/data-entry/upload.js +7 -2
- package/dist/components/feedback/dialog.d.ts +105 -38
- package/dist/components/feedback/dialog.js +272 -194
- package/dist/components/feedback/overlay-close-focus.d.ts +31 -0
- package/dist/components/feedback/overlay-close-focus.js +31 -0
- package/dist/components/feedback/overlay-header-tone.d.ts +1 -1
- package/dist/components/feedback/sheet.d.ts +53 -11
- package/dist/components/feedback/sheet.js +150 -81
- package/dist/components/feedback/tooltip.d.ts +51 -7
- package/dist/components/feedback/tooltip.js +107 -25
- package/dist/components/general/button.js +2 -2
- package/dist/components/general/logo.d.ts +17 -0
- package/dist/components/general/logo.js +22 -16
- package/dist/components/layout/app-shell.d.ts +1 -1
- package/dist/components/layout/app-shell.js +52 -8
- package/dist/components/layout/aspect-ratio.js +0 -1
- package/dist/components/layout/auth-divider.js +0 -1
- package/dist/components/layout/flex.d.ts +1 -1
- package/dist/components/layout/flex.js +19 -2
- package/dist/components/layout/nav-surface.d.ts +26 -0
- package/dist/components/layout/nav-surface.js +17 -0
- package/dist/components/layout/org-switcher.d.ts +5 -1
- package/dist/components/layout/org-switcher.js +5 -3
- package/dist/components/layout/separator.js +0 -1
- package/dist/components/layout/sidebar.js +4 -1
- package/dist/components/layout/split-pane.d.ts +14 -1
- package/dist/components/layout/split-pane.js +26 -13
- package/dist/components/layout/topbar-item.js +1 -2
- package/dist/components/navigation/app-setting-picker.js +21 -2
- package/dist/components/navigation/app-setting-toggle.d.ts +16 -0
- package/dist/components/navigation/app-setting-toggle.js +96 -0
- package/dist/components/navigation/dropdown-menu.d.ts +199 -18
- package/dist/components/navigation/dropdown-menu.js +344 -117
- package/dist/components/navigation/index.d.ts +2 -0
- package/dist/components/navigation/index.js +2 -0
- package/dist/components/navigation/pagination-utils.d.ts +2 -1
- package/dist/components/navigation/tabs.d.ts +47 -6
- package/dist/components/navigation/tabs.js +152 -90
- package/dist/components/ui/accordion.d.ts +50 -5
- package/dist/components/ui/accordion.js +239 -33
- package/dist/components/ui/aspect-ratio.d.ts +23 -2
- package/dist/components/ui/aspect-ratio.js +15 -13
- package/dist/components/ui/avatar.d.ts +29 -4
- package/dist/components/ui/avatar.js +111 -25
- package/dist/components/ui/hover-card.d.ts +42 -4
- package/dist/components/ui/hover-card.js +183 -27
- package/dist/components/ui/label.js +0 -1
- package/dist/components/ui/segmented.d.ts +3 -3
- package/dist/components/ui/separator.d.ts +8 -2
- package/dist/components/ui/separator.js +17 -9
- package/dist/components/ui/toggle-group.d.ts +50 -5
- package/dist/components/ui/toggle-group.js +79 -20
- package/dist/components/ui/toggle.d.ts +31 -5
- package/dist/components/ui/toggle.js +42 -3
- package/dist/i18n/messages/en.json +8 -0
- package/dist/i18n/messages/ja.json +8 -0
- package/dist/i18n/messages/vi.json +8 -0
- package/dist/lib/control-styles.d.ts +1 -1
- package/dist/lib/control-styles.js +1 -1
- package/dist/lib/slot.d.ts +32 -0
- package/dist/lib/slot.js +22 -0
- package/dist/lib/variants.d.ts +22 -3
- package/dist/lib/variants.js +56 -1
- package/dist/props/components/app.prop.d.ts +25 -1
- package/dist/props/components/data-display.prop.d.ts +17 -0
- package/dist/props/components/data-entry.prop.d.ts +32 -4
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/components/layout.prop.d.ts +118 -5
- package/dist/props/components/navigation.prop.d.ts +1 -1
- package/dist/props/registry.d.ts +22 -2
- package/dist/props/registry.js +33 -2
- package/dist/props/vocabulary/content.prop.d.ts +1 -1
- package/dist/props/vocabulary/index.d.ts +2 -2
- package/dist/props/vocabulary/interaction.prop.d.ts +17 -1
- package/dist/props/vocabulary/layout.prop.d.ts +57 -1
- package/dist/styles/control.css +142 -1
- package/dist/styles/core.css +5 -2
- package/dist/styles/data-display-layout.css +72 -0
- package/dist/styles/focus-ring.css +4 -0
- package/dist/styles/index.css +5 -2
- package/dist/styles/layout.css +106 -0
- package/dist/styles/shell-layout.css +140 -8
- package/dist/styles/text-layout.css +5 -4
- package/dist/tokens/base.css +1 -1
- package/dist/tokens/components/data-display.css +11 -0
- package/dist/tokens/components/shell.css +3 -1
- package/dist/tokens/foundation.css +2 -0
- package/docs/COMPONENTS.md +9 -3
- package/docs/DESIGN-AUTHORITY.md +96 -71
- package/docs/FRAME-COVERAGE-REPORT.md +5 -3
- package/docs/README.md +14 -14
- package/docs/WHAT-BELONGS-HERE.md +179 -0
- package/docs/data-display/legend.tsx +145 -0
- package/docs/data-display/progress.tsx +32 -0
- package/docs/data-entry/segmented.tsx +1 -1
- package/docs/data-entry/select.tsx +2 -2
- package/docs/feedback/sheet.tsx +1 -1
- package/docs/layout/app-shell-arrangements.tsx +225 -0
- package/docs/layout/aspect-ratio.tsx +1 -1
- package/docs/navigation/app-setting-picker.tsx +26 -1
- package/docs/navigation/app-setting-toggle.tsx +111 -0
- package/package.json +11 -8
- package/scripts/_agent-setup.mjs +165 -3
- package/scripts/consumer-rule.md +98 -0
- package/scripts/guinea-pig-skill.md +322 -0
- package/scripts/init-guinea-pig.mjs +62 -0
- package/scripts/postinstall.mjs +13 -2
- package/scripts/ui-audit.mjs +115 -14
- /package/dist/tokens/{antd.generated.css → derived.css} +0 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: godx-ui-guinea-pig
|
|
3
|
+
description: "Bắt buộc cho mọi kho là consumer CHUỘT BẠCH của @godxjp/ui. Kích hoạt khi: dựng hay sửa bất kỳ màn hình nào, gặp một prop còn thiếu, định viết class Tailwind để lách, định tự dựng component thay cho thứ DS đã có, chạy ui-audit, hoặc thấy chú thích 'chờ upstream'. Dạy MỘT việc mà không tài liệu nào khác dạy: cách KHÉP VÒNG từ 'app thiếu gì' sang 'DS đã sửa, đã phát hành, app đã nâng, vá tạm đã gỡ'. Không dùng cho consumer thường — chuột bạch có nghĩa vụ sửa ngược lên DS, consumer thường thì không."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Consumer chuột bạch của @godxjp/ui
|
|
7
|
+
|
|
8
|
+
> Bản gốc của tệp này nằm ở kho `godx-jp/godxjp-ui`. Sửa thì sửa ở đó rồi chép
|
|
9
|
+
> sang các consumer, đừng sửa bản chép — kho này đã hỏng đúng kiểu ấy một lần
|
|
10
|
+
> với catalog MCP (xem §4).
|
|
11
|
+
|
|
12
|
+
## 0. Bạn có HAI việc, không phải một
|
|
13
|
+
|
|
14
|
+
Việc thứ nhất là làm xong màn hình. Việc thứ hai là **để lại design system tốt
|
|
15
|
+
hơn lúc bạn gặp nó**. Chuột bạch là kho mà `@godxjp/ui` bị dùng thật lần đầu;
|
|
16
|
+
mọi khoảng trống lộ ra ở đây mà không được sửa ngược lên sẽ là khoảng trống
|
|
17
|
+
**vĩnh viễn** cho mọi consumer sau.
|
|
18
|
+
|
|
19
|
+
Vì vậy một bản vá tạm ở đây không phải là "nợ kỹ thuật của app". Nó là hai lần
|
|
20
|
+
thất bại: màn hình lệch chuẩn, VÀ khoảng trống bị giấu đi.
|
|
21
|
+
|
|
22
|
+
Bạn được toàn quyền sửa `@godxjp/ui`. Đó là điều kho này tồn tại để làm.
|
|
23
|
+
|
|
24
|
+
## 1. Trước khi viết dòng JSX đầu tiên
|
|
25
|
+
|
|
26
|
+
Định vị bản checkout của DS. Không có thì clone:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
ls ~/Herd/godxjp-ui 2>/dev/null || git clone git@github.com:godx-jp/godxjp-ui.git ~/Herd/godxjp-ui
|
|
30
|
+
cd ~/Herd/godxjp-ui && pnpm install # DS dùng pnpm, consumer thường dùng npm — đừng lẫn
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Và hỏi MCP `godxjp-ui`, đừng đoán tên prop: `search_components`, `get_component`,
|
|
34
|
+
`get_tokens`. **Catalog là nguồn sự thật, không phải trí nhớ của bạn.**
|
|
35
|
+
|
|
36
|
+
## 2. Kỷ luật — thứ bị cấm kể cả khi "chỉ tạm thôi"
|
|
37
|
+
|
|
38
|
+
Mười luật ở `docs/CONSUMER-RULES.md` của DS là hàng rào, `ui-audit` cưỡng chế
|
|
39
|
+
chúng. Không chép lại ở đây. Ba điều cấm riêng của **chuột bạch**:
|
|
40
|
+
|
|
41
|
+
1. **Tự dựng component thay cho thứ DS đã có hoặc lẽ ra phải có.** Hộp tự vẽ
|
|
42
|
+
thay `Card`, hàng tự ghép thay `ListRow`, palette tự viết thay
|
|
43
|
+
`CommandPalette`.
|
|
44
|
+
2. **Dùng class tiện ích để lách một prop còn thiếu** — `gap-3`, `p-4`,
|
|
45
|
+
`w-[240px]`, `text-muted-foreground`.
|
|
46
|
+
3. **Gõ mã màu hex hay số đo ngoài thang token.**
|
|
47
|
+
|
|
48
|
+
Thước đo, chạy trước mọi lần review:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
node node_modules/@godxjp/ui/scripts/ui-audit.mjs resources/js # 0 lỗi là mức đạt
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Một lỗi bạn **không sửa được ở phía consumer** chính là một khoảng trống của DS.
|
|
55
|
+
Nó là đầu vào của §3, không phải một ngoại lệ để nới.
|
|
56
|
+
|
|
57
|
+
## 3. Vòng lặp — sáu bước, và nó phải KHÉP
|
|
58
|
+
|
|
59
|
+
Đây là phần không tài liệu nào khác có. `design-to-page` và `compose-a-screen`
|
|
60
|
+
đều dừng ở `report-bug`; mở issue rồi để đó là hỏng nửa vời.
|
|
61
|
+
|
|
62
|
+
### Bước 1 — Chứng minh đó là khoảng trống, đừng cảm thấy
|
|
63
|
+
|
|
64
|
+
Viết ra đúng đoạn mã bạn **muốn** viết, rồi chạy `ui-audit` lên nó.
|
|
65
|
+
|
|
66
|
+
Audit xanh nghĩa là **không có gì CẤM** bạn — chưa phải là bạn có nước đi. Nước
|
|
67
|
+
đi chỉ có thật khi bạn MỞ TRANG và thấy nó đổi pixel. Đo được: consumer viết
|
|
68
|
+
`modifiers` + `modifiersClassNames` cho màu cuối tuần, audit xanh, tsc xanh,
|
|
69
|
+
build xanh, và **số màu chữ trên cả lưới vẫn là 1** — class rơi vào `<td>` còn
|
|
70
|
+
`<button>` tự đặt màu. Một API chết im lặng trông y hệt một API đang chạy.
|
|
71
|
+
|
|
72
|
+
Chỉ khi mọi prop và token hiện có đều không nói được điều cần nói, VÀ mọi đường
|
|
73
|
+
còn lại đều bị audit chặn, thì mới là bất khả.
|
|
74
|
+
|
|
75
|
+
### Bước 2 — Ba câu hỏi, phải đủ cả ba
|
|
76
|
+
|
|
77
|
+
Đọc `docs/WHAT-BELONGS-HERE.md` của DS. Tóm tắt không thay thế nó:
|
|
78
|
+
|
|
79
|
+
1. Consumer có thật sự **không có nước đi hợp lệ** nào không? (không phải "bất tiện")
|
|
80
|
+
2. Nó thuộc về **hình dạng** của component, hay **nội dung** của một màn?
|
|
81
|
+
3. Consumer **khác** có cần không?
|
|
82
|
+
|
|
83
|
+
Trượt bất kỳ câu nào → dựng ở consumer, và ghi rõ TRONG MÃ vì sao nó không
|
|
84
|
+
thuộc về DS.
|
|
85
|
+
|
|
86
|
+
### Bước 3 — Sửa trong DS, và sửa đủ bốn chỗ
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
src/… mã
|
|
90
|
+
src/…/__tests__/ test (§5)
|
|
91
|
+
mcp/src/data/ catalog (§4 — chỗ hay quên nhất, và tốn kém nhất)
|
|
92
|
+
docs/ nếu đổi hợp đồng công khai
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Thứ tự ưu tiên, chỉ tiến khi bước trước thật sự không diễn đạt nổi:
|
|
96
|
+
**dùng → ghép → thêm prop vào component đã có → tạo component mới.**
|
|
97
|
+
Một prop nữa hơn một component nữa.
|
|
98
|
+
|
|
99
|
+
### Bước 4 — Kiểm bằng tarball TRƯỚC khi phát hành
|
|
100
|
+
|
|
101
|
+
Đây là bước làm cho "vừa làm vừa trải nghiệm" thành thật. Đừng phát hành rồi
|
|
102
|
+
mới biết mình sửa trượt.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
cd ~/Herd/godxjp-ui
|
|
106
|
+
pnpm build && npm pack # ra @godxjp-ui-<version>.tgz
|
|
107
|
+
cd <kho consumer>
|
|
108
|
+
npm install ~/Herd/godxjp-ui/godxjp-ui-<version>.tgz
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Rồi chạy màn hình thật với bản sửa: `ui-audit` phải sạch **và** đoạn mã bạn
|
|
112
|
+
muốn viết ở Bước 1 phải chạy đúng. Nếu kho có bộ trình duyệt, chạy nó.
|
|
113
|
+
|
|
114
|
+
**Xong việc thì hoàn nguyên `package.json` về bản registry** — đừng để một
|
|
115
|
+
tarball đường dẫn máy bạn lọt vào commit. `main` phải `npm ci` được từ registry.
|
|
116
|
+
|
|
117
|
+
### Bước 5 — Cổng của DS
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# = verify:ci:static + check:frame-contracts + pnpm test, tức ĐÚNG những gì ci.yml chạy.
|
|
121
|
+
cd ~/Herd/godxjp-ui && pnpm verify:ci
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Không nới, không tắt, không thêm ngoại lệ để lấy màu xanh.** Một cổng đỏ là
|
|
125
|
+
một câu hỏi, không phải một chướng ngại. Nếu bạn tin cổng ấy sai thì nói ra và
|
|
126
|
+
đưa số đo, đừng lặng lẽ sửa nó.
|
|
127
|
+
|
|
128
|
+
### Bước 6 — Khép vòng
|
|
129
|
+
|
|
130
|
+
Phát hành → nâng gói ở consumer → **gỡ vá tạm** → **gỡ mọi chú thích "chờ
|
|
131
|
+
upstream"** → **đóng issue**.
|
|
132
|
+
|
|
133
|
+
Vòng chưa khép thì việc chưa xong. Đợt 07–08/09/2026 mở 18 issue cho godxjp-ui;
|
|
134
|
+
#401/#412 và #402 đã vá trên nhánh nhưng issue vẫn mở và consumer vẫn chờ —
|
|
135
|
+
đó là hình dạng của thất bại này.
|
|
136
|
+
|
|
137
|
+
## 4. Nghĩa vụ catalog — chỗ tốn kém nhất khi quên
|
|
138
|
+
|
|
139
|
+
**Một prop có trong mã nhưng không có trong catalog MCP là một prop KHÔNG TỒN
|
|
140
|
+
TẠI** với agent tiếp theo.
|
|
141
|
+
|
|
142
|
+
Đây không phải suy đoán. Đo được trong phiên 08/09/2026: một agent mới, làm
|
|
143
|
+
đúng mọi hướng dẫn (hỏi MCP, không đoán), kết luận _"Flex chỉ có gap"_ trong khi
|
|
144
|
+
`pad` và `padRaw` đã nằm trong gói đã cài — vì catalog đã phát hành chưa có
|
|
145
|
+
chúng. Nó làm đúng và vẫn ra sai.
|
|
146
|
+
|
|
147
|
+
Nên sau mỗi lần thêm hay đổi prop:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
node scripts/gen-component-api-manifest.mjs
|
|
151
|
+
pnpm check:mcp-sync && pnpm check:mcp-prop-sync && pnpm check:component-api-manifest
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Và kiểm ví dụ trong `mcp/src/data/{patterns,components}.ts` có **biên dịch được
|
|
155
|
+
và qua nổi ui-audit** không. Catalog đã từng dạy: `<Dialog mode="confirm">`
|
|
156
|
+
(không có API ấy), `variant="success"` (bị chính audit cấm),
|
|
157
|
+
`<Input onValueChange>` (không có prop ấy). Ví dụ sai trong catalog không phải
|
|
158
|
+
lỗi chính tả — nó là mã mà agent sau sẽ chép.
|
|
159
|
+
|
|
160
|
+
## 5. Kỷ luật test của DS — luật, không phải gợi ý
|
|
161
|
+
|
|
162
|
+
- **Test theo TỪNG component**, và chỉ những component **liên quan** tới nó.
|
|
163
|
+
`Dropdown` = menu + list + button + icon → chỉ quanh chừng đó. Không test lan man.
|
|
164
|
+
- **Test của story/example KHÔNG nằm trong CI.** Chúng ở `tests/manual/`, chạy
|
|
165
|
+
bằng tay. CI không tiêu thời gian vào thứ vô bổ.
|
|
166
|
+
- Test bám vào **role và nhãn**, không bám class Tailwind — có cổng
|
|
167
|
+
`check:no-tailwind-class-assertions` chặn.
|
|
168
|
+
|
|
169
|
+
Vì sao role/nhãn: 138 trên 160 selector của bộ Playwright ở consumer bám vào
|
|
170
|
+
`getByRole`/`getByLabel`. Chúng không phải nghi thức a11y — chúng là cái cân cho
|
|
171
|
+
biết một lần đổi nền thư viện có làm vỡ CẤU TRÚC hay không. Gỡ chúng là mất cân.
|
|
172
|
+
|
|
173
|
+
Nhưng chúng **không đo được màu, bo góc, hay phần tử nào đang được tô** — cả một
|
|
174
|
+
đợt lỗi calendar đi qua chúng mà không cái nào đỏ. Cấu trúc và diện mạo là hai
|
|
175
|
+
thước khác nhau; xem §5c.
|
|
176
|
+
|
|
177
|
+
## 5b. Một cổng canh được đúng thứ nó CHẠY QUA
|
|
178
|
+
|
|
179
|
+
Một cổng viết ra mà không nối vào CI là một cổng không tồn tại — y hệt một prop
|
|
180
|
+
không có trong catalog (§4). Nhưng "đã nối" chưa đủ, và đợt 08/09/2026 đo được
|
|
181
|
+
cả hai nửa của bài học này.
|
|
182
|
+
|
|
183
|
+
**Nửa thứ nhất — đừng đo bằng cái thước sai.** Bản trước của mục này viết rằng
|
|
184
|
+
"25 cổng `check:*` nằm ngoài `verify:ci`", và kết luận từ đó rằng chúng không
|
|
185
|
+
chạy. Kiểm lại bằng workflow thì 22 trong 25 cổng ấy VẪN chạy mỗi lần merge hoặc
|
|
186
|
+
mỗi đêm — chỉ là ở lane khác. Lý do rất đơn giản và rất dễ vấp: **không workflow
|
|
187
|
+
nào chạy `verify:ci`.** `ci.yml` chạy `verify:ci:static` + `check:frame-contracts`
|
|
188
|
+
|
|
189
|
+
- `pnpm test` chia shard; `ci-browser.yml` chạy `verify:browser` (gồm
|
|
190
|
+
`check:contrast`) và `check:frame-axe`; `ci-browser-full.yml` chạy các sweep rộng
|
|
191
|
+
theo lịch đêm; `release-integrity.yml` chạy `check:release-plan`. Hỏi "cổng này
|
|
192
|
+
có trong `verify:ci` không" là hỏi một script không ai gọi.
|
|
193
|
+
|
|
194
|
+
Nên đừng tự viết đoạn `node -e` so với `verify:ci`. Hỏi thẳng cổng canh-cổng, nó
|
|
195
|
+
đọc workflow rồi mới trả lời:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
pnpm check:gate-coverage --report # in ra: mỗi check:* chạy ở workflow nào
|
|
199
|
+
pnpm check:gate-coverage # đỏ nếu có cổng không ai chạy và không khai miễn trừ
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Cổng này nằm trong `verify:ci:static`, nên thêm một `check:` mới mà quên nối vào
|
|
203
|
+
đâu là CI đỏ ngay. Muốn để một cổng ngoài lề thì phải khai TƯỜNG MINH kèm lý do
|
|
204
|
+
trong `EXEMPT` của `scripts/check-gate-coverage.mjs` — hiện có đúng hai cái, và
|
|
205
|
+
cả hai đều là "cần người", không phải "chạy lâu": `check:voiceover-capture` (cần
|
|
206
|
+
VoiceOver thật do người bật) và `check:frame-runtime` (chỉ là alias gọi tám cổng
|
|
207
|
+
đã chạy riêng trong lane đêm). "Chạy lâu" không phải lý do để miễn trừ — đó là
|
|
208
|
+
lý do để nằm trong lane đêm, và lane đêm VẪN được tính là có chạy.
|
|
209
|
+
|
|
210
|
+
**Nửa thứ hai, và là nguyên nhân thật của lỗi đã lọt.** Một toast
|
|
211
|
+
`data-type="success"` với tương phản **1,02:1** — chữ gần như vô hình — phát hành
|
|
212
|
+
trong `@godxjp/ui@19.5.0` và bị bắt bởi **test trình duyệt của một consumer**.
|
|
213
|
+
Kho DS có sẵn hai thứ đáng lẽ phải bắt được nó, và **cả hai đều đã chạy trong
|
|
214
|
+
CI**:
|
|
215
|
+
|
|
216
|
+
- `scripts/check-contrast.mjs` chạy mỗi lần merge trong job "Contrast + visual
|
|
217
|
+
audit" (55–59s, và tên job nằm trong `REQUIRED_CI_CHECK_RUNS` nên bản phát
|
|
218
|
+
hành không đi qua nổi nếu nó đỏ). Nó xanh — vì danh sách `ROUTES` của nó có 11
|
|
219
|
+
route và **không route nào render một cái toast**.
|
|
220
|
+
- `src/components/feedback/__tests__/toast-tone-contrast.test.tsx` chạy trong
|
|
221
|
+
`pnpm test`. Nó xanh — vì nó đọc token trong `src/tokens/**` rồi tính tỉ số
|
|
222
|
+
trên giấy; nó chạy trong jsdom, mà jsdom **không tô màu**, nên nó không nhìn
|
|
223
|
+
thấy màu đã render thật.
|
|
224
|
+
|
|
225
|
+
Không cổng nào bị tắt. Không cổng nào bị bỏ quên. Cả hai đều xanh và cả hai đều
|
|
226
|
+
đúng với thứ chúng đo — chỉ là **không cái nào đo cái đã hỏng**. Đây là dạng
|
|
227
|
+
hỏng đắt hơn hẳn dạng "quên nối cổng", vì bảng CI toàn xanh trông y hệt như một
|
|
228
|
+
kho thật sự an toàn.
|
|
229
|
+
|
|
230
|
+
`check:contrast` đã dính đúng dạng này một lần rồi, và vết sẹo còn nằm trong
|
|
231
|
+
chú thích của chính nó: danh sách route từng trỏ vào `tiximax-*` sau khi các
|
|
232
|
+
route đó bị đổi tên, nên chúng render ra "Showcase not found" và sweep báo trang
|
|
233
|
+
rỗng ấy là AA sạch. Lần đó người ta thêm một guard chặn not-found. Lần này là
|
|
234
|
+
cùng một hình dạng ở một trục khác: route tồn tại, nhưng bề mặt cần soi thì
|
|
235
|
+
không có route nào chạm tới.
|
|
236
|
+
|
|
237
|
+
Nên khi bạn thêm hay sửa một cổng, hỏi HAI câu chứ không phải một:
|
|
238
|
+
|
|
239
|
+
1. **Nó có chạy không?** → `pnpm check:gate-coverage --report`.
|
|
240
|
+
2. **Nó có đi qua bề mặt tôi vừa đụng không?** → mở chính danh sách đầu vào của
|
|
241
|
+
cổng (`ROUTES` của `check-contrast.mjs`, danh sách frame của `check:frame-axe`,
|
|
242
|
+
`include` của `vitest.config.ts`) và tìm bề mặt ấy trong đó. Cổng xanh trên
|
|
243
|
+
một danh sách không chứa thứ bạn vừa sửa thì nó chưa nói gì về bản sửa của bạn.
|
|
244
|
+
|
|
245
|
+
Và một hệ quả cho phía consumer: **bộ test trình duyệt của bạn là lớp lưới cuối
|
|
246
|
+
cùng của DS.** Hai lỗi tương phản trên do `php artisan test` của consumer bắt
|
|
247
|
+
được, không phải do CI thư viện. Đừng bỏ axe ra khỏi bộ trình duyệt chỉ vì "hệ
|
|
248
|
+
thống nội bộ" — ở đây nó không đo tuân thủ, nó đo xem DS có phát ra chữ đọc được
|
|
249
|
+
hay không.
|
|
250
|
+
|
|
251
|
+
## 5c. CSS hỏng IM LẶNG theo ba cách — và cách duy nhất thấy được
|
|
252
|
+
|
|
253
|
+
Một luật CSS sai không báo lỗi, không cảnh báo, và đọc lên vẫn thuyết phục. Ba
|
|
254
|
+
cơ chế, cả ba đo được trong một ngày:
|
|
255
|
+
|
|
256
|
+
1. **Nhắm vào class không tồn tại.** `weekdays: cn("flex", …)` — không có class
|
|
257
|
+
DS, nên mọi luật viết cho `.ui-calendar-weekdays` chưa từng khớp lần nào.
|
|
258
|
+
2. **Thua tầng khác.** `buttonVariants` đặt `rounded-[var(--button-radius)]` như
|
|
259
|
+
một Tailwind **utility**, mà `utilities` sau `components` — nên không luật
|
|
260
|
+
components nào đổi được bo góc của một Button. Phải trỏ lại chính biến đó.
|
|
261
|
+
Cùng lớp: CSS bên thứ ba nhập KHÔNG layer thắng mọi thứ; nhập sai vị trí
|
|
262
|
+
layer thì thua cả reset. Thứ tự đúng: `theme, base, vendor, components, utilities`.
|
|
263
|
+
3. **Class ở phần tử này, sơn ở phần tử kia.** RDP đặt `day-selected` lên `<td>`,
|
|
264
|
+
còn nền/bo góc ở `<button>` bên trong → ngày chọn ra hình vuông sắc trong khi
|
|
265
|
+
hover thì tròn. Quy tắc: **một phần tử sở hữu bề mặt**, mọi trạng thái tô lên nó.
|
|
266
|
+
|
|
267
|
+
Cách duy nhất phát hiện: **mở trang, `getComputedStyle`, rồi CHỤP MÀN HÌNH.** Đo
|
|
268
|
+
đúng thuộc tính vừa sửa là chưa đủ — ba lần liên tiếp tôi báo "xong" trong khi
|
|
269
|
+
khối đó đang vỡ ở chỗ khác.
|
|
270
|
+
|
|
271
|
+
Token màu có HAI TẦNG: `--success/--warning/--info/--destructive` là màu **TÔ**;
|
|
272
|
+
chữ phải đọc `--text-success/-warning/-info/-error`. Đo: `Text tone="warning"`
|
|
273
|
+
đọc nhầm tầng cho **1,74:1**, đúng tầng cho **5,90:1**.
|
|
274
|
+
|
|
275
|
+
## 5d. Tra catalog TRƯỚC khi tự dựng — bốn lần trong một ngày
|
|
276
|
+
|
|
277
|
+
`CardContent flush` (đường kẻ chạm mép), `CardHeader banded` (header có kẻ khi
|
|
278
|
+
thân là danh sách flush), `ListRow asChild` (hàng LÀ liên kết, thay cho một nút
|
|
279
|
+
rời), `Calendar bordered` — cả bốn **đã có sẵn** và tôi vẫn tự dựng bằng thứ
|
|
280
|
+
khác, vì không hỏi. Lỗi không phải "đoán sai tên prop" mà là **cho rằng thứ đó
|
|
281
|
+
không tồn tại nên không hỏi**.
|
|
282
|
+
|
|
283
|
+
Trước khi viết bất kỳ bố cục nào: `search_components` + `get_component`. Rẻ hơn
|
|
284
|
+
mọi lần sửa sau.
|
|
285
|
+
|
|
286
|
+
**Nhưng catalog chở PROP, không chở LUẬT BỐ CỤC** — và đó là một khoảng trống
|
|
287
|
+
thật của catalog, không chỉ là lỗi của người dùng nó. Ví dụ đo được: `CardBar`
|
|
288
|
+
trong manifest có đúng một prop (`extra`), không dòng nào nói nó **tự lấy đường
|
|
289
|
+
kẻ theo VỊ TRÍ** — đầu thì kẻ dưới, cuối thì kẻ trên, ở giữa thì cả hai. Luật ấy
|
|
290
|
+
chỉ nằm trong chú thích của `src/styles/card-layout.css`, cùng chỗ định nghĩa hai
|
|
291
|
+
nhịp `section` (header phẳng) và `band` (header có kẻ).
|
|
292
|
+
|
|
293
|
+
Nên khi làm bố cục bên trong một component của DS: **mở tệp `*-layout.css` của
|
|
294
|
+
nó ra đọc**. Một agent hỏi MCP đúng cách vẫn sẽ không biết những luật này.
|
|
295
|
+
|
|
296
|
+
## 6. Thứ KHÔNG đẩy lên DS
|
|
297
|
+
|
|
298
|
+
- Bố cục của một trang cụ thể ("dashboard cần bốn thẻ ngang").
|
|
299
|
+
- Số đo của một màn ("cột vai trò rộng 8rem").
|
|
300
|
+
- Bất cứ thứ gì biết về miền nghiệp vụ của app này.
|
|
301
|
+
|
|
302
|
+
DS sở hữu **hình dạng**. Màn hình sở hữu **nội dung**. Đẩy nhầm hướng làm DS
|
|
303
|
+
phình ra thành thứ không ai nhớ nổi — cũng hỏng như để nó quá hẹp.
|
|
304
|
+
|
|
305
|
+
## 7. Năm cách hỏng đã đo được — đừng lặp lại
|
|
306
|
+
|
|
307
|
+
1. **Chẩn đoán bằng mắt rồi sửa.** Một lần đổ lỗi lệch header cho DS; hoá ra là
|
|
308
|
+
heuristic `onChat` của chính consumer. Đo trước, sửa sau.
|
|
309
|
+
2. **Làm tròn số đo cho sạch lint.** Thiết kế cần 12px, thang bậc tên có 8 và
|
|
310
|
+
16 — "gần nhất" là một phép đoán, và mỗi khe lệch 4px × n phần tử là cả khối
|
|
311
|
+
trôi. Giữ nguyên literal và mở đường cho DS.
|
|
312
|
+
3. **Đọc nhầm nguồn rồi kết luận chắc nịch.** Một lần đọc `.ui-inline-*` trong
|
|
313
|
+
khi thứ đang chạy là `.ui-flex-gap-*`, rồi tuyên bố "đã sửa rồi". Trích đúng
|
|
314
|
+
dòng đang chạy, không phải dòng trông giống.
|
|
315
|
+
4. **Chạy audit sai chỗ.** `ui-audit` chỉ báo lỗi khi chạy TRONG cây consumer;
|
|
316
|
+
chạy nó ở `/tmp` ra 0 lỗi và ru ngủ.
|
|
317
|
+
5. **Phép thử đột biến không thật sự đột biến.** Một lượt
|
|
318
|
+
`perl -0pi -e 's/data-slot="x"/BROKEN/'` thiếu cờ `/g` chỉ thay lần khớp ĐẦU
|
|
319
|
+
TIÊN — mà lần đầu lại nằm trong một dòng chú thích, nên mã chạy không hề đổi
|
|
320
|
+
và phép kiểm "không đỏ". Suýt kết luận rằng assertion là rỗng. Sau khi phá,
|
|
321
|
+
hãy XÁC NHẬN mình đã phá đúng chỗ (`git diff` một dòng) trước khi tin vào kết
|
|
322
|
+
quả màu.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* init-guinea-pig — install the GUINEA-PIG skill into a consumer app.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately NOT part of `init-agent-kit`. The agent kit is for every consumer; this skill
|
|
6
|
+
* carries an obligation only a guinea-pig repo accepts — that a gap found here is fixed UPSTREAM,
|
|
7
|
+
* in @godxjp/ui, rather than worked around locally. Installing it in an ordinary consumer would
|
|
8
|
+
* tell its agent to go edit a library it has no mandate over.
|
|
9
|
+
*/
|
|
10
|
+
import { copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
import { KIT_VERSION, shouldSkip } from "./_agent-setup.mjs";
|
|
15
|
+
|
|
16
|
+
const root = process.env.INIT_CWD || process.cwd();
|
|
17
|
+
const skip = shouldSkip(root);
|
|
18
|
+
|
|
19
|
+
if (skip === "self") {
|
|
20
|
+
console.error("init-guinea-pig is for CONSUMER apps, not the @godxjp/ui repo itself.");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
if (skip === "no-package") {
|
|
24
|
+
console.error(`No package.json at ${root} — run this from a consumer app's root.`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const source = join(dirname(fileURLToPath(import.meta.url)), "guinea-pig-skill.md");
|
|
29
|
+
const target = join(root, ".claude", "skills", "godx-ui-guinea-pig", "SKILL.md");
|
|
30
|
+
|
|
31
|
+
if (existsSync(target)) {
|
|
32
|
+
console.log(` guinea-pig skill already present:\n ${target}`);
|
|
33
|
+
console.log(
|
|
34
|
+
"\n Its BASE sections now refresh on `npm update @godxjp/ui`; anything you appended below\n" +
|
|
35
|
+
" the `# 8.` marker is preserved. Re-running this command changes nothing.\n",
|
|
36
|
+
);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
41
|
+
copyFileSync(source, target);
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
* Record the opt-in so `postinstall` can keep the skill current.
|
|
45
|
+
*
|
|
46
|
+
* Without this the file would be install-once like the rest of the kit used to be: a repo that
|
|
47
|
+
* opted in on 19.5 would still be reading 19.5's guidance after a dozen upgrades, which is the
|
|
48
|
+
* exact staleness this whole change exists to remove. The marker lives next to the skill rather
|
|
49
|
+
* than in package.json so it travels with the thing it describes.
|
|
50
|
+
*/
|
|
51
|
+
writeFileSync(join(dirname(target), ".guinea-pig-optin"), `${KIT_VERSION}\n`);
|
|
52
|
+
|
|
53
|
+
console.log(`
|
|
54
|
+
guinea-pig skill installed:
|
|
55
|
+
• ${target}
|
|
56
|
+
|
|
57
|
+
This repo now carries the guinea-pig obligation: a gap found here is fixed in @godxjp/ui,
|
|
58
|
+
not worked around locally. Append a repo-specific section to the file for anything only
|
|
59
|
+
this app knows — a deliberate audit exception, its package manager, its open findings.
|
|
60
|
+
|
|
61
|
+
Restart your agent to load the skill.
|
|
62
|
+
`);
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
* access to the component catalog + audit rules WITHOUT any manual step. Non-destructive (only
|
|
5
5
|
* adds a missing server entry) and guarded so it never runs in CI or in the library's own repo.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
ensureClaudeMd,
|
|
9
|
+
ensureMcpJson,
|
|
10
|
+
ensureConsumerRules,
|
|
11
|
+
refreshGuineaPigSkill,
|
|
12
|
+
shouldSkip,
|
|
13
|
+
writeWorkflowMd,
|
|
14
|
+
} from "./_agent-setup.mjs";
|
|
8
15
|
|
|
9
16
|
const root = process.env.INIT_CWD || process.cwd();
|
|
10
17
|
|
|
@@ -18,9 +25,13 @@ try {
|
|
|
18
25
|
// but no mandate). Only the hooks — which DO change the loop — stay behind `init-agent`.
|
|
19
26
|
const md = ensureClaudeMd(root);
|
|
20
27
|
const wf = writeWorkflowMd(root);
|
|
21
|
-
|
|
28
|
+
const skill = refreshGuineaPigSkill(root);
|
|
29
|
+
const rules = ensureConsumerRules(root);
|
|
30
|
+
if (r === "present" && md === "present" && !wf && !skill && !rules) process.exit(0); // current — stay quiet
|
|
22
31
|
console.log(
|
|
23
32
|
`\n @godxjp/ui → MCP in .mcp.json (${r}); workflow mandate in CLAUDE.md (${md}).\n` +
|
|
33
|
+
(rules ? ` common consumer rules in .ai/rules/godxjp-ui.md (glob ${rules}/**).\n` : "") +
|
|
34
|
+
(skill ? " guinea-pig skill refreshed to this version (your section 8 kept).\n" : "") +
|
|
24
35
|
" Your agent now has live component + audit guidance. Restart it to pick up the MCP.\n" +
|
|
25
36
|
" For auto-audit on every edit (PostToolUse + SessionStart hooks):\n" +
|
|
26
37
|
" npx @godxjp/ui init-agent\n",
|
package/scripts/ui-audit.mjs
CHANGED
|
@@ -28,6 +28,23 @@ const EMOJI = /\p{Extended_Pictographic}/u;
|
|
|
28
28
|
/** Regional-indicator pairs = emoji flags (🇯🇵) — broken on Win/Linux; use Intl.DisplayNames. */
|
|
29
29
|
const EMOJI_FLAG = /\p{Regional_Indicator}/u;
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* The attribute run of a JSX OPEN TAG, for rules that must see the whole element rather than one
|
|
33
|
+
* class name. Two things a plain `[^>]*` gets wrong, both of them the "green because it measured
|
|
34
|
+
* nothing" shape:
|
|
35
|
+
*
|
|
36
|
+
* 1. `[^>]` stops at the `>` of an arrow function, so `<Button onClick={() => go()} size="icon">`
|
|
37
|
+
* was invisible while the identical `<Button size="icon" onClick={() => go()}>` was flagged.
|
|
38
|
+
* A rule that depends on ATTRIBUTE ORDER is a rule an author flips by moving a prop — the same
|
|
39
|
+
* defect `no-hand-rolled-surface` had with CLASS order. `(?:=>[^>]*)*` lets the run step over
|
|
40
|
+
* an arrow and keep going; the tag's own `>` still ends it.
|
|
41
|
+
* 2. The scanner below feeds rules ONE LINE at a time, and prettier wraps any element wider than
|
|
42
|
+
* printWidth onto several lines. Every rule anchored on `<Tag …>` therefore only ever saw
|
|
43
|
+
* single-line elements. Rules carrying `spansElement: true` are matched against the whole file
|
|
44
|
+
* instead (line number derived from the match offset), so wrapping no longer hides a violation.
|
|
45
|
+
*/
|
|
46
|
+
const ATTRS = String.raw`[^>]*(?:=>[^>]*)*`;
|
|
47
|
+
|
|
31
48
|
/**
|
|
32
49
|
* @type {{id:string, severity:'error'|'warn', test:RegExp, message:string, standard?:string, exempt?:RegExp}[]}
|
|
33
50
|
*
|
|
@@ -70,7 +87,25 @@ const RULES = [
|
|
|
70
87
|
severity: "warn",
|
|
71
88
|
// rounded + border/bg on the consumer's own element = a fake Card / Badge / Avatar / ListRow
|
|
72
89
|
// that drifts from the real ones (a 38px account pill beside 32px controls).
|
|
73
|
-
|
|
90
|
+
//
|
|
91
|
+
// ORDER-INDEPENDENT, and it has to be: the old single regex required `rounded` to appear
|
|
92
|
+
// BEFORE `border`/`bg-`, so the identical element passed or failed depending on how the
|
|
93
|
+
// classes happened to be sorted — and prettier's Tailwind plugin sorts them. A `bg-current …
|
|
94
|
+
// rounded-full` dot was clean, `prettier --write` reordered it to `rounded-full bg-current`,
|
|
95
|
+
// and the warning appeared with no source change. A rule a formatter can flip is a rule
|
|
96
|
+
// nobody can trust.
|
|
97
|
+
test: (line) => {
|
|
98
|
+
const m = line.match(/className=(?:"([^"]*)"|'([^']*)'|\{`([^`]*)`)/);
|
|
99
|
+
if (!m) return false;
|
|
100
|
+
const cls = m[1] ?? m[2] ?? m[3] ?? "";
|
|
101
|
+
// A MARKER is not a surface. `size-1` is 4px — a dot, a pip, a status bead. The primitives
|
|
102
|
+
// this rule points at (Card, Badge, Avatar, ListRow, Descriptions, EmptyState) all start at
|
|
103
|
+
// control height, so none of them can express one, and flagging it sends the reader looking
|
|
104
|
+
// for a component that does not exist. The cut-off is `size-2` / 8px: below that there is no
|
|
105
|
+
// room for the padding and type that make something a surface.
|
|
106
|
+
if (/\b(?:size|[wh])-(?:0\.5|1|1\.5|2)\b/.test(cls)) return false;
|
|
107
|
+
return /\brounded(?:-(?:full|sm|md|lg|xl|2xl))?\b/.test(cls) && /\b(?:border|bg-)/.test(cls);
|
|
108
|
+
},
|
|
74
109
|
message:
|
|
75
110
|
"Hand-rolled surface (rounded + border/bg) — use Card, Badge, Avatar, ListRow, Descriptions or EmptyState so height, padding and radius come from the tokens (docs/CONSUMER-RULES.md §4).",
|
|
76
111
|
},
|
|
@@ -92,11 +127,15 @@ const RULES = [
|
|
|
92
127
|
{
|
|
93
128
|
id: "status-tone-not-variant",
|
|
94
129
|
severity: "error",
|
|
130
|
+
spansElement: true,
|
|
95
131
|
// Only the tone-driven status components (Badge/Tag/StatCard) are wrong here — they expose a
|
|
96
132
|
// `tone` prop and reserve `variant` for STRUCTURE (default|secondary|outline). Button, Alert,
|
|
97
133
|
// DropdownMenuItem, ContextMenuItem, AlertDialog etc. legitimately use `variant` for emphasis,
|
|
98
134
|
// so they must NOT be flagged.
|
|
99
|
-
test:
|
|
135
|
+
test: new RegExp(
|
|
136
|
+
`<(?:Badge|Tag|StatCard)\\b${ATTRS}\\bvariant=["'](?:success|warning|destructive|info|neutral)["']`,
|
|
137
|
+
"g",
|
|
138
|
+
),
|
|
100
139
|
message:
|
|
101
140
|
"Badge/Tag/StatCard status uses tone, not variant (variant is structural: default|secondary|outline). Use tone='success|warning|destructive|info|neutral'.",
|
|
102
141
|
},
|
|
@@ -110,7 +149,12 @@ const RULES = [
|
|
|
110
149
|
{
|
|
111
150
|
id: "value-callback-on-value-change",
|
|
112
151
|
severity: "error",
|
|
113
|
-
|
|
152
|
+
spansElement: true,
|
|
153
|
+
test: new RegExp(
|
|
154
|
+
"<(?:Checkbox\\.Group|Upload|Cascader|TreeSelect|Transfer|SearchSelect|DatePicker|" +
|
|
155
|
+
`DateRangePicker|TimePicker|ColorPicker|LocalePicker|TimezonePicker|DateFormatPicker|TimeFormatPicker)\\b${ATTRS}\\bonChange=`,
|
|
156
|
+
"g",
|
|
157
|
+
),
|
|
114
158
|
message:
|
|
115
159
|
"Abstract value components use onValueChange, not onChange. Reserve onChange for DOM events.",
|
|
116
160
|
},
|
|
@@ -219,7 +263,8 @@ const RULES = [
|
|
|
219
263
|
{
|
|
220
264
|
id: "card-manual-padding",
|
|
221
265
|
severity: "error",
|
|
222
|
-
|
|
266
|
+
spansElement: true,
|
|
267
|
+
test: new RegExp(`<Card\\b${ATTRS}\\bp-[1-9]`, "g"),
|
|
223
268
|
message:
|
|
224
269
|
"Don't hand-roll padding on <Card> (className='p-4'…) — wrap the body in <CardContent>. (p-0 for a full-bleed table is fine.)",
|
|
225
270
|
},
|
|
@@ -232,7 +277,8 @@ const RULES = [
|
|
|
232
277
|
{
|
|
233
278
|
id: "manual-field-error",
|
|
234
279
|
severity: "warn",
|
|
235
|
-
|
|
280
|
+
spansElement: true,
|
|
281
|
+
test: new RegExp(`<p${ATTRS}className="[^"]*text-(?:xs|sm)[^"]*text-destructive`, "g"),
|
|
236
282
|
message:
|
|
237
283
|
"Field errors should use <FormField error=…>, not a hand-rolled <p> (rules §1). OK only for checkbox/radio groups.",
|
|
238
284
|
},
|
|
@@ -281,9 +327,13 @@ const RULES = [
|
|
|
281
327
|
{
|
|
282
328
|
id: "icon-button-needs-name",
|
|
283
329
|
severity: "warn",
|
|
330
|
+
spansElement: true,
|
|
284
331
|
// An icon-only Button (size="icon") with no author-supplied accessible name. A combobox/icon
|
|
285
332
|
// button's name is computed from author (aria-label / aria-labelledby / title), not glyph content.
|
|
286
|
-
test:
|
|
333
|
+
test: new RegExp(
|
|
334
|
+
`<Button\\b(?=${ATTRS}\\bsize=["']icon["'])(?!${ATTRS}\\b(?:aria-label|aria-labelledby|title)=)${ATTRS}>`,
|
|
335
|
+
"g",
|
|
336
|
+
),
|
|
287
337
|
standard: "WCAG 2.2 SC 4.1.2 · 1.1.1 · WAI-ARIA 1.2",
|
|
288
338
|
message:
|
|
289
339
|
"Icon-only <Button size=\"icon\"> needs an accessible name — add aria-label={t('…')}. The icon is decorative (aria-hidden); the name comes from the author, not the glyph.",
|
|
@@ -291,7 +341,8 @@ const RULES = [
|
|
|
291
341
|
{
|
|
292
342
|
id: "img-needs-alt",
|
|
293
343
|
severity: "warn",
|
|
294
|
-
|
|
344
|
+
spansElement: true,
|
|
345
|
+
test: new RegExp(`<img\\b(?!${ATTRS}\\balt=)${ATTRS}>`, "g"),
|
|
295
346
|
standard: "WCAG 2.2 SC 1.1.1 · HTML Living Standard (WHATWG)",
|
|
296
347
|
message:
|
|
297
348
|
'Every <img> needs an alt attribute (alt="" for purely decorative images). Prefer the <Avatar>/<AspectRatio> primitives for product imagery.',
|
|
@@ -476,16 +527,22 @@ function walk(dir, acc = []) {
|
|
|
476
527
|
// Structural: a <Card> (without p-0) whose first child is body content rather than a Card
|
|
477
528
|
// sub-component sits FLUSH (no padding). Per-line regexes can't see across lines, so this is a
|
|
478
529
|
// whole-file pass. The body must be wrapped in <CardContent> (titles in <CardHeader>).
|
|
479
|
-
const CARD_FLUSH =
|
|
480
|
-
|
|
530
|
+
const CARD_FLUSH = new RegExp(
|
|
531
|
+
`<Card(?!${ATTRS}\\bp-0\\b)(?:\\s${ATTRS})?>\\s*<(?!CardContent|CardHeader|CardCover|CardFooter|CardBar|\\/Card)`,
|
|
532
|
+
"g",
|
|
533
|
+
);
|
|
481
534
|
|
|
482
535
|
// Structural: a bare <label>/<Label> paired with a TEXT control (its sibling) instead of a
|
|
483
536
|
// <FormField>. FormField OWNS the label↔control association (htmlFor/id), aria-describedby/
|
|
484
537
|
// error wiring, AND the field rhythm (label gap + field spacing) — a hand-rolled Label+Input
|
|
485
538
|
// loses all of it (the cramped login-form failure mode). Checkbox/Radio/Switch use Field/Label
|
|
486
539
|
// legitimately, so they are NOT matched. Whole-file pass (the pair spans lines).
|
|
487
|
-
const BARE_FIELD =
|
|
488
|
-
|
|
540
|
+
const BARE_FIELD = new RegExp(
|
|
541
|
+
`<(?:label|Label)\\b${ATTRS}>[\\s\\S]{0,240}?</(?:label|Label)>\\s*` +
|
|
542
|
+
"<(?:Input|Select|Textarea|NumberInput|SearchInput|SearchSelect|DatePicker|DateRangePicker|" +
|
|
543
|
+
"TimePicker|MonthPicker|MonthRangePicker|Cascader|TreeSelect|input)\\b",
|
|
544
|
+
"g",
|
|
545
|
+
);
|
|
489
546
|
|
|
490
547
|
// Two <Card> siblings written back to back at the same indentation with nothing between them.
|
|
491
548
|
// Direct children of PageContainer are spaced by the page; anywhere else they touch.
|
|
@@ -507,17 +564,20 @@ const ACTIVE_RULES =
|
|
|
507
564
|
SELF && !args.includes("--consumer") ? RULES.filter((r) => r.scope !== "consumer") : RULES;
|
|
508
565
|
|
|
509
566
|
const findings = [];
|
|
567
|
+
let filesScanned = 0;
|
|
510
568
|
for (const dir of SCAN_DIRS) {
|
|
511
569
|
for (const file of walk(isAbsolute(dir) ? dir : join(CWD, dir))) {
|
|
512
570
|
const rel = relative(CWD, file);
|
|
571
|
+
filesScanned += 1;
|
|
513
572
|
const content = readFileSync(file, "utf8");
|
|
514
573
|
const origLines = content.split("\n");
|
|
515
574
|
const scanContent = stripComments(content); // comments blanked; strings + line numbers kept
|
|
516
575
|
const scanLines = scanContent.split("\n");
|
|
517
576
|
scanLines.forEach((line, i) => {
|
|
518
577
|
for (const rule of ACTIVE_RULES) {
|
|
578
|
+
if (rule.spansElement) continue; // matched over the whole file below, not line by line
|
|
519
579
|
if (
|
|
520
|
-
rule.test.test(line) &&
|
|
580
|
+
(typeof rule.test === "function" ? rule.test(line) : rule.test.test(line)) &&
|
|
521
581
|
!isSuppressed(rule.id, origLines[i], origLines[i - 1]) &&
|
|
522
582
|
!isExempt(rule, origLines[i], origLines[i - 1])
|
|
523
583
|
) {
|
|
@@ -533,6 +593,26 @@ for (const dir of SCAN_DIRS) {
|
|
|
533
593
|
}
|
|
534
594
|
}
|
|
535
595
|
});
|
|
596
|
+
// Element-spanning rules: matched against the WHOLE file, because a JSX element prettier wrapped
|
|
597
|
+
// over five lines is invisible to a line-by-line scan. Same offset→line-number idiom as the
|
|
598
|
+
// block rules below.
|
|
599
|
+
for (const rule of ACTIVE_RULES) {
|
|
600
|
+
if (!rule.spansElement) continue;
|
|
601
|
+
for (const match of scanContent.matchAll(rule.test)) {
|
|
602
|
+
const lineNo = scanContent.slice(0, match.index).split("\n").length;
|
|
603
|
+
if (isSuppressed(rule.id, origLines[lineNo - 1], origLines[lineNo - 2])) continue;
|
|
604
|
+
if (isExempt(rule, origLines[lineNo - 1], origLines[lineNo - 2])) continue;
|
|
605
|
+
findings.push({
|
|
606
|
+
file: rel,
|
|
607
|
+
line: lineNo,
|
|
608
|
+
rule: rule.id,
|
|
609
|
+
severity: rule.severity,
|
|
610
|
+
standard: rule.standard,
|
|
611
|
+
message: rule.message,
|
|
612
|
+
snippet: match[0].replace(/\s+/g, " ").slice(0, 120),
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
536
616
|
for (const match of scanContent.matchAll(CARD_FLUSH)) {
|
|
537
617
|
const lineNo = scanContent.slice(0, match.index).split("\n").length;
|
|
538
618
|
if (isSuppressed("card-needs-content", origLines[lineNo - 1], origLines[lineNo - 2]))
|
|
@@ -589,7 +669,28 @@ for (const dir of SCAN_DIRS) {
|
|
|
589
669
|
const errors = findings.filter((f) => f.severity === "error");
|
|
590
670
|
const warnings = findings.filter((f) => f.severity === "warn");
|
|
591
671
|
|
|
592
|
-
|
|
672
|
+
// A run that opened NO file is not a clean run. The default SCAN_DIRS are a consumer's
|
|
673
|
+
// `resources/js/{components,pages,layouts}`; `walk()` swallows ENOENT and returns [], so in any
|
|
674
|
+
// tree without that layout — this repo included — `pnpm audit` printed
|
|
675
|
+
// "✓ No UI-standardization violations found." and exited 0 having read nothing at all.
|
|
676
|
+
if (filesScanned === 0) {
|
|
677
|
+
const message =
|
|
678
|
+
`ui-audit scanned 0 files — none of [${SCAN_DIRS.join(", ")}] exists (or all were filtered). ` +
|
|
679
|
+
`Pass the directories to scan, e.g. \`node scripts/ui-audit.mjs src docs\`. ` +
|
|
680
|
+
`Reporting a clean audit without opening a file is not a result.`;
|
|
681
|
+
if (asJson) {
|
|
682
|
+
process.stdout.write(
|
|
683
|
+
JSON.stringify({ summary: null, error: message, findings: [] }, null, 2) + "\n",
|
|
684
|
+
);
|
|
685
|
+
} else {
|
|
686
|
+
console.error(`✗ ${message}`);
|
|
687
|
+
}
|
|
688
|
+
process.exitCode = 2;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (filesScanned === 0) {
|
|
692
|
+
// already reported above
|
|
693
|
+
} else if (asJson) {
|
|
593
694
|
process.stdout.write(
|
|
594
695
|
JSON.stringify(
|
|
595
696
|
{ summary: { errors: errors.length, warnings: warnings.length }, findings },
|
|
@@ -622,4 +723,4 @@ if (asJson) {
|
|
|
622
723
|
}
|
|
623
724
|
|
|
624
725
|
// See the note above --rules: exitCode, so a large JSON report drains fully.
|
|
625
|
-
process.exitCode = errors.length > 0 ? 1 : 0;
|
|
726
|
+
if (filesScanned > 0) process.exitCode = errors.length > 0 ? 1 : 0;
|
|
File without changes
|