@cyberskill/design 1.1.1 → 1.2.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 (353) hide show
  1. package/DESIGN.md +25 -3
  2. package/README.md +3 -3
  3. package/SKILL.md +3 -3
  4. package/VERSION +1 -1
  5. package/_ds_bundle.js +819 -416
  6. package/_ds_manifest.json +1 -1
  7. package/_esm/cs.mjs +18 -4
  8. package/_esm/react.d.ts +2 -0
  9. package/_esm/react.mjs +1 -0
  10. package/base/a11y.css +18 -0
  11. package/base/ai.css +2 -2
  12. package/base/collections.css +8 -3
  13. package/base/components.css +11 -2
  14. package/base/controls.css +61 -48
  15. package/base/data.css +3 -3
  16. package/base/feedback.css +5 -0
  17. package/base/forms.css +9 -3
  18. package/base/glass.css +6 -0
  19. package/base/navigation.css +8 -2
  20. package/base/reset.css +1 -1
  21. package/base/responsive.css +25 -0
  22. package/base/shell.css +7 -1
  23. package/components/_i18n/i18n.js +13 -1
  24. package/components/_i18n/strings.js +52 -3
  25. package/components/ai/ConfidenceMeter.jsx +8 -6
  26. package/components/data/Watermark.jsx +11 -3
  27. package/components/datatable/DataGrid.jsx +10 -1
  28. package/components/dialog/Dialog.jsx +13 -45
  29. package/components/feedback/ProgressBar.jsx +4 -2
  30. package/components/forms/Cascader.jsx +69 -6
  31. package/components/forms/Editor.jsx +37 -2
  32. package/components/forms/FileUpload.jsx +22 -8
  33. package/components/forms/Switch.jsx +16 -2
  34. package/components/forms/TimePicker.jsx +3 -1
  35. package/components/icon/Icon.jsx +1 -0
  36. package/components/navigation/CommandPalette.jsx +22 -7
  37. package/components/navigation/Toolbar.jsx +2 -1
  38. package/components/overlays/AlertDialog.jsx +19 -49
  39. package/components/overlays/Drawer.jsx +24 -20
  40. package/components/overlays/OverlayManager.d.ts +40 -0
  41. package/components/overlays/OverlayManager.jsx +214 -0
  42. package/components/overlays/OverlayManager.prompt.md +12 -0
  43. package/components/overlays/Tour.jsx +14 -7
  44. package/dist/styles.min.css +2004 -0
  45. package/docs/benchmark-rubric.md +1 -1
  46. package/docs/ci-cd.md +3 -1
  47. package/docs/consuming.md +6 -6
  48. package/docs/conventions.md +4 -4
  49. package/docs/decisions/container-query-migration.md +18 -0
  50. package/docs/decisions/overlay-manager-design.md +101 -0
  51. package/docs/decisions.md +5 -5
  52. package/docs/design-styles.md +1 -1
  53. package/docs/plans/cyberskill-design-system-audit-and-12-month-evolution-plan.md +1490 -0
  54. package/docs/quality-gates.md +20 -8
  55. package/docs/release-notes.md +2 -2
  56. package/docs/status/data/status-feed.json +1 -1
  57. package/docs/status/index.html +9 -5
  58. package/docs/status/status-legacy.html +12 -7
  59. package/docs/sync.md +1 -1
  60. package/docs/tasks/_state/commit-links.yaml +14 -0
  61. package/docs/vi/benchmark-rubric.md +1 -1
  62. package/docs/vi/ci-cd.md +3 -1
  63. package/docs/vi/consuming.md +6 -6
  64. package/docs/vi/conventions.md +2 -2
  65. package/docs/vi/decisions.md +4 -4
  66. package/docs/vi/quality-gates.md +21 -9
  67. package/docs/vi/release-notes.md +2 -2
  68. package/docs/vi/sync.md +1 -1
  69. package/guidelines/atomic-view.html +11 -7
  70. package/llms.txt +2 -2
  71. package/package.json +14 -6
  72. package/templates/_ds-base.canonical.js +55 -0
  73. package/templates/ai-cluster-demo.html +2 -2
  74. package/templates/app-shell/AppShell.dc.html +5 -3
  75. package/templates/app-shell/ds-base.js +33 -1
  76. package/templates/app-shell/support.js +13 -20
  77. package/templates/article/Article.dc.html +5 -1
  78. package/templates/article/ds-base.js +33 -1
  79. package/templates/article/support.js +13 -20
  80. package/templates/auth/Auth.dc.html +10 -5
  81. package/templates/auth/content-schema.json +12 -0
  82. package/templates/auth/ds-base.js +33 -1
  83. package/templates/auth/support.js +13 -20
  84. package/templates/bod-deck/BodDeck.dc.html +11 -10
  85. package/templates/bod-deck/ds-base.js +33 -1
  86. package/templates/bod-deck/support.js +13 -20
  87. package/templates/bod-investor-update/BodInvestorUpdate.dc.html +4 -3
  88. package/templates/bod-investor-update/ds-base.js +33 -1
  89. package/templates/bod-investor-update/support.js +13 -20
  90. package/templates/bod-memo/BodMemo.dc.html +6 -5
  91. package/templates/bod-memo/ds-base.js +33 -1
  92. package/templates/bod-memo/support.js +13 -20
  93. package/templates/bod-minutes/BodMinutes.dc.html +2 -1
  94. package/templates/bod-minutes/ds-base.js +33 -1
  95. package/templates/bod-minutes/support.js +13 -20
  96. package/templates/bod-report/BodReport.dc.html +3 -2
  97. package/templates/bod-report/ds-base.js +33 -1
  98. package/templates/bod-report/support.js +13 -20
  99. package/templates/bod-resolution/BodResolution.dc.html +5 -4
  100. package/templates/bod-resolution/ds-base.js +33 -1
  101. package/templates/bod-resolution/support.js +13 -20
  102. package/templates/culture-event-invite/CultureEventInvite.dc.html +3 -2
  103. package/templates/culture-event-invite/ds-base.js +33 -1
  104. package/templates/culture-event-invite/support.js +13 -20
  105. package/templates/dashboard/Dashboard.dc.html +7 -3
  106. package/templates/dashboard/ds-base.js +33 -1
  107. package/templates/dashboard/support.js +13 -20
  108. package/templates/delivery-change-note/DeliveryChangeNote.dc.html +2 -1
  109. package/templates/delivery-change-note/ds-base.js +33 -1
  110. package/templates/delivery-change-note/support.js +13 -20
  111. package/templates/delivery-kickoff/DeliveryKickoff.dc.html +3 -2
  112. package/templates/delivery-kickoff/ds-base.js +33 -1
  113. package/templates/delivery-kickoff/support.js +13 -20
  114. package/templates/delivery-qbr-deck/DeliveryQbrDeck.dc.html +6 -5
  115. package/templates/delivery-qbr-deck/ds-base.js +33 -1
  116. package/templates/delivery-qbr-deck/support.js +13 -20
  117. package/templates/delivery-runbook/DeliveryRunbook.dc.html +3 -2
  118. package/templates/delivery-runbook/ds-base.js +33 -1
  119. package/templates/delivery-runbook/support.js +13 -20
  120. package/templates/delivery-status-email/DeliveryStatusEmail.dc.html +3 -2
  121. package/templates/delivery-status-email/ds-base.js +33 -1
  122. package/templates/delivery-status-email/support.js +13 -20
  123. package/templates/doc-style-guide/DocStyleGuide.dc.html +67 -64
  124. package/templates/doc-style-guide/ds-base.js +33 -1
  125. package/templates/doc-style-guide/support.js +13 -20
  126. package/templates/doc-suite-index/DocSuiteIndex.dc.html +46 -45
  127. package/templates/doc-suite-index/ds-base.js +33 -1
  128. package/templates/doc-suite-index/support.js +13 -20
  129. package/templates/doc-templates/DocTemplates.dc.html +42 -41
  130. package/templates/doc-templates/ds-base.js +33 -1
  131. package/templates/doc-templates/support.js +13 -20
  132. package/templates/email/Email.dc.html +3 -2
  133. package/templates/email/ds-base.js +33 -1
  134. package/templates/email/support.js +13 -20
  135. package/templates/finance-dunning-email/FinanceDunningEmail.dc.html +4 -3
  136. package/templates/finance-dunning-email/ds-base.js +33 -1
  137. package/templates/finance-dunning-email/support.js +13 -20
  138. package/templates/finance-expense-report/FinanceExpenseReport.dc.html +4 -3
  139. package/templates/finance-expense-report/ds-base.js +33 -1
  140. package/templates/finance-expense-report/support.js +13 -20
  141. package/templates/finance-invoice/FinanceInvoice.dc.html +4 -3
  142. package/templates/finance-invoice/ds-base.js +33 -1
  143. package/templates/finance-invoice/support.js +13 -20
  144. package/templates/finance-quote/FinanceQuote.dc.html +4 -3
  145. package/templates/finance-quote/ds-base.js +33 -1
  146. package/templates/finance-quote/support.js +13 -20
  147. package/templates/hr-announcement/HrAnnouncement.dc.html +5 -4
  148. package/templates/hr-announcement/ds-base.js +33 -1
  149. package/templates/hr-announcement/support.js +13 -20
  150. package/templates/hr-interview-kit/HrInterviewKit.dc.html +4 -3
  151. package/templates/hr-interview-kit/ds-base.js +33 -1
  152. package/templates/hr-interview-kit/support.js +13 -20
  153. package/templates/hr-offboarding/HrOffboarding.dc.html +2 -1
  154. package/templates/hr-offboarding/ds-base.js +33 -1
  155. package/templates/hr-offboarding/support.js +13 -20
  156. package/templates/hr-performance-review/HrPerformanceReview.dc.html +2 -1
  157. package/templates/hr-performance-review/ds-base.js +33 -1
  158. package/templates/hr-performance-review/support.js +13 -20
  159. package/templates/hr-pip/HrPip.dc.html +2 -1
  160. package/templates/hr-pip/ds-base.js +33 -1
  161. package/templates/hr-pip/support.js +13 -20
  162. package/templates/kitchen-sink.html +9 -4
  163. package/templates/legal-msa/LegalMsa.dc.html +2 -1
  164. package/templates/legal-msa/ds-base.js +33 -1
  165. package/templates/legal-msa/support.js +13 -20
  166. package/templates/legal-sow/LegalSow.dc.html +2 -1
  167. package/templates/legal-sow/ds-base.js +33 -1
  168. package/templates/legal-sow/support.js +13 -20
  169. package/templates/marketing-campaign-brief/MarketingCampaignBrief.dc.html +3 -2
  170. package/templates/marketing-campaign-brief/ds-base.js +33 -1
  171. package/templates/marketing-campaign-brief/support.js +13 -20
  172. package/templates/marketing-case-study/MarketingCaseStudy.dc.html +5 -4
  173. package/templates/marketing-case-study/ds-base.js +33 -1
  174. package/templates/marketing-case-study/support.js +13 -20
  175. package/templates/marketing-launch/MarketingLaunch.dc.html +6 -5
  176. package/templates/marketing-launch/ds-base.js +33 -1
  177. package/templates/marketing-launch/support.js +13 -20
  178. package/templates/marketing-newsletter/MarketingNewsletter.dc.html +13 -12
  179. package/templates/marketing-newsletter/ds-base.js +33 -1
  180. package/templates/marketing-newsletter/support.js +13 -20
  181. package/templates/marketing-page/MarketingPage.dc.html +17 -12
  182. package/templates/marketing-page/ds-base.js +33 -1
  183. package/templates/marketing-page/support.js +13 -20
  184. package/templates/marketing-press-release/MarketingPressRelease.dc.html +2 -1
  185. package/templates/marketing-press-release/ds-base.js +33 -1
  186. package/templates/marketing-press-release/support.js +13 -20
  187. package/templates/marketing-social-kit/MarketingSocialKit.dc.html +5 -4
  188. package/templates/marketing-social-kit/ds-base.js +33 -1
  189. package/templates/marketing-social-kit/support.js +13 -20
  190. package/templates/sales-one-pager/SalesOnePager.dc.html +5 -4
  191. package/templates/sales-one-pager/ds-base.js +33 -1
  192. package/templates/sales-one-pager/support.js +13 -20
  193. package/templates/sales-proposal-deck/SalesProposalDeck.dc.html +7 -6
  194. package/templates/sales-proposal-deck/ds-base.js +33 -1
  195. package/templates/sales-proposal-deck/support.js +13 -20
  196. package/templates/settings/Settings.dc.html +5 -1
  197. package/templates/settings/ds-base.js +33 -1
  198. package/templates/settings/support.js +13 -20
  199. package/templates/slide-deck/SlideDeck.dc.html +5 -4
  200. package/templates/slide-deck/ds-base.js +33 -1
  201. package/templates/slide-deck/support.js +13 -20
  202. package/templates/team-meeting-agenda/TeamMeetingAgenda.dc.html +2 -1
  203. package/templates/team-meeting-agenda/ds-base.js +33 -1
  204. package/templates/team-meeting-agenda/support.js +13 -20
  205. package/templates/tech-incident-report/TechIncidentReport.dc.html +3 -2
  206. package/templates/tech-incident-report/ds-base.js +33 -1
  207. package/templates/tech-incident-report/support.js +13 -20
  208. package/templates/tech-release-notes/TechReleaseNotes.dc.html +6 -5
  209. package/templates/tech-release-notes/ds-base.js +33 -1
  210. package/templates/tech-release-notes/support.js +13 -20
  211. package/templates/tech-retro/TechRetro.dc.html +4 -3
  212. package/templates/tech-retro/ds-base.js +33 -1
  213. package/templates/tech-retro/support.js +13 -20
  214. package/templates/tech-rfc/TechRfc.dc.html +3 -2
  215. package/templates/tech-rfc/ds-base.js +33 -1
  216. package/templates/tech-rfc/support.js +13 -20
  217. package/templates/vn-appointment-transfer/VnAppointmentTransfer.dc.html +36 -35
  218. package/templates/vn-appointment-transfer/ds-base.js +33 -1
  219. package/templates/vn-appointment-transfer/support.js +13 -20
  220. package/templates/vn-certificate-of-employment/VnCertificateOfEmployment.dc.html +27 -26
  221. package/templates/vn-certificate-of-employment/ds-base.js +33 -1
  222. package/templates/vn-certificate-of-employment/support.js +13 -20
  223. package/templates/vn-code-of-conduct/VnCodeOfConduct.dc.html +44 -43
  224. package/templates/vn-code-of-conduct/ds-base.js +33 -1
  225. package/templates/vn-code-of-conduct/support.js +13 -20
  226. package/templates/vn-compensation-regulation/VnCompensationRegulation.dc.html +53 -52
  227. package/templates/vn-compensation-regulation/ds-base.js +33 -1
  228. package/templates/vn-compensation-regulation/support.js +13 -20
  229. package/templates/vn-contract-amendment/VnContractAmendment.dc.html +41 -40
  230. package/templates/vn-contract-amendment/ds-base.js +33 -1
  231. package/templates/vn-contract-amendment/support.js +13 -20
  232. package/templates/vn-contractor-agreement/VnContractorAgreement.dc.html +50 -49
  233. package/templates/vn-contractor-agreement/ds-base.js +33 -1
  234. package/templates/vn-contractor-agreement/support.js +13 -20
  235. package/templates/vn-data-protection-policy/VnDataProtectionPolicy.dc.html +54 -53
  236. package/templates/vn-data-protection-policy/ds-base.js +33 -1
  237. package/templates/vn-data-protection-policy/support.js +13 -20
  238. package/templates/vn-disciplinary-case-file/VnDisciplinaryCaseFile.dc.html +72 -71
  239. package/templates/vn-disciplinary-case-file/ds-base.js +33 -1
  240. package/templates/vn-disciplinary-case-file/support.js +13 -20
  241. package/templates/vn-disciplinary-schedule/VnDisciplinarySchedule.dc.html +21 -20
  242. package/templates/vn-disciplinary-schedule/ds-base.js +33 -1
  243. package/templates/vn-disciplinary-schedule/support.js +13 -20
  244. package/templates/vn-employee-request/VnEmployeeRequest.dc.html +54 -53
  245. package/templates/vn-employee-request/ds-base.js +33 -1
  246. package/templates/vn-employee-request/support.js +13 -20
  247. package/templates/vn-exit-handover/VnExitHandover.dc.html +122 -121
  248. package/templates/vn-exit-handover/ds-base.js +33 -1
  249. package/templates/vn-exit-handover/support.js +13 -20
  250. package/templates/vn-foreign-employee-pack/VnForeignEmployeePack.dc.html +31 -30
  251. package/templates/vn-foreign-employee-pack/ds-base.js +33 -1
  252. package/templates/vn-foreign-employee-pack/support.js +13 -20
  253. package/templates/vn-grassroots-democracy/VnGrassrootsDemocracy.dc.html +54 -53
  254. package/templates/vn-grassroots-democracy/ds-base.js +33 -1
  255. package/templates/vn-grassroots-democracy/support.js +13 -20
  256. package/templates/vn-infosec-policy/VnInfosecPolicy.dc.html +50 -49
  257. package/templates/vn-infosec-policy/ds-base.js +33 -1
  258. package/templates/vn-infosec-policy/support.js +13 -20
  259. package/templates/vn-internal-labor-regulations/VnInternalLaborRegulations.dc.html +84 -83
  260. package/templates/vn-internal-labor-regulations/ds-base.js +33 -1
  261. package/templates/vn-internal-labor-regulations/support.js +13 -20
  262. package/templates/vn-internship-agreement/VnInternshipAgreement.dc.html +44 -43
  263. package/templates/vn-internship-agreement/ds-base.js +33 -1
  264. package/templates/vn-internship-agreement/support.js +13 -20
  265. package/templates/vn-job-description/VnJobDescription.dc.html +28 -27
  266. package/templates/vn-job-description/ds-base.js +33 -1
  267. package/templates/vn-job-description/support.js +13 -20
  268. package/templates/vn-labor-contract/VnLaborContract.dc.html +109 -108
  269. package/templates/vn-labor-contract/ds-base.js +33 -1
  270. package/templates/vn-labor-contract/support.js +13 -20
  271. package/templates/vn-labour-management-book/VnLabourManagementBook.dc.html +32 -31
  272. package/templates/vn-labour-management-book/ds-base.js +33 -1
  273. package/templates/vn-labour-management-book/support.js +13 -20
  274. package/templates/vn-mutual-nda/VnMutualNda.dc.html +45 -44
  275. package/templates/vn-mutual-nda/ds-base.js +33 -1
  276. package/templates/vn-mutual-nda/support.js +13 -20
  277. package/templates/vn-ndnca-ip/VnNdncaIp.dc.html +73 -72
  278. package/templates/vn-ndnca-ip/ds-base.js +33 -1
  279. package/templates/vn-ndnca-ip/support.js +13 -20
  280. package/templates/vn-offer-letter/VnOfferLetter.dc.html +32 -31
  281. package/templates/vn-offer-letter/ds-base.js +33 -1
  282. package/templates/vn-offer-letter/support.js +13 -20
  283. package/templates/vn-onboarding-checklist/VnOnboardingChecklist.dc.html +16 -15
  284. package/templates/vn-onboarding-checklist/ds-base.js +33 -1
  285. package/templates/vn-onboarding-checklist/support.js +13 -20
  286. package/templates/vn-pdpl-consent/VnPdplConsent.dc.html +43 -42
  287. package/templates/vn-pdpl-consent/ds-base.js +33 -1
  288. package/templates/vn-pdpl-consent/support.js +13 -20
  289. package/templates/vn-performance-evaluation-regulation/VnPerformanceEvaluationRegulation.dc.html +65 -64
  290. package/templates/vn-performance-evaluation-regulation/ds-base.js +33 -1
  291. package/templates/vn-performance-evaluation-regulation/support.js +13 -20
  292. package/templates/vn-performance-review-form/VnPerformanceReviewForm.dc.html +49 -48
  293. package/templates/vn-performance-review-form/ds-base.js +33 -1
  294. package/templates/vn-performance-review-form/support.js +13 -20
  295. package/templates/vn-phantom-stock/VnPhantomStock.dc.html +41 -40
  296. package/templates/vn-phantom-stock/ds-base.js +33 -1
  297. package/templates/vn-phantom-stock/support.js +13 -20
  298. package/templates/vn-probation-contract/VnProbationContract.dc.html +56 -55
  299. package/templates/vn-probation-contract/ds-base.js +33 -1
  300. package/templates/vn-probation-contract/support.js +13 -20
  301. package/templates/vn-receipt-acknowledgement/VnReceiptAcknowledgement.dc.html +28 -27
  302. package/templates/vn-receipt-acknowledgement/ds-base.js +33 -1
  303. package/templates/vn-receipt-acknowledgement/support.js +13 -20
  304. package/templates/vn-remote-work-policy/VnRemoteWorkPolicy.dc.html +44 -43
  305. package/templates/vn-remote-work-policy/ds-base.js +33 -1
  306. package/templates/vn-remote-work-policy/support.js +13 -20
  307. package/templates/vn-resignation-letter/VnResignationLetter.dc.html +20 -19
  308. package/templates/vn-resignation-letter/ds-base.js +33 -1
  309. package/templates/vn-resignation-letter/support.js +13 -20
  310. package/templates/vn-salary-scale/VnSalaryScale.dc.html +39 -38
  311. package/templates/vn-salary-scale/ds-base.js +33 -1
  312. package/templates/vn-salary-scale/support.js +13 -20
  313. package/templates/vn-sow-appendix/VnSowAppendix.dc.html +21 -20
  314. package/templates/vn-sow-appendix/ds-base.js +33 -1
  315. package/templates/vn-sow-appendix/support.js +13 -20
  316. package/templates/vn-termination-decision/VnTerminationDecision.dc.html +25 -24
  317. package/templates/vn-termination-decision/ds-base.js +33 -1
  318. package/templates/vn-termination-decision/support.js +13 -20
  319. package/templates/vn-total-rewards-appendix/VnTotalRewardsAppendix.dc.html +63 -62
  320. package/templates/vn-total-rewards-appendix/ds-base.js +33 -1
  321. package/templates/vn-total-rewards-appendix/support.js +13 -20
  322. package/templates/vn-training-commitment/VnTrainingCommitment.dc.html +39 -38
  323. package/templates/vn-training-commitment/ds-base.js +33 -1
  324. package/templates/vn-training-commitment/support.js +13 -20
  325. package/templates/vn-travel-expense-policy/VnTravelExpensePolicy.dc.html +50 -49
  326. package/templates/vn-travel-expense-policy/ds-base.js +33 -1
  327. package/templates/vn-travel-expense-policy/support.js +13 -20
  328. package/tokens/colors.css +19 -1
  329. package/tokens/component-tokens.css +1 -0
  330. package/tokens/elements.css +35 -2
  331. package/tokens/elevation.css +8 -2
  332. package/tokens/native/CSTokens.kt +35 -1
  333. package/tokens/native/CSTokens.swift +35 -1
  334. package/tokens/native/cs_tokens.dart +35 -1
  335. package/tokens/provenance.json +12 -12
  336. package/tokens/spacing.css +2 -0
  337. package/tokens/tokens.dtcg.json +111 -3
  338. package/tokens/tokens.js +46 -6
  339. package/tokens/tokens.json +45 -5
  340. package/tokens/typography.css +2 -0
  341. package/ui_kits/deck/index.html +4 -0
  342. package/ui_kits/status-hub/StatusHub.jsx +30 -4
  343. package/ui_kits/status-hub/index.html +3 -3
  344. package/ui_kits/status-hub/login.html +4 -0
  345. package/ui_kits/status-hub/project.html +4 -0
  346. package/ui_kits/status-hub/settings.html +35 -13
  347. package/ui_kits/status-hub/status.css +5 -1
  348. package/ui_kits/website/Website.jsx +25 -3
  349. package/ui_kits/website/careers.html +4 -0
  350. package/ui_kits/website/chat.html +5 -3
  351. package/ui_kits/website/index.html +3 -3
  352. package/ui_kits/website/site.css +2 -2
  353. package/ui_kits/website/work.html +4 -0
