@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
@@ -17,8 +17,8 @@ export const strings = {
17
17
  vi: { label: "Đường dẫn" },
18
18
  },
19
19
  CommandPalette: {
20
- en: { placeholder: "Type a command or search…", empty: "No results found", aria: "Command palette", esc: "Esc" },
21
- vi: { placeholder: "Nhập lệnh hoặc tìm kiếm…", empty: "Không tìm thấy kết quả", aria: "Bảng lệnh", esc: "Esc" },
20
+ en: { placeholder: "Type a command or search…", empty: "No results", aria: "Command palette", esc: "Esc" },
21
+ vi: { placeholder: "Nhập lệnh hoặc tìm kiếm…", empty: "Không có kết quả", aria: "Bảng lệnh", esc: "Esc" },
22
22
  },
23
23
  SearchField: {
24
24
  en: { placeholder: "Search…", clear: "Clear search" },
@@ -41,12 +41,22 @@ export const strings = {
41
41
  vi: { confirm: "Xác nhận", cancel: "Hủy" },
42
42
  },
43
43
  Drawer: {
44
- en: { close: "Close", panel: "Panel" },
45
- vi: { close: "Đóng", panel: "Bảng" },
44
+ en: { close: "Close", panel: "Side panel" },
45
+ vi: { close: "Đóng", panel: "Bảng bên" },
46
46
  },
47
47
  DataTable: {
48
- en: { empty: "No records" },
49
- vi: { empty: "Không có bản ghi" },
48
+ en: {
49
+ empty: "No results",
50
+ loading: "Loading table",
51
+ error: "We couldn't load this table",
52
+ errorHint: "Check your connection, then try again.",
53
+ },
54
+ vi: {
55
+ empty: "Không có kết quả",
56
+ loading: "Đang tải bảng",
57
+ error: "Không tải được bảng này",
58
+ errorHint: "Kiểm tra kết nối, rồi thử lại.",
59
+ },
50
60
  },
51
61
  ConfidenceMeter: {
52
62
  en: { label: "Confidence", low: "Low", medium: "Medium", high: "High" },
@@ -58,10 +68,10 @@ export const strings = {
58
68
  },
59
69
  Tag: {
60
70
  en: { remove: "Remove" },
61
- vi: { remove: "Gỡ bỏ" },
71
+ vi: { remove: "Gỡ" },
62
72
  },
63
73
  Toast: {
64
- en: { notifications: "Notifications", dismiss: "Dismiss" },
74
+ en: { notifications: "Notifications", dismiss: "Close" },
65
75
  vi: { notifications: "Thông báo", dismiss: "Đóng" },
66
76
  },
67
77
  TypingIndicator: {
@@ -73,12 +83,12 @@ export const strings = {
73
83
  vi: { toolbar: "Định dạng", bold: "Đậm", italic: "Nghiêng", list: "Danh sách", area: "Văn bản" },
74
84
  },
75
85
  Terminal: {
76
- en: { input: "Terminal input" },
77
- vi: { input: "Ô nhập lệnh" },
86
+ en: { input: "Terminal input", title: "cyberskill — zsh" },
87
+ vi: { input: "Ô nhập lệnh", title: "cyberskill — zsh" },
78
88
  },
79
89
  DataGrid: {
80
- en: { empty: "No records", selectAll: "Select all rows", selectRow: "Select row", pin: "Pin this column", pinNamed: "Pin column {name}" },
81
- vi: { empty: "Không có bản ghi", selectAll: "Chọn tất cả", selectRow: "Chọn dòng", pin: "Ghim cột này", pinNamed: "Ghim cột {name}" },
90
+ en: { empty: "No results", selectAll: "Select all rows", selectRow: "Select row", pin: "Pin this column", pinNamed: "Pin column {name}" },
91
+ vi: { empty: "Không có kết quả", selectAll: "Chọn tất cả", selectRow: "Chọn dòng", pin: "Ghim cột này", pinNamed: "Ghim cột {name}" },
82
92
  },
83
93
  Splitter: {
84
94
  en: { label: "Resize panes" },
@@ -112,9 +122,13 @@ export const strings = {
112
122
  en: { placeholder: "Add a note — @ to mention" },
113
123
  vi: { placeholder: "Thêm ghi chú — gõ @ để nhắc tên" },
114
124
  },
125
+ Sortable: {
126
+ en: { moveUp: "Move up", moveDown: "Move down" },
127
+ vi: { moveUp: "Di chuyển lên", moveDown: "Di chuyển xuống" },
128
+ },
115
129
  Form: {
116
- en: { summary: "Please fix the following:", required: "required", requiredField: "This field is required" },
117
- vi: { summary: "Vui lòng sửa các mục sau:", required: "bắt buộc", requiredField: "Trường này là bắt buộc" },
130
+ en: { summary: "Please fix the following:", required: "required", requiredField: "Enter a value" },
131
+ vi: { summary: "Vui lòng sửa các mục sau:", required: "bắt buộc", requiredField: "Vui lòng nhập giá trị" },
118
132
  },
119
133
  BackTop: {
120
134
  en: { label: "Back to top" },
@@ -122,7 +136,7 @@ export const strings = {
122
136
  },
123
137
  Carousel: {
124
138
  en: { prev: "Previous slide", next: "Next slide", slide: "Slide" },
125
- vi: { prev: "Trang trước", next: "Trang sau", slide: "Trang" },
139
+ vi: { prev: "Ảnh trước", next: "Ảnh sau", slide: "Ảnh" },
126
140
  },
127
141
  Comment: {
128
142
  en: { reply: "Reply" },
@@ -137,12 +151,12 @@ export const strings = {
137
151
  vi: { more: "Thao tác khác" },
138
152
  },
139
153
  Popconfirm: {
140
- en: { ok: "OK", cancel: "Cancel" },
141
- vi: { ok: "Đồng ý", cancel: "Hủy" },
154
+ en: { ok: "Confirm", cancel: "Cancel" },
155
+ vi: { ok: "Xác nhận", cancel: "Hủy" },
142
156
  },
143
157
  Result: {
144
- en: { success: "Done", error: "Something went wrong", warning: "Please check", info: "For your information" },
145
- vi: { success: "Hoàn tất", error: "Đã có lỗi xảy ra", warning: "Vui lòng kiểm tra", info: "Thông tin" },
158
+ en: { success: "Done", error: "We couldn't complete that", warning: "Please check", info: "Here's what happened" },
159
+ vi: { success: "Hoàn tất", error: "Chúng tôi chưa hoàn tất được", warning: "Vui lòng kiểm tra", info: "Đây là điều đã xảy ra" },
146
160
  },
147
161
  InlineEdit: {
148
162
  en: { edit: "Edit", empty: "Add text" },
@@ -161,8 +175,8 @@ export const strings = {
161
175
  vi: { label: "Giờ" },
162
176
  },
163
177
  Combobox: {
164
- en: { placeholder: "Select or type…", empty: "No matches" },
165
- vi: { placeholder: "Chọn hoặc nhập…", empty: "Không có kết quả phù hợp" },
178
+ en: { placeholder: "Select or type…", empty: "No results" },
179
+ vi: { placeholder: "Chọn hoặc nhập…", empty: "Không có kết quả" },
166
180
  },
167
181
  InputGroup: {
168
182
  en: { clear: "Clear", show: "Show password", hide: "Hide password", input: "Input" },
@@ -198,11 +212,11 @@ export const strings = {
198
212
  vi: { list: "Tab" },
199
213
  },
200
214
  EmptyState: {
201
- en: { title: "Nothing here yet", action: "Get started" },
202
- vi: { title: "Chưa có gì ở đây", action: "Bắt đầu" },
215
+ en: { title: "Nothing here yet", action: "Create your first item" },
216
+ vi: { title: "Chưa có gì ở đây", action: "Tạo mục đầu tiên" },
203
217
  },
204
218
  Alert: {
205
- en: { dismiss: "Dismiss" },
219
+ en: { dismiss: "Close" },
206
220
  vi: { dismiss: "Đóng" },
207
221
  },
208
222
  Select: {
@@ -237,4 +251,8 @@ export const strings = {
237
251
  en: { label: "Progress" },
238
252
  vi: { label: "Tiến độ" },
239
253
  },
254
+ ChatMessage: {
255
+ en: { you: "You" },
256
+ vi: { you: "Bạn" },
257
+ },
240
258
  };
@@ -11,6 +11,8 @@ export interface ChatMessageProps {
11
11
  name?: ReactNode;
12
12
  avatar?: ReactNode;
13
13
  children?: ReactNode;
14
+ /** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
15
+ lang?: string;
14
16
  className?: string;
15
17
  }
16
18
 
@@ -1,20 +1,24 @@
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
  /**
5
6
  * CyberSkill ChatMessage — one turn in a Lumi conversation. role "lumi" (genie,
6
7
  * Ochre avatar) or "user" (Umber, right-aligned). Pass `avatar` to override.
7
8
  */
8
- export function ChatMessage({ role = "lumi", name, avatar, children, className }) {
9
+ export function ChatMessage({ role = "lumi", name, avatar, lang, className, children }) {
9
10
  const isUser = role === "user";
11
+ const [ref, L] = useLang(lang);
12
+ const t = makeT("ChatMessage", L);
13
+ const you = t("you");
10
14
  const defaultAvatar = isUser
11
- ? <span aria-hidden="true">You</span>
15
+ ? <span aria-hidden="true">{you}</span>
12
16
  : <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M12 3l1.8 5.4L19 10l-5.2 1.6L12 17l-1.8-5.4L5 10l5.2-1.6z" /></svg>;
13
17
  return (
14
- <div className={cx("cs-chat-msg", isUser ? "cs-chat-msg--user" : "cs-chat-msg--lumi", className)}>
18
+ <div ref={ref} className={cx("cs-chat-msg", isUser ? "cs-chat-msg--user" : "cs-chat-msg--lumi", className)}>
15
19
  <div className="cs-chat-msg__avatar">{avatar ?? defaultAvatar}</div>
16
20
  <div className="cs-chat-msg__col">
17
- <div className="cs-chat-msg__name">{name ?? (isUser ? "You" : "Lumi")}</div>
21
+ <div className="cs-chat-msg__name">{name ?? (isUser ? you : "Lumi")}</div>
18
22
  <div className="cs-chat-msg__bubble">{children}</div>
19
23
  </div>
20
24
  </div>
@@ -11,6 +11,8 @@ export interface ConfidenceMeterProps {
11
11
  level?: "low" | "medium" | "high";
12
12
  segments?: number;
13
13
  label?: ReactNode;
14
+ /** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
15
+ lang?: string;
14
16
  className?: string;
15
17
  }
16
18
 
@@ -12,6 +12,8 @@ export interface HumanReviewGateProps {
12
12
  reviewer?: ReactNode;
13
13
  approveLabel?: string;
14
14
  rejectLabel?: string;
15
+ /** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
16
+ lang?: string;
15
17
  className?: string;
16
18
  onApprove?: () => void;
17
19
  onReject?: () => void;
@@ -15,6 +15,8 @@ export interface PromptInputProps {
15
15
  hint?: ReactNode;
16
16
  disabled?: boolean;
17
17
  busy?: boolean;
18
+ /** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
19
+ lang?: string;
18
20
  className?: string;
19
21
  }
20
22
 
@@ -4,6 +4,8 @@ import type * as React from "react";
4
4
  * under reduced-motion. */
5
5
  export interface TypingIndicatorProps {
6
6
  label?: string;
7
+ /** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
8
+ lang?: string;
7
9
  className?: string;
8
10
  }
9
11
  export function TypingIndicator(props: TypingIndicatorProps): React.ReactElement;
@@ -10,9 +10,9 @@
10
10
  .grid { display: grid; grid-template-columns: 1.2fr 1fr; gap: 24px; max-width: 660px; align-items: start; }
11
11
  .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; }
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>
@@ -9,9 +9,9 @@
9
9
  .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; }
10
10
  .blk { margin-bottom: 20px; }
11
11
  </style>
12
- <script src="https://unpkg.com/umd-react@19.2.8/dist/react.development.js" integrity="sha384-r22tQd8qRIHDPIkmdXZcVtvPi1Tj2x1QaW2tmFQHEvHscATUnHQ5siTyxWw5PMUs" crossorigin="anonymous"></script>
13
- <script src="https://unpkg.com/umd-react@19.2.8/dist/react-dom.development.js" integrity="sha384-gJZYmumEmwqVmX3JVsLZq+4QrxoPGh/BW47Rk7SqNPCPFpTGnRtvmeRPJ7//ZDPk" crossorigin="anonymous"></script>
14
- <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
12
+ <script src="../../_vendor/react/react.production.min.js"></script>
13
+ <script src="../../_vendor/react/react-dom.production.min.js"></script>
14
+ <script src="../../_vendor/babel/babel.min.js"></script>
15
15
  <script src="../../_ds_bundle.js"></script>
16
16
  </head>
17
17
  <body>
@@ -10,9 +10,9 @@
10
10
  .stack { display: grid; gap: 18px; max-width: 560px; }
11
11
  .lbl { font: 600 11px/1 var(--cs-font-family-ui); text-transform: uppercase; letter-spacing: .09em; color: var(--cs-color-text-muted); margin-bottom: 8px; }
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>
@@ -9,9 +9,9 @@
9
9
  .row { display: flex; align-items: center; gap: 24px; }
10
10
  .cap { font: 500 12px/1.4 var(--cs-font-family-ui); color: var(--cs-color-text-muted); margin-top: 8px; }
11
11
  </style>
12
- <script src="https://unpkg.com/umd-react@19.2.8/dist/react.development.js" integrity="sha384-r22tQd8qRIHDPIkmdXZcVtvPi1Tj2x1QaW2tmFQHEvHscATUnHQ5siTyxWw5PMUs" crossorigin="anonymous"></script>
13
- <script src="https://unpkg.com/umd-react@19.2.8/dist/react-dom.development.js" integrity="sha384-gJZYmumEmwqVmX3JVsLZq+4QrxoPGh/BW47Rk7SqNPCPFpTGnRtvmeRPJ7//ZDPk" crossorigin="anonymous"></script>
14
- <script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
12
+ <script src="../../_vendor/react/react.production.min.js"></script>
13
+ <script src="../../_vendor/react/react-dom.production.min.js"></script>
14
+ <script src="../../_vendor/babel/babel.min.js"></script>
15
15
  <script src="../../_ds_bundle.js"></script>
16
16
  </head>
17
17
  <body>
@@ -11,9 +11,9 @@
11
11
  .row:last-child { margin-bottom: 0; }
12
12
  .lbl { width: 100%; font: 600 11px/1 var(--cs-font-family-ui); text-transform: uppercase; letter-spacing: .09em; color: var(--cs-color-text-muted); margin-bottom: 2px; }
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>
@@ -15,13 +15,29 @@ export function Image({ src, alt = "", ratio, preview = false, fallback, lang, c
15
15
  document.addEventListener("keydown", k);
16
16
  return () => document.removeEventListener("keydown", k);
17
17
  }, [zoom]);
18
+ const openPreview = () => setZoom(true);
19
+ const onPreviewKey = (e) => {
20
+ if (e.key === "Enter" || e.key === " ") {
21
+ e.preventDefault();
22
+ openPreview();
23
+ }
24
+ };
18
25
  const body = state === "error"
19
26
  ? <span className="cs-image__fallback">{fallback || <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="9" cy="9" r="2" /><path d="M21 15l-5-5-9 9" /></svg>}</span>
20
27
  : <img {...props} src={src} alt={alt} onLoad={() => setState("ok")} onError={() => setState("error")} />;
28
+ const canPreview = preview && state === "ok";
21
29
  return (
22
30
  <>
23
- <span ref={ref} className={cx("cs-image", state === "loading" && "is-loading", preview && state === "ok" && "is-zoomable", className)} style={ratio ? { aspectRatio: ratio } : undefined}
24
- onClick={preview && state === "ok" ? () => setZoom(true) : undefined} role={preview && state === "ok" ? "button" : undefined} aria-label={preview && state === "ok" ? t("preview") + (alt ? ": " + alt : "") : undefined}>
31
+ <span
32
+ ref={ref}
33
+ className={cx("cs-image", state === "loading" && "is-loading", canPreview && "is-zoomable", className)}
34
+ style={ratio ? { aspectRatio: ratio } : undefined}
35
+ onClick={canPreview ? openPreview : undefined}
36
+ onKeyDown={canPreview ? onPreviewKey : undefined}
37
+ role={canPreview ? "button" : undefined}
38
+ tabIndex={canPreview ? 0 : undefined}
39
+ aria-label={canPreview ? t("preview") + (alt ? ": " + alt : "") : undefined}
40
+ >
25
41
  {body}
26
42
  </span>
27
43
  {zoom ? (
@@ -1,10 +1,14 @@
1
1
  import type { ReactNode } from "react";
2
2
 
3
- /** Drag-to-reorder list (HTML5 drag and drop). onChange receives the new order. */
3
+ /** Reorderable list item. */
4
4
  export interface SortableItem { key: string; label: ReactNode; }
5
5
  export interface SortableProps {
6
6
  items: SortableItem[];
7
7
  onChange?: (items: SortableItem[]) => void;
8
+ lang?: string;
8
9
  className?: string;
9
10
  }
11
+ /** Pure reorder helper shared by drag and move-button paths. */
12
+ export function reorderItems<T>(items: T[], from: number, to: number): T[];
13
+ /** Reorderable list — move-up/down buttons (WCAG 2.5.7) plus HTML5 DnD enhancement. */
10
14
  export function Sortable(props: SortableProps): React.ReactElement;
@@ -1,28 +1,61 @@
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
- /** CyberSkill Sortable — drag-to-reorder list (HTML5 DnD). items: [{key,label}]; onChange(newItems). */
5
- export function Sortable({ items = [], onChange, className }) {
5
+ /** Move item at `from` to index `to` (shared by drag drop and move buttons). */
6
+ export function reorderItems(items, from, to) {
7
+ if (from < 0 || to < 0 || from >= items.length || to >= items.length || from === to) return items;
8
+ const next = [...items];
9
+ next.splice(to, 0, next.splice(from, 1)[0]);
10
+ return next;
11
+ }
12
+
13
+ /** CyberSkill Sortable — reorderable list (move buttons + HTML5 DnD enhancement). items: [{key,label}]; onChange(newItems). */
14
+ export function Sortable({ items = [], onChange, lang, className }) {
6
15
  const [dragKey, setDragKey] = React.useState(null);
7
16
  const [over, setOver] = React.useState(null);
17
+ const [ref, L] = useLang(lang);
18
+ const t = makeT("Sortable", L);
19
+ const move = (from, to) => {
20
+ const next = reorderItems(items, from, to);
21
+ if (next !== items) onChange && onChange(next);
22
+ };
8
23
  const drop = () => {
9
24
  if (dragKey == null || over == null || dragKey === over) { setDragKey(null); setOver(null); return; }
10
25
  const from = items.findIndex((i) => i.key === dragKey);
11
26
  const to = items.findIndex((i) => i.key === over);
12
- const next = [...items];
13
- next.splice(to, 0, next.splice(from, 1)[0]);
14
- onChange && onChange(next);
27
+ move(from, to);
15
28
  setDragKey(null); setOver(null);
16
29
  };
17
30
  return (
18
- <ul className={cx("cs-sortable", className)}>
19
- {items.map((it) => (
31
+ <ul ref={ref} className={cx("cs-sortable", className)}>
32
+ {items.map((it, idx) => (
20
33
  <li key={it.key} draggable
21
34
  className={cx("cs-sortable__item", dragKey === it.key && "is-dragging", over === it.key && "is-over")}
22
- onDragStart={() => setDragKey(it.key)}
35
+ onDragStart={(e) => {
36
+ if (e.target.closest && e.target.closest(".cs-sortable__ops")) { e.preventDefault(); return; }
37
+ setDragKey(it.key);
38
+ }}
23
39
  onDragOver={(e) => { e.preventDefault(); setOver(it.key); }}
24
40
  onDrop={drop} onDragEnd={drop}>
25
- <span className="cs-sortable__grip" aria-hidden="true">⠿</span>{it.label}
41
+ <span className="cs-sortable__grip" aria-hidden="true">⠿</span>
42
+ <span className="cs-sortable__label">{it.label}</span>
43
+ <span className="cs-sortable__ops" onMouseDown={(e) => e.stopPropagation()}>
44
+ <button
45
+ type="button"
46
+ className="cs-button cs-button--secondary cs-button--xs"
47
+ aria-label={t("moveUp")}
48
+ disabled={idx === 0}
49
+ onClick={() => move(idx, idx - 1)}
50
+ >↑</button>
51
+ <button
52
+ type="button"
53
+ className="cs-button cs-button--secondary cs-button--xs"
54
+ aria-label={t("moveDown")}
55
+ disabled={idx === items.length - 1}
56
+ onClick={() => move(idx, idx + 1)}
57
+ >↓</button>
58
+ </span>
26
59
  </li>
27
60
  ))}
28
61
  </ul>
@@ -1,5 +1,7 @@
1
- **Sortable** — drag-to-reorder list (compose columns of it for a kanban).
1
+ **Sortable** — reorderable list with move-up/down buttons (keyboard + single-pointer) and optional HTML5 drag. Compose columns of it for a kanban.
2
2
 
3
3
  ```jsx
4
- <Sortable items={tasks} onChange={setTasks}/>
4
+ <Sortable items={tasks} onChange={setTasks} lang="en"/>
5
5
  ```
6
+
7
+ Buttons expose i18n `aria-label`s (`moveUp` / `moveDown`); first/last controls are disabled.
@@ -3,11 +3,12 @@ import { makeT, useLang } from "../_i18n/i18n.js";
3
3
  import { cx } from "../_utils/cx.js";
4
4
 
5
5
  /** CyberSkill Terminal — command console: history + prompt input; onCommand returns output (string or lines). */
6
- export function Terminal({ title = "cyberskill — zsh", welcome, onCommand, prompt = "➜", lang, className }) {
6
+ export function Terminal({ title, welcome, onCommand, prompt = "➜", lang, className }) {
7
7
  const [hist, setHist] = React.useState(() => (welcome ? [{ out: welcome }] : []));
8
8
  const [q, setQ] = React.useState("");
9
9
  const [ref, L] = useLang(lang);
10
10
  const t = makeT("Terminal", L);
11
+ const barTitle = title ?? t("title");
11
12
  const run = () => {
12
13
  if (!q.trim()) return;
13
14
  const res = onCommand ? onCommand(q.trim()) : "";
@@ -16,7 +17,7 @@ export function Terminal({ title = "cyberskill — zsh", welcome, onCommand, pro
16
17
  };
17
18
  return (
18
19
  <div ref={ref} className={cx("cs-terminal", className)}>
19
- <div className="cs-terminal__bar"><i /><i /><i /><span>{title}</span></div>
20
+ <div className="cs-terminal__bar"><i /><i /><i /><span>{barTitle}</span></div>
20
21
  <div className="cs-terminal__body">
21
22
  {hist.map((l, i) => l.cmd != null
22
23
  ? <div key={i} className="cs-terminal__line"><span className="p">{prompt}</span> {l.cmd}</div>
@@ -8,6 +8,8 @@ import type { ReactNode } from "react";
8
8
  export interface TooltipProps {
9
9
  label: ReactNode;
10
10
  children: ReactNode;
11
+ /** "en" | "vi" — else resolved from the nearest [lang] ancestor (vi default). */
12
+ lang?: string;
11
13
  className?: string;
12
14
  }
13
15
 
@@ -1,12 +1,15 @@
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 Tooltip — hover/focus bubble above the trigger. Keep label short. */
5
- export function Tooltip({ label, children, className }) {
6
+ export function Tooltip({ label, children, lang, className }) {
7
+ const [ref, L] = useLang(lang);
8
+ const t = makeT("Tooltip", L);
6
9
  return (
7
- <span className={cx("cs-tooltip", className)}>
10
+ <span ref={ref} className={cx("cs-tooltip", className)}>
8
11
  {children}
9
- <span className="cs-tooltip__bubble" role="tooltip">{label}</span>
12
+ <span className="cs-tooltip__bubble" role="tooltip">{label ?? t("label")}</span>
10
13
  </span>
11
14
  );
12
15
  }
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from "react";
2
2
 
3
- /** Expandable hierarchy (role=tree). Single-select via onSelect(key, node). */
3
+ /** Expandable hierarchy (APG tree: roving tabindex + arrow navigate/expand).
4
+ * Single-select via onSelect(key, node). */
4
5
  export interface TreeNode { key: string; label: ReactNode; children?: TreeNode[]; }
5
6
  export interface TreeProps {
6
7
  nodes: TreeNode[];