@godxjp/ui 28.12.0 → 29.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.
Files changed (178) hide show
  1. package/agent/START-HERE.md +29 -10
  2. package/agent/components/Anchor.json +6 -1
  3. package/agent/components/AppLauncher.json +10 -0
  4. package/agent/components/AppShell.json +1 -1
  5. package/agent/components/AreaChart.json +19 -1
  6. package/agent/components/Attachments.json +26 -1
  7. package/agent/components/BarChart.json +11 -1
  8. package/agent/components/BranchScopePicker.json +10 -0
  9. package/agent/components/Cascader.json +6 -1
  10. package/agent/components/Checkbox.json +6 -0
  11. package/agent/components/CompactBarTrend.json +1 -1
  12. package/agent/components/CredentialReveal.json +21 -0
  13. package/agent/components/DataState.json +1 -1
  14. package/agent/components/DataTable.json +4 -4
  15. package/agent/components/FormField.json +1 -1
  16. package/agent/components/InfiniteQueryState.json +1 -1
  17. package/agent/components/Input.json +1 -1
  18. package/agent/components/InputOTP.json +35 -0
  19. package/agent/components/LineChart.json +20 -2
  20. package/agent/components/ListRow.json +1 -1
  21. package/agent/components/Masonry.json +1 -1
  22. package/agent/components/MasterDetail.json +1 -1
  23. package/agent/components/PasswordStrength.json +1 -1
  24. package/agent/components/PermissionMatrix.json +6 -1
  25. package/agent/components/SearchInput.json +5 -0
  26. package/agent/components/Select.json +1 -0
  27. package/agent/components/ServiceRolePanel.json +5 -0
  28. package/agent/components/Sidebar.json +1 -1
  29. package/agent/components/Switch.json +6 -0
  30. package/agent/components/Table.json +8 -3
  31. package/agent/components/Tabs.json +10 -0
  32. package/agent/components/ThemeScope.json +49 -0
  33. package/agent/components/TimeRangePicker.json +5 -0
  34. package/agent/components/Topbar.json +1 -0
  35. package/agent/components/TopbarItem.json +2 -1
  36. package/agent/components/Transfer.json +6 -1
  37. package/agent/components/TreeSelect.json +1 -1
  38. package/agent/components/Upload.json +5 -0
  39. package/agent/components/UploadCropDialog.json +1 -1
  40. package/agent/components/formatDate.json +1 -1
  41. package/agent/components-index.json +5 -0
  42. package/agent/components.json +297 -31
  43. package/agent/index.json +19 -9
  44. package/agent/llms.txt +10 -10
  45. package/agent/patterns/tenant-brand-color.json +28 -0
  46. package/agent/patterns-index.json +27 -0
  47. package/agent/patterns.json +28 -0
  48. package/agent/rules.json +15 -0
  49. package/agent/tokens.json +4965 -970
  50. package/dist/app/index.d.ts +3 -0
  51. package/dist/app/index.js +3 -0
  52. package/dist/app/tenant-theme.d.ts +80 -0
  53. package/dist/app/tenant-theme.js +154 -0
  54. package/dist/app/theme-axes.d.ts +14 -1
  55. package/dist/app/theme-axes.js +24 -31
  56. package/dist/components/charts/chart-cartesian.d.ts +5 -1
  57. package/dist/components/charts/chart-cartesian.js +15 -8
  58. package/dist/components/data-display/badge.d.ts +1 -1
  59. package/dist/components/data-display/badge.js +20 -2
  60. package/dist/components/data-display/carousel.js +4 -4
  61. package/dist/components/data-display/data-table.js +13 -2
  62. package/dist/components/data-display/permission-matrix.js +1 -1
  63. package/dist/components/data-display/table.d.ts +11 -2
  64. package/dist/components/data-display/table.js +18 -2
  65. package/dist/components/data-entry/control-appearance.d.ts +12 -6
  66. package/dist/components/data-entry/control-appearance.js +1 -1
  67. package/dist/components/data-entry/select.js +4 -3
  68. package/dist/components/feedback/dialog.js +6 -3
  69. package/dist/components/feedback/overlay-header-tone.d.ts +7 -0
  70. package/dist/components/feedback/overlay-header-tone.js +4 -4
  71. package/dist/components/feedback/sheet.d.ts +1 -1
  72. package/dist/components/feedback/sheet.js +6 -9
  73. package/dist/components/feedback/sonner.js +16 -3
  74. package/dist/components/general/button.js +22 -5
  75. package/dist/components/layout/affix.js +15 -1
  76. package/dist/components/layout/sidebar.js +7 -1
  77. package/dist/components/navigation/anchor.d.ts +1 -1
  78. package/dist/components/navigation/anchor.js +5 -4
  79. package/dist/components/navigation/app-setting-picker.js +1 -1
  80. package/dist/components/navigation/pagination.js +1 -1
  81. package/dist/components/navigation/tabs.js +15 -2
  82. package/dist/components/query/infinite-query-state.d.ts +22 -6
  83. package/dist/contracts/measurement.json +1 -1
  84. package/dist/i18n/messages/en.json +0 -697
  85. package/dist/i18n/messages/ja.json +0 -691
  86. package/dist/i18n/messages/vi.json +0 -691
  87. package/dist/lib/control-styles.d.ts +31 -11
  88. package/dist/lib/control-styles.js +6 -6
  89. package/dist/lib/overlay-portal.d.ts +20 -0
  90. package/dist/lib/overlay-portal.js +93 -0
  91. package/dist/props/components/app.prop.d.ts +12 -0
  92. package/dist/props/components/charts.prop.d.ts +30 -0
  93. package/dist/props/components/index.d.ts +1 -1
  94. package/dist/props/components/navigation.prop.d.ts +21 -2
  95. package/dist/props/components/query.prop.d.ts +36 -2
  96. package/dist/props/registry.d.ts +46 -1
  97. package/dist/props/registry.js +38 -3
  98. package/dist/styles/alert-layout.css +34 -14
  99. package/dist/styles/badge-layout.css +10 -6
  100. package/dist/styles/base.css +14 -5
  101. package/dist/styles/card-layout.css +19 -8
  102. package/dist/styles/chart-layout.css +22 -3
  103. package/dist/styles/control.css +165 -59
  104. package/dist/styles/data-display-layout.css +129 -36
  105. package/dist/styles/data-entry-layout.css +23 -87
  106. package/dist/styles/dialog-layout.css +49 -19
  107. package/dist/styles/float-button-layout.css +5 -5
  108. package/dist/styles/focus-ring.css +9 -5
  109. package/dist/styles/layout.css +42 -15
  110. package/dist/styles/logo-layout.css +1 -1
  111. package/dist/styles/motion.css +1 -1
  112. package/dist/styles/navigation-layout.css +90 -29
  113. package/dist/styles/shell-layout.css +63 -40
  114. package/dist/styles/table-layout.css +56 -17
  115. package/dist/styles/text-layout.css +13 -4
  116. package/dist/styles/toggle.css +8 -2
  117. package/dist/tokens/components/actions.css +1 -1
  118. package/dist/tokens/components/attachments.css +4 -4
  119. package/dist/tokens/components/badge.css +4 -4
  120. package/dist/tokens/components/callout.css +1 -1
  121. package/dist/tokens/components/card.css +9 -4
  122. package/dist/tokens/components/chart.css +10 -1
  123. package/dist/tokens/components/chat-bubble.css +1 -1
  124. package/dist/tokens/components/control.css +28 -10
  125. package/dist/tokens/components/conversations.css +2 -1
  126. package/dist/tokens/components/data-display.css +12 -7
  127. package/dist/tokens/components/descriptions.css +1 -1
  128. package/dist/tokens/components/draggable-panel.css +1 -1
  129. package/dist/tokens/components/feedback.css +28 -8
  130. package/dist/tokens/components/float-button.css +1 -1
  131. package/dist/tokens/components/legal-document.css +1 -1
  132. package/dist/tokens/components/logo.css +1 -1
  133. package/dist/tokens/components/mega-menu.css +5 -3
  134. package/dist/tokens/components/navigation.css +21 -7
  135. package/dist/tokens/components/segmented.css +8 -3
  136. package/dist/tokens/components/shell.css +19 -5
  137. package/dist/tokens/components/table.css +9 -1
  138. package/dist/tokens/components/thought-chain.css +1 -1
  139. package/dist/tokens/components/toggle.css +2 -0
  140. package/dist/tokens/components/tree.css +3 -1
  141. package/dist/tokens/components/upload.css +6 -6
  142. package/dist/tokens/components/welcome.css +1 -1
  143. package/dist/tokens/foundation.css +28 -1
  144. package/docs/COMPOSITION-VS-COMPONENT.md +31 -0
  145. package/docs/CUSTOMER-THEMING.md +637 -1
  146. package/docs/DESIGN-AUTHORITY.md +13 -0
  147. package/docs/FRAME-COVERAGE-REPORT.md +3 -2
  148. package/docs/GLASSMORPHISM-STANDARD.md +196 -0
  149. package/docs/THEME-API-COVERAGE.md +538 -0
  150. package/docs/TOKEN-RESOLUTION.md +195 -0
  151. package/docs/TOKENS.md +63 -24
  152. package/docs/asset-modules.d.ts +7 -0
  153. package/docs/data-display/charts.tsx +80 -0
  154. package/docs/data-display/data-table/index.tsx +30 -0
  155. package/docs/data-display/popover.tsx +1 -1
  156. package/docs/data-display/table.tsx +52 -0
  157. package/docs/feedback/sheet.tsx +10 -10
  158. package/docs/foundation/density.tsx +4 -4
  159. package/docs/i18n/messages/en.json +1201 -0
  160. package/docs/i18n/messages/ja.json +1195 -0
  161. package/docs/i18n/messages/vi.json +1195 -0
  162. package/docs/layout/account-chip.tsx +2 -2
  163. package/docs/layout/responsive-grid.tsx +1 -1
  164. package/docs/navigation/toolbar.tsx +20 -12
  165. package/docs/providers/theme-scope.tsx +186 -0
  166. package/docs/showcase/caimono-price-comparison.tsx +911 -0
  167. package/docs/showcase/case4-login.tsx +2 -2
  168. package/docs/showcase/marketing-page.tsx +3 -2
  169. package/docs/showcase/permission-matrix.tsx +13 -5
  170. package/docs/showcase/table-pagination.tsx +2 -1
  171. package/docs/showcase/tenant-brand-color.tsx +338 -0
  172. package/docs/showcase/theme-customization.tsx +2 -1
  173. package/docs/showcase/theme-lab.tsx +2125 -0
  174. package/docs/themes/flat.css +462 -0
  175. package/docs/themes/glassmorphism.css +958 -0
  176. package/docs/themes/index.ts +200 -0
  177. package/package.json +4 -3
  178. package/scripts/explain-token.mjs +382 -0