@@ -13,6 +13,16 @@
13
13
  /* 1 — Media and embeds never blow out a column */
14
14
  img, svg, video, canvas { max-width: 100%; height: auto; }
15
15
 
16
+ /* G1 / CDS-RESP-001 — container-query migration (compatibility layer).
17
+ * Opt-in hosts mark a sizing context with `.cs-cq`. Children can use
18
+ * `@container (max-width: …)` without waiting for a full layout rewrite.
19
+ * Media queries above remain the default until packs generate CQ variants. */
20
+ .cs-cq { container-type: inline-size; container-name: cs; }
21
+ @container cs (max-width: 640px) {
22
+ .cs-cq-stack { display: flex !important; flex-direction: column !important; }
23
+ .cs-cq-stack > * { min-inline-size: 0; max-inline-size: 100%; }
24
+ }
25
+
16
26
  /* 2 — Print-doc archetype: shrink desk + sheet chrome as the screen narrows.
17
27
  * cs-desk is the grey backing; cs-sheet is the white page. */
18
28
  @media screen and (max-width: 900px) {
@@ -126,3 +136,18 @@ img, svg, video, canvas { max-width: 100%; height: auto; }
126
136
  main > * { min-width: 0; }
127
137
  .cs-table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
128
138
  }
139
+
140
+ /* ---- 320 reflow (UX-003 / WCAG 1.4.10) ---- */
141
+ @media (max-width: 360px) {
142
+ .cs-tabs { overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: thin; }
143
+ .cs-pagination { flex-wrap: wrap; max-inline-size: 100%; }
144
+ .cs-pagination button { min-inline-size: 32px; }
145
+ .cs-otp { gap: 4px; }
146
+ .cs-otp__box { inline-size: 36px; block-size: 40px; font-size: var(--cs-font-size-base); }
147
+ .cs-dl { grid-template-columns: 1fr; }
148
+ .cs-field { min-inline-size: 0; }
149
+ .cs-field__control { min-inline-size: 0; }
150
+ .card, button.card, article.case { min-width: 0 !important; }
151
+ [style*="minmax(320px"] { grid-template-columns: 1fr !important; }
152
+ [style*="grid-template-columns: 248px 1fr"] { grid-template-columns: 1fr !important; }
153
+ }
package/base/shell.css CHANGED
@@ -16,7 +16,7 @@
16
16
  .cs-steps { display: flex; gap: 0; }
