@godxjp/ui 20.0.0 → 20.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/app/theme-axes.d.ts +2 -0
- package/dist/app/theme-axes.js +45 -0
- package/dist/components/charts/chart-cartesian.d.ts +2 -1
- package/dist/components/charts/chart-cartesian.js +52 -3
- package/dist/components/charts/chart-category-axis.d.ts +55 -0
- package/dist/components/charts/chart-category-axis.js +93 -0
- package/dist/components/charts/chart-frame.d.ts +10 -1
- package/dist/components/charts/chart-frame.js +19 -3
- package/dist/components/charts/compact-bar-trend.d.ts +1 -1
- package/dist/components/charts/compact-bar-trend.js +2 -0
- package/dist/components/charts/pie-chart.d.ts +1 -1
- package/dist/components/charts/pie-chart.js +12 -1
- package/dist/components/charts/recharts-peer.d.ts +49 -0
- package/dist/components/charts/recharts-peer.js +45 -0
- package/dist/components/data-display/card.d.ts +12 -1
- package/dist/components/data-display/card.js +22 -4
- package/dist/components/data-display/code-block.js +6 -2
- package/dist/components/data-display/data-table.d.ts +39 -4
- package/dist/components/data-display/data-table.js +802 -270
- package/dist/components/data-display/descriptions.d.ts +27 -6
- package/dist/components/data-display/descriptions.js +58 -17
- package/dist/components/data-display/index.d.ts +2 -0
- package/dist/components/data-display/index.js +2 -0
- package/dist/components/data-display/list-row.d.ts +10 -1
- package/dist/components/data-display/list-row.js +7 -1
- package/dist/components/data-display/popover.d.ts +2 -2
- package/dist/components/data-display/popover.js +61 -11
- package/dist/components/data-display/range-timeline.d.ts +38 -0
- package/dist/components/data-display/range-timeline.js +161 -0
- package/dist/components/data-display/scroll-area.js +13 -2
- package/dist/components/data-display/service-launcher-card.js +10 -10
- package/dist/components/data-display/table.d.ts +11 -3
- package/dist/components/data-display/table.js +48 -18
- package/dist/components/data-display/tree-list.js +4 -9
- package/dist/components/data-entry/calendar.d.ts +2 -2
- package/dist/components/data-entry/calendar.js +12 -1
- package/dist/components/data-entry/cascader.d.ts +1 -1
- package/dist/components/data-entry/cascader.js +188 -24
- package/dist/components/data-entry/color-picker.d.ts +1 -1
- package/dist/components/data-entry/color-picker.js +17 -5
- package/dist/components/data-entry/control-appearance.d.ts +64 -0
- package/dist/components/data-entry/control-appearance.js +39 -0
- package/dist/components/data-entry/control-surface.d.ts +61 -0
- package/dist/components/data-entry/control-surface.js +39 -0
- package/dist/components/data-entry/date-picker.d.ts +1 -1
- package/dist/components/data-entry/date-picker.js +344 -113
- package/dist/components/data-entry/date-range-picker.d.ts +1 -1
- package/dist/components/data-entry/date-range-picker.js +278 -140
- package/dist/components/data-entry/form-field.d.ts +1 -1
- package/dist/components/data-entry/form-field.js +39 -4
- package/dist/components/data-entry/form.d.ts +4 -0
- package/dist/components/data-entry/form.js +4 -2
- package/dist/components/data-entry/index.d.ts +7 -3
- package/dist/components/data-entry/index.js +10 -1
- package/dist/components/data-entry/input-otp.d.ts +1 -1
- package/dist/components/data-entry/input.d.ts +12 -34
- package/dist/components/data-entry/input.js +92 -24
- package/dist/components/data-entry/month-picker.d.ts +1 -1
- package/dist/components/data-entry/month-picker.js +47 -10
- package/dist/components/data-entry/month-range-picker.d.ts +1 -1
- package/dist/components/data-entry/month-range-picker.js +51 -11
- package/dist/components/data-entry/number-input.d.ts +7 -0
- package/dist/components/data-entry/number-input.js +147 -98
- package/dist/components/data-entry/password-input.d.ts +1 -1
- package/dist/components/data-entry/radio.d.ts +1 -1
- package/dist/components/data-entry/radio.js +61 -13
- package/dist/components/data-entry/search-input.d.ts +1 -1
- package/dist/components/data-entry/search-input.js +16 -2
- package/dist/components/data-entry/search-select.d.ts +2 -2
- package/dist/components/data-entry/search-select.js +209 -78
- package/dist/components/data-entry/select.d.ts +22 -4
- package/dist/components/data-entry/select.js +215 -163
- package/dist/components/data-entry/slider.d.ts +9 -1
- package/dist/components/data-entry/slider.js +87 -9
- package/dist/components/data-entry/switch.d.ts +3 -0
- package/dist/components/data-entry/switch.js +29 -3
- package/dist/components/data-entry/textarea.d.ts +14 -56
- package/dist/components/data-entry/textarea.js +80 -33
- package/dist/components/data-entry/time-picker.d.ts +2 -2
- package/dist/components/data-entry/time-picker.js +333 -109
- package/dist/components/data-entry/time-range-picker.d.ts +5 -0
- package/dist/components/data-entry/time-range-picker.js +89 -0
- package/dist/components/data-entry/transfer.d.ts +1 -1
- package/dist/components/data-entry/transfer.js +86 -28
- package/dist/components/data-entry/tree-select.d.ts +1 -1
- package/dist/components/data-entry/tree-select.js +201 -113
- package/dist/components/data-entry/tree-utils.js +7 -14
- package/dist/components/data-entry/upload-files.d.ts +2 -0
- package/dist/components/data-entry/upload-files.js +31 -0
- package/dist/components/data-entry/upload-request.d.ts +4 -0
- package/dist/components/data-entry/upload-request.js +53 -0
- package/dist/components/data-entry/upload-types.d.ts +28 -0
- package/dist/components/data-entry/upload-types.js +2 -0
- package/dist/components/data-entry/upload.d.ts +2 -2
- package/dist/components/data-entry/upload.js +475 -115
- package/dist/components/feedback/dialog.js +16 -19
- package/dist/components/general/button.d.ts +1 -1
- package/dist/components/general/button.js +5 -1
- package/dist/components/general/index.d.ts +1 -0
- package/dist/components/general/index.js +2 -0
- package/dist/components/general/typography.d.ts +3 -0
- package/dist/components/general/typography.js +15 -1
- package/dist/components/general/visually-hidden.d.ts +3 -0
- package/dist/components/general/visually-hidden.js +9 -0
- package/dist/components/layout/app-launcher.d.ts +34 -0
- package/dist/components/layout/app-launcher.js +228 -0
- package/dist/components/layout/app-shell.d.ts +2 -0
- package/dist/components/layout/app-shell.js +20 -7
- package/dist/components/layout/breadcrumb.d.ts +14 -2
- package/dist/components/layout/breadcrumb.js +46 -4
- package/dist/components/layout/flex.d.ts +1 -1
- package/dist/components/layout/flex.js +23 -2
- package/dist/components/layout/index.d.ts +3 -0
- package/dist/components/layout/index.js +5 -1
- package/dist/components/layout/org-switcher.js +20 -1
- package/dist/components/layout/page-container.d.ts +1 -1
- package/dist/components/layout/page-container.js +5 -3
- package/dist/components/layout/responsive-grid.d.ts +16 -2
- package/dist/components/layout/responsive-grid.js +29 -2
- package/dist/components/layout/topbar-item.d.ts +3 -0
- package/dist/components/layout/topbar-item.js +18 -3
- package/dist/components/layout/topbar.d.ts +1 -1
- package/dist/components/layout/topbar.js +27 -6
- package/dist/components/navigation/dropdown-menu.d.ts +30 -2
- package/dist/components/navigation/dropdown-menu.js +68 -11
- package/dist/components/navigation/pagination.d.ts +2 -2
- package/dist/components/navigation/pagination.js +155 -83
- package/dist/components/navigation/steps.d.ts +2 -2
- package/dist/components/navigation/steps.js +29 -4
- package/dist/components/navigation/tabs.d.ts +5 -33
- package/dist/components/navigation/tabs.js +232 -64
- package/dist/components/ui/input-otp.d.ts +26 -28
- package/dist/components/ui/input-otp.js +50 -7
- package/dist/components/ui/password-input.d.ts +36 -2
- package/dist/components/ui/password-input.js +27 -7
- package/dist/components/ui/rating.d.ts +25 -0
- package/dist/components/ui/rating.js +67 -27
- package/dist/components/ui/segmented.d.ts +9 -0
- package/dist/components/ui/segmented.js +17 -2
- package/dist/components/ui/separator.d.ts +4 -0
- package/dist/components/ui/separator.js +13 -0
- package/dist/components/ui/tag-input.d.ts +45 -0
- package/dist/components/ui/tag-input.js +109 -27
- package/dist/form/form-context.d.ts +29 -0
- package/dist/form/form-context.js +44 -4
- package/dist/form/form-field-array.d.ts +22 -0
- package/dist/form/form-field-array.js +50 -0
- package/dist/form/form-field-control.d.ts +2 -1
- package/dist/form/form-field-control.js +116 -40
- package/dist/form/form-root.d.ts +2 -1
- package/dist/form/form-root.js +142 -23
- package/dist/form/index.d.ts +3 -1
- package/dist/form/index.js +12 -1
- package/dist/i18n/messages/en.json +56 -5
- package/dist/i18n/messages/ja.json +56 -5
- package/dist/i18n/messages/vi.json +56 -5
- package/dist/inertia/index.d.ts +20 -0
- package/dist/inertia/index.js +60 -1
- package/dist/lib/control-styles.d.ts +25 -3
- package/dist/lib/control-styles.js +9 -3
- package/dist/lib/datetime/picker-format.d.ts +8 -0
- package/dist/lib/datetime/picker-format.js +54 -0
- package/dist/props/components/charts.prop.d.ts +21 -0
- package/dist/props/components/data-display.prop.d.ts +55 -4
- package/dist/props/components/data-entry.prop.d.ts +687 -43
- package/dist/props/components/form.prop.d.ts +142 -4
- package/dist/props/components/general.prop.d.ts +17 -1
- package/dist/props/components/index.d.ts +2 -2
- package/dist/props/components/layout.prop.d.ts +163 -6
- package/dist/props/components/navigation.prop.d.ts +113 -3
- package/dist/props/registry.d.ts +376 -10
- package/dist/props/registry.js +493 -9
- package/dist/props/vocabulary/data.prop.d.ts +179 -1
- package/dist/props/vocabulary/index.d.ts +4 -4
- package/dist/props/vocabulary/interaction.prop.d.ts +51 -2
- package/dist/props/vocabulary/navigation.prop.d.ts +40 -0
- package/dist/props/vocabulary/shared.prop.d.ts +33 -0
- package/dist/styles/badge-layout.css +2 -1
- package/dist/styles/card-layout.css +26 -4
- package/dist/styles/chart-layout.css +15 -0
- package/dist/styles/control.css +515 -14
- package/dist/styles/data-display-layout.css +241 -6
- package/dist/styles/data-entry-layout.css +13 -0
- package/dist/styles/layout.css +221 -4
- package/dist/styles/navigation-layout.css +227 -0
- package/dist/styles/shell-layout.css +229 -5
- package/dist/styles/table-layout.css +95 -6
- package/dist/styles/text-layout.css +5 -0
- package/dist/tokens/base.css +1 -0
- package/dist/tokens/components/badge.css +1 -0
- package/dist/tokens/components/chart.css +6 -0
- package/dist/tokens/components/control.css +79 -2
- package/dist/tokens/components/data-display.css +9 -0
- package/dist/tokens/components/descriptions.css +11 -0
- package/dist/tokens/components/flex.css +8 -0
- package/dist/tokens/components/navigation.css +55 -0
- package/dist/tokens/components/shell.css +50 -2
- package/dist/tokens/components/table.css +14 -0
- package/dist/tokens/semantic/layout.css +9 -0
- package/docs/CONSUMER-RULES.md +13 -0
- package/docs/DESIGN-AUTHORITY.md +43 -6
- package/docs/DEVELOPMENT.md +4 -3
- package/docs/FORMS.md +151 -6
- package/docs/FRAME-COVERAGE-REPORT.md +29 -16
- package/docs/README.md +1 -1
- package/docs/STANDARDS-vocabulary-tokens.md +1 -1
- package/docs/TESTING.md +15 -6
- package/docs/charts/cjk-category-axis.tsx +81 -0
- package/docs/data-display/card/index.tsx +22 -0
- package/docs/data-display/data-table/examples/antd-parity.tsx +257 -0
- package/docs/data-display/data-table/index.tsx +23 -0
- package/docs/data-display/descriptions.tsx +41 -0
- package/docs/data-display/scroll-area.tsx +31 -25
- package/docs/data-display/table.tsx +104 -45
- package/docs/data-display/timeline.tsx +41 -0
- package/docs/data-entry/date-picker.tsx +85 -0
- package/docs/data-entry/date-range-picker.tsx +26 -0
- package/docs/data-entry/form-dynamic-fields.tsx +199 -0
- package/docs/data-entry/form.tsx +74 -4
- package/docs/data-entry/input-otp.tsx +24 -0
- package/docs/data-entry/input.tsx +46 -1
- package/docs/data-entry/month-range-picker.tsx +1 -1
- package/docs/data-entry/select.tsx +27 -0
- package/docs/data-entry/switch.tsx +41 -0
- package/docs/data-entry/textarea.tsx +38 -0
- package/docs/data-entry/time-picker.tsx +85 -1
- package/docs/data-entry/time-range-picker.tsx +55 -0
- package/docs/data-entry/transfer.tsx +17 -0
- package/docs/data-entry/upload.tsx +56 -12
- package/docs/feedback/tooltip.tsx +1 -1
- package/docs/general/activity.tsx +2 -2
- package/docs/general/button/index.tsx +14 -1
- package/docs/general/typography.tsx +14 -1
- package/docs/layout/app-launcher.tsx +151 -0
- package/docs/layout/app-shell.tsx +11 -0
- package/docs/layout/flex.tsx +50 -0
- package/docs/layout/responsive-grid.tsx +21 -1
- package/docs/layout/topbar.tsx +5 -9
- package/docs/navigation/app-setting-picker.tsx +11 -0
- package/docs/navigation/breadcrumb.tsx +48 -0
- package/docs/navigation/dropdown-menu.tsx +21 -0
- package/docs/navigation/pagination.tsx +52 -0
- package/docs/navigation/steps.tsx +37 -0
- package/docs/navigation/tabs.tsx +97 -1
- package/docs/query/button-refetch.tsx +1 -0
- package/package.json +19 -2
- package/scripts/_agent-setup.mjs +19 -2
- package/scripts/init-guinea-pig.mjs +26 -4
- package/scripts/ui-audit.mjs +243 -30
package/scripts/ui-audit.mjs
CHANGED
|
@@ -9,13 +9,22 @@ import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
|
9
9
|
import { isAbsolute, join, relative } from "node:path";
|
|
10
10
|
|
|
11
11
|
const CWD = process.cwd();
|
|
12
|
+
const SELF = (() => {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(join(CWD, "package.json"), "utf8")).name === "@godxjp/ui";
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
})();
|
|
12
19
|
const args = process.argv.slice(2);
|
|
13
20
|
const asJson = args.includes("--format") && args[args.indexOf("--format") + 1] === "json";
|
|
14
21
|
const quiet = args.includes("--quiet");
|
|
15
22
|
const dirArgs = args.filter((a) => !a.startsWith("--") && a !== "json");
|
|
16
23
|
const SCAN_DIRS = dirArgs.length
|
|
17
24
|
? dirArgs
|
|
18
|
-
:
|
|
25
|
+
: SELF
|
|
26
|
+
? ["src", "docs"]
|
|
27
|
+
: ["resources/js/components", "resources/js/pages", "resources/js/layouts"];
|
|
19
28
|
|
|
20
29
|
const PALETTE =
|
|
21
30
|
"red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|gray|grey|slate|zinc|neutral|stone";
|
|
@@ -43,10 +52,15 @@ const EMOJI_FLAG = /\p{Regional_Indicator}/u;
|
|
|
43
52
|
* single-line elements. Rules carrying `spansElement: true` are matched against the whole file
|
|
44
53
|
* instead (line number derived from the match offset), so wrapping no longer hides a violation.
|
|
45
54
|
*/
|
|
46
|
-
const ATTRS = String.raw`[
|
|
55
|
+
const ATTRS = String.raw`(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|=>|[^>"'])*`;
|
|
47
56
|
|
|
48
57
|
/**
|
|
49
|
-
* @type {{id:string, severity:'error'|'warn', test:RegExp, message:string, standard?:string,
|
|
58
|
+
* @type {{id:string, severity:'error'|'warn', test:RegExp, message:string, standard?:string,
|
|
59
|
+
* exempt?:RegExp, classOnly?:boolean}[]}
|
|
60
|
+
*
|
|
61
|
+
* `classOnly` marks a rule whose pattern is a CLASS NAME (`gap-3`, `bg-red-500`, `pr-4`). Those are
|
|
62
|
+
* matched against the class-expression mask (see `classExpressionsOnly`), never the raw line, so
|
|
63
|
+
* product copy, an i18n value or a JSX text node that merely spells a utility is not a finding.
|
|
50
64
|
*
|
|
51
65
|
* `exempt` is an optional SECOND escape a rule may declare, matched against the ORIGINAL (un-blanked)
|
|
52
66
|
* line or the one above it. `ui-audit-disable-line <id>` silences any rule; an `exempt` marker is for
|
|
@@ -62,6 +76,7 @@ const ATTRS = String.raw`[^>]*(?:=>[^>]*)*`;
|
|
|
62
76
|
const RULES = [
|
|
63
77
|
{
|
|
64
78
|
id: "no-utility-spacing",
|
|
79
|
+
classOnly: true,
|
|
65
80
|
scope: "consumer",
|
|
66
81
|
severity: "error",
|
|
67
82
|
// Any Tailwind spacing step on the consumer's own markup. Arbitrary values ([13px]) have their
|
|
@@ -111,12 +126,14 @@ const RULES = [
|
|
|
111
126
|
},
|
|
112
127
|
{
|
|
113
128
|
id: "no-space-xy",
|
|
129
|
+
classOnly: true,
|
|
114
130
|
severity: "error",
|
|
115
131
|
test: /\bspace-[xy]-\d/,
|
|
116
132
|
message: "Use <Stack>/<Inline> with gap, not space-x/y-* (rules §5).",
|
|
117
133
|
},
|
|
118
134
|
{
|
|
119
135
|
id: "no-raw-palette-color",
|
|
136
|
+
classOnly: true,
|
|
120
137
|
severity: "error",
|
|
121
138
|
test: new RegExp(
|
|
122
139
|
`\\b(bg|text|border|ring|fill|stroke|from|to|via|divide|outline)-(${PALETTE})-\\d`,
|
|
@@ -151,7 +168,7 @@ const RULES = [
|
|
|
151
168
|
severity: "error",
|
|
152
169
|
spansElement: true,
|
|
153
170
|
test: new RegExp(
|
|
154
|
-
"<(?:Checkbox\\.Group|Upload|Cascader|TreeSelect|Transfer|SearchSelect|DatePicker|" +
|
|
171
|
+
"<(?:Checkbox\\.Group|Upload|Cascader|TreeSelect|Transfer|Select|SearchSelect|DatePicker|" +
|
|
155
172
|
`DateRangePicker|TimePicker|ColorPicker|LocalePicker|TimezonePicker|DateFormatPicker|TimeFormatPicker)\\b${ATTRS}\\bonChange=`,
|
|
156
173
|
"g",
|
|
157
174
|
),
|
|
@@ -160,6 +177,7 @@ const RULES = [
|
|
|
160
177
|
},
|
|
161
178
|
{
|
|
162
179
|
id: "no-arbitrary-hex",
|
|
180
|
+
classOnly: true,
|
|
163
181
|
severity: "error",
|
|
164
182
|
test: /(bg|text|border|ring|fill|stroke|from|to|via)-\[#[0-9a-fA-F]{3,8}\]/,
|
|
165
183
|
message: "No hardcoded hex colors in className; use design-system tokens (rules §4).",
|
|
@@ -169,6 +187,7 @@ const RULES = [
|
|
|
169
187
|
// (they start with a letter), so token-driven values stay legal (rules §4–§5).
|
|
170
188
|
{
|
|
171
189
|
id: "no-arbitrary-spacing",
|
|
190
|
+
classOnly: true,
|
|
172
191
|
severity: "error",
|
|
173
192
|
test: /\b(p|px|py|pt|pb|pl|pr|pe|ps|m|mx|my|mt|mb|ml|mr|me|ms|gap|gap-x|gap-y|inset|inset-x|inset-y|top|right|bottom|left)-\[-?\.?\d/,
|
|
174
193
|
message:
|
|
@@ -176,6 +195,7 @@ const RULES = [
|
|
|
176
195
|
},
|
|
177
196
|
{
|
|
178
197
|
id: "no-arbitrary-size",
|
|
198
|
+
classOnly: true,
|
|
179
199
|
severity: "error",
|
|
180
200
|
// `min-w-[…]` / `min-h-[…]` are allowed: a MINIMUM dimension is the legit responsive
|
|
181
201
|
// pattern (horizontal-scroll tables `<Table className="min-w-[720px]">`, collapse
|
|
@@ -187,6 +207,7 @@ const RULES = [
|
|
|
187
207
|
},
|
|
188
208
|
{
|
|
189
209
|
id: "no-arbitrary-typography",
|
|
210
|
+
classOnly: true,
|
|
190
211
|
severity: "error",
|
|
191
212
|
test: /\b(text|leading|tracking|font)-\[-?\.?\d/,
|
|
192
213
|
message:
|
|
@@ -194,6 +215,7 @@ const RULES = [
|
|
|
194
215
|
},
|
|
195
216
|
{
|
|
196
217
|
id: "no-arbitrary-radius",
|
|
218
|
+
classOnly: true,
|
|
197
219
|
severity: "error",
|
|
198
220
|
test: /\brounded(?:-[a-z]+)?-\[-?\.?\d/,
|
|
199
221
|
message: "No arbitrary radius (rounded-[6px]…). Use rounded-sm/md/lg radius tokens (rules §4).",
|
|
@@ -232,6 +254,7 @@ const RULES = [
|
|
|
232
254
|
},
|
|
233
255
|
{
|
|
234
256
|
id: "no-raw-select",
|
|
257
|
+
scope: "consumer-control",
|
|
235
258
|
severity: "error",
|
|
236
259
|
test: /<select[\s>]/,
|
|
237
260
|
message: "Use <Select> from @godxjp/ui, not a raw <select> (rules §3).",
|
|
@@ -244,18 +267,22 @@ const RULES = [
|
|
|
244
267
|
},
|
|
245
268
|
{
|
|
246
269
|
id: "no-raw-textarea",
|
|
270
|
+
scope: "consumer-control",
|
|
247
271
|
severity: "warn",
|
|
248
272
|
test: /<textarea[\s>]/,
|
|
249
273
|
message: "Use <Textarea> from @godxjp/ui, not a raw <textarea> (rules §3).",
|
|
250
274
|
},
|
|
251
275
|
{
|
|
252
276
|
id: "no-raw-input",
|
|
277
|
+
scope: "consumer-control",
|
|
253
278
|
severity: "error",
|
|
254
|
-
|
|
279
|
+
spansElement: true,
|
|
280
|
+
test: new RegExp(`<input\\b(?!${ATTRS}\\btype=["']hidden["'])${ATTRS}>`, "g"),
|
|
255
281
|
message: "Use <Input> from @godxjp/ui, not a raw <input> (rules §3).",
|
|
256
282
|
},
|
|
257
283
|
{
|
|
258
284
|
id: "no-raw-button",
|
|
285
|
+
scope: "consumer-control",
|
|
259
286
|
severity: "error",
|
|
260
287
|
test: /<button[\s>]/,
|
|
261
288
|
message: "Use <Button> from @godxjp/ui, not a raw <button> (rules §3).",
|
|
@@ -270,6 +297,7 @@ const RULES = [
|
|
|
270
297
|
},
|
|
271
298
|
{
|
|
272
299
|
id: "no-dark-color-override",
|
|
300
|
+
classOnly: true,
|
|
273
301
|
severity: "warn",
|
|
274
302
|
test: /\bdark:(bg|text|border|ring|fill|stroke)-/,
|
|
275
303
|
message: "Don't add dark: color overrides — semantic tokens already adapt (rules §4).",
|
|
@@ -291,6 +319,7 @@ const RULES = [
|
|
|
291
319
|
},
|
|
292
320
|
{
|
|
293
321
|
id: "raw-white-black",
|
|
322
|
+
classOnly: true,
|
|
294
323
|
severity: "warn",
|
|
295
324
|
test: /\b(bg|text|border)-(white|black)\b/,
|
|
296
325
|
message:
|
|
@@ -316,10 +345,12 @@ const RULES = [
|
|
|
316
345
|
},
|
|
317
346
|
{
|
|
318
347
|
id: "no-physical-direction",
|
|
348
|
+
classOnly: true,
|
|
319
349
|
severity: "warn",
|
|
320
350
|
// Physical-edge utilities break RTL. Logical equivalents: ms-/me-/ps-/pe-, start-/end-,
|
|
321
351
|
// text-start/end, border-s/e, rounded-s/e. `-mx-`/`-px-` (both edges) are RTL-safe → not matched.
|
|
322
|
-
|
|
352
|
+
exempt: /rtl-ignore:\s*\S.{10,}/,
|
|
353
|
+
test: /(?<![\w-])-?(ml|mr|pl|pr|left|right)-(?:\d|\[|auto|px|full)|(?<![\w-])(?:rounded-[tb]?[lr]|border-[lr]|text-(?:left|right))\b/,
|
|
323
354
|
standard: "W3C CSS Logical Properties & Values L1 · WCAG 2.2 (1.3.2 / reflow)",
|
|
324
355
|
message:
|
|
325
356
|
"Use LOGICAL direction utilities for RTL safety: ms-/me-/ps-/pe-, start-/end-, text-start/end, border-s/e, rounded-s/e — not physical ml-/mr-/pl-/pr-/left-/right-/text-left|right.",
|
|
@@ -357,9 +388,14 @@ const RULES = [
|
|
|
357
388
|
},
|
|
358
389
|
{
|
|
359
390
|
id: "hardcoded-currency",
|
|
391
|
+
matches: currencyMatches,
|
|
360
392
|
severity: "warn",
|
|
361
393
|
// A currency glyph glued to an interpolation in JSX text: `>¥{amount}` / `>{x}円`.
|
|
362
|
-
|
|
394
|
+
spansElement: true,
|
|
395
|
+
test: new RegExp(
|
|
396
|
+
`<(?:[A-Za-z][\\w.:]*(?:\\s${ATTRS})?)?(?<!=)>[^<>{}]*[¥$€£₫]\\s*\\{|\\}\\s*円\\s*</`,
|
|
397
|
+
"g",
|
|
398
|
+
),
|
|
363
399
|
standard: "ISO 4217 · ECMA-402 Intl.NumberFormat",
|
|
364
400
|
message:
|
|
365
401
|
"Don't hand-format currency (¥{amount}). Use Intl.NumberFormat(locale, { style: 'currency', currency }) — ISO 4217 code drives the symbol and minor units per locale.",
|
|
@@ -390,7 +426,12 @@ const RULES = [
|
|
|
390
426
|
id: "no-em-dash-in-copy",
|
|
391
427
|
severity: "warn",
|
|
392
428
|
// Em-dash (U+2014) in JSX text — the reference design uses the middot `·` for JP/EN pairs and calm copy.
|
|
393
|
-
|
|
429
|
+
spansElement: true,
|
|
430
|
+
test: new RegExp(
|
|
431
|
+
`<(?:[A-Za-z][\\w.:]*(?:\\s${ATTRS})?)?>[^<>]*[A-Za-z0-9-ヿ一-鿿]\\s*—\\s*[A-Za-z0-9-ヿ一-鿿]|` +
|
|
432
|
+
String.raw`\b(?:label|title|placeholder|description|aria-label)=["'][^"']*[A-Za-z0-9-ヿ一-鿿]\s*—\s*[A-Za-z0-9-ヿ一-鿿][^"']*["']`,
|
|
433
|
+
"g",
|
|
434
|
+
),
|
|
394
435
|
standard: "@godxjp/ui reference-design typography (best-ux) · Unicode punctuation",
|
|
395
436
|
message:
|
|
396
437
|
"No em-dash (—) in product copy. Use a middot `·` for JP/EN label pairs, or restructure into two calm sentences. Keep copy factual and quiet.",
|
|
@@ -474,6 +515,128 @@ function stripComments(src) {
|
|
|
474
515
|
return out;
|
|
475
516
|
}
|
|
476
517
|
|
|
518
|
+
/** End index (inclusive) of the string / template literal opening at `i`. `${…}` is stepped over as
|
|
519
|
+
* a brace group, so a quote nested inside an interpolation never closes the template early. */
|
|
520
|
+
function endOfString(src, i) {
|
|
521
|
+
const quote = src[i];
|
|
522
|
+
for (let j = i + 1; j < src.length; j++) {
|
|
523
|
+
const c = src[j];
|
|
524
|
+
if (c === "\\") j++;
|
|
525
|
+
else if (quote === "`" && c === "$" && src[j + 1] === "{") {
|
|
526
|
+
const close = matchBracket(src, j + 1);
|
|
527
|
+
if (close < 0) return src.length - 1;
|
|
528
|
+
j = close;
|
|
529
|
+
} else if (c === quote) return j;
|
|
530
|
+
}
|
|
531
|
+
return src.length - 1;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** End index (inclusive) of the bracket group opening at `i`, or -1 when it never closes. */
|
|
535
|
+
function matchBracket(src, i) {
|
|
536
|
+
const CLOSER = { "{": "}", "[": "]", "(": ")" };
|
|
537
|
+
const stack = [CLOSER[src[i]]];
|
|
538
|
+
for (let j = i + 1; j < src.length; j++) {
|
|
539
|
+
const c = src[j];
|
|
540
|
+
if (c === '"' || c === "'" || c === "`") j = endOfString(src, j);
|
|
541
|
+
else if (CLOSER[c]) stack.push(CLOSER[c]);
|
|
542
|
+
else if (c === stack[stack.length - 1]) {
|
|
543
|
+
stack.pop();
|
|
544
|
+
if (stack.length === 0) return j;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return -1;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** The value that starts at `i`: a literal or bracket group in full, otherwise the rest of the line
|
|
551
|
+
* (`= base + " p-2"`, a ternary) — enough to see the classes, never the next statement. */
|
|
552
|
+
function valueEnd(src, i) {
|
|
553
|
+
const c = src[i];
|
|
554
|
+
if (c === '"' || c === "'" || c === "`") return endOfString(src, i);
|
|
555
|
+
if (c === "{" || c === "[" || c === "(") {
|
|
556
|
+
const close = matchBracket(src, i);
|
|
557
|
+
return close < 0 ? src.length - 1 : close;
|
|
558
|
+
}
|
|
559
|
+
const nl = src.indexOf("\n", i);
|
|
560
|
+
return nl < 0 ? src.length - 1 : nl - 1;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** A call that BUILDS a class list — its arguments are classes wherever they are written. */
|
|
564
|
+
const CLASS_HELPER = /(?<![\w$.])(?:cn|clsx|classnames|classNames|cx|cva|tv|twMerge|twJoin)\s*\(/g;
|
|
565
|
+
/** `className={…}` / `wrapperClass = "…"` / `const flexGapClass: Record<…> = {…}` — an assignment
|
|
566
|
+
* (JSX attribute, variable, default) whose NAME says class. The optional `: …` is a type annotation. */
|
|
567
|
+
const CLASS_ASSIGN = /(?<![\w$.])([A-Za-z_$][\w$]*)\s*(?::[^=;\n]*?)?=(?!=)\s*/g;
|
|
568
|
+
/** `className: "…"` — the same, written as an object key (a DataTable column, a slots map). */
|
|
569
|
+
const CLASS_KEY = /(?<![\w$.])([A-Za-z_$][\w$]*)\s*:\s*/g;
|
|
570
|
+
/**
|
|
571
|
+
* The names a class list is actually bound to, as a SUFFIX so every prefix works: `className`,
|
|
572
|
+
* `rowClassName`, `baseClass`, `cellClasses` — plus the three conventional aliases for a class MAP,
|
|
573
|
+
* `statusStyles` / `badgeVariants` / `toneVariant`, which are how a palette of class strings is
|
|
574
|
+
* written when it is not inside `cva()`. A class list bound to a name outside this set (`const map =
|
|
575
|
+
* { … }`) is NOT scanned — that is the cost of the mask, and the reason the set errs wide.
|
|
576
|
+
*/
|
|
577
|
+
const CLASS_NAMED = /(?:class(?:es|name|names)?|styles?|variants?)$/i;
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Blank everything that is NOT a class expression, keeping newlines so line numbers stay true.
|
|
581
|
+
*
|
|
582
|
+
* The `classOnly` rules are plain regexes over source text, so before this pass any line that merely
|
|
583
|
+
* CONTAINED a utility-shaped word was reported: product copy, an i18n value, a test fixture, a JSX
|
|
584
|
+
* text node (`<Text>văn bản JSX nhắc gap-3</Text>`). The bar is zero errors, so a false positive is
|
|
585
|
+
* not a nuisance — it blocks the review. What survives the mask is the three places a class can
|
|
586
|
+
* actually be written: a class-named binding's value, a class-named object key's value, and the
|
|
587
|
+
* arguments of a class-building helper. The NAME stays inside the region, so rules that anchor on
|
|
588
|
+
* `className="` keep matching.
|
|
589
|
+
*/
|
|
590
|
+
function classExpressionsOnly(src) {
|
|
591
|
+
const keep = new Uint8Array(src.length);
|
|
592
|
+
const mark = (from, to) => {
|
|
593
|
+
for (let i = Math.max(from, 0); i <= Math.min(to, src.length - 1); i++) keep[i] = 1;
|
|
594
|
+
};
|
|
595
|
+
for (const m of src.matchAll(CLASS_HELPER)) {
|
|
596
|
+
const close = matchBracket(src, m.index + m[0].length - 1);
|
|
597
|
+
mark(m.index, close < 0 ? src.length - 1 : close);
|
|
598
|
+
}
|
|
599
|
+
for (const pattern of [CLASS_ASSIGN, CLASS_KEY]) {
|
|
600
|
+
for (const m of src.matchAll(pattern)) {
|
|
601
|
+
if (!CLASS_NAMED.test(m[1])) continue;
|
|
602
|
+
mark(m.index, valueEnd(src, m.index + m[0].length));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
const out = Array.from(src, (c, i) => (keep[i] || c === "\n" ? c : " "));
|
|
606
|
+
return out.join("");
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** `ui-audit-disable-begin <rule> — <why>` … `ui-audit-disable-end <rule>`: ONE marker for a whole
|
|
610
|
+
* region instead of one per line (prettier wrapping a 24-item literal used to cost 24 identical
|
|
611
|
+
* comments, so the formatter was being bent to satisfy the linter). The reason is MANDATORY — a
|
|
612
|
+
* marker without `— <12+ chars>` is ignored and the finding stands, so an opt-out always says why.
|
|
613
|
+
* A block that is never closed runs to the end of the file. */
|
|
614
|
+
const DISABLE_BEGIN = /ui-audit-disable-begin\s+([a-z][a-z0-9-]*)\s*[—:]\s*(\S[^\n]{11,})/;
|
|
615
|
+
const DISABLE_END = /ui-audit-disable-end\s+([a-z][a-z0-9-]*)/;
|
|
616
|
+
|
|
617
|
+
/** @returns {(ruleId: string, lineIndex: number) => boolean} */
|
|
618
|
+
function blockSuppressions(lines) {
|
|
619
|
+
/** @type {Map<string, {from:number, to:number}[]>} */
|
|
620
|
+
const ranges = new Map();
|
|
621
|
+
/** @type {Map<string, number>} */
|
|
622
|
+
const open = new Map();
|
|
623
|
+
const close = (ruleId, to) => {
|
|
624
|
+
const from = open.get(ruleId);
|
|
625
|
+
open.delete(ruleId);
|
|
626
|
+
if (!ranges.has(ruleId)) ranges.set(ruleId, []);
|
|
627
|
+
ranges.get(ruleId).push({ from, to });
|
|
628
|
+
};
|
|
629
|
+
lines.forEach((line, i) => {
|
|
630
|
+
const begin = DISABLE_BEGIN.exec(line);
|
|
631
|
+
if (begin && !open.has(begin[1])) open.set(begin[1], i);
|
|
632
|
+
const end = DISABLE_END.exec(line);
|
|
633
|
+
if (end && open.has(end[1])) close(end[1], i);
|
|
634
|
+
});
|
|
635
|
+
for (const ruleId of [...open.keys()]) close(ruleId, lines.length - 1);
|
|
636
|
+
return (ruleId, lineIndex) =>
|
|
637
|
+
(ranges.get(ruleId) ?? []).some((r) => lineIndex >= r.from && lineIndex <= r.to);
|
|
638
|
+
}
|
|
639
|
+
|
|
477
640
|
/** A finding is suppressed by `ui-audit-disable-line <rule>` on the same line, or
|
|
478
641
|
* `ui-audit-disable-next-line <rule>` on the line above (explicit rule id required). */
|
|
479
642
|
function isSuppressed(ruleId, sameLine, prevLine) {
|
|
@@ -524,6 +687,33 @@ function walk(dir, acc = []) {
|
|
|
524
687
|
return acc;
|
|
525
688
|
}
|
|
526
689
|
|
|
690
|
+
/** Match real JSX text after a balanced opening tag, including props with comparisons. */
|
|
691
|
+
function* currencyMatches(source) {
|
|
692
|
+
for (const opening of source.matchAll(/<(?:[A-Za-z][\w.:]*\b|(?=>))/g)) {
|
|
693
|
+
const end = jsxOpeningEnd(source, opening.index);
|
|
694
|
+
const text = source.slice(end + 1).match(/^[^<>{}]*[¥$€£₫]\s*\{/);
|
|
695
|
+
if (text) yield { 0: source.slice(opening.index, end + 1) + text[0], index: opening.index };
|
|
696
|
+
}
|
|
697
|
+
yield* source.matchAll(/\}\s*円\s*</g);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** Find the tag boundary without confusing a comparison, callback or string for markup. */
|
|
701
|
+
function jsxOpeningEnd(source, start) {
|
|
702
|
+
let braces = 0;
|
|
703
|
+
let quote = "";
|
|
704
|
+
for (let i = start + 1; i < source.length; i++) {
|
|
705
|
+
const char = source[i];
|
|
706
|
+
if (quote) {
|
|
707
|
+
if (char === "\\") i++;
|
|
708
|
+
else if (char === quote) quote = "";
|
|
709
|
+
} else if (char === '"' || char === "'" || char === "`") quote = char;
|
|
710
|
+
else if (char === "{") braces++;
|
|
711
|
+
else if (char === "}") braces--;
|
|
712
|
+
else if (char === ">" && braces === 0) return i;
|
|
713
|
+
}
|
|
714
|
+
return source.length;
|
|
715
|
+
}
|
|
716
|
+
|
|
527
717
|
// Structural: a <Card> (without p-0) whose first child is body content rather than a Card
|
|
528
718
|
// sub-component sits FLUSH (no padding). Per-line regexes can't see across lines, so this is a
|
|
529
719
|
// whole-file pass. The body must be wrapped in <CardContent> (titles in <CardHeader>).
|
|
@@ -553,13 +743,7 @@ const SIBLING_CARDS = /^([ \t]*)<\/Card>\s*\n\1<Card\b/gm;
|
|
|
553
743
|
* utilities are the design system itself (that is where `flex`/`gap-*` are supposed to live), so
|
|
554
744
|
* those rules are skipped when the CWD package is @godxjp/ui — unless `--consumer` forces them.
|
|
555
745
|
*/
|
|
556
|
-
|
|
557
|
-
try {
|
|
558
|
-
return JSON.parse(readFileSync(join(CWD, "package.json"), "utf8")).name === "@godxjp/ui";
|
|
559
|
-
} catch {
|
|
560
|
-
return false;
|
|
561
|
-
}
|
|
562
|
-
})();
|
|
746
|
+
|
|
563
747
|
const ACTIVE_RULES =
|
|
564
748
|
SELF && !args.includes("--consumer") ? RULES.filter((r) => r.scope !== "consumer") : RULES;
|
|
565
749
|
|
|
@@ -568,17 +752,41 @@ let filesScanned = 0;
|
|
|
568
752
|
for (const dir of SCAN_DIRS) {
|
|
569
753
|
for (const file of walk(isAbsolute(dir) ? dir : join(CWD, dir))) {
|
|
570
754
|
const rel = relative(CWD, file);
|
|
755
|
+
// Framework test support is executable fixture markup, not a shipped product screen.
|
|
756
|
+
if (SELF && !args.includes("--consumer") && rel.startsWith("src/test/")) continue;
|
|
571
757
|
filesScanned += 1;
|
|
758
|
+
// A primitive implements native controls; asking Input to render Input recurses.
|
|
759
|
+
// Consumer applications and executable docs still receive these composition checks.
|
|
760
|
+
const fileRules =
|
|
761
|
+
SELF && !args.includes("--consumer") && rel.startsWith("src/components/")
|
|
762
|
+
? ACTIVE_RULES.filter((rule) => rule.scope !== "consumer-control")
|
|
763
|
+
: ACTIVE_RULES;
|
|
572
764
|
const content = readFileSync(file, "utf8");
|
|
573
765
|
const origLines = content.split("\n");
|
|
574
766
|
const scanContent = stripComments(content); // comments blanked; strings + line numbers kept
|
|
575
767
|
const scanLines = scanContent.split("\n");
|
|
768
|
+
// `classOnly` rules read this instead — everything but a real class expression blanked.
|
|
769
|
+
const classLines = classExpressionsOnly(scanContent).split("\n");
|
|
770
|
+
const inDisabledBlock = blockSuppressions(origLines);
|
|
771
|
+
/** Both opt-outs, by line index: the per-line markers and the reason-carrying block. */
|
|
772
|
+
const suppressed = (ruleId, i) =>
|
|
773
|
+
isSuppressed(ruleId, origLines[i], origLines[i - 1]) || inDisabledBlock(ruleId, i);
|
|
774
|
+
const isJsx = file.endsWith(".tsx");
|
|
775
|
+
// This compiler output intentionally resolves CSS variables to email-safe literals.
|
|
776
|
+
// gen-email-tokens.mjs --check verifies it against its canonical token sources.
|
|
777
|
+
const compiledEmailTokens =
|
|
778
|
+
SELF &&
|
|
779
|
+
rel === "src/email/tokens.generated.ts" &&
|
|
780
|
+
content.startsWith("// AUTO-GENERATED by scripts/gen-email-tokens.mjs");
|
|
576
781
|
scanLines.forEach((line, i) => {
|
|
577
|
-
for (const rule of
|
|
782
|
+
for (const rule of fileRules) {
|
|
783
|
+
if (!isJsx && rule.scope === "consumer-control") continue;
|
|
784
|
+
if (compiledEmailTokens && rule.id === "no-off-scale-token-value") continue;
|
|
578
785
|
if (rule.spansElement) continue; // matched over the whole file below, not line by line
|
|
786
|
+
const target = rule.classOnly ? (classLines[i] ?? "") : line;
|
|
579
787
|
if (
|
|
580
|
-
(typeof rule.test === "function" ? rule.test(
|
|
581
|
-
!
|
|
788
|
+
(typeof rule.test === "function" ? rule.test(target) : rule.test.test(target)) &&
|
|
789
|
+
!suppressed(rule.id, i) &&
|
|
582
790
|
!isExempt(rule, origLines[i], origLines[i - 1])
|
|
583
791
|
) {
|
|
584
792
|
findings.push({
|
|
@@ -596,11 +804,20 @@ for (const dir of SCAN_DIRS) {
|
|
|
596
804
|
// Element-spanning rules: matched against the WHOLE file, because a JSX element prettier wrapped
|
|
597
805
|
// over five lines is invisible to a line-by-line scan. Same offset→line-number idiom as the
|
|
598
806
|
// block rules below.
|
|
599
|
-
for (const rule of
|
|
600
|
-
if (!rule.spansElement) continue;
|
|
601
|
-
for (const match of
|
|
807
|
+
for (const rule of fileRules) {
|
|
808
|
+
if (!rule.spansElement || !isJsx) continue;
|
|
809
|
+
for (const match of rule.matches
|
|
810
|
+
? rule.matches(scanContent)
|
|
811
|
+
: scanContent.matchAll(rule.test)) {
|
|
812
|
+
// A comparison `>` inside a JSX prop is not the end of its opening tag.
|
|
813
|
+
if (
|
|
814
|
+
(rule.id === "hardcoded-currency" || rule.id === "no-em-dash-in-copy") &&
|
|
815
|
+
match[0].startsWith("<") &&
|
|
816
|
+
jsxOpeningEnd(scanContent, match.index) >= match.index + match[0].length
|
|
817
|
+
)
|
|
818
|
+
continue;
|
|
602
819
|
const lineNo = scanContent.slice(0, match.index).split("\n").length;
|
|
603
|
-
if (
|
|
820
|
+
if (suppressed(rule.id, lineNo - 1)) continue;
|
|
604
821
|
if (isExempt(rule, origLines[lineNo - 1], origLines[lineNo - 2])) continue;
|
|
605
822
|
findings.push({
|
|
606
823
|
file: rel,
|
|
@@ -613,10 +830,10 @@ for (const dir of SCAN_DIRS) {
|
|
|
613
830
|
});
|
|
614
831
|
}
|
|
615
832
|
}
|
|
833
|
+
if (!isJsx) continue;
|
|
616
834
|
for (const match of scanContent.matchAll(CARD_FLUSH)) {
|
|
617
835
|
const lineNo = scanContent.slice(0, match.index).split("\n").length;
|
|
618
|
-
if (
|
|
619
|
-
continue;
|
|
836
|
+
if (suppressed("card-needs-content", lineNo - 1)) continue;
|
|
620
837
|
findings.push({
|
|
621
838
|
file: rel,
|
|
622
839
|
line: lineNo,
|
|
@@ -633,8 +850,7 @@ for (const dir of SCAN_DIRS) {
|
|
|
633
850
|
) {
|
|
634
851
|
for (const match of scanContent.matchAll(SIBLING_CARDS)) {
|
|
635
852
|
const lineNo = scanContent.slice(0, match.index).split("\n").length + 1;
|
|
636
|
-
if (
|
|
637
|
-
continue;
|
|
853
|
+
if (suppressed("sibling-cards-need-flex", lineNo - 1)) continue;
|
|
638
854
|
findings.push({
|
|
639
855
|
file: rel,
|
|
640
856
|
line: lineNo,
|
|
@@ -648,10 +864,7 @@ for (const dir of SCAN_DIRS) {
|
|
|
648
864
|
}
|
|
649
865
|
for (const match of scanContent.matchAll(BARE_FIELD)) {
|
|
650
866
|
const lineNo = scanContent.slice(0, match.index).split("\n").length;
|
|
651
|
-
if (
|
|
652
|
-
isSuppressed("bare-control-needs-formfield", origLines[lineNo - 1], origLines[lineNo - 2])
|
|
653
|
-
)
|
|
654
|
-
continue;
|
|
867
|
+
if (suppressed("bare-control-needs-formfield", lineNo - 1)) continue;
|
|
655
868
|
findings.push({
|
|
656
869
|
file: rel,
|
|
657
870
|
line: lineNo,
|