@cyberskill/design 1.2.0 → 1.3.8

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 (215) hide show
  1. package/DESIGN.md +4 -3
  2. package/README.md +20 -19
  3. package/SKILL.md +5 -5
  4. package/THIRD-PARTY-NOTICES.md +35 -0
  5. package/VERSION +1 -1
  6. package/_ds_bundle.js +701 -1342
  7. package/_esm/cs.mjs +2 -2
  8. package/_esm/react.mjs +6 -1
  9. package/_vendor/NOTICE.md +16 -0
  10. package/_vendor/babel/README.md +7 -0
  11. package/_vendor/babel/babel.min.js +4 -0
  12. package/_vendor/react/README.md +3 -0
  13. package/_vendor/react/react-dom.production.min.js +1 -0
  14. package/_vendor/react/react.production.min.js +1 -0
  15. package/assets/aurora-hoa.webp +0 -0
  16. package/assets/aurora-kim.webp +0 -0
  17. package/assets/aurora-moc.webp +0 -0
  18. package/assets/aurora-tho.webp +0 -0
  19. package/assets/aurora-thuy.webp +0 -0
  20. package/base/ai.css +8 -3
  21. package/base/collections.css +10 -4
  22. package/base/components.css +5 -2
  23. package/base/controls.css +58 -21
  24. package/base/data.css +1 -1
  25. package/base/feedback.css +9 -2
  26. package/base/forms.css +1 -1
  27. package/base/glass.css +1 -1
  28. package/base/interaction.css +6 -2
  29. package/base/navigation.css +1 -1
  30. package/base/reset.css +7 -1
  31. package/base/responsive.css +13 -12
  32. package/base/shell.css +8 -2
  33. package/components/_i18n/i18n.js +28 -3
  34. package/components/_i18n/strings.js +42 -24
  35. package/components/ai/ChatMessage.d.ts +2 -0
  36. package/components/ai/ChatMessage.jsx +8 -4
  37. package/components/ai/ConfidenceMeter.d.ts +2 -0
  38. package/components/ai/HumanReviewGate.d.ts +2 -0
  39. package/components/ai/PromptInput.d.ts +2 -0
  40. package/components/ai/TypingIndicator.d.ts +2 -0
  41. package/components/ai/ai-conversation.card.html +3 -3
  42. package/components/ai/ai-lumi.card.html +3 -3
  43. package/components/ai/ai.card.html +3 -3
  44. package/components/brand/brand.card.html +3 -3
  45. package/components/button/buttons.card.html +3 -3
  46. package/components/data/Image.jsx +18 -2
  47. package/components/data/Sortable.d.ts +5 -1
  48. package/components/data/Sortable.jsx +42 -9
  49. package/components/data/Sortable.prompt.md +4 -2
  50. package/components/data/Terminal.jsx +3 -2
  51. package/components/data/Tooltip.d.ts +2 -0
  52. package/components/data/Tooltip.jsx +6 -3
  53. package/components/data/Tree.d.ts +2 -1
  54. package/components/data/Tree.jsx +137 -16
  55. package/components/data/Tree.prompt.md +1 -1
  56. package/components/data/collections.card.html +3 -3
  57. package/components/data/data.card.html +3 -3
  58. package/components/datatable/DataTable.d.ts +24 -1
  59. package/components/datatable/DataTable.jsx +78 -5
  60. package/components/datatable/DataTable.prompt.md +2 -1
  61. package/components/datatable/datatable.card.html +20 -16
  62. package/components/dialog/dialog.card.html +3 -3
  63. package/components/feedback/Alert.d.ts +2 -0
  64. package/components/feedback/Alert.jsx +8 -2
  65. package/components/feedback/EmptyState.d.ts +2 -0
  66. package/components/feedback/EmptyState.jsx +6 -3
  67. package/components/feedback/ProgressBar.d.ts +2 -0
  68. package/components/feedback/ProgressBar.jsx +6 -3
  69. package/components/feedback/Toast.jsx +15 -1
  70. package/components/feedback/Toast.prompt.md +2 -1
  71. package/components/feedback/feedback-2.card.html +3 -3
  72. package/components/feedback/feedback.card.html +3 -3
  73. package/components/forms/Cascader.jsx +7 -2
  74. package/components/forms/FileUpload.d.ts +2 -0
  75. package/components/forms/Form.d.ts +3 -1
  76. package/components/forms/Form.jsx +9 -1
  77. package/components/forms/Form.prompt.md +1 -1
  78. package/components/forms/Mentions.d.ts +1 -1
  79. package/components/forms/Mentions.jsx +59 -7
  80. package/components/forms/Mentions.prompt.md +3 -1
  81. package/components/forms/Rating.d.ts +3 -2
  82. package/components/forms/Rating.jsx +34 -3
  83. package/components/forms/Rating.prompt.md +1 -1
  84. package/components/forms/Select.d.ts +2 -0
  85. package/components/forms/Select.jsx +6 -2
  86. package/components/forms/Slider.d.ts +2 -0
  87. package/components/forms/Slider.jsx +5 -2
  88. package/components/forms/Switch.d.ts +2 -0
  89. package/components/forms/Switch.jsx +7 -9
  90. package/components/forms/Toggle.d.ts +2 -0
  91. package/components/forms/Toggle.jsx +14 -2
  92. package/components/forms/forms-2.card.html +3 -3
  93. package/components/forms/forms.card.html +3 -3
  94. package/components/icon/Icon.d.ts +1 -1
  95. package/components/icon/Icon.prompt.md +1 -1
  96. package/components/icon/icon.card.html +3 -3
  97. package/components/logo/logo.card.html +3 -3
  98. package/components/navigation/CommandPalette.d.ts +2 -0
  99. package/components/navigation/Menu.d.ts +2 -0
  100. package/components/navigation/Menu.jsx +60 -6
  101. package/components/navigation/Steps.d.ts +2 -0
  102. package/components/navigation/Steps.jsx +6 -3
  103. package/components/navigation/Tabs.d.ts +2 -0
  104. package/components/navigation/Tabs.jsx +6 -3
  105. package/components/navigation/Toolbar.d.ts +3 -2
  106. package/components/navigation/Toolbar.jsx +117 -11
  107. package/components/navigation/Toolbar.prompt.md +1 -1
  108. package/components/navigation/navigation.card.html +3 -3
  109. package/components/navigation/shell.card.html +3 -3
  110. package/components/overlays/ContextMenu.d.ts +2 -0
  111. package/components/overlays/ContextMenu.jsx +6 -3
  112. package/components/overlays/Drawer.d.ts +2 -0
  113. package/components/overlays/alertdialog.card.html +3 -3
  114. package/components/overlays/overlays.card.html +3 -3
  115. package/components/textfield/textfield.card.html +3 -3
  116. package/dist/styles.min.css +312 -64
  117. package/docs/ci-cd.md +18 -13
  118. package/docs/consumer-grant.md +3 -0
  119. package/docs/consuming.md +7 -7
  120. package/docs/contrast-report.md +79 -1
  121. package/docs/conventions.md +17 -4
  122. package/docs/decisions.md +40 -2
  123. package/docs/deploy.md +17 -6
  124. package/docs/design-styles.md +1 -1
  125. package/docs/quality-gates.md +19 -11
  126. package/docs/release-runbook.md +78 -0
  127. package/docs/sync.md +1 -1
  128. package/docs/vi/ci-cd.md +16 -13
  129. package/docs/vi/consumer-grant.md +3 -0
  130. package/docs/vi/consuming.md +7 -7
  131. package/docs/vi/contrast-report.md +79 -1
  132. package/docs/vi/conventions.md +18 -5
  133. package/docs/vi/decisions.md +39 -2
  134. package/docs/vi/deploy.md +17 -6
  135. package/docs/vi/quality-gates.md +18 -11
  136. package/docs/vi/release-runbook.md +78 -0
  137. package/docs/vi/sync.md +1 -1
  138. package/docs/viewer.html +2 -2
  139. package/fonts/OFL.txt +98 -0
  140. package/guidelines/atomic-view.html +2 -2
  141. package/guidelines/brand-imagery.html +1 -1
  142. package/guidelines/elements-variants.html +5 -5
  143. package/guidelines/spacing-breakpoints.html +2 -1
  144. package/guidelines/surfaces-glass.html +1 -1
  145. package/llms.txt +4 -4
  146. package/package.json +28 -8
  147. package/templates/app-shell/AppShell.dc.html +3 -3
  148. package/templates/app-shell/content-schema.json +7 -0
  149. package/templates/article/Article.dc.html +3 -3
  150. package/templates/article/content-schema.json +7 -0
  151. package/templates/auth/Auth.dc.html +26 -10
  152. package/templates/auth/content-schema.json +25 -0
  153. package/templates/dashboard/Dashboard.dc.html +4 -4
  154. package/templates/dashboard/content-schema.json +7 -0
  155. package/templates/delivery-change-note/DeliveryChangeNote.dc.html +2 -2
  156. package/templates/delivery-runbook/DeliveryRunbook.dc.html +2 -2
  157. package/templates/email-safe/email-announcement.html +2 -2
  158. package/templates/email-safe/email-dunning.html +2 -2
  159. package/templates/email-safe/email-investor.html +2 -2
  160. package/templates/email-safe/email-launch.html +2 -2
  161. package/templates/email-safe/email-newsletter.html +2 -2
  162. package/templates/email-safe/email-status.html +2 -2
  163. package/templates/email-safe/email-transactional.html +2 -2
  164. package/templates/finance-dunning-email/FinanceDunningEmail.dc.html +2 -2
  165. package/templates/finance-expense-report/FinanceExpenseReport.dc.html +2 -2
  166. package/templates/finance-invoice/FinanceInvoice.dc.html +2 -2
  167. package/templates/finance-quote/FinanceQuote.dc.html +4 -4
  168. package/templates/hr-interview-kit/HrInterviewKit.dc.html +1 -1
  169. package/templates/marketing-case-study/MarketingCaseStudy.dc.html +1 -1
  170. package/templates/marketing-page/MarketingPage.dc.html +3 -3
  171. package/templates/marketing-page/content-schema.json +1 -0
  172. package/templates/settings/Settings.dc.html +3 -3
  173. package/templates/settings/content-schema.json +7 -0
  174. package/tokens/element-seeds.json +2 -2
  175. package/tokens/elements.css +193 -19
  176. package/tokens/native/CSTokens.kt +3 -2
  177. package/tokens/native/CSTokens.swift +3 -2
  178. package/tokens/native/cs_tokens.dart +3 -2
  179. package/tokens/provenance.json +8 -8
  180. package/tokens/spacing.css +2 -1
  181. package/tokens/tokens.dtcg.json +19 -14
  182. package/tokens/tokens.js +16 -15
  183. package/tokens/tokens.json +15 -14
  184. package/ui_kits/deck/index.html +1 -1
  185. package/ui_kits/status-hub/StatusHub.jsx +1 -1
  186. package/ui_kits/status-hub/login.html +52 -10
  187. package/ui_kits/status-hub/project.html +1 -1
  188. package/ui_kits/status-hub/settings.html +1 -1
  189. package/ui_kits/website/Website.jsx +1 -1
  190. package/ui_kits/website/careers.html +2 -2
  191. package/ui_kits/website/chat.html +1 -1
  192. package/ui_kits/website/site.css +1 -1
  193. package/ui_kits/website/work.html +2 -2
  194. package/assets/aurora-hoa.png +0 -0
  195. package/assets/aurora-kim.png +0 -0
  196. package/assets/aurora-moc.png +0 -0
  197. package/assets/aurora-thuy.png +0 -0
  198. package/docs/decisions/container-query-migration.md +0 -18
  199. package/docs/decisions/overlay-manager-design.md +0 -101
  200. package/docs/plans/cyberskill-design-system-audit-and-12-month-evolution-plan.md +0 -1490
  201. package/docs/status/assets/favicon.svg +0 -5
  202. package/docs/status/assets/status-legacy.css +0 -399
  203. package/docs/status/assets/status-legacy.js +0 -505
  204. package/docs/status/assets/status.css +0 -644
  205. package/docs/status/assets/status.js +0 -1308
  206. package/docs/status/data/status-feed.json +0 -1
  207. package/docs/status/data/task/TASK-REL-001.js +0 -1
  208. package/docs/status/data/task/TASK-REL-002.js +0 -1
  209. package/docs/status/index.html +0 -46
  210. package/docs/status/roadmap.html +0 -4
  211. package/docs/status/status-legacy.html +0 -122
  212. package/docs/tasks/BACKLOG.md +0 -26
  213. package/docs/tasks/_state/commit-links.yaml +0 -14
  214. package/docs/tasks/release/TASK-REL-001-launch-1-1-0/spec.md +0 -55
  215. package/docs/tasks/release/TASK-REL-002-republish-1-1-1/spec.md +0 -54
