@godxjp/ui 5.0.2 → 6.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/README.md +101 -142
- package/package.json +124 -128
- package/scripts/ui-audit.mjs +179 -0
- package/src/app/__tests__/app-provider.test.tsx +232 -0
- package/src/app/__tests__/date-format-labels.test.ts +36 -0
- package/src/app/__tests__/date-formats.test.ts +44 -0
- package/src/app/__tests__/timezones.test.ts +65 -0
- package/src/app/app-provider.tsx +227 -0
- package/src/app/date-format-labels.ts +21 -0
- package/src/app/date-formats.ts +30 -0
- package/src/app/index.ts +40 -0
- package/src/app/locales.ts +32 -0
- package/src/app/request-headers.ts +31 -0
- package/src/app/storage.ts +44 -0
- package/src/app/time-format-labels.ts +19 -0
- package/src/app/time-formats.ts +15 -0
- package/src/app/timezones.ts +208 -0
- package/src/app/types.ts +39 -0
- package/src/app/use-formatting.ts +47 -0
- package/src/components/__tests__/accessibility-primitives.test.tsx +65 -0
- package/src/components/__tests__/docs-parity.test.ts +41 -0
- package/src/components/__tests__/shadcn-release-guardrails.test.ts +71 -0
- package/src/components/__tests__/theme-axes-integration.test.tsx +242 -0
- package/src/components/admin/index.ts +76 -0
- package/src/components/data-display/__tests__/card-table.test.tsx +328 -0
- package/src/components/data-display/__tests__/data-display.test.tsx +73 -0
- package/src/components/data-display/__tests__/data-table.test.tsx +84 -0
- package/src/components/data-display/__tests__/popover.test.tsx +92 -0
- package/src/components/data-display/__tests__/scroll-area-collapsible.test.tsx +66 -0
- package/src/components/data-display/badge.tsx +27 -0
- package/src/components/data-display/card.tsx +194 -0
- package/src/components/data-display/code-badge.tsx +28 -0
- package/src/components/data-display/collapsible.tsx +5 -0
- package/src/components/data-display/data-table.tsx +476 -0
- package/src/components/data-display/empty-state.tsx +22 -0
- package/src/components/data-display/index.ts +41 -0
- package/src/components/data-display/key-value-grid.tsx +46 -0
- package/src/components/data-display/popover.tsx +62 -0
- package/src/components/data-display/progress-meter.tsx +20 -0
- package/src/components/data-display/scan-panel.tsx +16 -0
- package/src/components/data-display/scroll-area.tsx +42 -0
- package/src/components/data-display/status-badge.tsx +83 -0
- package/src/components/data-display/table.tsx +59 -0
- package/src/components/data-display/timeline.tsx +42 -0
- package/src/components/data-display/tree-list.tsx +42 -0
- package/src/components/data-entry/__fixtures__/tree-options.ts +80 -0
- package/src/components/data-entry/__tests__/cascader-tree-transfer.test.tsx +417 -0
- package/src/components/data-entry/__tests__/checkbox-group.test.tsx +40 -0
- package/src/components/data-entry/__tests__/checkbox.test.tsx +20 -0
- package/src/components/data-entry/__tests__/date-autocomplete.test.tsx +94 -0
- package/src/components/data-entry/__tests__/form-field.test.tsx +49 -0
- package/src/components/data-entry/__tests__/input-textarea.test.tsx +38 -0
- package/src/components/data-entry/__tests__/label-select.test.tsx +62 -0
- package/src/components/data-entry/__tests__/pickers.test.tsx +74 -0
- package/src/components/data-entry/__tests__/radio.test.tsx +46 -0
- package/src/components/data-entry/__tests__/search-input.test.tsx +32 -0
- package/src/components/data-entry/__tests__/switch-field.test.tsx +52 -0
- package/src/components/data-entry/__tests__/upload.test.tsx +125 -0
- package/src/components/data-entry/autocomplete.tsx +91 -0
- package/src/components/data-entry/calendar.tsx +90 -0
- package/src/components/data-entry/cascader.tsx +305 -0
- package/src/components/data-entry/checkbox-group.tsx +90 -0
- package/src/components/data-entry/checkbox.tsx +30 -0
- package/src/components/data-entry/choice-field.tsx +27 -0
- package/src/components/data-entry/choice-option.ts +20 -0
- package/src/components/data-entry/color-picker.tsx +75 -0
- package/src/components/data-entry/command.tsx +56 -0
- package/src/components/data-entry/country-select.tsx +88 -0
- package/src/components/data-entry/date-picker.tsx +69 -0
- package/src/components/data-entry/date-range-picker.tsx +75 -0
- package/src/components/data-entry/form-field.tsx +59 -0
- package/src/components/data-entry/index.ts +62 -0
- package/src/components/data-entry/input.tsx +26 -0
- package/src/components/data-entry/label.tsx +25 -0
- package/src/components/data-entry/radio.tsx +109 -0
- package/src/components/data-entry/search-input.tsx +103 -0
- package/src/components/data-entry/select.tsx +149 -0
- package/src/components/data-entry/slider.tsx +38 -0
- package/src/components/data-entry/switch-field.tsx +91 -0
- package/src/components/data-entry/switch.tsx +24 -0
- package/src/components/data-entry/textarea.tsx +12 -0
- package/src/components/data-entry/time-picker.tsx +214 -0
- package/src/components/data-entry/transfer.tsx +231 -0
- package/src/components/data-entry/tree-select-strategy.ts +6 -0
- package/src/components/data-entry/tree-select.tsx +279 -0
- package/src/components/data-entry/tree-utils.ts +221 -0
- package/src/components/data-entry/upload-crop-dialog.tsx +109 -0
- package/src/components/data-entry/upload-types.ts +86 -0
- package/src/components/data-entry/upload.tsx +498 -0
- package/src/components/data-entry/use-upload-draft.ts +93 -0
- package/src/components/feedback/__tests__/alert.test.tsx +127 -0
- package/src/components/feedback/__tests__/dialog.test.tsx +290 -0
- package/src/components/feedback/__tests__/sheet.test.tsx +94 -0
- package/src/components/feedback/__tests__/skeleton.test.tsx +25 -0
- package/src/components/feedback/__tests__/toast.test.tsx +52 -0
- package/src/components/feedback/alert.tsx +167 -0
- package/src/components/feedback/dialog.tsx +325 -0
- package/src/components/feedback/index.ts +53 -0
- package/src/components/feedback/sheet.tsx +130 -0
- package/src/components/feedback/skeleton.tsx +95 -0
- package/src/components/feedback/sonner.tsx +54 -0
- package/src/components/feedback/toaster.tsx +1 -0
- package/src/components/feedback/use-toast.ts +62 -0
- package/src/components/general/__tests__/button.test.tsx +71 -0
- package/src/components/general/button.tsx +61 -0
- package/src/components/general/index.ts +2 -0
- package/src/components/layout/__tests__/page-container.test.tsx +69 -0
- package/src/components/layout/__tests__/page-inset.test.tsx +14 -0
- package/src/components/layout/__tests__/stack-inline.test.tsx +39 -0
- package/src/components/layout/app-shell.tsx +42 -0
- package/src/components/layout/breadcrumb.tsx +35 -0
- package/src/components/layout/index.ts +31 -0
- package/src/components/layout/inline.tsx +13 -0
- package/src/components/layout/menu.tsx +34 -0
- package/src/components/layout/mobile-frame.tsx +57 -0
- package/src/components/layout/page-container.tsx +81 -0
- package/src/components/layout/page-inset.tsx +16 -0
- package/src/components/layout/responsive-grid.tsx +14 -0
- package/src/components/layout/shell-app.tsx +30 -0
- package/src/components/layout/sidebar.tsx +98 -0
- package/src/components/layout/split-pane.tsx +16 -0
- package/src/components/layout/stack.tsx +13 -0
- package/src/components/layout/topbar.tsx +108 -0
- package/src/components/navigation/__tests__/app-pickers.test.tsx +118 -0
- package/src/components/navigation/__tests__/dropdown-menu.test.tsx +104 -0
- package/src/components/navigation/__tests__/navigation.test.tsx +61 -0
- package/src/components/navigation/__tests__/pagination-steps-tabs.test.tsx +76 -0
- package/src/components/navigation/date-format-picker.tsx +55 -0
- package/src/components/navigation/dropdown-menu.tsx +190 -0
- package/src/components/navigation/filter-bar.tsx +38 -0
- package/src/components/navigation/index.ts +28 -0
- package/src/components/navigation/locale-picker.tsx +49 -0
- package/src/components/navigation/page-header.tsx +50 -0
- package/src/components/navigation/pagination-utils.ts +35 -0
- package/src/components/navigation/pagination.tsx +168 -0
- package/src/components/navigation/steps.tsx +163 -0
- package/src/components/navigation/tabs-items.tsx +69 -0
- package/src/components/navigation/tabs.tsx +67 -0
- package/src/components/navigation/time-format-picker.tsx +55 -0
- package/src/components/navigation/timezone-picker.tsx +63 -0
- package/src/components/query/__tests__/data-state.test.tsx +214 -0
- package/src/components/query/__tests__/infinite-prefetch.test.tsx +105 -0
- package/src/components/query/__tests__/query-helpers.test.tsx +61 -0
- package/src/components/query/data-state.tsx +58 -0
- package/src/components/query/index.ts +10 -0
- package/src/components/query/infinite-query-state.tsx +99 -0
- package/src/components/query/mutation-feedback.tsx +31 -0
- package/src/components/query/prefetch-link.tsx +45 -0
- package/src/components/query/query-refetch-button.tsx +41 -0
- package/src/components/ui/alert-dialog.tsx +1 -0
- package/src/components/ui/alert.tsx +1 -0
- package/src/components/ui/autocomplete.tsx +1 -0
- package/src/components/ui/badge.tsx +1 -0
- package/src/components/ui/button.tsx +1 -0
- package/src/components/ui/calendar.tsx +1 -0
- package/src/components/ui/card.tsx +1 -0
- package/src/components/ui/checkbox.tsx +1 -0
- package/src/components/ui/color-picker.tsx +1 -0
- package/src/components/ui/command.tsx +1 -0
- package/src/components/ui/date-picker.tsx +1 -0
- package/src/components/ui/date-range-picker.tsx +1 -0
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/dropdown-menu.tsx +1 -0
- package/src/components/ui/index.tsx +31 -0
- package/src/components/ui/input.tsx +1 -0
- package/src/components/ui/label.tsx +1 -0
- package/src/components/ui/pagination.tsx +1 -0
- package/src/components/ui/popover.tsx +1 -0
- package/src/components/ui/radio.tsx +1 -0
- package/src/components/ui/scroll-area.tsx +1 -0
- package/src/components/ui/select.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/ui/slider.tsx +1 -0
- package/src/components/ui/sonner.tsx +1 -0
- package/src/components/ui/switch.tsx +1 -0
- package/src/components/ui/table.tsx +1 -0
- package/src/components/ui/tabs-items.tsx +1 -0
- package/src/components/ui/tabs.tsx +1 -0
- package/src/components/ui/textarea.tsx +1 -0
- package/src/components/ui/time-picker.tsx +1 -0
- package/src/components/ui/upload.tsx +1 -0
- package/src/form/__tests__/use-zod-form.test.tsx +97 -0
- package/src/form/form-field-control.tsx +44 -0
- package/src/form/form-root.tsx +29 -0
- package/src/form/index.ts +7 -0
- package/src/form/use-zod-form.ts +29 -0
- package/src/i18n/__tests__/translate.test.ts +23 -0
- package/src/i18n/index.ts +9 -0
- package/src/i18n/messages/en.json +171 -0
- package/src/i18n/messages/ja.json +171 -0
- package/src/i18n/messages/vi.json +171 -0
- package/src/i18n/translate.ts +74 -0
- package/src/i18n/use-translation.ts +53 -0
- package/src/index.ts +3 -0
- package/src/lib/__tests__/control-styles.test.ts +78 -0
- package/src/lib/__tests__/datetime.test.ts +77 -0
- package/src/lib/__tests__/format-date.test.ts +97 -0
- package/src/lib/__tests__/format.test.ts +62 -0
- package/src/lib/__tests__/theme-tokens-audit.test.ts +176 -0
- package/src/lib/__tests__/theme-tokens-css.test.ts +118 -0
- package/src/lib/__tests__/token-governance.test.ts +191 -0
- package/src/lib/__tests__/variants.test.ts +18 -0
- package/src/lib/control-styles.ts +33 -0
- package/src/lib/datetime/detect.ts +25 -0
- package/src/lib/datetime/format-date.ts +100 -0
- package/src/lib/datetime/format.ts +140 -0
- package/src/lib/datetime/index.ts +25 -0
- package/src/lib/datetime/parse.ts +51 -0
- package/src/lib/datetime/sync.ts +48 -0
- package/src/lib/format.ts +114 -0
- package/src/lib/hooks.ts +54 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/variants.ts +40 -0
- package/src/props/components/app.prop.ts +99 -0
- package/src/props/components/data-display.prop.ts +73 -0
- package/src/props/components/data-entry.prop.ts +334 -0
- package/src/props/components/feedback.prop.ts +80 -0
- package/src/props/components/form.prop.ts +46 -0
- package/src/props/components/general.prop.ts +18 -0
- package/src/props/components/index.ts +99 -0
- package/src/props/components/layout.prop.ts +130 -0
- package/src/props/components/navigation.prop.ts +88 -0
- package/src/props/components/query.prop.ts +94 -0
- package/src/props/index.ts +17 -0
- package/src/props/registry.ts +603 -0
- package/src/props/vocabulary/content.prop.ts +35 -0
- package/src/props/vocabulary/data.prop.ts +46 -0
- package/src/props/vocabulary/index.ts +73 -0
- package/src/props/vocabulary/interaction.prop.ts +42 -0
- package/src/props/vocabulary/layout.prop.ts +25 -0
- package/src/props/vocabulary/navigation.prop.ts +19 -0
- package/src/props/vocabulary/shared.prop.ts +59 -0
- package/src/styles/alert-layout.css +191 -0
- package/src/styles/badge-layout.css +22 -0
- package/src/styles/card-layout.css +373 -0
- package/src/styles/control.css +504 -0
- package/src/styles/data-display-layout.css +246 -0
- package/src/styles/density.css +43 -0
- package/src/styles/dialog-layout.css +84 -0
- package/src/styles/index.css +105 -0
- package/src/styles/layout.css +479 -0
- package/src/styles/shell-layout.css +604 -0
- package/src/styles/table-layout.css +109 -0
- package/src/test/__tests__/render-loop-guard.test.tsx +38 -0
- package/src/test/jest-dom.d.ts +4 -0
- package/src/test/render-loop-guard.tsx +50 -0
- package/src/test/render.tsx +29 -0
- package/src/test/theme-globals.test.ts +77 -0
- package/src/test/theme-globals.ts +134 -0
- package/src/test/theme-test-utils.tsx +67 -0
- package/src/theme/example.service.css +37 -0
- package/src/tokens/base.css +13 -0
- package/src/tokens/foundation.css +151 -0
- package/src/tokens/primitives/badge.css +13 -0
- package/src/tokens/primitives/card.css +29 -0
- package/src/tokens/primitives/control.css +55 -0
- package/src/tokens/primitives/feedback.css +17 -0
- package/src/tokens/primitives/layout.css +20 -0
- package/src/tokens/primitives/navigation.css +13 -0
- package/src/tokens/primitives/table.css +10 -0
- package/BRAND.md +0 -296
- package/CHANGELOG.md +0 -668
- package/config/eslint.js +0 -54
- package/config/prettier.cjs +0 -20
- package/config/tsconfig.base.json +0 -22
- package/config/vitest.base.ts +0 -26
- package/dist/MiniMonth-YAmPGEpC.d.ts +0 -143
- package/dist/Table.types-BbsxoIYE.d.ts +0 -352
- package/dist/color-DO0qqUAb.d.ts +0 -38
- package/dist/components/composites.d.ts +0 -963
- package/dist/components/composites.js +0 -7343
- package/dist/components/composites.js.map +0 -1
- package/dist/components/primitives.d.ts +0 -2744
- package/dist/components/primitives.js +0 -7356
- package/dist/components/primitives.js.map +0 -1
- package/dist/components/shell.d.ts +0 -182
- package/dist/components/shell.js +0 -774
- package/dist/components/shell.js.map +0 -1
- package/dist/hooks.d.ts +0 -100
- package/dist/hooks.js +0 -558
- package/dist/hooks.js.map +0 -1
- package/dist/i18n.d.ts +0 -61
- package/dist/i18n.js +0 -860
- package/dist/i18n.js.map +0 -1
- package/dist/index.d.ts +0 -33
- package/dist/index.js +0 -13062
- package/dist/index.js.map +0 -1
- package/dist/padding-DY0JV5Ja.d.ts +0 -16
- package/dist/preferences.d.ts +0 -132
- package/dist/preferences.js +0 -262
- package/dist/preferences.js.map +0 -1
- package/dist/props.d.ts +0 -86
- package/dist/props.js +0 -16
- package/dist/props.js.map +0 -1
- package/dist/size-CQwNvOWd.d.ts +0 -19
- package/dist/types-LTj-2bl-.d.ts +0 -30
- package/dist/useTableViews-D5NIAJ7h.d.ts +0 -154
- package/src/tokens/tailwind.css +0 -158
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach } from "vitest";
|
|
2
|
+
import { getDateFnsLocale } from "../../app/locales";
|
|
3
|
+
import {
|
|
4
|
+
detectFormatDateKind,
|
|
5
|
+
formatDate,
|
|
6
|
+
formatCalendarDate,
|
|
7
|
+
resetDatetimeContextForTests,
|
|
8
|
+
syncDatetimeContext,
|
|
9
|
+
} from "../datetime";
|
|
10
|
+
|
|
11
|
+
describe("formatDate (unified) — regression suite", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
resetDatetimeContextForTests();
|
|
14
|
+
syncDatetimeContext({
|
|
15
|
+
locale: "en",
|
|
16
|
+
timezone: "Asia/Ho_Chi_Minh",
|
|
17
|
+
timeFormat: "24h",
|
|
18
|
+
dateFormat: "iso",
|
|
19
|
+
dateFnsLocale: getDateFnsLocale("en"),
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("auto-detects ISO date-only string", () => {
|
|
24
|
+
expect(formatDate("2026-05-01")).toBe("2026-05-01");
|
|
25
|
+
expect(detectFormatDateKind("2026-05-01")).toBe("date");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("auto-detects HH:mm time string", () => {
|
|
29
|
+
expect(formatDate("14:30", { kind: "auto" })).toBe("14:30");
|
|
30
|
+
expect(detectFormatDateKind("14:30")).toBe("time");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("auto-detects ISO datetime instant with timezone shift", () => {
|
|
34
|
+
expect(formatDate("2026-05-01T14:30:00Z")).toMatch(/2026-05-01 21:30/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("uses AppProvider defaults when options omitted", () => {
|
|
38
|
+
expect(formatDate("2026-05-01T14:30:00Z", { kind: "datetime" })).toMatch(/21:30/);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("allows per-call timezone override", () => {
|
|
42
|
+
expect(formatDate("2026-05-01T14:30:00Z", { kind: "datetime", timezone: "UTC" })).toMatch(
|
|
43
|
+
/2026-05-01 14:30/,
|
|
44
|
+
);
|
|
45
|
+
expect(
|
|
46
|
+
formatDate("2026-05-01T14:30:00Z", { kind: "datetime", timezone: "Asia/Tokyo" }),
|
|
47
|
+
).toMatch(/2026-05-01 23:30/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("formats calendar Date with kind calendar", () => {
|
|
51
|
+
expect(formatDate(new Date(2026, 4, 2), { kind: "calendar" })).toBe("2026-05-02");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("uses dateFormat from context for date-only strings (dmy)", () => {
|
|
55
|
+
syncDatetimeContext({
|
|
56
|
+
locale: "vi",
|
|
57
|
+
timezone: "Asia/Ho_Chi_Minh",
|
|
58
|
+
timeFormat: "24h",
|
|
59
|
+
dateFormat: "dmy",
|
|
60
|
+
dateFnsLocale: getDateFnsLocale("vi"),
|
|
61
|
+
});
|
|
62
|
+
expect(formatDate("2026-05-01")).toBe("01/05/2026");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("uses dateFormat iso for ja default (yyyy-MM-dd)", () => {
|
|
66
|
+
syncDatetimeContext({
|
|
67
|
+
locale: "ja",
|
|
68
|
+
timezone: "Asia/Tokyo",
|
|
69
|
+
timeFormat: "24h",
|
|
70
|
+
dateFormat: "iso",
|
|
71
|
+
dateFnsLocale: getDateFnsLocale("ja"),
|
|
72
|
+
});
|
|
73
|
+
expect(formatDate("2026-05-01")).toBe("2026-05-01");
|
|
74
|
+
expect(formatDate("2026-05-01T14:30:00Z", { kind: "datetime" })).toMatch(/2026-05-01 23:30/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("uses dateFormat mdy for en (MM/dd/yyyy)", () => {
|
|
78
|
+
syncDatetimeContext({
|
|
79
|
+
locale: "en",
|
|
80
|
+
timezone: "UTC",
|
|
81
|
+
timeFormat: "12h",
|
|
82
|
+
dateFormat: "mdy",
|
|
83
|
+
dateFnsLocale: getDateFnsLocale("en"),
|
|
84
|
+
});
|
|
85
|
+
expect(formatDate("2026-05-01")).toBe("05/01/2026");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("formatCalendarDate respects dateFormat override per call", () => {
|
|
89
|
+
expect(formatCalendarDate(new Date(2026, 4, 2), { dateFormat: "dmy" })).toBe("02/05/2026");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns em dash for empty values", () => {
|
|
93
|
+
expect(formatDate(null)).toBe("—");
|
|
94
|
+
expect(formatDate("")).toBe("—");
|
|
95
|
+
expect(formatDate(undefined)).toBe("—");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { getDateFnsLocale } from "../../app/locales";
|
|
3
|
+
import { syncDatetimeContext } from "../datetime";
|
|
4
|
+
import {
|
|
5
|
+
formatBytes,
|
|
6
|
+
formatCurrency,
|
|
7
|
+
formatDateTime,
|
|
8
|
+
formatRelative,
|
|
9
|
+
formatTime,
|
|
10
|
+
humanError,
|
|
11
|
+
shortId,
|
|
12
|
+
} from "../format";
|
|
13
|
+
|
|
14
|
+
describe("format helpers", () => {
|
|
15
|
+
it("formatDateTime returns dash for null", () => {
|
|
16
|
+
expect(formatDateTime(null)).toBe("—");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("formatDateTime formats ISO string in synced timezone", () => {
|
|
20
|
+
syncDatetimeContext({
|
|
21
|
+
locale: "en",
|
|
22
|
+
timezone: "UTC",
|
|
23
|
+
timeFormat: "24h",
|
|
24
|
+
dateFormat: "iso",
|
|
25
|
+
dateFnsLocale: getDateFnsLocale("en"),
|
|
26
|
+
});
|
|
27
|
+
expect(formatDateTime("2026-05-01T14:30:00Z")).toMatch(/2026-05-01 14:30/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("formatTime respects 12h vs 24h", () => {
|
|
31
|
+
const d = "2026-05-01T14:30:00Z";
|
|
32
|
+
expect(formatTime(d, { timeFormat: "24h" })).toMatch(/\d{2}:\d{2}/);
|
|
33
|
+
expect(formatTime(d, { timeFormat: "12h" })).toMatch(/PM|AM|am|pm/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("formatRelative returns relative phrase", () => {
|
|
37
|
+
const recent = new Date(Date.now() - 60_000).toISOString();
|
|
38
|
+
expect(formatRelative(recent)).toMatch(/minute/i);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("formatBytes scales units", () => {
|
|
42
|
+
expect(formatBytes(512)).toBe("512 B");
|
|
43
|
+
expect(formatBytes(2048)).toBe("2.0 KB");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("formatCurrency formats minor units", () => {
|
|
47
|
+
expect(formatCurrency(1995, "USD")).toMatch(/19\.95/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("shortId truncates long ids", () => {
|
|
51
|
+
expect(shortId("0123456789abcdef")).toBe("01234567…");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("humanError extracts message from Error", () => {
|
|
55
|
+
expect(humanError(new Error("fail"))).toBe("fail");
|
|
56
|
+
expect(humanError(new Error("500 Bad: body"))).toBe("body");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("humanError returns generic for non-Error", () => {
|
|
60
|
+
expect(humanError("plain")).toMatch(/retry|thử lại|再試行/i);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
const componentsRoot = join(dirname(fileURLToPath(import.meta.url)), "../../components");
|
|
7
|
+
const srcRoot = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
8
|
+
|
|
9
|
+
/** Raw Tailwind palette in components breaks theme axes — use semantic tokens instead. */
|
|
10
|
+
const FORBIDDEN_PALETTE = [
|
|
11
|
+
/\bbg-green-/,
|
|
12
|
+
/\bbg-blue-/,
|
|
13
|
+
/\bbg-amber-/,
|
|
14
|
+
/\bbg-red-\d/,
|
|
15
|
+
/\btext-green-/,
|
|
16
|
+
/\btext-blue-/,
|
|
17
|
+
/\btext-amber-/,
|
|
18
|
+
/\bborder-green-/,
|
|
19
|
+
/\bborder-blue-/,
|
|
20
|
+
/\bborder-amber-/,
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/** Fixed control heights bypass --control-height density axis. */
|
|
24
|
+
const FORBIDDEN_CONTROL_HEIGHT = [
|
|
25
|
+
/\bh-8\b/,
|
|
26
|
+
/\bh-9\b/,
|
|
27
|
+
/\bh-10\b/,
|
|
28
|
+
/\bh-11\b/,
|
|
29
|
+
/\bh-12\b/,
|
|
30
|
+
/\bsize-7\b/,
|
|
31
|
+
/\bsize-8\b/,
|
|
32
|
+
/\bsize-9\b/,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** Legacy date formatting — must use formatDate. */
|
|
36
|
+
const FORBIDDEN_DATE_FORMAT = [
|
|
37
|
+
/from ["']date-fns["']/,
|
|
38
|
+
/\.toLocaleString\s*\(/,
|
|
39
|
+
/\.toLocaleDateString\s*\(/,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const SKIP_CONTROL_HEIGHT_DIRS = new Set(["feedback/skeleton.tsx"]);
|
|
43
|
+
|
|
44
|
+
const INTERACTIVE_MUST_USE_CONTROL_STYLES = [
|
|
45
|
+
"data-entry/select.tsx",
|
|
46
|
+
"data-entry/textarea.tsx",
|
|
47
|
+
"data-entry/command.tsx",
|
|
48
|
+
"data-display/table.tsx",
|
|
49
|
+
"data-display/data-table.tsx",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const RTL_LOGICAL_PRIMITIVES = [
|
|
53
|
+
"data-entry/select.tsx",
|
|
54
|
+
"navigation/dropdown-menu.tsx",
|
|
55
|
+
"feedback/sheet.tsx",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const FORBIDDEN_RTL_PHYSICAL_SNIPPETS = [
|
|
59
|
+
"absolute left-",
|
|
60
|
+
"absolute right-",
|
|
61
|
+
" pl-",
|
|
62
|
+
" pr-",
|
|
63
|
+
" ml-auto",
|
|
64
|
+
" mr-auto",
|
|
65
|
+
"data-[inset=true]:pl-",
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
function listTsxFiles(dir: string): string[] {
|
|
69
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
70
|
+
const files: string[] = [];
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const full = join(dir, entry.name);
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
if (entry.name === "__tests__") continue;
|
|
75
|
+
files.push(...listTsxFiles(full));
|
|
76
|
+
} else if (entry.name.endsWith(".tsx")) {
|
|
77
|
+
files.push(full);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return files;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function findAllViolations(patterns: RegExp[], content: string): string[] {
|
|
84
|
+
const hits: string[] = [];
|
|
85
|
+
for (const pattern of patterns) {
|
|
86
|
+
const matches = content.match(new RegExp(pattern.source, "g"));
|
|
87
|
+
if (matches) hits.push(...matches);
|
|
88
|
+
}
|
|
89
|
+
return [...new Set(hits)];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
describe("theme token audit (components)", () => {
|
|
93
|
+
const files = listTsxFiles(componentsRoot);
|
|
94
|
+
|
|
95
|
+
it("does not use raw palette colors in any component file", () => {
|
|
96
|
+
const offenders: string[] = [];
|
|
97
|
+
for (const file of files) {
|
|
98
|
+
const rel = relative(componentsRoot, file);
|
|
99
|
+
const content = readFileSync(file, "utf8");
|
|
100
|
+
const hits = findAllViolations(FORBIDDEN_PALETTE, content);
|
|
101
|
+
if (hits.length > 0) offenders.push(`${rel}: ${hits.join(", ")}`);
|
|
102
|
+
}
|
|
103
|
+
expect(offenders, "Use semantic tokens (primary, success, warning, info, accent)").toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("does not hardcode control heights in interactive components", () => {
|
|
107
|
+
const offenders: string[] = [];
|
|
108
|
+
for (const file of files) {
|
|
109
|
+
const rel = relative(componentsRoot, file);
|
|
110
|
+
if (SKIP_CONTROL_HEIGHT_DIRS.has(rel)) continue;
|
|
111
|
+
const content = readFileSync(file, "utf8");
|
|
112
|
+
const hits = findAllViolations(FORBIDDEN_CONTROL_HEIGHT, content);
|
|
113
|
+
if (hits.length > 0) offenders.push(`${rel}: ${hits.join(", ")}`);
|
|
114
|
+
}
|
|
115
|
+
expect(offenders, "Use var(--control-height) via control-styles.ts").toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("interactive primitives import control-styles", () => {
|
|
119
|
+
const missing: string[] = [];
|
|
120
|
+
for (const rel of INTERACTIVE_MUST_USE_CONTROL_STYLES) {
|
|
121
|
+
const content = readFileSync(join(componentsRoot, rel), "utf8");
|
|
122
|
+
if (!content.includes("control-styles")) missing.push(rel);
|
|
123
|
+
}
|
|
124
|
+
expect(missing, "Must import shared control-styles").toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("components do not call toLocaleString for display dates", () => {
|
|
128
|
+
const offenders: string[] = [];
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
const rel = relative(componentsRoot, file);
|
|
131
|
+
const content = readFileSync(file, "utf8");
|
|
132
|
+
const hits = findAllViolations(FORBIDDEN_DATE_FORMAT, content);
|
|
133
|
+
if (hits.length > 0) offenders.push(`${rel}: ${hits.join(", ")}`);
|
|
134
|
+
}
|
|
135
|
+
expect(offenders, "Use formatDate from @/lib/datetime").toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("shadcn primitive chrome uses logical direction utilities", () => {
|
|
139
|
+
const offenders: string[] = [];
|
|
140
|
+
for (const rel of RTL_LOGICAL_PRIMITIVES) {
|
|
141
|
+
const content = readFileSync(join(componentsRoot, rel), "utf8");
|
|
142
|
+
const hits = FORBIDDEN_RTL_PHYSICAL_SNIPPETS.filter((snippet) => content.includes(snippet));
|
|
143
|
+
if (hits.length > 0) offenders.push(`${rel}: ${hits.join(", ")}`);
|
|
144
|
+
}
|
|
145
|
+
expect(offenders, "Use start/end, ps/pe, ms/me for RTL-safe primitive chrome").toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("dialog close offset uses logical CSS", () => {
|
|
149
|
+
const content = readFileSync(join(srcRoot, "styles/dialog-layout.css"), "utf8");
|
|
150
|
+
expect(content).toContain("inset-inline-end: var(--space-dialog-close-offset)");
|
|
151
|
+
expect(content).not.toContain("right: var(--space-dialog-close-offset)");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("theme token audit (lib/datetime)", () => {
|
|
156
|
+
it("format-date module exists and exports formatDate", () => {
|
|
157
|
+
const index = readFileSync(join(srcRoot, "lib/datetime/index.ts"), "utf8");
|
|
158
|
+
expect(index).toContain("formatDate");
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("theme token audit (badge/status)", () => {
|
|
163
|
+
it("status-badge uses tone classes from control-styles", () => {
|
|
164
|
+
const content = readFileSync(join(componentsRoot, "data-display/status-badge.tsx"), "utf8");
|
|
165
|
+
expect(content).toContain("toneSuccessClass");
|
|
166
|
+
expect(content).toContain("toneWarningClass");
|
|
167
|
+
expect(content).toContain("toneInfoClass");
|
|
168
|
+
expect(content).not.toMatch(/green-500/);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("badge success variant uses toneSuccessClass", () => {
|
|
172
|
+
const content = readFileSync(join(componentsRoot, "data-display/badge.tsx"), "utf8");
|
|
173
|
+
expect(content).toContain("toneSuccessClass");
|
|
174
|
+
expect(content).not.toMatch(/bg-green-/);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
7
|
+
|
|
8
|
+
function readSrc(relative: string): string {
|
|
9
|
+
return readFileSync(join(root, relative), "utf8");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function readStyle(name: string): string {
|
|
13
|
+
return readSrc(`styles/${name}`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("theme CSS tokens (base.css + layout owners)", () => {
|
|
17
|
+
const base = readSrc("tokens/base.css");
|
|
18
|
+
const tokenCss = [
|
|
19
|
+
base,
|
|
20
|
+
readSrc("tokens/foundation.css"),
|
|
21
|
+
readSrc("tokens/primitives/layout.css"),
|
|
22
|
+
readSrc("tokens/primitives/control.css"),
|
|
23
|
+
readSrc("tokens/primitives/card.css"),
|
|
24
|
+
readSrc("tokens/primitives/table.css"),
|
|
25
|
+
readSrc("tokens/primitives/feedback.css"),
|
|
26
|
+
readSrc("tokens/primitives/badge.css"),
|
|
27
|
+
].join("\n");
|
|
28
|
+
const index = readSrc("styles/index.css");
|
|
29
|
+
const density = readStyle("density.css");
|
|
30
|
+
const control = readStyle("control.css");
|
|
31
|
+
|
|
32
|
+
it("defines semantic color tokens in :root", () => {
|
|
33
|
+
for (const token of [
|
|
34
|
+
"--primary:",
|
|
35
|
+
"--accent:",
|
|
36
|
+
"--ring:",
|
|
37
|
+
"--success:",
|
|
38
|
+
"--warning:",
|
|
39
|
+
"--info:",
|
|
40
|
+
"--attention:",
|
|
41
|
+
"--tracking-internal:",
|
|
42
|
+
"--tracking-seller:",
|
|
43
|
+
"--tracking-yamato:",
|
|
44
|
+
"--destructive:",
|
|
45
|
+
]) {
|
|
46
|
+
expect(tokenCss, `missing ${token} in token graph`).toContain(token);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("defines density primitives in :root", () => {
|
|
51
|
+
for (const token of [
|
|
52
|
+
"--control-height-compact",
|
|
53
|
+
"--control-height-default",
|
|
54
|
+
"--control-height-comfortable",
|
|
55
|
+
"--table-row-height-compact",
|
|
56
|
+
"--table-row-height-default",
|
|
57
|
+
"--table-row-height-comfortable",
|
|
58
|
+
"--table-cell-padding-y",
|
|
59
|
+
]) {
|
|
60
|
+
expect(tokenCss, `missing ${token}`).toContain(token);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("uses base.css as the token manifest", () => {
|
|
65
|
+
for (const file of [
|
|
66
|
+
"foundation.css",
|
|
67
|
+
"primitives/layout.css",
|
|
68
|
+
"primitives/control.css",
|
|
69
|
+
"primitives/card.css",
|
|
70
|
+
"primitives/table.css",
|
|
71
|
+
"primitives/feedback.css",
|
|
72
|
+
"primitives/badge.css",
|
|
73
|
+
]) {
|
|
74
|
+
expect(base).toContain(`@import "./${file}"`);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("maps semantic colors in @theme for Tailwind", () => {
|
|
79
|
+
for (const token of [
|
|
80
|
+
"--color-primary:",
|
|
81
|
+
"--color-accent:",
|
|
82
|
+
"--color-ring:",
|
|
83
|
+
"--color-success:",
|
|
84
|
+
"--color-warning:",
|
|
85
|
+
"--color-info:",
|
|
86
|
+
"--color-attention:",
|
|
87
|
+
"--color-tracking-internal:",
|
|
88
|
+
"--color-tracking-seller:",
|
|
89
|
+
"--color-tracking-yamato:",
|
|
90
|
+
]) {
|
|
91
|
+
expect(index, `missing ${token} in index.css @theme`).toContain(token);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("defines ui-control utilities in control.css", () => {
|
|
96
|
+
expect(control).toContain(".ui-control {");
|
|
97
|
+
expect(control).toContain("height: var(--control-height)");
|
|
98
|
+
expect(control).toContain(".ui-control-multiline {");
|
|
99
|
+
expect(control).toContain("font-size: var(--font-size-sm)");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("sets --table-cell-padding-y per density class", () => {
|
|
103
|
+
expect(density).toContain(".ui-density-compact {");
|
|
104
|
+
expect(density).toMatch(/ui-density-compact[\s\S]*--table-cell-padding-y/);
|
|
105
|
+
expect(density).toMatch(/ui-density-comfortable[\s\S]*--table-cell-padding-y/);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("wires Tailwind text-* to typography tokens", () => {
|
|
109
|
+
expect(index).toContain("--text-sm: var(--font-size-sm)");
|
|
110
|
+
expect(index).toContain("--text-xs: var(--font-size-xs)");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("imports split layout CSS owners", () => {
|
|
114
|
+
for (const file of ["density.css", "layout.css", "card-layout.css", "table-layout.css"]) {
|
|
115
|
+
expect(index).toContain(`@import "./${file}"`);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
const root = join(import.meta.dirname, "../..");
|
|
6
|
+
|
|
7
|
+
const LAYOUT_CSS_FILES = [
|
|
8
|
+
"density.css",
|
|
9
|
+
"layout.css",
|
|
10
|
+
"control.css",
|
|
11
|
+
"card-layout.css",
|
|
12
|
+
"table-layout.css",
|
|
13
|
+
"dialog-layout.css",
|
|
14
|
+
"alert-layout.css",
|
|
15
|
+
"badge-layout.css",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/** Components fully on data-slot + *-layout.css — must stay free of Tailwind spacing utilities. */
|
|
19
|
+
const MIGRATED_LAYOUT_COMPONENTS = [
|
|
20
|
+
"components/data-display/table.tsx",
|
|
21
|
+
"components/data-display/empty-state.tsx",
|
|
22
|
+
"components/data-display/status-badge.tsx",
|
|
23
|
+
"components/data-display/data-table.tsx",
|
|
24
|
+
"components/feedback/dialog.tsx",
|
|
25
|
+
"components/feedback/alert.tsx",
|
|
26
|
+
"components/data-display/card.tsx",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/** Pending migration — tracked separately; do not add new files without a layout owner. */
|
|
30
|
+
const SPACING_UTILITY_ALLOWLIST = new Set([
|
|
31
|
+
"calendar.tsx",
|
|
32
|
+
"command.tsx",
|
|
33
|
+
"upload.tsx",
|
|
34
|
+
"upload-crop-dialog.tsx",
|
|
35
|
+
"cascader.tsx",
|
|
36
|
+
"tree-select.tsx",
|
|
37
|
+
"transfer.tsx",
|
|
38
|
+
"time-picker.tsx",
|
|
39
|
+
"tabs.tsx",
|
|
40
|
+
"tabs-items.tsx",
|
|
41
|
+
"steps.tsx",
|
|
42
|
+
"sheet.tsx",
|
|
43
|
+
"dropdown-menu.tsx",
|
|
44
|
+
"pagination.tsx",
|
|
45
|
+
"skeleton.tsx",
|
|
46
|
+
"choice-field.tsx",
|
|
47
|
+
"radio.tsx",
|
|
48
|
+
"color-picker.tsx",
|
|
49
|
+
"autocomplete.tsx",
|
|
50
|
+
"date-range-picker.tsx",
|
|
51
|
+
"locale-picker.tsx",
|
|
52
|
+
"timezone-picker.tsx",
|
|
53
|
+
"date-format-picker.tsx",
|
|
54
|
+
"query-refetch-button.tsx",
|
|
55
|
+
"key-value-grid.tsx",
|
|
56
|
+
"card.tsx", // CardStat title uses text-2xl override (value typography)
|
|
57
|
+
"button.tsx", // size variants use token-backed arbitrary lengths
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const FORBIDDEN_IN_COMPONENTS =
|
|
61
|
+
/\b(p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap|space-[xy])-(0|0\.5|1|1\.5|2|2\.5|3|3\.5|4|5|6|7|8|9|10|11|12|14|16|20|24|28|32|36|40|44|48|52|56|60|64|72|80|96)\b/;
|
|
62
|
+
|
|
63
|
+
const FORBIDDEN_RAW_COLORS =
|
|
64
|
+
/\b(text|bg|border)-(emerald|red|green|blue|yellow|orange|pink|purple|gray|slate|zinc|neutral|stone|amber|lime|teal|cyan|indigo|violet|fuchsia|rose)-/;
|
|
65
|
+
|
|
66
|
+
function walkComponents(dir: string): string[] {
|
|
67
|
+
const out: string[] = [];
|
|
68
|
+
for (const entry of readdirSync(dir)) {
|
|
69
|
+
const full = join(dir, entry);
|
|
70
|
+
if (statSync(full).isDirectory()) {
|
|
71
|
+
out.push(...walkComponents(full));
|
|
72
|
+
} else if (entry.endsWith(".tsx") && !entry.endsWith(".test.tsx")) {
|
|
73
|
+
out.push(full);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("token governance", () => {
|
|
80
|
+
const base = readFileSync(join(root, "tokens/base.css"), "utf8");
|
|
81
|
+
const tokenCss = [
|
|
82
|
+
base,
|
|
83
|
+
readFileSync(join(root, "tokens/foundation.css"), "utf8"),
|
|
84
|
+
readFileSync(join(root, "tokens/primitives/layout.css"), "utf8"),
|
|
85
|
+
readFileSync(join(root, "tokens/primitives/control.css"), "utf8"),
|
|
86
|
+
readFileSync(join(root, "tokens/primitives/card.css"), "utf8"),
|
|
87
|
+
readFileSync(join(root, "tokens/primitives/table.css"), "utf8"),
|
|
88
|
+
readFileSync(join(root, "tokens/primitives/feedback.css"), "utf8"),
|
|
89
|
+
readFileSync(join(root, "tokens/primitives/badge.css"), "utf8"),
|
|
90
|
+
].join("\n");
|
|
91
|
+
|
|
92
|
+
it("imports all layout CSS owners from index.css", () => {
|
|
93
|
+
const index = readFileSync(join(root, "styles/index.css"), "utf8");
|
|
94
|
+
for (const file of LAYOUT_CSS_FILES) {
|
|
95
|
+
expect(index, `missing @import ./${file}`).toContain(`@import "./${file}"`);
|
|
96
|
+
}
|
|
97
|
+
expect(index).not.toContain(".ui-stack-md {");
|
|
98
|
+
expect(index).not.toContain('[data-slot="card-header"]');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("uses base.css as the token manifest", () => {
|
|
102
|
+
for (const file of [
|
|
103
|
+
"foundation.css",
|
|
104
|
+
"primitives/layout.css",
|
|
105
|
+
"primitives/control.css",
|
|
106
|
+
"primitives/card.css",
|
|
107
|
+
"primitives/table.css",
|
|
108
|
+
"primitives/feedback.css",
|
|
109
|
+
"primitives/badge.css",
|
|
110
|
+
]) {
|
|
111
|
+
expect(base).toContain(`@import "./${file}"`);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("defines component slot tokens in the token graph", () => {
|
|
116
|
+
for (const token of [
|
|
117
|
+
"--space-dialog-inset",
|
|
118
|
+
"--space-alert-inset",
|
|
119
|
+
"--space-table-cell-x",
|
|
120
|
+
"--space-badge-x",
|
|
121
|
+
"--card-title-font-size",
|
|
122
|
+
"--control-height-default",
|
|
123
|
+
]) {
|
|
124
|
+
expect(tokenCss).toContain(token);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("keeps migrated components free of Tailwind spacing utilities", () => {
|
|
129
|
+
const violations: string[] = [];
|
|
130
|
+
|
|
131
|
+
for (const relative of MIGRATED_LAYOUT_COMPONENTS) {
|
|
132
|
+
const src = readFileSync(join(root, relative), "utf8");
|
|
133
|
+
if (FORBIDDEN_IN_COMPONENTS.test(src)) {
|
|
134
|
+
violations.push(relative.split("/").pop()!);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
expect(violations, `move spacing to *-layout.css: ${violations.join(", ")}`).toEqual([]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("keeps non-allowlisted components on migration track (informational cap)", () => {
|
|
142
|
+
const componentsDir = join(root, "components");
|
|
143
|
+
const pending: string[] = [];
|
|
144
|
+
|
|
145
|
+
for (const file of walkComponents(componentsDir)) {
|
|
146
|
+
const name = file.split("/").pop()!;
|
|
147
|
+
const relative = file.slice(root.length + 1);
|
|
148
|
+
if (MIGRATED_LAYOUT_COMPONENTS.some((m) => relative.endsWith(m.replace(/^components\//, ""))))
|
|
149
|
+
continue;
|
|
150
|
+
if (SPACING_UTILITY_ALLOWLIST.has(name)) continue;
|
|
151
|
+
|
|
152
|
+
const src = readFileSync(file, "utf8");
|
|
153
|
+
if (FORBIDDEN_IN_COMPONENTS.test(src)) {
|
|
154
|
+
pending.push(name);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
expect(pending.length, `pending layout migration: ${pending.join(", ")}`).toBeLessThan(30);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("keeps migrated components free of raw Tailwind palette colors", () => {
|
|
162
|
+
const violations: string[] = [];
|
|
163
|
+
|
|
164
|
+
for (const relative of MIGRATED_LAYOUT_COMPONENTS) {
|
|
165
|
+
if (relative.endsWith("card.tsx")) continue;
|
|
166
|
+
const src = readFileSync(join(root, relative), "utf8");
|
|
167
|
+
if (FORBIDDEN_RAW_COLORS.test(src)) {
|
|
168
|
+
violations.push(relative.split("/").pop()!);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
expect(violations, `use semantic tokens: ${violations.join(", ")}`).toEqual([]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("legacy scan — non-migrated must not grow unbounded", () => {
|
|
176
|
+
const componentsDir = join(root, "components");
|
|
177
|
+
const violations: string[] = [];
|
|
178
|
+
|
|
179
|
+
for (const file of walkComponents(componentsDir)) {
|
|
180
|
+
const name = file.split("/").pop()!;
|
|
181
|
+
if (SPACING_UTILITY_ALLOWLIST.has(name)) continue;
|
|
182
|
+
|
|
183
|
+
const src = readFileSync(file, "utf8");
|
|
184
|
+
if (FORBIDDEN_RAW_COLORS.test(src)) {
|
|
185
|
+
violations.push(name);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
expect(violations.length).toBeLessThan(15);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { densityClass, inlineGapClass, stackGapClass } from "../variants";
|
|
3
|
+
|
|
4
|
+
describe("variants maps", () => {
|
|
5
|
+
it("maps page density to ui classes", () => {
|
|
6
|
+
expect(densityClass.compact).toBe("ui-density-compact");
|
|
7
|
+
expect(densityClass.default).toBe("ui-density-default");
|
|
8
|
+
expect(densityClass.comfortable).toBe("ui-density-comfortable");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("maps stack gaps", () => {
|
|
12
|
+
expect(stackGapClass.md).toBe("ui-stack-md");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("maps inline gaps", () => {
|
|
16
|
+
expect(inlineGapClass.sm).toBe("ui-inline-sm");
|
|
17
|
+
});
|
|
18
|
+
});
|