@godxjp/ui 19.5.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/general/typography.d.ts +7 -1
- package/dist/components/general/typography.js +20 -5
- 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/general.prop.d.ts +31 -1
- 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 +13 -1
- 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 +16 -4
- package/dist/tokens/base.css +2 -1
- package/dist/tokens/components/data-display.css +11 -0
- package/dist/tokens/components/shell.css +3 -1
- package/dist/tokens/components/text.css +7 -0
- 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/general/typography.tsx +84 -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,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
|