@@ -1,23 +1,144 @@
1
1
  import React from "react";
2
2
  import { cx } from "../_utils/cx.js";
3
3
 
4
- function Node({ n, depth, selected, onSelect, defaultOpen }) {
5
- const [open, setOpen] = React.useState(!!defaultOpen);
6
- const kids = n.children || [];
7
- return (
8
- <li role="treeitem" aria-expanded={kids.length ? open : undefined} aria-selected={selected === n.key}>
9
- <span className={cx("cs-tree__row", selected === n.key && "is-selected")} style={{ paddingInlineStart: depth * 18 + 6 }}>
10
- {kids.length ? (
11
- <button type="button" className="cs-tree__twist" aria-hidden="true" tabIndex={-1} onClick={() => setOpen((o) => !o)}>{open ? "▾" : "▸"}</button>
12
- ) : <span className="cs-tree__twist" aria-hidden="true" />}
13
- <button type="button" className="cs-tree__label" onClick={() => { onSelect && onSelect(n.key, n); if (kids.length) setOpen((o) => !o); }}>{n.label}</button>
14
- </span>
15
- {kids.length && open ? <ul role="group">{kids.map((c) => <Node key={c.key} n={c} depth={depth + 1} selected={selected} onSelect={onSelect} defaultOpen={defaultOpen} />)}</ul> : null}
16
- </li>
17
- );
4
+ function collectVisible(nodes, openMap, out = []) {
5
+ for (const n of nodes) {
6
+ out.push(n);
7
+ const kids = n.children || [];
8
+ if (kids.length && openMap[n.key]) collectVisible(kids, openMap, out);
9
+ }
10
+ return out;
18
11
  }
19
12
 
20
- /** CyberSkill Tree — expandable hierarchy. nodes: {key,label,children[]}. Single select via onSelect. */
13
+ function parentOf(nodes, key, parent = null) {
14
+ for (const n of nodes) {
15
+ if (n.key === key) return parent;
16
+ const kids = n.children || [];
17
+ if (kids.length) {
18
+ const p = parentOf(kids, key, n);
19
+ if (p !== undefined) return p;
20
+ }
21
+ }
22
+ return undefined;
23
+ }
24
+
25
+ function findNode(nodes, key) {
26
+ for (const n of nodes) {
27
+ if (n.key === key) return n;
28
+ const kids = n.children || [];
29
+ if (kids.length) {
30
+ const hit = findNode(kids, key);
31
+ if (hit) return hit;
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+
37
+ function seedOpen(nodes, defaultOpen, map = {}) {
38
+ for (const n of nodes) {
39
+ const kids = n.children || [];
40
+ if (kids.length) {
41
+ map[n.key] = !!defaultOpen;
42
+ seedOpen(kids, defaultOpen, map);
43
+ }
44
+ }
45
+ return map;
46
+ }
47
+
48
+ /** CyberSkill Tree — expandable hierarchy with APG tree keyboard (roving tabindex + arrows). nodes: {key,label,children[]}. Single select via onSelect. */
21
49
  export function Tree({ nodes = [], selected, onSelect, defaultOpen = false, className }) {
22
- return <ul role="tree" className={cx("cs-tree", className)}>{nodes.map((n) => <Node key={n.key} n={n} depth={0} selected={selected} onSelect={onSelect} defaultOpen={defaultOpen} />)}</ul>;
50
+ const [openMap, setOpenMap] = React.useState(() => seedOpen(nodes, defaultOpen));
51
+ const [focusKey, setFocusKey] = React.useState(() => {
52
+ if (selected && findNode(nodes, selected)) return selected;
53
+ return nodes[0] ? nodes[0].key : null;
54
+ });
55
+ const itemRefs = React.useRef({});
56
+
57
+ React.useEffect(() => {
58
+ setOpenMap((prev) => {
59
+ const next = seedOpen(nodes, defaultOpen);
60
+ for (const k of Object.keys(next)) if (k in prev) next[k] = prev[k];
61
+ return next;
62
+ });
63
+ }, [nodes, defaultOpen]);
64
+
65
+ const visible = React.useMemo(() => collectVisible(nodes, openMap), [nodes, openMap]);
66
+ React.useEffect(() => {
67
+ if (!visible.length) { setFocusKey(null); return; }
68
+ if (!focusKey || !visible.some((n) => n.key === focusKey)) setFocusKey(visible[0].key);
69
+ }, [visible, focusKey]);
70
+
71
+ const toggle = (key) => setOpenMap((m) => ({ ...m, [key]: !m[key] }));
72
+
73
+ const focusItem = (key) => {
74
+ setFocusKey(key);
75
+ requestAnimationFrame(() => {
76
+ const el = itemRefs.current[key];
77
+ if (el) el.focus();
78
+ });
79
+ };
80
+
81
+ const onKeyDown = (e, key) => {
82
+ if (e.nativeEvent.isComposing || e.keyCode === 229) return;
83
+ const idx = visible.findIndex((n) => n.key === key);
84
+ if (idx < 0) return;
85
+ const node = visible[idx];
86
+ const kids = node.children || [];
87
+ const open = !!openMap[key];
88
+
89
+ if (e.key === "ArrowDown") {
90
+ e.preventDefault();
91
+ if (idx < visible.length - 1) focusItem(visible[idx + 1].key);
92
+ } else if (e.key === "ArrowUp") {
93
+ e.preventDefault();
94
+ if (idx > 0) focusItem(visible[idx - 1].key);
95
+ } else if (e.key === "Home") {
96
+ e.preventDefault();
97
+ focusItem(visible[0].key);
98
+ } else if (e.key === "End") {
99
+ e.preventDefault();
100
+ focusItem(visible[visible.length - 1].key);
101
+ } else if (e.key === "ArrowRight") {
102
+ e.preventDefault();
103
+ if (kids.length && !open) toggle(key);
104
+ else if (kids.length && open) focusItem(kids[0].key);
105
+ } else if (e.key === "ArrowLeft") {
106
+ e.preventDefault();
107
+ if (kids.length && open) toggle(key);
108
+ else {
109
+ const p = parentOf(nodes, key);
110
+ if (p) focusItem(p.key);
111
+ }
112
+ } else if (e.key === "Enter" || e.key === " ") {
113
+ e.preventDefault();
114
+ onSelect && onSelect(key, node);
115
+ if (kids.length) toggle(key);
116
+ }
117
+ };
118
+
119
+ const renderNodes = (list, depth) => list.map((n) => {
120
+ const kids = n.children || [];
121
+ const open = !!openMap[n.key];
122
+ return (
123
+ <li key={n.key} role="treeitem" aria-expanded={kids.length ? open : undefined} aria-selected={selected === n.key}>
124
+ <span className={cx("cs-tree__row", selected === n.key && "is-selected")} style={{ paddingInlineStart: depth * 18 + 6 }}>
125
+ {kids.length ? (
126
+ <button type="button" className="cs-tree__twist" aria-hidden="true" tabIndex={-1} onClick={() => toggle(n.key)}>{open ? "▾" : "▸"}</button>
127
+ ) : <span className="cs-tree__twist" aria-hidden="true" />}
128
+ <button
129
+ type="button"
130
+ className="cs-tree__label"
131
+ tabIndex={focusKey === n.key ? 0 : -1}
132
+ ref={(el) => { itemRefs.current[n.key] = el; }}
133
+ onClick={() => { onSelect && onSelect(n.key, n); if (kids.length) toggle(n.key); }}
134
+ onKeyDown={(e) => onKeyDown(e, n.key)}
135
+ onFocus={() => setFocusKey(n.key)}
136
+ >{n.label}</button>
137
+ </span>
138
+ {kids.length && open ? <ul role="group">{renderNodes(kids, depth + 1)}</ul> : null}
139
+ </li>
140
+ );
141
+ });
142
+
143
+ return <ul role="tree" className={cx("cs-tree", className)}>{renderNodes(nodes, 0)}</ul>;
23
144
  }
@@ -1,4 +1,4 @@
1
- **Tree** — expandable hierarchy with single select.
1
+ **Tree** — expandable hierarchy with APG keyboard (ArrowUp/Down move, ArrowRight/Left expand-collapse, Home/End) and single select.
2
2
 
3
3
  ```jsx
4
4
  <Tree selected={k} onSelect={setK} nodes={[{key:"p",label:"Platform",children:[{key:"ds",label:"Design System"}]}]}/>
@@ -10,9 +10,9 @@
10
10
  .lbl { font: 600 11px/1 var(--cs-font-family-ui); text-transform: uppercase; letter-spacing: .09em; color: var(--cs-color-text-muted); margin-bottom: 10px; }
11
11
  .blk { margin-bottom: 20px; }
12
12
  </style>
13
- <script src="https://unpkg.com/umd-react@19.2.8/dist/react.development.js" integrity="sha384-r22tQd8qRIHDPIkmdXZcVtvPi1Tj2x1QaW2tmFQHEvHscATUnHQ5siTyxWw5PMUs" crossorigin="anonymous"></script>
14
- <script src="https://unpkg.com/umd-react@19.2.8/dist/react-dom.development.js" integrity="sha384-gJZYmumEmwqVmX3JVsLZq+4QrxoPGh/BW47Rk7SqNPCPFpTGnRtvmeRPJ7//ZDPk" crossorigin="anonymous"></script>
15
- <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
13
+ <script src="../../_vendor/react/react.production.min.js"></script>
14
+ <script src="../../_vendor/react/react-dom.production.min.js"></script>
15
+ <script src="../../_vendor/babel/babel.min.js"></script>
16
16
  <script src="../../_ds_bundle.js"></script>
17
17
  </head>
18
18
  <body>
@@ -11,9 +11,9 @@
11
11
  .stats { display: flex; gap: 26px; align-items: center; }
12
12
  .lbl { font: 600 11px/1 var(--cs-font-family-ui); text-transform: uppercase; letter-spacing: .09em; color: var(--cs-color-text-muted); margin-bottom: 10px; }
13
13
  </style>
14
- <script src="https://unpkg.com/umd-react@19.2.8/dist/react.development.js" integrity="sha384-r22tQd8qRIHDPIkmdXZcVtvPi1Tj2x1QaW2tmFQHEvHscATUnHQ5siTyxWw5PMUs" crossorigin="anonymous"></script>
15
- <script src="https://unpkg.com/umd-react@19.2.8/dist/react-dom.development.js" integrity="sha384-gJZYmumEmwqVmX3JVsLZq+4QrxoPGh/BW47Rk7SqNPCPFpTGnRtvmeRPJ7//ZDPk" crossorigin="anonymous"></script>
16
- <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
14
+ <script src="../../_vendor/react/react.production.min.js"></script>
15
+ <script src="../../_vendor/react/react-dom.production.min.js"></script>
16
+ <script src="../../_vendor/babel/babel.min.js"></script>
17
17
  <script src="../../_ds_bundle.js"></script>
18
18
  </head>
19
19
  <body>
@@ -8,9 +8,17 @@ export interface DataTableColumn<Row> {
8
8
  render?: (row: Row) => ReactNode;
9
9
  }
10
10
 
11
+ /**
12
+ * Async triad for collection surfaces (FIND-020). Prefer composing existing
13
+ * feedback: Skeleton for loading, Result for error, emptyState for idle empty.
14
+ * Idle is the default — omit `state` for static tables.
15
+ */
16
+ export type DataTableState = "idle" | "loading" | "error";
17
+
11
18
  /**
12
19
  * Enterprise data table: caption, scoped column headers, per-column render, and
13
- * an empty state. Dense content — keep it on a solid surface (no glass).
20
+ * idle / loading / error / empty states. Dense content — keep it on a solid
21
+ * surface (no glass).
14
22
  */
15
23
  export interface DataTableProps<Row extends Record<string, unknown>> {
16
24
  caption?: ReactNode;
@@ -19,6 +27,21 @@ export interface DataTableProps<Row extends Record<string, unknown>> {
19
27
  /** Field used as React key. Default "id". */
20
28
  rowKey?: keyof Row & string;
21
29
  emptyState?: ReactNode;
30
+ /**
31
+ * Async data ownership. Default `"idle"`.
32
+ * - `loading` — Skeleton rows (or `loadingState`) + polite status text
33
+ * - `error` — Result (or `errorState`) with `role="alert"`
34
+ * - `idle` — rows or empty cell
35
+ */
36
+ state?: DataTableState;
37
+ /** Override the default Result error view. */
38
+ errorState?: ReactNode;
39
+ /** Override the default Skeleton-row loading view. */
40
+ loadingState?: ReactNode;
41
+ /** Skeleton row count when `state="loading"` and no `loadingState`. Default 5. */
42
+ loadingRows?: number;
43
+ /** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
44
+ lang?: string;
22
45
  className?: string;
23
46
  }
24
47
 
@@ -1,10 +1,15 @@
1
1
  import React from "react";
2
2
  import { makeT, useLang } from "../_i18n/i18n.js";
3
+ import { Skeleton } from "../feedback/Skeleton.jsx";
4
+ import { Result } from "../feedback/Result.jsx";
3
5
  import { cx } from "../_utils/cx.js";
4
6
 
5
7
  /**
6
8
  * CyberSkill DataTable — semantic table with caption, column scope headers,
7
- * per-column render, and an empty state. Solid surface (dense content).
9
+ * per-column render, and idle / loading / error / empty async states.
10
+ * Solid surface (dense content). Reference composition for collection async
11
+ * data (FIND-020): Skeleton rows while loading, Result on error, empty cell
12
+ * when idle with no rows. Other data surfaces should follow the same triad.
8
13
  */
9
14
  export function DataTable({
10
15
  caption,
@@ -12,26 +17,94 @@ export function DataTable({
12
17
  rows,
13
18
  rowKey = "id",
14
19
  emptyState,
20
+ state = "idle",
21
+ errorState,
22
+ loadingState,
23
+ loadingRows = 5,
15
24
  lang,
16
25
  className,
17
26
  }) {
18
27
  const normalized = Array.isArray(rows) ? rows : [];
19
28
  const [ref, L] = useLang(lang);
20
- const es = emptyState != null ? emptyState : makeT("DataTable", L)("empty");
29
+ const t = makeT("DataTable", L);
30
+ const es = emptyState != null ? emptyState : t("empty");
31
+ const colCount = Math.max(1, (columns && columns.length) || 1);
32
+
33
+ if (state === "loading") {
34
+ return (
35
+ <div ref={ref} className={cx("cs-table-wrap", className)} aria-busy="true">
36
+ <span className="cs-sr-only">{t("loading")}</span>
37
+ {loadingState != null ? (
38
+ loadingState
39
+ ) : (
40
+ <table className="cs-table">
41
+ {caption ? <caption>{caption}</caption> : null}
42
+ <thead>
43
+ <tr>
44
+ {(columns || []).map((c) => (
45
+ <th key={c.key} scope="col">
46
+ {c.header}
47
+ </th>
48
+ ))}
49
+ </tr>
50
+ </thead>
51
+ <tbody aria-hidden="true">
52
+ {Array.from({ length: Math.max(1, loadingRows) }).map((_, i) => (
53
+ <tr key={i}>
54
+ {Array.from({ length: colCount }).map((__, j) => (
55
+ <td key={j}>
56
+ <Skeleton lines={1} />
57
+ </td>
58
+ ))}
59
+ </tr>
60
+ ))}
61
+ </tbody>
62
+ </table>
63
+ )}
64
+ </div>
65
+ );
66
+ }
67
+
68
+ if (state === "error") {
69
+ return (
70
+ <div ref={ref} className={cx("cs-table-wrap", "cs-table-wrap--status", className)} role="alert">
71
+ {errorState != null ? (
72
+ errorState
73
+ ) : (
74
+ <Result status="error" lang={L} title={t("error")}>
75
+ {t("errorHint")}
76
+ </Result>
77
+ )}
78
+ </div>
79
+ );
80
+ }
81
+
21
82
  return (
22
83
  <div ref={ref} className={cx("cs-table-wrap", className)}>
23
84
  <table className="cs-table">
24
85
  {caption ? <caption>{caption}</caption> : null}
25
86
  <thead>
26
- <tr>{columns.map((c) => <th key={c.key} scope="col">{c.header}</th>)}</tr>
87
+ <tr>
88
+ {(columns || []).map((c) => (
89
+ <th key={c.key} scope="col">
90
+ {c.header}
91
+ </th>
92
+ ))}
93
+ </tr>
27
94
  </thead>
28
95
  <tbody>
29
96
  {normalized.length === 0 ? (
30
- <tr><td colSpan={columns.length} className="cs-table__empty">{es}</td></tr>
97
+ <tr>
98
+ <td colSpan={colCount} className="cs-table__empty">
99
+ {es}
100
+ </td>
101
+ </tr>
31
102
  ) : (
32
103
  normalized.map((row, i) => (
33
104
  <tr key={row[rowKey] ?? i}>
34
- {columns.map((c) => <td key={c.key}>{c.render ? c.render(row) : row[c.key]}</td>)}
105
+ {(columns || []).map((c) => (
106
+ <td key={c.key}>{c.render ? c.render(row) : row[c.key]}</td>
107
+ ))}
35
108
  </tr>
36
109
  ))
37
110
  )}
@@ -1,4 +1,4 @@
1
- **DataTable** — semantic table with a caption, scoped column headers, per-column `render`, and an empty state. Dense content, so it stays on a solid surface (never glass).
1
+ **DataTable** — semantic table with a caption, scoped column headers, per-column `render`, and an async triad (`idle` / `loading` / `error`). Dense content, so it stays on a solid surface (never glass). Use `state` when the host owns a fetch — do not show empty while loading.
2
2
 
3
3
  ```jsx
4
4
  <DataTable
@@ -10,6 +10,7 @@
10
10
  ]}
11
11
  rows={releases}
12
12
  rowKey="ver"
13
+ state={status} /* "idle" | "loading" | "error" */
13
14
  emptyState="No releases yet"
14
15
  />
15
16
  ```
@@ -1,4 +1,4 @@
1
- <!-- @dsCard group="Components" viewport="700x300" name="DataTable" subtitle="Caption, scoped headers, custom cells, empty state" -->
1
+ <!-- @dsCard group="Components" viewport="700x420" name="DataTable" subtitle="Caption, scoped headers, idle / loading / error / empty" -->
2
2
  <!DOCTYPE html>
3
3
  <html lang="en">
4
4
  <head>
@@ -14,9 +14,9 @@
14
14
  .pill.hold { background: var(--cs-color-semantic-danger); color: #fff; }
15
15
  .mono { font-family: var(--cs-font-family-mono); font-weight: 600; }
16
16
  </style>
17
- <script src="https://unpkg.com/umd-react@19.2.8/dist/react.development.js" integrity="sha384-r22tQd8qRIHDPIkmdXZcVtvPi1Tj2x1QaW2tmFQHEvHscATUnHQ5siTyxWw5PMUs" crossorigin="anonymous"></script>
18
- <script src="https://unpkg.com/umd-react@19.2.8/dist/react-dom.development.js" integrity="sha384-gJZYmumEmwqVmX3JVsLZq+4QrxoPGh/BW47Rk7SqNPCPFpTGnRtvmeRPJ7//ZDPk" crossorigin="anonymous"></script>
19
- <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
17
+ <script src="../../_vendor/react/react.production.min.js"></script>
18
+ <script src="../../_vendor/react/react-dom.production.min.js"></script>
19
+ <script src="../../_vendor/babel/babel.min.js"></script>
20
20
  <script src="../../_ds_bundle.js"></script>
21
21
  </head>
22
22
  <body>
@@ -28,20 +28,24 @@ const rows = [
28
28
  { ver: "v1.6.0", ships: "This week", status: "active", note: "Glass bindings" },
29
29
  { ver: "v1.7.0", ships: "Q3 2026", status: "hold", note: "Awaiting RFC" },
30
30
  ];
31
+ const cols = [
32
+ { key: "ver", header: "Version", render: (r) => <span className="mono">{r.ver}</span> },
33
+ { key: "ships", header: "Ships" },
34
+ { key: "note", header: "Notes" },
35
+ { key: "status", header: "Status", render: (r) => <span className={"pill " + r.status}>{r.status}</span> },
36
+ ];
31
37
  function Demo() {
32
38
  return (
33
- <div className="wrap">
34
- <DataTable
35
- caption="Releases"
36
- rowKey="ver"
37
- columns={[
38
- { key: "ver", header: "Version", render: (r) => <span className="mono">{r.ver}</span> },
39
- { key: "ships", header: "Ships" },
40
- { key: "note", header: "Notes" },
41
- { key: "status", header: "Status", render: (r) => <span className={"pill " + r.status}>{r.status}</span> },
42
- ]}
43
- rows={rows}
44
- />
39
+ <div style={{ display: "grid", gap: 16 }}>
40
+ <div className="wrap">
41
+ <DataTable caption="Releases" rowKey="ver" columns={cols} rows={rows} />
42
+ </div>
43
+ <div className="wrap">
44
+ <DataTable caption="Loading" rowKey="ver" columns={cols} rows={[]} state="loading" loadingRows={3} />
45
+ </div>
46
+ <div className="wrap">
47
+ <DataTable caption="Error" rowKey="ver" columns={cols} rows={[]} state="error" />
48
+ </div>
45
49
  </div>
46
50
  );
47
51
  }
@@ -12,9 +12,9 @@
12
12
  .frame .cs-dialog-layer { position: absolute; }
13
13
  .frame .cs-dialog { top: 24px; }
14
14
  </style>
15
- <script src="https://unpkg.com/umd-react@19.2.8/dist/react.development.js" integrity="sha384-r22tQd8qRIHDPIkmdXZcVtvPi1Tj2x1QaW2tmFQHEvHscATUnHQ5siTyxWw5PMUs" crossorigin="anonymous"></script>
16
- <script src="https://unpkg.com/umd-react@19.2.8/dist/react-dom.development.js" integrity="sha384-gJZYmumEmwqVmX3JVsLZq+4QrxoPGh/BW47Rk7SqNPCPFpTGnRtvmeRPJ7//ZDPk" crossorigin="anonymous"></script>
17
- <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
15
+ <script src="../../_vendor/react/react.production.min.js"></script>
16
+ <script src="../../_vendor/react/react-dom.production.min.js"></script>
17
+ <script src="../../_vendor/babel/babel.min.js"></script>
18
18
  <script src="../../_ds_bundle.js"></script>
19
19
  </head>
20
20
  <body>
@@ -11,6 +11,8 @@ export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
11
11
  title?: ReactNode;
12
12
  icon?: ReactNode;
13
13
  children?: ReactNode;
14
+ /** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
15
+ lang?: string;
14
16
  }
15
17
 
16
18
  export function Alert(props: AlertProps): React.ReactElement;
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { makeT, useLang } from "../_i18n/i18n.js";
2
3
  import { cx } from "../_utils/cx.js";
3
4
 
4
5
  const PATHS = {
@@ -16,14 +17,19 @@ function DefaultIcon({ variant }) {
16
17
  }
17
18
 
18
19
  /** CyberSkill Alert — inline banner. variant: info | success | warning | danger. */
19
- export function Alert({ variant = "info", title, icon, children, className, ...props }) {
20
+ export function Alert({ variant = "info", title, icon, children, onDismiss, lang, className, ...props }) {
21
+ const [ref, L] = useLang(lang);
22
+ const t = makeT("Alert", L);
20
23
  return (
21
- <div role="status" className={cx("cs-alert", `cs-alert--${variant}`, className)} {...props}>
24
+ <div ref={ref} role="status" className={cx("cs-alert", `cs-alert--${variant}`, className)} {...props}>
22
25
  <span className="cs-alert__icon">{icon ?? <DefaultIcon variant={variant} />}</span>
23
26
  <div>
24
27
  {title ? <p className="cs-alert__title">{title}</p> : null}
25
28
  {children ? <div className="cs-alert__body">{children}</div> : null}
26
29
  </div>
30
+ {onDismiss ? (
31
+ <button type="button" className="cs-alert__dismiss" aria-label={t("dismiss")} onClick={onDismiss}>×</button>
32
+ ) : null}
27
33
  </div>
28
34
  );
29
35
  }
@@ -8,6 +8,8 @@ export interface EmptyStateProps {
8
8
  title?: ReactNode;
9
9
  children?: ReactNode;
10
10
  actions?: ReactNode;
11
+ /** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
12
+ lang?: string;
11
13
  className?: string;
12
14
  }
13
15
  export function EmptyState(props: EmptyStateProps): React.ReactElement;
@@ -1,14 +1,17 @@
1
1
  import React from "react";
2
+ import { makeT, useLang } from "../_i18n/i18n.js";
2
3
  import { cx } from "../_utils/cx.js";
3
4
 
4
5
  /** CyberSkill EmptyState — friendly placeholder for empty views. */
5
- export function EmptyState({ icon, title, children, actions, className }) {
6
+ export function EmptyState({ icon, title, children, actions, lang, className }) {
7
+ const [ref, L] = useLang(lang);
8
+ const t = makeT("EmptyState", L);
6
9
  return (
7
- <div className={cx("cs-empty", className)}>
10
+ <div ref={ref} className={cx("cs-empty", className)}>
8
11
  <span className="cs-empty__icon">
9
12
  {icon ?? <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M3 8l2-4h14l2 4M3 8v10a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V8M3 8h6l1 3h4l1-3h6" /></svg>}
10
13
  </span>
11
- {title ? <div className="cs-empty__title">{title}</div> : null}
14
+ <div className="cs-empty__title">{title ?? t("title")}</div>
12
15
  {children ? <div className="cs-empty__body">{children}</div> : null}
13
16
  {actions ? <div className="cs-empty__actions">{actions}</div> : null}
14
17
  </div>
@@ -9,6 +9,8 @@ export interface ProgressBarProps {
9
9
  max?: number;
10
10
  variant?: "ochre" | "umber" | "success";
11
11
  label?: string;
12
+ /** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
13
+ lang?: string;
12
14
  className?: string;
13
15
  }
14
16
 
@@ -1,14 +1,17 @@
1
1
  import React from "react";
2
+ import { makeT, useLang } from "../_i18n/i18n.js";
2
3
  import { cx } from "../_utils/cx.js";
3
4
 
4
5
  /** CyberSkill ProgressBar — determinate progress. variant: ochre (default) | umber | success. */
5
- export function ProgressBar({ value = 0, max = 100, variant, label, className }) {
6
+ export function ProgressBar({ value = 0, max = 100, variant, label, lang, className }) {
6
7
  const safeMax = max > 0 ? max : 100;
7
8
  const clamped = Math.max(0, Math.min(safeMax, Number(value) || 0));
8
9
  const pct = Math.max(0, Math.min(100, (clamped / safeMax) * 100));
10
+ const [ref, L] = useLang(lang);
11
+ const t = makeT("ProgressBar", L);
9
12
  return (
10
- <div className={cx("cs-progress", variant && `cs-progress--${variant}`, className)}
11
- role="progressbar" aria-valuenow={Math.round(clamped)} aria-valuemin={0} aria-valuemax={safeMax} aria-label={label}>
13
+ <div ref={ref} className={cx("cs-progress", variant && `cs-progress--${variant}`, className)}
14
+ role="progressbar" aria-valuenow={Math.round(clamped)} aria-valuemin={0} aria-valuemax={safeMax} aria-label={label ?? t("label")}>
12
15
  <span className="cs-progress__fill" style={{ inlineSize: pct + "%", width: pct + "%" }} />
13
16
  </div>
14
17
  );
@@ -2,6 +2,20 @@ import React from "react";
2
2
  import { makeT, useLang } from "../_i18n/i18n.js";
3
3
  import { cx } from "../_utils/cx.js";
4
4
 
5
+ /** Variant glyphs — aligned with Alert so status is never colour-alone (FIND-036). */
6
+ const PATHS = {
7
+ default: "M12 8h.01M11 12h1v4h1 M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z",
8
+ success: "M4 12.5l5 5 11-11",
9
+ danger: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18z M9 9l6 6 M15 9l-6 6",
10
+ };
11
+ function DefaultIcon({ variant }) {
12
+ return (
13
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
14
+ <path d={PATHS[variant] || PATHS.default} />
15
+ </svg>
16
+ );
17
+ }
18
+
5
19
  /** Fixed-position stack for Toasts (bottom-right). */
6
20
  export function ToastStack({ children, lang, className }) {
7
21
  const [ref, L] = useLang(lang);
@@ -14,7 +28,7 @@ export function Toast({ variant = "default", title, icon, onClose, lang, childre
14
28
  return (
15
29
  <div ref={ref} className={cx("cs-toast", `cs-toast--${variant}`, className)} role="status" {...props}>
16
30
  <span className="cs-toast__icon" aria-hidden="true">
17
- {icon ?? <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 12.5l5 5 11-11" /></svg>}
31
+ {icon ?? <DefaultIcon variant={variant} />}
18
32
  </span>
19
33
  <div>
20
34
  {title ? <div className="cs-toast__title">{title}</div> : null}
@@ -1,7 +1,8 @@
1
- **Toast / ToastStack** — transient notification inside a fixed bottom-right stack. Presentational; manage timing + queue in app state.
1
+ **Toast / ToastStack** — transient notification inside a fixed bottom-right stack. Presentational; manage timing + queue in app state. Variants `default` | `success` | `danger` each ship a distinct glyph (info / check / X) plus a status-coloured start border — never a red check-mark for errors.
2
2
 
3
3
  ```jsx
4
4
  <ToastStack>
5
5
  <Toast variant="success" title="Saved" onClose={dismiss}>Your wish is on its way.</Toast>
6
+ <Toast variant="danger" title="Couldn’t save">Check the network and try again.</Toast>
6
7
  </ToastStack>
7
8
  ```
@@ -10,9 +10,9 @@
10
10
  .lbl { width: 100%; font: 600 11px/1 var(--cs-font-family-ui); text-transform: uppercase; letter-spacing: .09em; color: var(--cs-color-text-muted); }
11
11
  .frame { border: 1px solid var(--cs-color-border-default); border-radius: var(--cs-radius-lg); background: var(--cs-color-surface-panel); }
12
12
  </style>
13
- <script src="https://unpkg.com/umd-react@19.2.8/dist/react.development.js" integrity="sha384-r22tQd8qRIHDPIkmdXZcVtvPi1Tj2x1QaW2tmFQHEvHscATUnHQ5siTyxWw5PMUs" crossorigin="anonymous"></script>
14
- <script src="https://unpkg.com/umd-react@19.2.8/dist/react-dom.development.js" integrity="sha384-gJZYmumEmwqVmX3JVsLZq+4QrxoPGh/BW47Rk7SqNPCPFpTGnRtvmeRPJ7//ZDPk" crossorigin="anonymous"></script>
15
- <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
13
+ <script src="../../_vendor/react/react.production.min.js"></script>
14
+ <script src="../../_vendor/react/react-dom.production.min.js"></script>
15
+ <script src="../../_vendor/babel/babel.min.js"></script>
16
16
  <script src="../../_ds_bundle.js"></script>
17
17
  </head>
18
18
  <body>
@@ -13,9 +13,9 @@
13
13
  .frame { position: relative; height: 96px; border: 1px solid var(--cs-color-border-default); border-radius: var(--cs-radius-md); background: var(--cs-color-surface-raised); }
14
14
  .frame .cs-toast-stack { position: absolute; inset-block-end: 12px; inset-inline-end: 12px; }
15
15
  </style>
16
- <script src="https://unpkg.com/umd-react@19.2.8/dist/react.development.js" integrity="sha384-r22tQd8qRIHDPIkmdXZcVtvPi1Tj2x1QaW2tmFQHEvHscATUnHQ5siTyxWw5PMUs" crossorigin="anonymous"></script>
17
- <script src="https://unpkg.com/umd-react@19.2.8/dist/react-dom.development.js" integrity="sha384-gJZYmumEmwqVmX3JVsLZq+4QrxoPGh/BW47Rk7SqNPCPFpTGnRtvmeRPJ7//ZDPk" crossorigin="anonymous"></script>
18
- <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
16
+ <script src="../../_vendor/react/react.production.min.js"></script>
17
+ <script src="../../_vendor/react/react-dom.production.min.js"></script>
18
+ <script src="../../_vendor/babel/babel.min.js"></script>
19
19
  <script src="../../_ds_bundle.js"></script>
20
20
  </head>
21
21
  <body>
@@ -2,8 +2,10 @@ import React from "react";
2
2
  import { makeT, useLang } from "../_i18n/i18n.js";
3
3
  import { cx } from "../_utils/cx.js";
4
4
 
5
+ const EMPTY_PATH = [];
6
+
5
7
  /** CyberSkill Cascader — column-per-level picker for hierarchies (province → district style). */
6
- export function Cascader({ nodes = [], value = [], onChange, placeholder, label, disabled = false, lang, className }) {
8
+ export function Cascader({ nodes = [], value = EMPTY_PATH, onChange, placeholder, label, disabled = false, lang, className }) {
7
9
  const [open, setOpen] = React.useState(false);
8
10
  const [path, setPath] = React.useState(value);
9
11
  const wrap = React.useRef(null);
@@ -11,7 +13,10 @@ export function Cascader({ nodes = [], value = [], onChange, placeholder, label,
11
13
  const [ref, L] = useLang(lang);
12
14
  const t = makeT("Cascader", L);
13
15
  const ph = placeholder != null ? placeholder : t("placeholder");
14
- React.useEffect(() => { setPath(value); }, [open]);
16
+ // Controlled value must re-sync even when the popover stays closed (FIND-014).
17
+ // Depend on a stable key — default `[]` must not reset path every render.
18
+ const valueKey = Array.isArray(value) ? value.join("\0") : "";
19
+ React.useEffect(() => { setPath(Array.isArray(value) ? value : EMPTY_PATH); }, [valueKey]);
15
20
  React.useEffect(() => {
16
21
  if (!open) return;
17
22
  const d = (e) => { if (wrap.current && !wrap.current.contains(e.target)) setOpen(false); };