@@ -129,8 +129,8 @@ export default function LoginShowcase() {
129
129
  /* Page-level controls, pinned to the banner's inline end. */
130
130
  actions={
131
131
  <>
132
- <Select value={locale} onValueChange={setLocale}>
133
- <SelectTrigger size="sm" className="w-32" aria-label="言語を選択">
132
+ <Select aria-label="言語を選択" value={locale} onValueChange={setLocale}>
133
+ <SelectTrigger size="sm" className="w-32">
134
134
  <Languages aria-hidden="true" className="text-muted-foreground" />
135
135
  <SelectValue />
136
136
  </SelectTrigger>
@@ -112,8 +112,9 @@
112
112
  * elements cross the inline edge. The two boxes that do scroll are the `Marquee` viewport and the
113
113
  * `Carousel` viewport, which is what both components are.
114
114
  *
115
- * Every string and every `aria-label` comes from `t()` (`src/i18n/messages/{en,ja,vi}.json`,
116
- * namespace `marketingShowcase`), and the locale picker in the bar switches all three live. Money
115
+ * Every string and every `aria-label` comes from `t()` (`docs/i18n/messages/{en,ja,vi}.json`,
116
+ * namespace `marketingShowcase` — demo copy the preview registers at startup, never shipped to a
117
+ * consumer; gh#858), and the locale picker in the bar switches all three live. Money
117
118
  * is `Intl.NumberFormat` with the ACTIVE locale and an ISO 4217 code that lives in data, never a
118
119
  * symbol in the copy. The year is `Intl.DateTimeFormat`. Domains are RFC 2606 reserved
119
120
  * (`*.example.jp`), the company is fictional, and every wordmark in the logo wall is invented.
@@ -137,7 +137,10 @@ function GrantCell({ granted }: { granted: boolean }) {
137
137
  }
138
138
 
139
139
  // 固定列(先頭 = 権限)— スクロールする本体が下を通るので不透明背景 + トークン端。
140
- const PIN_START = "sticky start-0 z-20 bg-inherit border-e border-border";
140
+ // `.ui-permission-matrix-pin` is the packaged component's own class (gh#874 fixed its opaque
141
+ // base + tone/hover/selected compositing in `data-display-layout.css`) — reused here rather than
142
+ // hand-rolled so this showcase gets the identical answer instead of drifting from it again.
143
+ const PIN_START = "ui-permission-matrix-pin";
141
144
 
142
145
  export default function Demo() {
143
146
  const [compareA, setCompareA] = React.useState("editor");
@@ -225,11 +228,11 @@ export default function Demo() {
225
228
  </CardAction>
226
229
  </CardHeader>
227
230
  <CardContent flush>
228
- <div className="overflow-x-auto">
231
+ <div className="ui-permission-matrix overflow-x-auto">
229
232
  <Table className="min-w-[760px]">
230
233
  <TableHeader className="bg-secondary [&_tr]:bg-secondary">
231
234
  <TableRow className="bg-secondary hover:bg-secondary">
232
- <TableHead className={cn(PIN_START, "w-64 min-w-64")}>権限</TableHead>
235
+ <TableHead className={PIN_START}>権限</TableHead>
233
236
  {ROLES.map((role) => {
234
237
  const isCompared = !sameRole && compared.has(role.id);
235
238
  return (
@@ -263,10 +266,15 @@ export default function Demo() {
263
266
  return (
264
267
  <TableRow
265
268
  key={perm.id}
266
- className={cn(isDiffRow && "bg-warning/[0.07] hover:bg-warning/10")}
269
+ // Same hand-off the PermissionMatrix component uses (gh#872): the wash,
270
+ // the leading rail and the hover step belong to `styles/table-layout.css`,
271
+ // which reads `--surface-warning` first — so this hand-composed table is
272
+ // re-groundable by a tenant theme exactly like the packaged one. A local
273
+ // `bg-warning/[0.07]` here would be a second, private warning surface.
274
+ data-tone={isDiffRow ? "warning" : undefined}
267
275
  aria-label={isDiffRow ? `${perm.name} · 比較ロール間で差分あり` : undefined}
268
276
  >
269
- <TableCell className={cn(PIN_START, "w-64 min-w-64 align-middle")}>
277
+ <TableCell className={cn(PIN_START, "align-middle")}>
270
278
  <Flex direction="col" className="leading-tight">
271
279
  <Flex align="center" gap="xs">
272
280
  <Text weight="medium">{perm.name}</Text>
@@ -312,13 +312,14 @@ function CursorPeriodCard() {
312
312
  <CardTitle level={2}>カーソル / 期間ジャンプ</CardTitle>
313
313
  <CardAction>
314
314
  <Select
315
+ aria-label="期間を選択"
315
316
  value={period}
316
317
  onValueChange={(v: string) => {
317
318
  setPeriod(v);
318
319
  setCursor(undefined); // jumping period resets the cursor to the first page
319
320
  }}
320
321
  >
321
- <SelectTrigger size="sm" aria-label="期間を選択" className="w-36">
322
+ <SelectTrigger size="sm" className="w-36">
322
323
  <SelectValue />
323
324
  </SelectTrigger>
324
325
  <SelectContent>
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Showcase · tenant-brand-color — "Màu thương hiệu của KHÁCH, ở một vùng, lúc chạy" (gh#861, gh#868).
3
+ *
4
+ * ── Gate 0 verdict: COMPOSITION PATTERN ──────────────────────────────────────────────────────
5
+ * Hai kho tiêu thụ xin một `<TenantTheme primary={hex}>`. Chạy Framework-Component Test
6
+ * (docs/COMPOSITION-VS-COMPONENT.md §3.2) thì nó TRƯỢT: bọc `<div>` không giữ state, không bàn
7
+ * phím, không focus, không ARIA (C2); nó đúng là "token override theo scope" mà chính doctrine
8
+ * chỉ định làm composition (C3); API `primary`/`secondary`/`tenant` không map vào
9
+ * `value`/`onValueChange`/`size` nào (C4); và nó không render chữ nào để `t()` hay APG nói về
10
+ * (C6). Thứ hai consumer thật sự chép tay là PHÉP TÍNH — hex → triplet HSL, và quy tắc tương phản
11
+ * chọn màu chữ. Phép tính ấy ship trong gói (`tenantTheme`, `@godxjp/ui/app`); cái `<div>` ở lại
12
+ * trang này. Không component mới, không token mới.
13
+ *
14
+ * ── Trang này chứng minh bốn thứ, mỗi thứ đo được trên màn hình ──────────────────────────────
15
+ * 1. PHẠM VI. Cùng một hàng component, một bản TRONG vùng `[data-tenant]` và một bản NGOÀI.
16
+ * Chỉ bản trong đổi màu. Không đụng `<html>`, không stylesheet thứ hai.
17
+ * 2. CẶP MÀU. `--primary` không đi một mình: `--primary-foreground` đi kèm, chọn bằng tương
18
+ * phản WCAG 2.2 SC 1.4.3 (4.5:1), và tỷ số đạt được HIỆN trên màn hình chứ không giấu.
19
+ * 3. HOVER / ACTIVE KHÔNG IM LẶNG. `tenantTheme` phát hai bước ấy thành TRIPLET THẬT, không
20
+ * phải `initial` + relative colour, nên màu khách áp ở mọi trạng thái trên mọi engine —
21
+ * không cần `CSS.supports("color", "hsl(from red h s l)")` như bản tự chế của một kho tiêu thụ.
22
+ * 4. CA ĐÃ CHẠY, KHÔNG PHẢI HAPPY PATH. Bốn hàng cuối là hex không hợp lệ (vùng trở về theme
23
+ * của app, trang không vỡ), một cặp do máy chủ cấp KHÔNG đạt AA (API nói KHÔNG, kèm số), và
24
+ * hai màu khách hay chọn nhất mà quy tắc ngây thơ "luôn chữ trắng" sẽ hỏng.
25
+ *
26
+ * ── Điều trang này CỐ TÌNH không làm ─────────────────────────────────────────────────────────
27
+ * Không đụng `--text-link` / `--text-brand` / `--text-primary`. Đó là MỰC trên nền trang, không
28
+ * phải nhãn trên nền nút: độ đọc của nó phụ thuộc `--background` — thứ màu của khách không quyết
29
+ * định — và hướng bước là theo THEME chứ không theo nhãn. Một seed nhạt (#FFD400) cho link 1.4:1
30
+ * trên nền trắng. Xem hàng "mực thương hiệu" trong bảng.
31
+ */
32
+ import * as React from "react";
33
+
34
+ import { AA_NORMAL_TEXT, tenantTheme } from "@godxjp/ui/app";
35
+ import { Button, Text } from "@godxjp/ui/general";
36
+ import { Flex, PageContainer, ResponsiveGrid } from "@godxjp/ui/layout";
37
+ import {
38
+ Badge,
39
+ Card,
40
+ CardContent,
41
+ CardDescription,
42
+ CardHeader,
43
+ CardTitle,
44
+ CodeBlock,
45
+ DataTable,
46
+ Progress,
47
+ Swatch,
48
+ type ColumnDef,
49
+ } from "@godxjp/ui/data-display";
50
+ import { ColorPicker, FormField, Input, Switch } from "@godxjp/ui/data-entry";
51
+ import { Callout } from "@godxjp/ui/feedback";
52
+
53
+ /** Màu thật của khách — hai đầu mút là chỗ quy tắc "luôn chữ trắng" hỏng. */
54
+ const CUSTOMERS = [
55
+ { hex: "#0071bd", name: "Biển Đông Logistics" },
56
+ { hex: "#FFD400", name: "Vàng Mặt Trời" },
57
+ { hex: "#0A1F44", name: "Hàng hải Bắc Hà" },
58
+ { hex: "#00A86B", name: "Ngọc Bích Foods" },
59
+ { hex: "#E30613", name: "Đỏ Sao Vàng" },
60
+ { hex: "#767676", name: "Xám Trung Tính" },
61
+ ] as const;
62
+
63
+ type Row = {
64
+ hex: string;
65
+ name: string;
66
+ foreground: string;
67
+ ratio: string;
68
+ meetsAA: boolean;
69
+ };
70
+
71
+ const columns: ColumnDef<Row>[] = [
72
+ {
73
+ key: "hex",
74
+ header: "Màu khách",
75
+ render: (r) => (
76
+ <Flex align="center" gap="sm">
77
+ <Swatch color={r.hex} aria-label={`Màu chính của ${r.name}: ${r.hex}`} />
78
+ <Text as="span" size="xs" mono>
79
+ {r.hex}
80
+ </Text>
81
+ </Flex>
82
+ ),
83
+ },
84
+ { key: "name", header: "Khách", hiddenOnMobile: true },
85
+ {
86
+ key: "foreground",
87
+ header: "Màu chữ đã chọn",
88
+ render: (r) => (
89
+ <Flex align="center" gap="sm">
90
+ <Swatch color={r.foreground} aria-label={`Màu chữ an toàn: ${r.foreground}`} />
91
+ <Text as="span" size="xs" mono>
92
+ {r.foreground}
93
+ </Text>
94
+ </Flex>
95
+ ),
96
+ },
97
+ {
98
+ key: "ratio",
99
+ header: "Tương phản",
100
+ align: "right",
101
+ render: (r) => <Text tabular>{r.ratio}:1</Text>,
102
+ },
103
+ {
104
+ key: "meetsAA",
105
+ header: "WCAG 2.2 AA",
106
+ align: "center",
107
+ render: (r) => (
108
+ <Badge tone={r.meetsAA ? "success" : "destructive"}>{r.meetsAA ? "Đạt" : "Không đạt"}</Badge>
109
+ ),
110
+ },
111
+ ];
112
+
113
+ /**
114
+ * Một hàng component THẬT đọc `--primary`. Dùng hai lần trên trang: một lần trong vùng khách,
115
+ * một lần ngoài — cùng markup, khác đúng một `style` của thẻ cha.
116
+ */
117
+ function BrandedRow({ idPrefix }: { idPrefix: string }) {
118
+ return (
119
+ <Flex direction="col" gap="md">
120
+ <Flex direction="row" gap="sm" wrap align="center">
121
+ <Button type="button">Mở ứng dụng</Button>
122
+ <Button type="button" variant="outline">
123
+ Chi tiết
124
+ </Button>
125
+ <Badge tone="info">12 dịch vụ</Badge>
126
+ <Switch id={`${idPrefix}-switch`} defaultChecked aria-label="Bật thông báo" />
127
+ </Flex>
128
+ <FormField id={`${idPrefix}-input`} label="Mã tra cứu" helper="Bấm vào ô để thấy vòng focus">
129
+ <Input id={`${idPrefix}-input`} defaultValue="GDX-1042" />
130
+ </FormField>
131
+ <Progress value={62} label="Hạn mức đã dùng" />
132
+ </Flex>
133
+ );
134
+ }
135
+
136
+ export default function Demo() {
137
+ const [hex, setHex] = React.useState("#0071bd");
138
+ const [serverForeground, setServerForeground] = React.useState(false);
139
+
140
+ // Cặp do máy chủ cấp — Platform lưu sẵn ở `brands.theme_tokens`. #3b82f6 trên #0071bd chỉ đạt
141
+ // 2.02:1: một cặp THẬT có thể sai, và API phải nói ra chứ không lặng lẽ trả về trắng.
142
+ const seed = tenantTheme(hex, { foreground: serverForeground ? "#3b82f6" : null });
143
+ const invalid = tenantTheme("#nope00");
144
+
145
+ const rows: Row[] = CUSTOMERS.map((customer) => {
146
+ const computed = tenantTheme(customer.hex);
147
+ return {
148
+ hex: customer.hex,
149
+ name: customer.name,
150
+ foreground: computed.foreground ?? "—",
151
+ ratio: computed.contrast.toFixed(2),
152
+ meetsAA: computed.meetsAA,
153
+ };
154
+ });
155
+
156
+ const declarations = Object.entries(seed.vars)
157
+ .map(([name, value]) => ` ${name}: ${value};`)
158
+ .join("\n");
159
+
160
+ return (
161
+ <PageContainer
162
+ title="Màu thương hiệu của khách · một vùng, lúc chạy"
163
+ subtitle="tenantTheme(hex) → khai báo token cho MỘT vùng. Cặp màu chữ chọn bằng WCAG 2.2 SC 1.4.3; hover/pressed là triplet thật nên không im lặng trên engine thiếu relative colour."
164
+ >
165
+ <Flex direction="col" gap="lg">
166
+ <Card>
167
+ <CardHeader>
168
+ <CardTitle level={2}>Màu khách chọn</CardTitle>
169
+ <CardDescription>
170
+ Hex — đúng thứ `ColorPicker` trả về và đúng thứ mọi kho tiêu thụ lưu. Ba hoặc sáu chữ
171
+ số; giá trị hỏng bị bỏ qua chứ không làm vỡ trang.
172
+ </CardDescription>
173
+ </CardHeader>
174
+ <CardContent>
175
+ <Flex direction="col" gap="md">
176
+ <FormField id="tenant-hex" label="Màu chính của khách">
177
+ <ColorPicker id="tenant-hex" value={hex} onValueChange={setHex} />
178
+ </FormField>
179
+ <Flex direction="row" gap="sm" wrap>
180
+ {CUSTOMERS.map((customer) => (
181
+ <Button
182
+ key={customer.hex}
183
+ type="button"
184
+ variant="outline"
185
+ size="sm"
186
+ onClick={() => setHex(customer.hex)}
187
+ >
188
+ {customer.name}
189
+ </Button>
190
+ ))}
191
+ </Flex>
192
+ <Flex direction="row" gap="md" align="center" wrap>
193
+ <Swatch color={seed.primary ?? "#ffffff"} aria-label={`Màu nền: ${seed.primary}`} />
194
+ <Swatch
195
+ color={seed.foreground ?? "#000000"}
196
+ aria-label={`Màu chữ: ${seed.foreground}`}
197
+ />
198
+ <Text as="span" size="sm">
199
+ Tương phản đo được{" "}
200
+ <Text as="strong" weight="medium" tabular>
201
+ {seed.contrast.toFixed(2)}:1
202
+ </Text>{" "}
203
+ (ngưỡng AA {AA_NORMAL_TEXT}:1)
204
+ </Text>
205
+ <Badge tone={seed.meetsAA ? "success" : "destructive"}>
206
+ {seed.meetsAA ? "Đạt WCAG 2.2 AA" : "KHÔNG đạt WCAG 2.2 AA"}
207
+ </Badge>
208
+ </Flex>
209
+ <Flex direction="row" gap="sm" align="center">
210
+ <Switch
211
+ id="server-foreground"
212
+ checked={serverForeground}
213
+ onCheckedChange={setServerForeground}
214
+ />
215
+ <Text as="label" size="sm" htmlFor="server-foreground">
216
+ Dùng cặp do máy chủ cấp (#3b82f6) — một cặp THẬT nhưng dưới ngưỡng
217
+ </Text>
218
+ </Flex>
219
+ {!seed.meetsAA ? (
220
+ <Callout kind="warning">
221
+ <Callout.Title>API nói KHÔNG, kèm số</Callout.Title>
222
+ <Callout.Description>
223
+ Cặp được cấp đo {seed.contrast.toFixed(2)}:1 trên {seed.primary} — dưới 4.5:1
224
+ của WCAG 2.2 SC 1.4.3. Gói KHÔNG lặng lẽ đổi sang trắng: nó giữ đúng cặp bạn
225
+ cấp, trả `meetsAA: false`, và cảnh báo ở bản dev. Bỏ trống `foreground` để gói
226
+ tự chọn màu chữ an toàn.
227
+ </Callout.Description>
228
+ </Callout>
229
+ ) : null}
230
+ </Flex>
231
+ </CardContent>
232
+ </Card>
233
+
234
+ <ResponsiveGrid columns={2}>
235
+ {/* TRONG vùng — đúng một thẻ cha mang data-tenant + style */}
236
+ <Card data-tenant="customer" style={seed.vars}>
237
+ <CardHeader>
238
+ <CardTitle level={2}>Trong vùng của khách</CardTitle>
239
+ <CardDescription>
240
+ `&lt;div data-tenant="…" style=&#123;tenantTheme(hex).vars&#125;&gt;`. Rê chuột và
241
+ giữ chuột trên nút. Hover và pressed đi theo màu khách, không phải xanh mặc định.
242
+ </CardDescription>
243
+ </CardHeader>
244
+ <CardContent>
245
+ <BrandedRow idPrefix="inside" />
246
+ </CardContent>
247
+ </Card>
248
+
249
+ {/* NGOÀI vùng — cùng markup, không style */}
250
+ <Card>
251
+ <CardHeader>
252
+ <CardTitle level={2}>Ngoài vùng</CardTitle>
253
+ <CardDescription>
254
+ Cùng một hàng component, không thẻ cha nào mang khai báo. Vẫn là theme của ứng dụng
255
+ — đây là bằng chứng phạm vi, không phải ảnh chụp thứ hai.
256
+ </CardDescription>
257
+ </CardHeader>
258
+ <CardContent>
259
+ <BrandedRow idPrefix="outside" />
260
+ </CardContent>
261
+ </Card>
262
+ </ResponsiveGrid>
263
+
264
+ <Card>
265
+ <CardHeader>
266
+ <CardTitle level={2}>Khai báo mà vùng đang mang</CardTitle>
267
+ <CardDescription>
268
+ Năm dòng, không hơn. `--ring` có mặt vì `derived.css` khai `--ring: var(--primary)` ở
269
+ `:root`, nơi var() thay thế MỘT LẦN. Một scope dưới `&lt;html&gt;` thừa kế vòng focus
270
+ của root nếu không khai lại (docs/TOKENS.md · freeze rule).
271
+ </CardDescription>
272
+ </CardHeader>
273
+ <CardContent>
274
+ <CodeBlock language="css" wrap={false}>
275
+ {`[data-tenant="customer"] {\n${declarations}\n}`}
276
+ </CodeBlock>
277
+ </CardContent>
278
+ </Card>
279
+
280
+ <Card>
281
+ <CardHeader>
282
+ <CardTitle level={2}>Bảng tương phản · màu khách thật</CardTitle>
283
+ <CardDescription>
284
+ Màu chữ được chọn bằng độ chói tương đối của WCAG, không phải hằng số. Hai hàng đáng
285
+ nhìn nhất: #FFD400 nhận chữ ĐEN (quy tắc "luôn trắng" chỉ đạt 1.29:1) và #0A1F44 nhận
286
+ chữ TRẮNG (quy tắc "luôn đen" chỉ đạt 1.29:1).
287
+ </CardDescription>
288
+ </CardHeader>
289
+ <CardContent flush>
290
+ <DataTable
291
+ columns={columns}
292
+ data={rows}
293
+ getRowId={(row) => row.hex}
294
+ label="Bảng tương phản màu khách"
295
+ />
296
+ </CardContent>
297
+ </Card>
298
+
299
+ <Card>
300
+ <CardHeader>
301
+ <CardTitle level={2}>Ca đã chạy: hex không hợp lệ</CardTitle>
302
+ </CardHeader>
303
+ <CardContent>
304
+ <Flex direction="col" gap="md">
305
+ <Text as="p" size="sm" tone="muted">
306
+ `tenantTheme("#nope00")` trả `vars` RỖNG ({Object.keys(invalid.vars).length} khai
307
+ báo), nên vùng dưới đây giữ nguyên theme của ứng dụng thay vì vỡ. Cùng hợp đồng
308
+ `ColorPicker` đã có: hex hỏng bị bỏ qua, không ném lỗi.
309
+ </Text>
310
+ <div data-tenant="broken" style={invalid.vars}>
311
+ <BrandedRow idPrefix="broken" />
312
+ </div>
313
+ </Flex>
314
+ </CardContent>
315
+ </Card>
316
+
317
+ <Card>
318
+ <CardHeader>
319
+ <CardTitle level={2}>Mực thương hiệu · thứ gói KHÔNG tự động đổi</CardTitle>
320
+ </CardHeader>
321
+ <CardContent>
322
+ <Callout kind="important">
323
+ <Callout.Title>
324
+ `--text-link` / `--text-brand` / `--text-primary` là việc của bạn
325
+ </Callout.Title>
326
+ <Callout.Description>
327
+ Chúng là mực trên NỀN TRANG, không phải nhãn trên nền nút, nên độ đọc phụ thuộc
328
+ `--background` — thứ màu của khách không quyết định. Một seed nhạt như #FFD400 cho
329
+ link ~1.4:1 trên nền trắng. `tenantTheme` để yên ba vai đó; nếu bạn muốn link mang
330
+ màu thương hiệu, hãy tự đặt chúng SAU KHI đo bằng `contrastRatio()`.
331
+ </Callout.Description>
332
+ </Callout>
333
+ </CardContent>
334
+ </Card>
335
+ </Flex>
336
+ </PageContainer>
337
+ );
338
+ }
@@ -68,7 +68,8 @@
68
68
  *
69
69
  * ── i18n / a11y / RTL ────────────────────────────────────────────────────────────────────────
70
70
  * Every string and every `aria-label` comes from `t()` (`themeShowcase.*` in
71
- * src/i18n/messages/{en,ja,vi}.json); the locale picker drives the SAME `AppProvider` locale the
71
+ * docs/i18n/messages/{en,ja,vi}.json — demo copy the preview registers at startup rather than
72
+ * ship to consumers; gh#858); the locale picker drives the SAME `AppProvider` locale the
72
73
  * library's own chrome reads, so the page and its components switch together. Numbers, currency,
73
74
  * dates, lists, regions and plurals go through `Intl` with that locale — no hand-built formats, no
74
75
  * emoji flags. Logical CSS only; no Tailwind spacing or layout utilities on this page's own