@godxjp/ui 30.8.0 → 31.0.2
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 +25 -2
- package/agent/START-HERE.md +1 -1
- package/agent/components/PrefetchLink.json +3 -3
- package/agent/components-index.json +1 -1
- package/agent/components.json +3 -3
- package/agent/index.json +2 -2
- package/agent/llms.txt +3 -3
- package/agent/patterns/settings-page-responsive.json +1 -1
- package/agent/patterns.json +1 -1
- package/dist/components/query/index.d.ts +0 -2
- package/dist/components/query/index.js +0 -2
- package/dist/components/react-router/index.d.ts +2 -0
- package/dist/components/react-router/index.js +4 -0
- package/dist/contracts/measurement.json +1 -1
- package/dist/styles/alert-layout.css +4 -2
- package/dist/styles/layers.json +1182 -0
- package/dist/styles/vendor-day-picker.css +2 -0
- package/dist/styles/vendor-sonner.css +2 -0
- package/docs/CONSUMER-RULES.md +1 -1
- package/docs/data-display/permission-matrix.tsx +30 -28
- package/docs/data-entry/cascader.tsx +22 -20
- package/docs/data-entry/date-picker.tsx +47 -43
- package/docs/data-entry/form-field/examples/a11y-contract.tsx +80 -76
- package/docs/data-entry/form-field/examples/create-form.tsx +33 -31
- package/docs/data-entry/form-field/index.tsx +61 -57
- package/docs/data-entry/form.tsx +112 -106
- package/docs/data-entry/input.tsx +297 -270
- package/docs/data-entry/label.tsx +11 -9
- package/docs/data-entry/number-input.tsx +86 -78
- package/docs/data-entry/radio-group.tsx +33 -26
- package/docs/data-entry/rating.tsx +29 -25
- package/docs/data-entry/select-matrix.tsx +49 -47
- package/docs/data-entry/select.tsx +38 -35
- package/docs/data-entry/slider.tsx +45 -41
- package/docs/data-entry/textarea.tsx +44 -42
- package/docs/data-entry/time-picker.tsx +51 -47
- package/docs/feedback/sheet.tsx +84 -79
- package/docs/foundation/theme-editor.tsx +133 -124
- package/docs/general/button/examples/form-actions.tsx +17 -15
- package/docs/layout/auth-recovery/examples/password-recovery.tsx +31 -29
- package/docs/layout/auth-shell-registration.tsx +75 -72
- package/docs/layout/auth-shell-variants.tsx +55 -51
- package/docs/layout/space-compact.tsx +11 -8
- package/docs/{query → react-router}/prefetch-link.tsx +1 -1
- package/docs/showcase/caimono-price-comparison.tsx +59 -51
- package/docs/showcase/case3-approval-workflow.tsx +27 -25
- package/docs/showcase/case4-login.tsx +37 -34
- package/docs/showcase/permission-matrix.tsx +44 -42
- package/docs/showcase/theme-customization.tsx +108 -99
- package/package.json +10 -3
- package/scripts/cli.mjs +15 -0
- package/scripts/consumer-rule.md +14 -0
- package/scripts/prune-css.mjs +221 -0
- package/scripts/ui-audit.mjs +99 -2
- /package/dist/components/{query → react-router}/prefetch-link.d.ts +0 -0
- /package/dist/components/{query → react-router}/prefetch-link.js +0 -0
|
@@ -136,6 +136,7 @@ import {
|
|
|
136
136
|
type TimelineItem,
|
|
137
137
|
} from "@godxjp/ui/data-display";
|
|
138
138
|
import {
|
|
139
|
+
Form,
|
|
139
140
|
CheckboxGroup,
|
|
140
141
|
ColorPicker,
|
|
141
142
|
DatePicker,
|
|
@@ -586,85 +587,91 @@ function ComponentsBoard(props: {
|
|
|
586
587
|
pad={{ inline: "lg", block: "md" }}
|
|
587
588
|
>
|
|
588
589
|
{/* 1 · text entry */}
|
|
589
|
-
<
|
|
590
|
-
<
|
|
591
|
-
|
|
592
|
-
label={t("themeShowcase.form.email")}
|
|
593
|
-
helper={t("themeShowcase.form.emailHint")}
|
|
594
|
-
>
|
|
595
|
-
<Input
|
|
590
|
+
<Form>
|
|
591
|
+
<Flex direction="col" gap="md">
|
|
592
|
+
<FormField
|
|
596
593
|
id="board-email"
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
594
|
+
label={t("themeShowcase.form.email")}
|
|
595
|
+
helper={t("themeShowcase.form.emailHint")}
|
|
596
|
+
>
|
|
597
|
+
<Input
|
|
598
|
+
id="board-email"
|
|
599
|
+
type="email"
|
|
600
|
+
autoComplete="email"
|
|
601
|
+
prefix={<Mail aria-hidden="true" />}
|
|
602
|
+
placeholder={t("themeShowcase.form.emailPlaceholder")}
|
|
603
|
+
defaultValue="release-desk@ops.example.jp"
|
|
604
|
+
/>
|
|
605
|
+
</FormField>
|
|
606
|
+
<FormField id="board-handle" label={t("themeShowcase.form.handle")}>
|
|
607
|
+
<Input
|
|
608
|
+
id="board-handle"
|
|
609
|
+
addonBefore={<AtSign aria-hidden="true" />}
|
|
610
|
+
defaultValue="release-desk"
|
|
611
|
+
count={{ max: 24, show: true }}
|
|
612
|
+
/>
|
|
613
|
+
</FormField>
|
|
614
|
+
</Flex>
|
|
615
|
+
</Form>
|
|
613
616
|
|
|
614
617
|
{/* 2 · multi-select with removable tags + free tags */}
|
|
615
|
-
<
|
|
616
|
-
<
|
|
617
|
-
|
|
618
|
-
label={t("themeShowcase.form.regions")}
|
|
619
|
-
helper={regions.length > 0 ? regionList : t("themeShowcase.form.regionsEmpty")}
|
|
620
|
-
>
|
|
621
|
-
<Select
|
|
618
|
+
<Form>
|
|
619
|
+
<Flex direction="col" gap="md">
|
|
620
|
+
<FormField
|
|
622
621
|
id="board-regions"
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
622
|
+
label={t("themeShowcase.form.regions")}
|
|
623
|
+
helper={regions.length > 0 ? regionList : t("themeShowcase.form.regionsEmpty")}
|
|
624
|
+
>
|
|
625
|
+
<Select
|
|
626
|
+
id="board-regions"
|
|
627
|
+
mode="multiple"
|
|
628
|
+
maxTagCount={2}
|
|
629
|
+
value={regions}
|
|
630
|
+
onValueChange={setRegions}
|
|
631
|
+
placeholder={t("themeShowcase.form.regionsPlaceholder")}
|
|
632
|
+
options={REGION_CODES.map((code) => ({
|
|
633
|
+
value: code,
|
|
634
|
+
label: new Intl.DisplayNames([props.locale], { type: "region" }).of(code) ?? code,
|
|
635
|
+
}))}
|
|
636
|
+
/>
|
|
637
|
+
</FormField>
|
|
638
|
+
<FormField id="board-labels" label={t("themeShowcase.form.labels")}>
|
|
639
|
+
<TagInput
|
|
640
|
+
id="board-labels"
|
|
641
|
+
value={labels}
|
|
642
|
+
onValueChange={setLabels}
|
|
643
|
+
maxTagCount={2}
|
|
644
|
+
/* The ceiling the 71-character id is measured against (gh#840). 16 is the widest cut
|
|
641
645
|
that still leaves room for a second chip and the `+1` on one row at 375px; the value
|
|
642
646
|
is untouched — it stays in the chip's `title` and in the remover's accessible name,
|
|
643
647
|
which is the only reason truncating an identifier is allowed at all. */
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
648
|
+
maxTagTextLength={16}
|
|
649
|
+
placeholder={t("themeShowcase.form.labelsPlaceholder")}
|
|
650
|
+
/>
|
|
651
|
+
</FormField>
|
|
652
|
+
</Flex>
|
|
653
|
+
</Form>
|
|
649
654
|
|
|
650
655
|
{/* 3 · dropdown + date */}
|
|
651
|
-
<
|
|
652
|
-
<
|
|
653
|
-
<
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
{
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
<
|
|
666
|
-
|
|
667
|
-
|
|
656
|
+
<Form>
|
|
657
|
+
<Flex direction="col" gap="md">
|
|
658
|
+
<FormField id="board-plan" label={t("themeShowcase.form.plan")}>
|
|
659
|
+
<Select
|
|
660
|
+
id="board-plan"
|
|
661
|
+
value={plan}
|
|
662
|
+
onValueChange={setPlan}
|
|
663
|
+
options={[
|
|
664
|
+
{ value: "starter", label: t("themeShowcase.plan.starter") },
|
|
665
|
+
{ value: "standard", label: t("themeShowcase.plan.standard") },
|
|
666
|
+
{ value: "scale", label: t("themeShowcase.plan.scale") },
|
|
667
|
+
]}
|
|
668
|
+
/>
|
|
669
|
+
</FormField>
|
|
670
|
+
<FormField id="board-date" label={t("themeShowcase.form.releaseDate")}>
|
|
671
|
+
<DatePicker id="board-date" defaultValue={new Date("2026-09-12T00:00:00Z")} />
|
|
672
|
+
</FormField>
|
|
673
|
+
</Flex>
|
|
674
|
+
</Form>
|
|
668
675
|
|
|
669
676
|
{/* 4 · checkboxes — a three-line label beside a one-word one, and a disabled row */}
|
|
670
677
|
<Flex direction="col" gap="md">
|
|
@@ -929,35 +936,37 @@ function ComponentsBoard(props: {
|
|
|
929
936
|
<CardDescription>{t("themeShowcase.signup.description")}</CardDescription>
|
|
930
937
|
</CardHeader>
|
|
931
938
|
<CardContent>
|
|
932
|
-
<
|
|
933
|
-
<
|
|
934
|
-
<
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
<
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
<
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
<
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
939
|
+
<Form>
|
|
940
|
+
<Flex direction="col" gap="md">
|
|
941
|
+
<FormField id="signup-email" label={t("themeShowcase.signup.email")}>
|
|
942
|
+
<Input
|
|
943
|
+
id="signup-email"
|
|
944
|
+
type="email"
|
|
945
|
+
autoComplete="email"
|
|
946
|
+
placeholder="you@example.jp"
|
|
947
|
+
/>
|
|
948
|
+
</FormField>
|
|
949
|
+
<FormField id="signup-secret" label={t("themeShowcase.signup.password")}>
|
|
950
|
+
<PasswordInput
|
|
951
|
+
id="signup-secret"
|
|
952
|
+
value={secret}
|
|
953
|
+
onChange={(event) => setSecret(event.target.value)}
|
|
954
|
+
autoComplete="new-password"
|
|
955
|
+
/>
|
|
956
|
+
</FormField>
|
|
957
|
+
<PasswordStrength value={secret} />
|
|
958
|
+
<Button fullWidth>{t("themeShowcase.signup.submit")}</Button>
|
|
959
|
+
<AuthDivider label={t("themeShowcase.signup.or")} />
|
|
960
|
+
<Button variant="outline" fullWidth>
|
|
961
|
+
<ShieldCheck aria-hidden="true" />
|
|
962
|
+
{t("themeShowcase.signup.sso")}
|
|
963
|
+
</Button>
|
|
964
|
+
<Button variant="outline" fullWidth>
|
|
965
|
+
<Fingerprint aria-hidden="true" />
|
|
966
|
+
{t("themeShowcase.signup.passkey")}
|
|
967
|
+
</Button>
|
|
968
|
+
</Flex>
|
|
969
|
+
</Form>
|
|
961
970
|
</CardContent>
|
|
962
971
|
</Card>
|
|
963
972
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@godxjp/ui",
|
|
3
|
-
"version": "
|
|
4
|
-
"godxUiMcp": "
|
|
3
|
+
"version": "31.0.2",
|
|
4
|
+
"godxUiMcp": "31.0.2",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"agent",
|
|
27
27
|
"dist",
|
|
28
28
|
"scripts/ui-audit.mjs",
|
|
29
|
+
"scripts/prune-css.mjs",
|
|
29
30
|
"README.md",
|
|
30
31
|
"scripts/visual-audit.mjs",
|
|
31
32
|
"scripts/visual-audit-rules.mjs",
|
|
@@ -140,6 +141,10 @@
|
|
|
140
141
|
"types": "./dist/inertia/index.d.ts",
|
|
141
142
|
"import": "./dist/inertia/index.js"
|
|
142
143
|
},
|
|
144
|
+
"./react-router": {
|
|
145
|
+
"types": "./dist/components/react-router/index.d.ts",
|
|
146
|
+
"import": "./dist/components/react-router/index.js"
|
|
147
|
+
},
|
|
143
148
|
"./app": {
|
|
144
149
|
"types": "./dist/app/index.d.ts",
|
|
145
150
|
"import": "./dist/app/index.js"
|
|
@@ -313,6 +318,7 @@
|
|
|
313
318
|
"build:jis-level1-fonts": "node scripts/build-jis-level1-fonts.mjs",
|
|
314
319
|
"capture:voiceover": "node scripts/capture-voiceover-evidence.mjs",
|
|
315
320
|
"check:agent-catalog": "node scripts/gen-agent-catalog.mjs --check",
|
|
321
|
+
"check:style-layers": "node scripts/gen-style-layers.mjs --check",
|
|
316
322
|
"check:app-shell-narrow-grid": "node scripts/check-app-shell-narrow-grid.mjs",
|
|
317
323
|
"check:app-shell-page-width": "node scripts/check-app-shell-page-width.mjs",
|
|
318
324
|
"check:audit-sync": "node scripts/check-audit-sync.mjs",
|
|
@@ -388,6 +394,7 @@
|
|
|
388
394
|
"gen:email-tokens": "node scripts/gen-email-tokens.mjs",
|
|
389
395
|
"gen:frame-coverage-ledger": "node scripts/gen-frame-coverage-ledger.mjs",
|
|
390
396
|
"gen:registry": "node scripts/gen-registry.mjs",
|
|
397
|
+
"gen:style-layers": "node scripts/gen-style-layers.mjs",
|
|
391
398
|
"init-agent": "node scripts/init-agent-kit.mjs",
|
|
392
399
|
"lint": "eslint . --cache --cache-strategy content",
|
|
393
400
|
"lint:fix": "eslint . --fix",
|
|
@@ -415,7 +422,7 @@
|
|
|
415
422
|
"verify": "pnpm typecheck && pnpm lint && pnpm format && pnpm build && pnpm preview:build && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:token-width-wins && pnpm check:no-external-assets && pnpm check:token-scale-bypass && pnpm check:radix-surface && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:disclosure-duplication && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm check:frame-coverage && pnpm test",
|
|
416
423
|
"verify:browser": "pnpm check:contrast && pnpm check:text-ink-clip && pnpm check:frame-overflow && pnpm check:font-fallback-metrics && pnpm check:visual-audit",
|
|
417
424
|
"verify:ci": "pnpm verify:ci:static && pnpm check:frame-contracts && pnpm test",
|
|
418
|
-
"verify:ci:static:gates": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:registry && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:token-width-wins && pnpm check:no-external-assets && pnpm check:token-scale-bypass && pnpm check:no-antd-runtime && pnpm check:radix-surface && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:disclosure-duplication && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:measurement-contract && pnpm check:mcp-token-sync && pnpm check:email-token-sync && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-catalog-completeness && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm run audit && pnpm check:frame-coverage-report && pnpm check:gate-coverage && pnpm check:mcp-prop-sync && pnpm check:catalog-contradictions && pnpm check:catalog-snippets && pnpm check:absorbed-names && pnpm check:agent-catalog",
|
|
425
|
+
"verify:ci:static:gates": "pnpm build && pnpm check:packed-public-contract && pnpm typecheck && pnpm typecheck:docs && pnpm lint && pnpm preview:build && pnpm check:registry && pnpm check:example-imports && pnpm check:core-isolation && pnpm check:no-consumer-coupling && pnpm check:use-client && pnpm check:prop-vocabulary && pnpm check:token-tiers && pnpm check:token-width-wins && pnpm check:no-external-assets && pnpm check:token-scale-bypass && pnpm check:no-antd-runtime && pnpm check:radix-surface && pnpm check:dist-tokens-resolve && pnpm check:no-hardcoded-geometry && pnpm check:no-hardcoded-css-values && pnpm check:disclosure-duplication && pnpm check:no-inline-magic-numbers && pnpm check:no-tailwind-class-assertions && pnpm check:control-sizing && pnpm check:rtl && pnpm check:typography && pnpm check:mcp-lockstep && pnpm check:measurement-contract && pnpm check:mcp-token-sync && pnpm check:email-token-sync && pnpm check:mcp-sync && pnpm check:doc-prop-existence && pnpm check:mcp-catalog-coverage && pnpm check:mcp-orphans && pnpm check:mcp-catalog-completeness && pnpm check:mcp-pattern-imports && pnpm check:audit-sync && pnpm run audit && pnpm check:frame-coverage-report && pnpm check:gate-coverage && pnpm check:mcp-prop-sync && pnpm check:catalog-contradictions && pnpm check:catalog-snippets && pnpm check:absorbed-names && pnpm check:agent-catalog && pnpm check:style-layers",
|
|
419
426
|
"verify:ci:static": "node scripts/run-gate-list.mjs verify:ci:static:gates",
|
|
420
427
|
"verify:publish-tree": "pnpm build && pnpm check:packed-public-contract && pnpm check:use-client && pnpm check:dist-tokens-resolve",
|
|
421
428
|
"verify:release": "pnpm verify:static && pnpm check:frame-contracts && pnpm check:frame-coverage",
|
package/scripts/cli.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* sync-rules refresh package-owned agent rules (same path as postinstall)
|
|
6
6
|
* audit static UI audit (regex over source)
|
|
7
7
|
* visual-audit runtime audit (Playwright + axe-core) against a running app
|
|
8
|
+
* prune-css emit a stylesheet with only the CSS layers the app uses (gh#971)
|
|
8
9
|
*/
|
|
9
10
|
import { spawnSync } from "node:child_process";
|
|
10
11
|
import { dirname, join } from "node:path";
|
|
@@ -16,6 +17,7 @@ const MAP = {
|
|
|
16
17
|
"sync-rules": "postinstall.mjs",
|
|
17
18
|
audit: "ui-audit.mjs",
|
|
18
19
|
"visual-audit": "visual-audit.mjs",
|
|
20
|
+
"prune-css": "prune-css.mjs",
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
// `<command> --help` is answered HERE: the scripts take free positional arguments, so a `--help`
|
|
@@ -52,6 +54,18 @@ const HELP = {
|
|
|
52
54
|
--quiet print errors only (warnings hidden)
|
|
53
55
|
--rules print the rule catalog as JSON and exit
|
|
54
56
|
Pre-commit: npx godxjp-ui audit resources/js || exit 1`,
|
|
57
|
+
"prune-css": `godxjp-ui prune-css <src dir/glob …> [--out <file>] [--fonts]
|
|
58
|
+
|
|
59
|
+
Emit a stylesheet with only the component CSS layers your app uses (gh#971). Scans the given
|
|
60
|
+
sources for @godxjp/ui imports, resolves the layer dependency closure from the graph the
|
|
61
|
+
package ships (dist/styles/layers.json), and writes a css file that imports the foundation
|
|
62
|
+
plus only the needed *-layout.css layers, in the exact order styles/index.css loads them.
|
|
63
|
+
<src dir/glob …> your app's source (directories are walked)
|
|
64
|
+
--out <file> output path (default: godx-ui.css)
|
|
65
|
+
--fonts include the bundled @font-face declarations (default mirrors styles/core)
|
|
66
|
+
Import the emitted file INSTEAD of "@godxjp/ui/styles". Re-run when your component usage
|
|
67
|
+
changes and after every upgrade; it refuses on a package/manifest version mismatch.
|
|
68
|
+
Hand cherry-picking *-layout.css stays forbidden — this tool is the only thing allowed to slice.`,
|
|
55
69
|
"visual-audit": `godxjp-ui visual-audit [--format json] [--strict] <baseUrl> [route …]
|
|
56
70
|
|
|
57
71
|
Runtime audit (Playwright + axe-core) against an app you are ALREADY running locally.
|
|
@@ -70,6 +84,7 @@ commands:
|
|
|
70
84
|
sync-rules refresh package-owned agent rules (postinstall, by hand)
|
|
71
85
|
audit static UI audit over source
|
|
72
86
|
visual-audit runtime audit (Playwright + axe-core) against a running app
|
|
87
|
+
prune-css emit a stylesheet with only the CSS layers your app uses
|
|
73
88
|
|
|
74
89
|
godxjp-ui <command> --help details and flags for one command`;
|
|
75
90
|
|
package/scripts/consumer-rule.md
CHANGED
|
@@ -120,6 +120,20 @@ ngày: năm thứ cần đều ĐÃ CÓ và vẫn bị dựng lại bằng thứ
|
|
|
120
120
|
Lỗi không phải "đoán sai tên prop" mà là **cho rằng nó không tồn tại nên không
|
|
121
121
|
hỏi**.
|
|
122
122
|
|
|
123
|
+
## Form: `Form` bọc `FormField`, bề rộng theo nội dung, form dài là PAGE
|
|
124
|
+
|
|
125
|
+
godx-mailer dính bốn lỗi form trong một ngày, cả bốn đều là kit ĐÃ CÓ đồ đúng
|
|
126
|
+
(gh#998). Giờ ui-audit chặn:
|
|
127
|
+
|
|
128
|
+
| Sai | Đúng | rule |
|
|
129
|
+
| ----------------------------------------------------------- | -------------------------------------------------------------------------------------------- | ---------------------- |
|
|
130
|
+
| ≥2 `FormField` không có `Form`, hoặc xếp hàng bằng `<Flex>` | `<Form layout="horizontal" labelWidth controlWidth>`; hàng = `SpaceCompact` / `Form columns` | `formfield-needs-form` |
|
|
131
|
+
| ≥3 `FormField` trong `DialogBody` | page riêng (route riêng) | `dialog-form-too-big` |
|
|
132
|
+
| `Select` hai lựa chọn kéo full width | `controlWidth` trên field hoặc một lần trên `Form` | `select-width-hint` |
|
|
133
|
+
|
|
134
|
+
Ca kit không diễn đạt được thì **mở issue ở godx-jp/godxjp-ui**, tạm dùng cách
|
|
135
|
+
hợp lệ gần nhất kèm `// TODO(godxjp-ui#<n>)` — không tự chế.
|
|
136
|
+
|
|
123
137
|
## Dialog và AlertDialog là MỘT họ — `variant` là lối chuẩn
|
|
124
138
|
|
|
125
139
|
Đừng với tay sang 12 export `AlertDialog*` nữa. Chúng **vẫn chạy y như cũ** (gỡ
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* prune-css — emit a consumer stylesheet with only the component CSS layers the app uses
|
|
4
|
+
* (gh#971). Runs in the CONSUMER repo:
|
|
5
|
+
*
|
|
6
|
+
* node node_modules/@godxjp/ui/scripts/prune-css.mjs resources/js [more dirs/globs …]
|
|
7
|
+
* npx @godxjp/ui prune-css resources/js --out resources/css/godx-ui.css
|
|
8
|
+
*
|
|
9
|
+
* It scans the given sources for imports from `@godxjp/ui` (any subpath), maps the imported
|
|
10
|
+
* components to CSS layer files through the dependency graph the package ships in
|
|
11
|
+
* dist/styles/layers.json, takes the closure, and writes a css file that @imports the
|
|
12
|
+
* foundation ALWAYS (base — Tailwind entry, tokens, theme, density, focus ring) plus only the
|
|
13
|
+
* needed `*-layout.css` layers, in exactly the order styles/index.css loads them — layer order
|
|
14
|
+
* is load-bearing there (icon-layout.css is last on purpose, and the vendor sheets sit in
|
|
15
|
+
* `layer(vendor)` before base).
|
|
16
|
+
*
|
|
17
|
+
* This is the sanctioned alternative to the forbidden act: README still says "do not
|
|
18
|
+
* cherry-pick *-layout.css files", and that rule is WHY this tool exists — the layer
|
|
19
|
+
* dependency graph is owned and shipped by the package, so the slice is computed, never
|
|
20
|
+
* hand-guessed. A missing layer fails silently (naked menus, unsized rows); a computed one
|
|
21
|
+
* cannot go missing without the manifest's own CI guard going red first.
|
|
22
|
+
*
|
|
23
|
+
* Fonts mirror `@godxjp/ui/styles/core`: NO bundled `@font-face` by default (that is where
|
|
24
|
+
* 77% of the all-in entry's weight lives — gh#971). Pass `--fonts` for the bundled faces,
|
|
25
|
+
* the same thing `@godxjp/ui/styles` adds over `core`.
|
|
26
|
+
*
|
|
27
|
+
* Re-run it whenever the set of components the app uses changes, and after every
|
|
28
|
+
* @godxjp/ui upgrade. The emitted file refuses to be hand-edited by carrying its own
|
|
29
|
+
* provenance header, and this script refuses to run against a manifest whose version does
|
|
30
|
+
* not match the installed package — a half-upgraded node_modules must fail loudly, not
|
|
31
|
+
* emit a slice computed from another release's graph.
|
|
32
|
+
*/
|
|
33
|
+
import { existsSync, globSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
34
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
35
|
+
import { fileURLToPath } from "node:url";
|
|
36
|
+
|
|
37
|
+
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
38
|
+
const args = process.argv.slice(2);
|
|
39
|
+
|
|
40
|
+
if (args.includes("--help") || args.includes("-h") || args.length === 0) {
|
|
41
|
+
console.log(`prune-css — emit a stylesheet with only the @godxjp/ui CSS layers your app uses
|
|
42
|
+
|
|
43
|
+
usage: node node_modules/@godxjp/ui/scripts/prune-css.mjs <src dir/glob …> [--out <file>] [--fonts]
|
|
44
|
+
|
|
45
|
+
<src dir/glob …> where your app's source lives (directories are walked; globs expand)
|
|
46
|
+
--out <file> where to write the css (default: godx-ui.css in the current directory)
|
|
47
|
+
--fonts include the bundled M PLUS 2 / Noto Sans JP @font-face declarations
|
|
48
|
+
(default mirrors @godxjp/ui/styles/core: none — bring your own fonts)
|
|
49
|
+
|
|
50
|
+
Then import the emitted file INSTEAD of "@godxjp/ui/styles" and re-run this command whenever
|
|
51
|
+
the set of components you use changes, and after every @godxjp/ui upgrade.`);
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const outFlag = args.indexOf("--out");
|
|
56
|
+
const OUT = outFlag !== -1 ? args[outFlag + 1] : "godx-ui.css";
|
|
57
|
+
if (outFlag !== -1 && (!OUT || OUT.startsWith("--"))) {
|
|
58
|
+
console.error("prune-css: --out needs a file path");
|
|
59
|
+
process.exit(2);
|
|
60
|
+
}
|
|
61
|
+
const FONTS = args.includes("--fonts");
|
|
62
|
+
const positionals = args.filter(
|
|
63
|
+
(a, i) => !a.startsWith("--") && (outFlag === -1 || i !== outFlag + 1),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// ── the package's own graph, version-locked ──────────────────────────────────
|
|
67
|
+
|
|
68
|
+
const pkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
69
|
+
const manifestPath = join(PKG_ROOT, "dist/styles/layers.json");
|
|
70
|
+
if (!existsSync(manifestPath)) {
|
|
71
|
+
console.error(
|
|
72
|
+
`prune-css: ${manifestPath} not found — the installed @godxjp/ui build does not ship the ` +
|
|
73
|
+
"layer manifest (upgrade the package; inside the repo itself, run `pnpm build` first).",
|
|
74
|
+
);
|
|
75
|
+
process.exit(2);
|
|
76
|
+
}
|
|
77
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
78
|
+
if (manifest.version !== pkg.version) {
|
|
79
|
+
console.error(
|
|
80
|
+
`prune-css: refusing to emit — the layer manifest says ${manifest.version ?? "null"} but the ` +
|
|
81
|
+
`installed @godxjp/ui is ${pkg.version}. A slice computed from another release's dependency ` +
|
|
82
|
+
"graph can silently miss a layer, which is the exact failure this tool exists to prevent. " +
|
|
83
|
+
"Reinstall @godxjp/ui (or rebuild dist/ if this is a linked checkout) and re-run.",
|
|
84
|
+
);
|
|
85
|
+
process.exit(2);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── scan consumer sources ────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
const SCANNABLE = /\.(tsx|jsx|ts|mjs|js|cjs|mts)$/;
|
|
91
|
+
|
|
92
|
+
function walk(dir, into) {
|
|
93
|
+
for (const name of readdirSync(dir)) {
|
|
94
|
+
if (name === "node_modules" || name.startsWith(".")) continue;
|
|
95
|
+
const p = join(dir, name);
|
|
96
|
+
if (statSync(p).isDirectory()) walk(p, into);
|
|
97
|
+
else if (SCANNABLE.test(name)) into.push(p);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const sources = [];
|
|
102
|
+
for (const arg of positionals) {
|
|
103
|
+
const asPath = resolve(process.cwd(), arg);
|
|
104
|
+
if (existsSync(asPath) && statSync(asPath).isDirectory()) walk(asPath, sources);
|
|
105
|
+
else if (existsSync(asPath) && SCANNABLE.test(asPath)) sources.push(asPath);
|
|
106
|
+
else {
|
|
107
|
+
for (const hit of globSync(arg, { cwd: process.cwd() })) {
|
|
108
|
+
const p = resolve(process.cwd(), hit);
|
|
109
|
+
if (statSync(p).isDirectory()) walk(p, sources);
|
|
110
|
+
else if (SCANNABLE.test(p)) sources.push(p);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (sources.length === 0) {
|
|
115
|
+
console.error(
|
|
116
|
+
`prune-css: nothing to scan — no source files matched ${positionals.join(" ")}. ` +
|
|
117
|
+
"An empty scan must never emit an empty stylesheet, so this is an error, not a result.",
|
|
118
|
+
);
|
|
119
|
+
process.exit(2);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const componentNames = Object.keys(manifest.components);
|
|
123
|
+
// Longest name first, so `CardBar…` resolves before `Card` when matching sub-part imports.
|
|
124
|
+
const byLength = [...componentNames].sort((a, b) => b.length - a.length);
|
|
125
|
+
|
|
126
|
+
const used = new Set();
|
|
127
|
+
let sawNamespace = false;
|
|
128
|
+
const IMPORT =
|
|
129
|
+
/(?:import|export)\s+(type\s+)?([^;'"]*?)\s*from\s*["'](@godxjp\/ui(?:\/[^"']*)?)["']/g;
|
|
130
|
+
for (const file of new Set(sources)) {
|
|
131
|
+
const src = readFileSync(file, "utf8");
|
|
132
|
+
for (const m of src.matchAll(IMPORT)) {
|
|
133
|
+
const [, typeOnly, clause, spec] = m;
|
|
134
|
+
if (typeOnly) continue; // a type renders nothing
|
|
135
|
+
if (spec.startsWith("@godxjp/ui/styles")) continue; // css imports carry no components
|
|
136
|
+
if (/\*\s*as\s/.test(clause)) {
|
|
137
|
+
// `import * as UI` — anything may be rendered through it. Correct beats minimal:
|
|
138
|
+
// include everything rather than guess at property accesses.
|
|
139
|
+
sawNamespace = true;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const braces = /\{([^}]*)\}/.exec(clause);
|
|
143
|
+
if (!braces) continue; // a bare side-effect or default import names no component
|
|
144
|
+
for (let entry of braces[1].split(",")) {
|
|
145
|
+
entry = entry.trim();
|
|
146
|
+
if (!entry || entry.startsWith("type ")) continue;
|
|
147
|
+
const name = entry.split(/\s+as\s+/)[0].trim();
|
|
148
|
+
if (!/^[A-Z]/.test(name)) continue; // hooks and utils style nothing
|
|
149
|
+
// Sub-parts (SheetContent, DropdownMenuTrigger, CardHeader…) are not manifest rows;
|
|
150
|
+
// their root component is, and the root's layers cover the whole compound family.
|
|
151
|
+
const root = byLength.find((c) => name === c || name.startsWith(c));
|
|
152
|
+
if (root) used.add(root);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// A dynamic `import("@godxjp/ui…")` names no bindings this regex can see.
|
|
156
|
+
if (/import\s*\(\s*["']@godxjp\/ui(?!\/styles)/.test(src)) sawNamespace = true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (sawNamespace) {
|
|
160
|
+
console.error(
|
|
161
|
+
"prune-css: found a namespace or dynamic import of @godxjp/ui — cannot tell which " +
|
|
162
|
+
"components it renders, so ALL component layers are included (still no bundled fonts " +
|
|
163
|
+
"unless --fonts). Use named imports to get a real slice.",
|
|
164
|
+
);
|
|
165
|
+
for (const c of componentNames) used.add(c);
|
|
166
|
+
}
|
|
167
|
+
if (used.size === 0) {
|
|
168
|
+
console.error(
|
|
169
|
+
`prune-css: scanned ${sources.length} files and found no @godxjp/ui component imports — ` +
|
|
170
|
+
"check the paths you passed. Refusing to emit a component-less stylesheet.",
|
|
171
|
+
);
|
|
172
|
+
process.exit(2);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── closure → ordered emit ───────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
const layers = new Set();
|
|
178
|
+
const vendors = new Set();
|
|
179
|
+
for (const name of used) {
|
|
180
|
+
const entry = manifest.components[name];
|
|
181
|
+
for (const l of entry.layers) layers.add(l);
|
|
182
|
+
for (const v of entry.vendor ?? []) vendors.add(v);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const keptLayers = manifest.order.filter((f) => layers.has(f));
|
|
186
|
+
const keptVendors = manifest.vendor.filter((f) => vendors.has(f));
|
|
187
|
+
const asImport = (f) => `@import "@godxjp/ui/styles/${f.replace(/\.css$/, "")}";`;
|
|
188
|
+
|
|
189
|
+
const lines = [
|
|
190
|
+
"/*",
|
|
191
|
+
` * GENERATED by @godxjp/ui prune-css — DO NOT EDIT.`,
|
|
192
|
+
` *`,
|
|
193
|
+
` * @godxjp/ui version: ${pkg.version}`,
|
|
194
|
+
` * components detected: ${[...used].sort().join(", ")}`,
|
|
195
|
+
` * layers: ${keptLayers.length} of ${manifest.order.length} (+base${FONTS ? "+fonts" : ", no bundled fonts — like styles/core"})`,
|
|
196
|
+
` *`,
|
|
197
|
+
` * Re-run when the components your app uses change, and after every @godxjp/ui upgrade:`,
|
|
198
|
+
` * node node_modules/@godxjp/ui/scripts/prune-css.mjs <src …> --out <this file>${FONTS ? " --fonts" : ""}`,
|
|
199
|
+
` *`,
|
|
200
|
+
` * Hand-editing this file is the cherry-picking the README forbids: layers share rules and`,
|
|
201
|
+
` * a missing one fails silently. Change your imports, then re-run the tool.`,
|
|
202
|
+
" */",
|
|
203
|
+
// Layer order must be declared before the first @import — same statement, same reasoning,
|
|
204
|
+
// as styles/index.css (vendor between base and components; see the comment there).
|
|
205
|
+
"@layer theme, base, vendor, components, utilities;",
|
|
206
|
+
...keptVendors.map(asImport),
|
|
207
|
+
asImport(manifest.base),
|
|
208
|
+
...(FONTS ? [asImport(manifest.fonts)] : []),
|
|
209
|
+
...keptLayers.map(asImport),
|
|
210
|
+
"",
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
writeFileSync(resolve(process.cwd(), OUT), lines.join("\n"));
|
|
214
|
+
|
|
215
|
+
const dropped = manifest.order.filter((f) => !layers.has(f));
|
|
216
|
+
console.error(
|
|
217
|
+
`prune-css: ${relative(process.cwd(), resolve(process.cwd(), OUT)) || OUT} — ` +
|
|
218
|
+
`${used.size} components across ${sources.length} files → ${keptLayers.length}/${manifest.order.length} layers` +
|
|
219
|
+
(keptVendors.length ? `, vendor: ${keptVendors.join(", ")}` : "") +
|
|
220
|
+
(dropped.length ? `\n dropped: ${dropped.join(", ")}` : "\n (every layer is in use)"),
|
|
221
|
+
);
|