17
17
  .cs-step { flex: 1; display: flex; flex-direction: column; align-items: flex-start; gap: 8px; position: relative; padding-inline-end: 12px; }
18
18
  .cs-step:not(:last-child)::after { content: ""; position: absolute; inset-block-start: 13px; inset-inline-start: 34px; inline-size: calc(100% - 46px); block-size: 2px; background: var(--cs-color-border-default); }
19
- .cs-step__marker { inline-size: 28px; block-size: 28px; border-radius: 50%; display: grid; place-items: center; font-family: var(--cs-font-family-mono); font-weight: 700; font-size: 13px; background: var(--cs-color-surface-raised); color: var(--cs-color-text-muted); border: 1px solid var(--cs-color-border-default); z-index: 1; }
19
+ .cs-step__marker { inline-size: 28px; block-size: 28px; border-radius: 50%; display: grid; place-items: center; font-family: var(--cs-font-family-mono); font-weight: 700; font-size: var(--cs-font-size-sm); background: var(--cs-color-surface-raised); color: var(--cs-color-text-muted); border: 1px solid var(--cs-color-border-default); z-index: 1; }
20
20
  .cs-step--done .cs-step__marker { background: var(--cs-color-brand-umber); color: #fff; border-color: transparent; }
21
21
  .cs-step--done:not(:last-child)::after { background: var(--cs-color-brand-umber); }
22
22
  .cs-step--active .cs-step__marker { background: var(--cs-color-brand-ochre); color: var(--cs-color-brand-umber); border-color: transparent; box-shadow: 0 0 0 4px color-mix(in oklab, var(--cs-color-brand-ochre) 28%, transparent); }
@@ -53,3 +53,9 @@
53
53
  .cs-cmdk__item { display: flex; align-items: center; gap: 11px; inline-size: 100%; text-align: start; border: 0; background: transparent; font: inherit; font-family: var(--cs-font-family-ui); font-size: var(--cs-font-size-sm); color: var(--cs-color-text-primary); padding: 10px 12px; border-radius: var(--cs-radius-md); cursor: pointer; }
54
54
  .cs-cmdk__item:hover, .cs-cmdk__item[aria-selected="true"] { background: var(--cs-color-surface-raised); }
55
55
  .cs-cmdk__item .cs-kbd { margin-inline-start: auto; }
56
+
57
+ /* ---- system-theme dark parity (UX-010 / CDS-THEME-001) ---- */
58
+ @media (prefers-color-scheme: dark) {
59
+ [data-theme="system"] .cs-nav-item.is-active { color: var(--cs-color-brand-ochre); }
60
+ [data-theme="system"] .cs-nav-item.is-active .cs-nav-item__icon { color: var(--cs-color-brand-ochre); }
61
+ }
@@ -31,7 +31,19 @@ export function makeT(component, lang) { return (key) => tr(component, key, lang
31
31
  export function useLang(propLang) {
32
32
  const ref = React.useRef(null);
33
33
  const [lang, setLang] = React.useState(() => resolveLang(propLang, null));
34
- React.useLayoutEffect(() => { setLang(resolveLang(propLang, ref.current)); }, [propLang]);
34
+ React.useLayoutEffect(() => {
35
+ const el = ref.current;
36
+ setLang(resolveLang(propLang, el));
37
+ if (!el || propLang) return undefined;
38
+ // Re-resolve when an ancestor [lang] changes (template Language axis / __dcSetProps).
39
+ const obs = new MutationObserver(() => setLang(resolveLang(propLang, el)));
40
+ let node = el.parentElement;
41
+ while (node) {
42
+ obs.observe(node, { attributes: true, attributeFilter: ["lang"] });
43
+ node = node.parentElement;
44
+ }
45
+ return () => obs.disconnect();
46
+ }, [propLang]);
35
47
  return [ref, lang];
36
48
  }
37
49
 
@@ -1,4 +1,4 @@
1
- /* CyberSkill — central bilingual string registry (v3 Batch 0/1).
1
+ /* CyberSkill — central bilingual string registry (v3 Batch 0/1/2).
2
2
  * Every component with BUILT-IN UI text (text the consumer does NOT pass as a prop)
3
3
  * registers its strings here, EN + VI. _audit/bilingual-parity.html enforces:
4
4
  * • each component has both `en` and `vi`
@@ -77,8 +77,8 @@ export const strings = {
77
77
  vi: { input: "Ô nhập lệnh" },
78
78
  },
79
79
  DataGrid: {
80
- en: { empty: "No records", selectAll: "Select all rows", selectRow: "Select row" },
81
- vi: { empty: "Không có bản ghi", selectAll: "Chọn tất cả", selectRow: "Chọn dòng" },
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}" },
82
82
  },
83
83
  Splitter: {
84
84
  en: { label: "Resize panes" },
@@ -188,4 +188,53 @@ export const strings = {
188
188
  en: { aria: "Human review gate", risk: "review required", approve: "Approve", reject: "Reject", reviewer: "Reviewer" },
189
189
  vi: { aria: "Cổng kiểm duyệt của con người", risk: "cần xem xét", approve: "Phê duyệt", reject: "Từ chối", reviewer: "Người duyệt" },
190
190
  },
191
+
192
+ Switch: {
193
+ en: { on: "On", off: "Off" },
194
+ vi: { on: "Bật", off: "Tắt" },
195
+ },
196
+ Tabs: {
197
+ en: { list: "Tabs" },
198
+ vi: { list: "Tab" },
199
+ },
200
+ EmptyState: {
201
+ en: { title: "Nothing here yet", action: "Get started" },
202
+ vi: { title: "Chưa có gì ở đây", action: "Bắt đầu" },
203
+ },
204
+ Alert: {
205
+ en: { dismiss: "Dismiss" },
206
+ vi: { dismiss: "Đóng" },
207
+ },
208
+ Select: {
209
+ en: { placeholder: "Select…" },
210
+ vi: { placeholder: "Chọn…" },
211
+ },
212
+ Slider: {
213
+ en: { label: "Slider" },
214
+ vi: { label: "Thanh trượt" },
215
+ },
216
+ Steps: {
217
+ en: { label: "Steps" },
218
+ vi: { label: "Các bước" },
219
+ },
220
+ Toggle: {
221
+ en: { pressed: "Pressed", unpressed: "Not pressed" },
222
+ vi: { pressed: "Đang bật", unpressed: "Chưa bật" },
223
+ },
224
+ Menu: {
225
+ en: { menu: "Menu" },
226
+ vi: { menu: "Trình đơn" },
227
+ },
228
+ ContextMenu: {
229
+ en: { menu: "Context menu" },
230
+ vi: { menu: "Trình đơn ngữ cảnh" },
231
+ },
232
+ Tooltip: {
233
+ en: { label: "Tooltip" },
234
+ vi: { label: "Chú thích" },
235
+ },
236
+ ProgressBar: {
237
+ en: { label: "Progress" },
238
+ vi: { label: "Tiến độ" },
239
+ },
191
240
  };
@@ -15,12 +15,14 @@ const TONE = {
15
15
  */
16
16
  export function ConfidenceMeter({ value, level, segments = 5, label, lang, className }) {
17
17
  let tone, filled;
18
+ const segs = Math.max(1, Number(segments) || 5);
18
19
  if (value != null) {
19
- tone = value < 0.4 ? "low" : value < 0.75 ? "medium" : "high";
20
- filled = Math.max(1, Math.round(value * segments));
20
+ const v = Math.max(0, Math.min(1, Number(value) || 0));
21
+ tone = v < 0.4 ? "low" : v < 0.75 ? "medium" : "high";
22
+ filled = Math.max(0, Math.min(segs, Math.round(v * segs)));
21
23
  } else {
22
- tone = level || "medium";
23
- filled = Math.round((TONE[tone].fill / 5) * segments);
24
+ tone = TONE[level] ? level : "medium";
25
+ filled = Math.max(0, Math.min(segs, Math.round((TONE[tone].fill / 5) * segs)));
24
26
  }
25
27
  const meta = TONE[tone];
26
28
  const [ref, L] = useLang(lang);
@@ -33,8 +35,8 @@ export function ConfidenceMeter({ value, level, segments = 5, label, lang, class
33
35
  <span>{lbl}</span>
34
36
  <span className="cs-confidence__level" style={{ color: meta.color }}>{levelText}</span>
35
37
  </div>
36
- <div className="cs-confidence__track" role="meter" aria-valuemin={0} aria-valuemax={segments} aria-valuenow={filled} aria-label={lbl + ": " + levelText}>
37
- {Array.from({ length: segments }).map((_, i) => (
38
+ <div className="cs-confidence__track" role="meter" aria-valuemin={0} aria-valuemax={segs} aria-valuenow={filled} aria-label={lbl + ": " + levelText}>
39
+ {Array.from({ length: segs }).map((_, i) => (
38
40
  <span key={i} className="cs-confidence__seg" style={i < filled ? { background: meta.color } : undefined} />
39
41
  ))}
40
42
  </div>
@@ -1,13 +1,21 @@
1
1
  import React from "react";
2
2
  import { cx } from "../_utils/cx.js";
3
3
 
4
- /** CyberSkill Watermark — repeats rotated text behind children (draft/confidential marking). */
4
+ /** CyberSkill Watermark — repeats rotated text behind children (draft/confidential marking).
5
+ * Fill colour is read from --cs-color-text-primary at render time (data-URIs cannot resolve CSS vars). */
5
6
  export function Watermark({ text = "CyberSkill", opacity = 0.09, gap = 140, rotate = -22, children, className }) {
7
+ const host = React.useRef(null);
8
+ const [fill, setFill] = React.useState("#45210E");
9
+ React.useLayoutEffect(() => {
10
+ if (!host.current || typeof getComputedStyle === "undefined") return;
11
+ const c = getComputedStyle(host.current).getPropertyValue("--cs-color-text-primary").trim();
12
+ if (c) setFill(c);
13
+ }, []);
6
14
  const svg = encodeURIComponent(
7
- `<svg xmlns="http://www.w3.org/2000/svg" width="${gap}" height="${gap}"><text x="50%" y="50%" font-family="Be Vietnam Pro, sans-serif" font-size="14" font-weight="700" fill="#45210E" fill-opacity="${opacity}" text-anchor="middle" transform="rotate(${rotate} ${gap / 2} ${gap / 2})">${String(text).replace(/&/g, "&amp;").replace(/</g, "&lt;")}</text></svg>`
15
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${gap}" height="${gap}"><text x="50%" y="50%" font-family="Be Vietnam Pro, sans-serif" font-size="14" font-weight="700" fill="${fill}" fill-opacity="${opacity}" text-anchor="middle" transform="rotate(${rotate} ${gap / 2} ${gap / 2})">${String(text).replace(/&/g, "&amp;").replace(/</g, "&lt;")}</text></svg>`
8
16
  );
9
17
  return (
10
- <div className={cx("cs-watermark", className)} style={{ position: "relative" }}>
18
+ <div ref={host} className={cx("cs-watermark", className)} style={{ position: "relative" }}>
11
19
  {children}
12
20
  <div aria-hidden="true" style={{ position: "absolute", inset: 0, pointerEvents: "none", backgroundImage: `url("data:image/svg+xml,${svg}")` }} />
13
21
  </div>
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { makeT, useLang } from "../_i18n/i18n.js";
3
+ import { Icon } from "../icon/Icon.jsx";
3
4
  import { cx } from "../_utils/cx.js";
4
5
 
5
6
  /**
@@ -114,7 +115,15 @@ export function DataGrid({
114
115
  </button>
115
116
  ) : c.header}
116
117
  {persistKey ? (
117
- <button type="button" className="cs-datagrid__pin" title="Pin column first" onClick={() => pinColumn(c.key)} aria-label={"Pin " + c.key}>⟂</button>
118
+ <button
119
+ type="button"
120
+ className="cs-datagrid__pin"
121
+ title={t("pin")}
122
+ onClick={() => pinColumn(c.key)}
123
+ aria-label={t("pinNamed").replace("{name}", String(c.header != null ? c.header : c.key))}
124
+ >
125
+ <Icon name="pin" size="sm" />
126
+ </button>
118
127
  ) : null}
119
128
  </th>
120
129
  ))}
@@ -2,27 +2,12 @@ import React from "react";
2
2
  import { makeT, useLang } from "../_i18n/i18n.js";
3
3
  import { Icon } from "../icon/Icon.jsx";
4
4
  import { cx } from "../_utils/cx.js";
5
-
6
- const FOCUSABLE = [
7
- 'a[href]',
8
- 'area[href]',
9
- 'button:not([disabled])',
10
- 'input:not([disabled]):not([type="hidden"])',
11
- 'select:not([disabled])',
12
- 'textarea:not([disabled])',
13
- 'audio[controls]',
14
- 'video[controls]',
15
- 'summary',
16
- 'iframe',
17
- '[contenteditable]:not([contenteditable="false"])',
18
- '[tabindex]:not([tabindex="-1"])',
19
- ].join(",");
5
+ import { useOverlayLayer } from "../overlays/OverlayManager.jsx";
20
6
 
21
7
  /**
22
8
  * CyberSkill Dialog — modal overlay + panel. aria-modal, labelled by title and
23
9
  * described by its body, scrim click closes. Body scroll is locked while open
24
- * (prior overflow restored on close/unmount). Compose actions in the footer
25
- * with Buttons.
10
+ * via the overlay manager (nested-safe). Compose actions in the footer with Buttons.
26
11
  */
27
12
  export function Dialog({
28
13
  open,
@@ -40,34 +25,17 @@ export function Dialog({
40
25
  const bodyId = children == null ? undefined : baseId + "-body";
41
26
  const [ref, L] = useLang(lang);
42
27
  const panel = React.useRef(null);
43
- const closeRef = React.useRef(onClose); closeRef.current = onClose;
44
- React.useEffect(() => {
45
- if (!open) return;
46
- const prev = document.activeElement;
47
- const prevOverflow = document.body.style.overflow;
48
- document.body.style.overflow = "hidden";
49
- const focusables = () => (panel.current ? [...panel.current.querySelectorAll(FOCUSABLE)] : []);
50
- const first = focusables()[0];
51
- (first || panel.current) && (first || panel.current).focus();
52
- const k = (e) => {
53
- if (e.key === "Escape") { closeRef.current && closeRef.current(); return; }
54
- if (e.key !== "Tab" || !panel.current) return;
55
- const f = focusables();
56
- if (!f.length) { e.preventDefault(); panel.current.focus(); return; }
57
- const a = f[0], z = f[f.length - 1];
58
- const active = document.activeElement;
59
- const inside = panel.current.contains(active);
60
- // Wrap at either edge; if focus somehow escaped the panel, pull it back in.
61
- if (e.shiftKey && (!inside || active === a)) { e.preventDefault(); z.focus(); }
62
- else if (!e.shiftKey && (!inside || active === z)) { e.preventDefault(); a.focus(); }
63
- };
64
- document.addEventListener("keydown", k);
65
- return () => {
66
- document.removeEventListener("keydown", k);
67
- document.body.style.overflow = prevOverflow;
68
- if (prev && prev.focus) prev.focus();
69
- };
70
- }, [open]);
28
+ const closeRef = React.useRef(onClose);
29
+ closeRef.current = onClose;
30
+
31
+ useOverlayLayer({
32
+ open: !!open,
33
+ kind: "modal",
34
+ trapFocus: true,
35
+ onEscape: () => closeRef.current && closeRef.current(),
36
+ panelRef: panel,
37
+ });
38
+
71
39
  const cl = closeLabel != null ? closeLabel : makeT("Dialog", L)("close");
72
40
  if (!open) return null;
73
41
  return (
@@ -3,10 +3,12 @@ import { cx } from "../_utils/cx.js";
3
3
 
4
4
  /** CyberSkill ProgressBar — determinate progress. variant: ochre (default) | umber | success. */
5
5
  export function ProgressBar({ value = 0, max = 100, variant, label, className }) {
6
- const pct = Math.max(0, Math.min(100, (value / max) * 100));
6
+ const safeMax = max > 0 ? max : 100;
7
+ const clamped = Math.max(0, Math.min(safeMax, Number(value) || 0));
8
+ const pct = Math.max(0, Math.min(100, (clamped / safeMax) * 100));
7
9
  return (
8
10
  <div className={cx("cs-progress", variant && `cs-progress--${variant}`, className)}
9
- role="progressbar" aria-valuenow={Math.round(value)} aria-valuemin={0} aria-valuemax={max} aria-label={label}>
11
+ role="progressbar" aria-valuenow={Math.round(clamped)} aria-valuemin={0} aria-valuemax={safeMax} aria-label={label}>
10
12
  <span className="cs-progress__fill" style={{ inlineSize: pct + "%", width: pct + "%" }} />
11
13
  </div>
12
14
  );
@@ -15,11 +15,52 @@ export function Cascader({ nodes = [], value = [], onChange, placeholder, label,
15
15
  React.useEffect(() => {
16
16
  if (!open) return;
17
17
  const d = (e) => { if (wrap.current && !wrap.current.contains(e.target)) setOpen(false); };
18
- const k = (e) => { if (e.key === "Escape") setOpen(false); };
18
+ const k = (e) => {
19
+ if (e.key === "Escape") {
20
+ setOpen(false);
21
+ const field = wrap.current && wrap.current.querySelector(".cs-treeselect__field");
22
+ field && field.focus && field.focus();
23
+ return;
24
+ }
25
+ if (!wrap.current) return;
26
+ const opts = [...wrap.current.querySelectorAll('[role="option"]')];
27
+ if (!opts.length) return;
28
+ const active = document.activeElement;
29
+ const idx = opts.indexOf(active);
30
+ if (e.key === "ArrowDown") {
31
+ e.preventDefault();
32
+ (opts[idx < 0 ? 0 : Math.min(opts.length - 1, idx + 1)] || opts[0]).focus();
33
+ } else if (e.key === "ArrowUp") {
34
+ e.preventDefault();
35
+ (opts[idx < 0 ? opts.length - 1 : Math.max(0, idx - 1)] || opts[0]).focus();
36
+ } else if (e.key === "Home") {
37
+ e.preventDefault();
38
+ opts[0].focus();
39
+ } else if (e.key === "End") {
40
+ e.preventDefault();
41
+ opts[opts.length - 1].focus();
42
+ } else if (e.key === "ArrowRight" && idx >= 0) {
43
+ // Expand into children by activating the selected option when it has a chevron.
44
+ active.click && active.click();
45
+ } else if (e.key === "ArrowLeft") {
46
+ e.preventDefault();
47
+ if (path.length) {
48
+ const next = path.slice(0, -1);
49
+ setPath(next);
50
+ } else {
51
+ setOpen(false);
52
+ const field = wrap.current.querySelector(".cs-treeselect__field");
53
+ field && field.focus && field.focus();
54
+ }
55
+ }
56
+ };
19
57
  document.addEventListener("mousedown", d);
20
58
  document.addEventListener("keydown", k);
21
- return () => { document.removeEventListener("mousedown", d); document.removeEventListener("keydown", k); };
22
- }, [open]);
59
+ return () => {
60
+ document.removeEventListener("mousedown", d);
61
+ document.removeEventListener("keydown", k);
62
+ };
63
+ }, [open, path]);
23
64
  const cols = [];
24
65
  let level = nodes;
25
66
  for (let i = 0; level && level.length; i++) {
@@ -31,7 +72,22 @@ export function Cascader({ nodes = [], value = [], onChange, placeholder, label,
31
72
  { let lv = nodes; for (const k of value) { const n = (lv || []).find((x) => x.key === k); if (!n) break; labels.push(n.label); lv = n.children; } }
32
73
  return (
33
74
  <div ref={(el) => { wrap.current = el; ref.current = el; }} className={cx("cs-cascader", className)}>
34
- <button type="button" id={fieldId} className="cs-treeselect__field" disabled={disabled} aria-haspopup="listbox" aria-expanded={open} aria-label={label} onClick={() => setOpen((o) => !o)}>
75
+ <button
76
+ type="button"
77
+ id={fieldId}
78
+ className="cs-treeselect__field"
79
+ disabled={disabled}
80
+ aria-haspopup="listbox"
81
+ aria-expanded={open}
82
+ aria-label={label}
83
+ onClick={() => setOpen((o) => !o)}
84
+ onKeyDown={(e) => {
85
+ if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
86
+ e.preventDefault();
87
+ setOpen(true);
88
+ }
89
+ }}
90
+ >
35
91
  <span className={labels.length ? undefined : "ph"}>{labels.length ? labels.join(" / ") : ph}</span><span aria-hidden="true">▾</span>
36
92
  </button>
37
93
  {open ? (
@@ -41,12 +97,19 @@ export function Cascader({ nodes = [], value = [], onChange, placeholder, label,
41
97
  {col.map((n) => (
42
98
  // role="none" — the <li> wrapper must not sit between listbox and option.
43
99
  <li key={n.key} role="none">
44
- <button type="button" role="option" aria-selected={path[i] === n.key} className={cx("cs-cascader__opt", path[i] === n.key && "on")}
100
+ <button
101
+ type="button"
102
+ role="option"
103
+ aria-selected={path[i] === n.key}
104
+ className={cx("cs-cascader__opt", path[i] === n.key && "on")}
105
+ // Keep options in the focusable set for arrow models (E3).
106
+ data-cs-focusable=""
45
107
  onClick={() => {
46
108
  const next = [...path.slice(0, i), n.key];
47
109
  setPath(next);
48
110
  if (!(n.children && n.children.length)) { onChange && onChange(next); setOpen(false); }
49
- }}>
111
+ }}
112
+ >
50
113
  {n.label}{n.children && n.children.length ? <span aria-hidden="true"> ›</span> : null}
51
114
  </button>
52
115
  </li>
@@ -2,13 +2,39 @@ import React from "react";
2
2
  import { makeT, useLang } from "../_i18n/i18n.js";
3
3
  import { cx } from "../_utils/cx.js";
4
4
 
5
+ /** Strip script/style/event-handler markup before injecting HTML (CDS-SEC-001). */
6
+ function sanitizeHtml(html) {
7
+ if (html == null || html === "") return "";
8
+ if (typeof document === "undefined") {
9
+ return String(html)
10
+ .replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "")
11
+ .replace(/on\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi, "")
12
+ .replace(/javascript:/gi, "");
13
+ }
14
+ const tpl = document.createElement("template");
15
+ tpl.innerHTML = String(html);
16
+ tpl.content.querySelectorAll("script,style,iframe,object,embed,link").forEach((n) => n.remove());
17
+ tpl.content.querySelectorAll("*").forEach((n) => {
18
+ [...n.attributes].forEach((a) => {
19
+ if (/^on/i.test(a.name) || /javascript:/i.test(a.value)) n.removeAttribute(a.name);
20
+ });
21
+ });
22
+ return tpl.innerHTML;
23
+ }
24
+
5
25
  /** CyberSkill Editor — light rich-text (contentEditable): bold · italic · bullet list. onChange(html). */
6
26
  export function Editor({ defaultValue = "", onChange, minHeight = 120, lang, className }) {
7
27
  const box = React.useRef(null);
28
+ const seeded = React.useRef(false);
8
29
  const [ref, L] = useLang(lang);
9
30
  const t = makeT("Editor", L);
10
31
  const cmd = (c) => { document.execCommand(c); box.current && box.current.focus(); emit(); };
11
32
  const emit = () => { onChange && box.current && onChange(box.current.innerHTML); };
33
+ React.useLayoutEffect(() => {
34
+ if (!box.current || seeded.current) return;
35
+ box.current.innerHTML = sanitizeHtml(defaultValue);
36
+ seeded.current = true;
37
+ }, [defaultValue]);
12
38
  const B = ({ c, label, children }) => (
13
39
  <button type="button" className="cs-toolbar__btn" aria-label={label} onMouseDown={(e) => { e.preventDefault(); cmd(c); }}>{children}</button>
14
40
  );
@@ -19,8 +45,17 @@ export function Editor({ defaultValue = "", onChange, minHeight = 120, lang, cla
19
45
  <B c="italic" label={t("italic")}><i>I</i></B>
20
46
  <B c="insertUnorderedList" label={t("list")}>≔</B>
21
47
  </div>
22
- <div ref={box} className="cs-editor__area" contentEditable suppressContentEditableWarning role="textbox" aria-multiline="true" aria-label={t("area")}
23
- style={{ minHeight }} onInput={emit} dangerouslySetInnerHTML={{ __html: defaultValue }} />
48
+ <div
49
+ ref={box}
50
+ className="cs-editor__area"
51
+ contentEditable
52
+ suppressContentEditableWarning
53
+ role="textbox"
54
+ aria-multiline="true"
55
+ aria-label={t("area")}
56
+ style={{ minHeight }}
57
+ onInput={emit}
58
+ />
24
59
  </div>
25
60
  );
26
61
  }
@@ -2,28 +2,42 @@ import React from "react";
2
2
  import { makeT, useLang } from "../_i18n/i18n.js";
3
3
  import { cx } from "../_utils/cx.js";
4
4
 
5
- /** CyberSkill FileUpload — click-or-drag dropzone. Calls onFiles(File[]) on select/drop. */
5
+ /** CyberSkill FileUpload — click-or-drag dropzone. Calls onFiles(File[]) on select/drop.
6
+ * Uses a single labeled <input type="file"> (no nested interactive) for axe name/role. */
6
7
  export function FileUpload({ title, hint, accept, multiple = false, onFiles, icon, lang, className }) {
7
8
  const [drag, setDrag] = React.useState(false);
9
+ const inputRef = React.useRef(null);
8
10
  const pick = (files) => { if (files && files.length && onFiles) onFiles(Array.from(files)); };
9
11
  const [ref, L] = useLang(lang);
10
12
  const t = makeT("FileUpload", L);
11
13
  const tt = title != null ? title : t("title");
12
14
  const hh = hint != null ? hint : t("hint");
15
+ const id = React.useId();
13
16
  return (
14
- <label
17
+ <div
15
18
  ref={ref}
16
19
  className={cx("cs-dropzone", drag && "is-dragging", className)}
17
20
  onDragOver={(e) => { e.preventDefault(); setDrag(true); }}
18
21
  onDragLeave={() => setDrag(false)}
19
22
  onDrop={(e) => { e.preventDefault(); setDrag(false); pick(e.dataTransfer.files); }}
23
+ onClick={() => inputRef.current && inputRef.current.click()}
20
24
  >
21
- <span className="cs-dropzone__icon">
22
- {icon ?? <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d="M12 16V4M7 9l5-5 5 5" /><path d="M5 16v3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3" /></svg>}
25
+ <span className="cs-dropzone__icon" aria-hidden="true">
26
+ {icon ?? <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M12 16V4M7 9l5-5 5 5" /><path d="M5 16v3a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-3" /></svg>}
23
27
  </span>
24
- <span className="cs-dropzone__title">{tt}</span>
25
- <span className="cs-dropzone__hint">{hh}</span>
26
- <input type="file" accept={accept} multiple={multiple} style={{ display: "none" }} onChange={(e) => pick(e.target.files)} />
27
- </label>
28
+ <label className="cs-dropzone__title" htmlFor={id}>{tt}</label>
29
+ <span className="cs-dropzone__hint" id={id + "-hint"}>{hh}</span>
30
+ <input
31
+ id={id}
32
+ ref={inputRef}
33
+ type="file"
34
+ accept={accept}
35
+ multiple={multiple}
36
+ className="cs-sr-only"
37
+ aria-describedby={id + "-hint"}
38
+ onChange={(e) => pick(e.target.files)}
39
+ onClick={(e) => e.stopPropagation()}
40
+ />
41
+ </div>
28
42
  );
29
43
  }
@@ -1,12 +1,26 @@
1
1
  import React from "react";
2
2
  import { cx } from "../_utils/cx.js";
3
3
 
4
+ let warnedNameless = false;
5
+
4
6
  /** CyberSkill Switch — accessible toggle (role="switch"); Umber on, Ochre knob-track in dark. */
5
7
  // `children` is destructured but never rendered on purpose: keeps stray children out of {...props} → void <input>.
6
- export function Switch({ label, disabled = false, className, children, ...props }) {
8
+ export function Switch({ label, disabled = false, className, children, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, ...props }) {
9
+ const named = !!(label || ariaLabel || ariaLabelledby);
10
+ if (!named && !warnedNameless && typeof console !== "undefined" && console.warn) {
11
+ warnedNameless = true;
12
+ console.warn("CyberSkill Switch: render with label, aria-label, or aria-labelledby.");
13
+ }
7
14
  return (
8
15
  <label className={cx("cs-switch", disabled && "is-disabled", className)}>
9
- <input type="checkbox" role="switch" disabled={disabled} {...props} />
16
+ <input
17
+ type="checkbox"
18
+ role="switch"
19
+ disabled={disabled}
20
+ aria-label={ariaLabel}
21
+ aria-labelledby={ariaLabelledby}
22
+ {...props}
23
+ />
10
24
  <span className="cs-switch__track" aria-hidden="true" />
11
25
  {label ? <span className="cs-switch__label">{label}</span> : null}
12
26
  </label>
@@ -7,8 +7,10 @@ const CLOCK = <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke
7
7
  export function TimePicker({ value = "09:00", onChange, step = 30, label, disabled = false, lang, className }) {
8
8
  const [ref, L] = useLang(lang);
9
9
  const t = makeT("TimePicker", L);
10
+ const stepMin = Number(step);
11
+ const safeStep = Number.isFinite(stepMin) && stepMin > 0 ? stepMin : 30;
10
12
  const opts = [];
11
- for (let m = 0; m < 24 * 60; m += step) {
13
+ for (let m = 0; m < 24 * 60; m += safeStep) {
12
14
  opts.push(String(Math.floor(m / 60)).padStart(2, "0") + ":" + String(m % 60).padStart(2, "0"));
13
15
  }
14
16
  return (
@@ -49,6 +49,7 @@ export const CS_ICONS = {
49
49
  copy: { viewBox: "0 0 24 24", els: [["rect", { x: 8, y: 8, width: 12, height: 12, rx: 2 }], ["path", { d: "M4 16V6a2 2 0 0 1 2-2h10" }]] },
50
50
  info: { viewBox: "0 0 24 24", els: [["circle", { cx: 12, cy: 12, r: 9 }], ["path", { d: "M12 11v5M12 7.75v.01" }]] },
51
51
  "alert-triangle": { viewBox: "0 0 24 24", els: [["path", { d: "M12 3.5l9 16H3z" }], ["path", { d: "M12 10v4M12 17.5v.01" }]] },
52
+ pin: { viewBox: "0 0 24 24", els: [["path", { d: "M12 21v-7" }], ["path", { d: "M8.5 3.5h7l-1.2 6.5h2.7L12 14.5 6.999 10h2.7z" }]] },
52
53
  };
53
54
 
54
55
  export function Icon({ name, size = "md", label, className, strokeWidth = 1.75, ...props }) {
@@ -1,23 +1,38 @@
1
1
  import React from "react";
2
2
  import { makeT, useLang } from "../_i18n/i18n.js";
3
3
  import { cx } from "../_utils/cx.js";
4
+ import { useOverlayLayer } from "../overlays/OverlayManager.jsx";
4
5
 
5
6
  /** CyberSkill CommandPalette — ⌘K overlay with search + grouped actions. Controlled via open/onClose. */
6
7
  export function CommandPalette({ open, onClose, placeholder, groups = [], lang, className }) {
7
8
  const [q, setQ] = React.useState("");
8
- React.useEffect(() => {
9
- if (!open) return;
10
- const onKey = (e) => { if (e.key === "Escape") onClose && onClose(); };
11
- document.addEventListener("keydown", onKey);
12
- return () => document.removeEventListener("keydown", onKey);
13
- }, [open]);
9
+ const panel = React.useRef(null);
10
+ const closeRef = React.useRef(onClose);
11
+ closeRef.current = onClose;
12
+
13
+ useOverlayLayer({
14
+ open: !!open,
15
+ kind: "modal",
16
+ trapFocus: true,
17
+ preferFocusSelector: ".cs-cmdk__search input",
18
+ onEscape: () => closeRef.current && closeRef.current(),
19
+ panelRef: panel,
20
+ });
21
+
14
22
  const [ref, L] = useLang(lang);
15
23
  const t = makeT("CommandPalette", L);
16
24
  const ph = placeholder != null ? placeholder : t("placeholder");
17
25
  if (!open) return null;
18
26
  const needle = q.trim().toLowerCase();
19
27
  return (
20
- <div ref={ref} className="cs-cmdk-scrim" onClick={onClose}>
28
+ <div
29
+ ref={(el) => {
30
+ ref.current = el;
31
+ panel.current = el;
32
+ }}
33
+ className="cs-cmdk-scrim"
34
+ onClick={onClose}
35
+ >
21
36
  <div className={cx("cs-cmdk", className)} role="dialog" aria-modal="true" aria-label={t("aria")} onClick={(e) => e.stopPropagation()}>
22
37
  <div className="cs-cmdk__search">
23
38
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7" /><path d="M21 21l-4.3-4.3" /></svg>