@cyberskill/design 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (970) hide show
  1. package/DESIGN.md +267 -0
  2. package/README.md +316 -0
  3. package/SKILL.md +64 -0
  4. package/VERSION +1 -0
  5. package/_ds_bundle.js +5377 -0
  6. package/_ds_manifest.json +1 -0
  7. package/_esm/cs.mjs +137 -0
  8. package/assets/aurora-gold.jpg +0 -0
  9. package/assets/aurora-hoa.png +0 -0
  10. package/assets/aurora-kim.png +0 -0
  11. package/assets/aurora-moc.png +0 -0
  12. package/assets/aurora-thuy.png +0 -0
  13. package/assets/favicon.svg +1 -0
  14. package/assets/logo-mark.png +0 -0
  15. package/assets/logo-mark.svg +10 -0
  16. package/assets/lumi-poster.webp +0 -0
  17. package/assets/og-dashboard.png +0 -0
  18. package/base/a11y.css +57 -0
  19. package/base/ai.css +67 -0
  20. package/base/collections.css +53 -0
  21. package/base/components.css +212 -0
  22. package/base/controls.css +329 -0
  23. package/base/data.css +48 -0
  24. package/base/feedback.css +60 -0
  25. package/base/forms.css +32 -0
  26. package/base/glass.css +119 -0
  27. package/base/interaction.css +50 -0
  28. package/base/navigation.css +39 -0
  29. package/base/reset.css +17 -0
  30. package/base/responsive.css +128 -0
  31. package/base/shell.css +55 -0
  32. package/base/typography.css +14 -0
  33. package/components/_i18n/i18n.js +56 -0
  34. package/components/_i18n/strings.js +187 -0
  35. package/components/_utils/cx.js +4 -0
  36. package/components/ai/AIDisclosureBadge.d.ts +16 -0
  37. package/components/ai/AIDisclosureBadge.figma.tsx +12 -0
  38. package/components/ai/AIDisclosureBadge.jsx +39 -0
  39. package/components/ai/AIDisclosureBadge.prompt.md +9 -0
  40. package/components/ai/ChatMessage.d.ts +17 -0
  41. package/components/ai/ChatMessage.figma.tsx +12 -0
  42. package/components/ai/ChatMessage.jsx +22 -0
  43. package/components/ai/ChatMessage.prompt.md +8 -0
  44. package/components/ai/CitationList.d.ts +13 -0
  45. package/components/ai/CitationList.figma.tsx +12 -0
  46. package/components/ai/CitationList.jsx +22 -0
  47. package/components/ai/CitationList.prompt.md +5 -0
  48. package/components/ai/ConfidenceMeter.d.ts +17 -0
  49. package/components/ai/ConfidenceMeter.figma.tsx +12 -0
  50. package/components/ai/ConfidenceMeter.jsx +43 -0
  51. package/components/ai/ConfidenceMeter.prompt.md +6 -0
  52. package/components/ai/HumanReviewGate.d.ts +20 -0
  53. package/components/ai/HumanReviewGate.figma.tsx +12 -0
  54. package/components/ai/HumanReviewGate.jsx +37 -0
  55. package/components/ai/HumanReviewGate.prompt.md +13 -0
  56. package/components/ai/PromptInput.d.ts +21 -0
  57. package/components/ai/PromptInput.figma.tsx +12 -0
  58. package/components/ai/PromptInput.jsx +48 -0
  59. package/components/ai/PromptInput.prompt.md +5 -0
  60. package/components/ai/PromptSuggestions.d.ts +12 -0
  61. package/components/ai/PromptSuggestions.figma.tsx +12 -0
  62. package/components/ai/PromptSuggestions.jsx +20 -0
  63. package/components/ai/PromptSuggestions.prompt.md +5 -0
  64. package/components/ai/TypingIndicator.d.ts +9 -0
  65. package/components/ai/TypingIndicator.figma.tsx +12 -0
  66. package/components/ai/TypingIndicator.jsx +14 -0
  67. package/components/ai/TypingIndicator.prompt.md +5 -0
  68. package/components/ai/ai-conversation.card.html +49 -0
  69. package/components/ai/ai-lumi.card.html +39 -0
  70. package/components/ai/ai.card.html +43 -0
  71. package/components/brand/LumiAvatar.d.ts +14 -0
  72. package/components/brand/LumiAvatar.figma.tsx +12 -0
  73. package/components/brand/LumiAvatar.jsx +11 -0
  74. package/components/brand/LumiAvatar.prompt.md +5 -0
  75. package/components/brand/brand.card.html +37 -0
  76. package/components/button/Button.d.ts +24 -0
  77. package/components/button/Button.figma.tsx +31 -0
  78. package/components/button/Button.jsx +42 -0
  79. package/components/button/Button.prompt.md +12 -0
  80. package/components/button/ButtonGroup.d.ts +11 -0
  81. package/components/button/ButtonGroup.figma.tsx +12 -0
  82. package/components/button/ButtonGroup.jsx +7 -0
  83. package/components/button/ButtonGroup.prompt.md +8 -0
  84. package/components/button/FloatingActionButton.d.ts +15 -0
  85. package/components/button/FloatingActionButton.figma.tsx +12 -0
  86. package/components/button/FloatingActionButton.jsx +20 -0
  87. package/components/button/FloatingActionButton.prompt.md +6 -0
  88. package/components/button/buttons.card.html +52 -0
  89. package/components/data/Accordion.d.ts +14 -0
  90. package/components/data/Accordion.figma.tsx +12 -0
  91. package/components/data/Accordion.jsx +25 -0
  92. package/components/data/Accordion.prompt.md +5 -0
  93. package/components/data/Avatar.d.ts +18 -0
  94. package/components/data/Avatar.figma.tsx +12 -0
  95. package/components/data/Avatar.jsx +17 -0
  96. package/components/data/Avatar.prompt.md +6 -0
  97. package/components/data/Card.d.ts +23 -0
  98. package/components/data/Card.figma.tsx +12 -0
  99. package/components/data/Card.jsx +27 -0
  100. package/components/data/Card.prompt.md +9 -0
  101. package/components/data/Carousel.d.ts +13 -0
  102. package/components/data/Carousel.figma.tsx +12 -0
  103. package/components/data/Carousel.jsx +26 -0
  104. package/components/data/Carousel.prompt.md +5 -0
  105. package/components/data/Chart.d.ts +17 -0
  106. package/components/data/Chart.figma.tsx +12 -0
  107. package/components/data/Chart.jsx +52 -0
  108. package/components/data/Chart.prompt.md +5 -0
  109. package/components/data/CodeBlock.d.ts +12 -0
  110. package/components/data/CodeBlock.figma.tsx +12 -0
  111. package/components/data/CodeBlock.jsx +21 -0
  112. package/components/data/CodeBlock.prompt.md +5 -0
  113. package/components/data/Comment.d.ts +18 -0
  114. package/components/data/Comment.figma.tsx +12 -0
  115. package/components/data/Comment.jsx +24 -0
  116. package/components/data/Comment.prompt.md +7 -0
  117. package/components/data/DescriptionList.d.ts +10 -0
  118. package/components/data/DescriptionList.figma.tsx +12 -0
  119. package/components/data/DescriptionList.jsx +16 -0
  120. package/components/data/DescriptionList.prompt.md +5 -0
  121. package/components/data/Divider.d.ts +13 -0
  122. package/components/data/Divider.figma.tsx +12 -0
  123. package/components/data/Divider.jsx +11 -0
  124. package/components/data/Divider.prompt.md +6 -0
  125. package/components/data/Image.d.ts +18 -0
  126. package/components/data/Image.figma.tsx +12 -0
  127. package/components/data/Image.jsx +37 -0
  128. package/components/data/Image.prompt.md +5 -0
  129. package/components/data/Kbd.d.ts +8 -0
  130. package/components/data/Kbd.figma.tsx +12 -0
  131. package/components/data/Kbd.jsx +7 -0
  132. package/components/data/Kbd.prompt.md +5 -0
  133. package/components/data/List.d.ts +16 -0
  134. package/components/data/List.figma.tsx +12 -0
  135. package/components/data/List.jsx +24 -0
  136. package/components/data/List.prompt.md +7 -0
  137. package/components/data/Masonry.d.ts +13 -0
  138. package/components/data/Masonry.figma.tsx +12 -0
  139. package/components/data/Masonry.jsx +11 -0
  140. package/components/data/Masonry.prompt.md +5 -0
  141. package/components/data/QRCode.d.ts +12 -0
  142. package/components/data/QRCode.figma.tsx +12 -0
  143. package/components/data/QRCode.jsx +17 -0
  144. package/components/data/QRCode.prompt.md +5 -0
  145. package/components/data/Sortable.d.ts +10 -0
  146. package/components/data/Sortable.figma.tsx +12 -0
  147. package/components/data/Sortable.jsx +30 -0
  148. package/components/data/Sortable.prompt.md +5 -0
  149. package/components/data/Splitter.d.ts +16 -0
  150. package/components/data/Splitter.figma.tsx +12 -0
  151. package/components/data/Splitter.jsx +31 -0
  152. package/components/data/Splitter.prompt.md +5 -0
  153. package/components/data/Stat.d.ts +15 -0
  154. package/components/data/Stat.figma.tsx +12 -0
  155. package/components/data/Stat.jsx +19 -0
  156. package/components/data/Stat.prompt.md +5 -0
  157. package/components/data/Terminal.d.ts +13 -0
  158. package/components/data/Terminal.figma.tsx +12 -0
  159. package/components/data/Terminal.jsx +32 -0
  160. package/components/data/Terminal.prompt.md +5 -0
  161. package/components/data/Timeline.d.ts +17 -0
  162. package/components/data/Timeline.figma.tsx +12 -0
  163. package/components/data/Timeline.jsx +20 -0
  164. package/components/data/Timeline.prompt.md +8 -0
  165. package/components/data/Tooltip.d.ts +14 -0
  166. package/components/data/Tooltip.figma.tsx +12 -0
  167. package/components/data/Tooltip.jsx +12 -0
  168. package/components/data/Tooltip.prompt.md +5 -0
  169. package/components/data/Tree.d.ts +13 -0
  170. package/components/data/Tree.figma.tsx +12 -0
  171. package/components/data/Tree.jsx +23 -0
  172. package/components/data/Tree.prompt.md +5 -0
  173. package/components/data/Watermark.d.ts +15 -0
  174. package/components/data/Watermark.figma.tsx +12 -0
  175. package/components/data/Watermark.jsx +15 -0
  176. package/components/data/Watermark.prompt.md +5 -0
  177. package/components/data/collections.card.html +52 -0
  178. package/components/data/data.card.html +50 -0
  179. package/components/data/qr-encode.js +80 -0
  180. package/components/datatable/DataGrid.d.ts +42 -0
  181. package/components/datatable/DataGrid.figma.tsx +12 -0
  182. package/components/datatable/DataGrid.jsx +149 -0
  183. package/components/datatable/DataGrid.prompt.md +12 -0
  184. package/components/datatable/DataTable.d.ts +25 -0
  185. package/components/datatable/DataTable.figma.tsx +12 -0
  186. package/components/datatable/DataTable.jsx +42 -0
  187. package/components/datatable/DataTable.prompt.md +15 -0
  188. package/components/datatable/TreeTable.d.ts +14 -0
  189. package/components/datatable/TreeTable.figma.tsx +12 -0
  190. package/components/datatable/TreeTable.jsx +47 -0
  191. package/components/datatable/TreeTable.prompt.md +6 -0
  192. package/components/datatable/datatable.card.html +51 -0
  193. package/components/dialog/Dialog.d.ts +22 -0
  194. package/components/dialog/Dialog.figma.tsx +12 -0
  195. package/components/dialog/Dialog.jsx +99 -0
  196. package/components/dialog/Dialog.prompt.md +17 -0
  197. package/components/dialog/dialog.card.html +46 -0
  198. package/components/feedback/Alert.d.ts +16 -0
  199. package/components/feedback/Alert.figma.tsx +17 -0
  200. package/components/feedback/Alert.jsx +29 -0
  201. package/components/feedback/Alert.prompt.md +6 -0
  202. package/components/feedback/Badge.d.ts +15 -0
  203. package/components/feedback/Badge.figma.tsx +12 -0
  204. package/components/feedback/Badge.jsx +12 -0
  205. package/components/feedback/Badge.prompt.md +6 -0
  206. package/components/feedback/EmptyState.d.ts +13 -0
  207. package/components/feedback/EmptyState.figma.tsx +12 -0
  208. package/components/feedback/EmptyState.jsx +16 -0
  209. package/components/feedback/EmptyState.prompt.md +7 -0
  210. package/components/feedback/ProgressBar.d.ts +15 -0
  211. package/components/feedback/ProgressBar.figma.tsx +12 -0
  212. package/components/feedback/ProgressBar.jsx +13 -0
  213. package/components/feedback/ProgressBar.prompt.md +5 -0
  214. package/components/feedback/Result.d.ts +15 -0
  215. package/components/feedback/Result.figma.tsx +12 -0
  216. package/components/feedback/Result.jsx +24 -0
  217. package/components/feedback/Result.prompt.md +7 -0
  218. package/components/feedback/Skeleton.d.ts +18 -0
  219. package/components/feedback/Skeleton.figma.tsx +12 -0
  220. package/components/feedback/Skeleton.jsx +22 -0
  221. package/components/feedback/Skeleton.prompt.md +6 -0
  222. package/components/feedback/Spinner.d.ts +11 -0
  223. package/components/feedback/Spinner.figma.tsx +12 -0
  224. package/components/feedback/Spinner.jsx +10 -0
  225. package/components/feedback/Spinner.prompt.md +5 -0
  226. package/components/feedback/StatusIndicator.d.ts +11 -0
  227. package/components/feedback/StatusIndicator.figma.tsx +12 -0
  228. package/components/feedback/StatusIndicator.jsx +12 -0
  229. package/components/feedback/StatusIndicator.prompt.md +5 -0
  230. package/components/feedback/Tag.d.ts +14 -0
  231. package/components/feedback/Tag.figma.tsx +12 -0
  232. package/components/feedback/Tag.jsx +19 -0
  233. package/components/feedback/Tag.prompt.md +6 -0
  234. package/components/feedback/Toast.d.ts +19 -0
  235. package/components/feedback/Toast.figma.tsx +12 -0
  236. package/components/feedback/Toast.jsx +30 -0
  237. package/components/feedback/Toast.prompt.md +7 -0
  238. package/components/feedback/feedback-2.card.html +43 -0
  239. package/components/feedback/feedback.card.html +50 -0
  240. package/components/forms/Calendar.d.ts +10 -0
  241. package/components/forms/Calendar.figma.tsx +12 -0
  242. package/components/forms/Calendar.jsx +43 -0
  243. package/components/forms/Calendar.prompt.md +5 -0
  244. package/components/forms/Cascader.d.ts +17 -0
  245. package/components/forms/Cascader.figma.tsx +12 -0
  246. package/components/forms/Cascader.jsx +58 -0
  247. package/components/forms/Cascader.prompt.md +5 -0
  248. package/components/forms/Checkbox.d.ts +13 -0
  249. package/components/forms/Checkbox.figma.tsx +12 -0
  250. package/components/forms/Checkbox.jsx +16 -0
  251. package/components/forms/Checkbox.prompt.md +6 -0
  252. package/components/forms/ColorPicker.d.ts +12 -0
  253. package/components/forms/ColorPicker.figma.tsx +12 -0
  254. package/components/forms/ColorPicker.jsx +44 -0
  255. package/components/forms/ColorPicker.prompt.md +5 -0
  256. package/components/forms/Combobox.d.ts +17 -0
  257. package/components/forms/Combobox.figma.tsx +12 -0
  258. package/components/forms/Combobox.jsx +49 -0
  259. package/components/forms/Combobox.prompt.md +6 -0
  260. package/components/forms/DatePicker.d.ts +15 -0
  261. package/components/forms/DatePicker.figma.tsx +12 -0
  262. package/components/forms/DatePicker.jsx +35 -0
  263. package/components/forms/DatePicker.prompt.md +5 -0
  264. package/components/forms/Editor.d.ts +12 -0
  265. package/components/forms/Editor.figma.tsx +12 -0
  266. package/components/forms/Editor.jsx +26 -0
  267. package/components/forms/Editor.prompt.md +5 -0
  268. package/components/forms/FileUpload.d.ts +14 -0
  269. package/components/forms/FileUpload.figma.tsx +12 -0
  270. package/components/forms/FileUpload.jsx +29 -0
  271. package/components/forms/FileUpload.prompt.md +5 -0
  272. package/components/forms/Form.d.ts +87 -0
  273. package/components/forms/Form.figma.tsx +12 -0
  274. package/components/forms/Form.jsx +316 -0
  275. package/components/forms/Form.prompt.md +46 -0
  276. package/components/forms/InlineEdit.d.ts +13 -0
  277. package/components/forms/InlineEdit.figma.tsx +12 -0
  278. package/components/forms/InlineEdit.jsx +27 -0
  279. package/components/forms/InlineEdit.prompt.md +5 -0
  280. package/components/forms/InputGroup.d.ts +20 -0
  281. package/components/forms/InputGroup.figma.tsx +12 -0
  282. package/components/forms/InputGroup.jsx +25 -0
  283. package/components/forms/InputGroup.prompt.md +6 -0
  284. package/components/forms/InputOTP.d.ts +16 -0
  285. package/components/forms/InputOTP.figma.tsx +12 -0
  286. package/components/forms/InputOTP.jsx +47 -0
  287. package/components/forms/InputOTP.prompt.md +5 -0
  288. package/components/forms/Mentions.d.ts +13 -0
  289. package/components/forms/Mentions.figma.tsx +12 -0
  290. package/components/forms/Mentions.jsx +31 -0
  291. package/components/forms/Mentions.prompt.md +5 -0
  292. package/components/forms/NumberField.d.ts +14 -0
  293. package/components/forms/NumberField.figma.tsx +12 -0
  294. package/components/forms/NumberField.jsx +21 -0
  295. package/components/forms/NumberField.prompt.md +5 -0
  296. package/components/forms/RadioGroup.d.ts +24 -0
  297. package/components/forms/RadioGroup.figma.tsx +12 -0
  298. package/components/forms/RadioGroup.jsx +32 -0
  299. package/components/forms/RadioGroup.prompt.md +6 -0
  300. package/components/forms/Rating.d.ts +16 -0
  301. package/components/forms/Rating.figma.tsx +12 -0
  302. package/components/forms/Rating.jsx +22 -0
  303. package/components/forms/Rating.prompt.md +6 -0
  304. package/components/forms/SearchField.d.ts +11 -0
  305. package/components/forms/SearchField.figma.tsx +12 -0
  306. package/components/forms/SearchField.jsx +25 -0
  307. package/components/forms/SearchField.prompt.md +5 -0
  308. package/components/forms/SegmentedControl.d.ts +13 -0
  309. package/components/forms/SegmentedControl.figma.tsx +12 -0
  310. package/components/forms/SegmentedControl.jsx +31 -0
  311. package/components/forms/SegmentedControl.prompt.md +6 -0
  312. package/components/forms/Select.d.ts +19 -0
  313. package/components/forms/Select.figma.tsx +12 -0
  314. package/components/forms/Select.jsx +27 -0
  315. package/components/forms/Select.prompt.md +6 -0
  316. package/components/forms/Slider.d.ts +6 -0
  317. package/components/forms/Slider.figma.tsx +12 -0
  318. package/components/forms/Slider.jsx +8 -0
  319. package/components/forms/Slider.prompt.md +5 -0
  320. package/components/forms/Switch.d.ts +12 -0
  321. package/components/forms/Switch.figma.tsx +12 -0
  322. package/components/forms/Switch.jsx +14 -0
  323. package/components/forms/Switch.prompt.md +5 -0
  324. package/components/forms/TagInput.d.ts +16 -0
  325. package/components/forms/TagInput.figma.tsx +12 -0
  326. package/components/forms/TagInput.jsx +39 -0
  327. package/components/forms/TagInput.prompt.md +5 -0
  328. package/components/forms/Textarea.d.ts +14 -0
  329. package/components/forms/Textarea.figma.tsx +12 -0
  330. package/components/forms/Textarea.jsx +19 -0
  331. package/components/forms/Textarea.prompt.md +5 -0
  332. package/components/forms/TimePicker.d.ts +16 -0
  333. package/components/forms/TimePicker.figma.tsx +12 -0
  334. package/components/forms/TimePicker.jsx +22 -0
  335. package/components/forms/TimePicker.prompt.md +5 -0
  336. package/components/forms/Toggle.d.ts +16 -0
  337. package/components/forms/Toggle.figma.tsx +12 -0
  338. package/components/forms/Toggle.jsx +15 -0
  339. package/components/forms/Toggle.prompt.md +6 -0
  340. package/components/forms/Transfer.d.ts +14 -0
  341. package/components/forms/Transfer.figma.tsx +12 -0
  342. package/components/forms/Transfer.jsx +41 -0
  343. package/components/forms/Transfer.prompt.md +5 -0
  344. package/components/forms/TreeSelect.d.ts +15 -0
  345. package/components/forms/TreeSelect.figma.tsx +12 -0
  346. package/components/forms/TreeSelect.jsx +35 -0
  347. package/components/forms/TreeSelect.prompt.md +5 -0
  348. package/components/forms/forms-2.card.html +40 -0
  349. package/components/forms/forms.card.html +40 -0
  350. package/components/icon/Icon.d.ts +26 -0
  351. package/components/icon/Icon.figma.tsx +12 -0
  352. package/components/icon/Icon.jsx +74 -0
  353. package/components/icon/Icon.prompt.md +8 -0
  354. package/components/icon/icon.card.html +38 -0
  355. package/components/logo/Logo.d.ts +18 -0
  356. package/components/logo/Logo.figma.tsx +12 -0
  357. package/components/logo/Logo.jsx +25 -0
  358. package/components/logo/Logo.prompt.md +10 -0
  359. package/components/logo/logo-data.js +214 -0
  360. package/components/logo/logo.card.html +44 -0
  361. package/components/navigation/Anchor.d.ts +11 -0
  362. package/components/navigation/Anchor.figma.tsx +12 -0
  363. package/components/navigation/Anchor.jsx +25 -0
  364. package/components/navigation/Anchor.prompt.md +5 -0
  365. package/components/navigation/BackTop.d.ts +9 -0
  366. package/components/navigation/BackTop.figma.tsx +12 -0
  367. package/components/navigation/BackTop.jsx +23 -0
  368. package/components/navigation/BackTop.prompt.md +5 -0
  369. package/components/navigation/Breadcrumb.d.ts +14 -0
  370. package/components/navigation/Breadcrumb.figma.tsx +12 -0
  371. package/components/navigation/Breadcrumb.jsx +24 -0
  372. package/components/navigation/Breadcrumb.prompt.md +5 -0
  373. package/components/navigation/CommandPalette.d.ts +19 -0
  374. package/components/navigation/CommandPalette.figma.tsx +12 -0
  375. package/components/navigation/CommandPalette.jsx +48 -0
  376. package/components/navigation/CommandPalette.prompt.md +7 -0
  377. package/components/navigation/Dock.d.ts +6 -0
  378. package/components/navigation/Dock.figma.tsx +12 -0
  379. package/components/navigation/Dock.jsx +22 -0
  380. package/components/navigation/Dock.prompt.md +5 -0
  381. package/components/navigation/HotKeys.d.ts +13 -0
  382. package/components/navigation/HotKeys.figma.tsx +12 -0
  383. package/components/navigation/HotKeys.jsx +44 -0
  384. package/components/navigation/HotKeys.prompt.md +5 -0
  385. package/components/navigation/Link.d.ts +12 -0
  386. package/components/navigation/Link.figma.tsx +12 -0
  387. package/components/navigation/Link.jsx +13 -0
  388. package/components/navigation/Link.prompt.md +5 -0
  389. package/components/navigation/Menu.d.ts +23 -0
  390. package/components/navigation/Menu.figma.tsx +12 -0
  391. package/components/navigation/Menu.jsx +36 -0
  392. package/components/navigation/Menu.prompt.md +9 -0
  393. package/components/navigation/Menubar.d.ts +7 -0
  394. package/components/navigation/Menubar.figma.tsx +12 -0
  395. package/components/navigation/Menubar.jsx +48 -0
  396. package/components/navigation/Menubar.prompt.md +5 -0
  397. package/components/navigation/NavigationMenu.d.ts +7 -0
  398. package/components/navigation/NavigationMenu.figma.tsx +12 -0
  399. package/components/navigation/NavigationMenu.jsx +38 -0
  400. package/components/navigation/NavigationMenu.prompt.md +5 -0
  401. package/components/navigation/Pagination.d.ts +13 -0
  402. package/components/navigation/Pagination.figma.tsx +12 -0
  403. package/components/navigation/Pagination.jsx +32 -0
  404. package/components/navigation/Pagination.prompt.md +5 -0
  405. package/components/navigation/Sidebar.d.ts +20 -0
  406. package/components/navigation/Sidebar.figma.tsx +12 -0
  407. package/components/navigation/Sidebar.jsx +24 -0
  408. package/components/navigation/Sidebar.prompt.md +8 -0
  409. package/components/navigation/Steps.d.ts +13 -0
  410. package/components/navigation/Steps.figma.tsx +12 -0
  411. package/components/navigation/Steps.jsx +20 -0
  412. package/components/navigation/Steps.prompt.md +5 -0
  413. package/components/navigation/Tabs.d.ts +21 -0
  414. package/components/navigation/Tabs.figma.tsx +12 -0
  415. package/components/navigation/Tabs.jsx +41 -0
  416. package/components/navigation/Tabs.prompt.md +6 -0
  417. package/components/navigation/Toolbar.d.ts +16 -0
  418. package/components/navigation/Toolbar.figma.tsx +12 -0
  419. package/components/navigation/Toolbar.jsx +32 -0
  420. package/components/navigation/Toolbar.prompt.md +5 -0
  421. package/components/navigation/navigation.card.html +51 -0
  422. package/components/navigation/shell.card.html +52 -0
  423. package/components/overlays/ContextMenu.d.ts +10 -0
  424. package/components/overlays/ContextMenu.figma.tsx +12 -0
  425. package/components/overlays/ContextMenu.jsx +28 -0
  426. package/components/overlays/ContextMenu.prompt.md +7 -0
  427. package/components/overlays/Drawer.d.ts +18 -0
  428. package/components/overlays/Drawer.figma.tsx +12 -0
  429. package/components/overlays/Drawer.jsx +44 -0
  430. package/components/overlays/Drawer.prompt.md +8 -0
  431. package/components/overlays/HoverCard.d.ts +13 -0
  432. package/components/overlays/HoverCard.figma.tsx +12 -0
  433. package/components/overlays/HoverCard.jsx +17 -0
  434. package/components/overlays/HoverCard.prompt.md +5 -0
  435. package/components/overlays/Popconfirm.d.ts +18 -0
  436. package/components/overlays/Popconfirm.figma.tsx +12 -0
  437. package/components/overlays/Popconfirm.jsx +33 -0
  438. package/components/overlays/Popconfirm.prompt.md +6 -0
  439. package/components/overlays/Popover.d.ts +17 -0
  440. package/components/overlays/Popover.figma.tsx +12 -0
  441. package/components/overlays/Popover.jsx +24 -0
  442. package/components/overlays/Popover.prompt.md +7 -0
  443. package/components/overlays/Tour.d.ts +13 -0
  444. package/components/overlays/Tour.figma.tsx +12 -0
  445. package/components/overlays/Tour.jsx +46 -0
  446. package/components/overlays/Tour.prompt.md +5 -0
  447. package/components/overlays/overlays.card.html +50 -0
  448. package/components/textfield/TextField.d.ts +18 -0
  449. package/components/textfield/TextField.figma.tsx +14 -0
  450. package/components/textfield/TextField.jsx +44 -0
  451. package/components/textfield/TextField.prompt.md +11 -0
  452. package/components/textfield/textfield.card.html +34 -0
  453. package/docs/ci-cd.md +84 -0
  454. package/docs/consuming.md +109 -0
  455. package/docs/contrast-report.md +125 -0
  456. package/docs/conventions.md +114 -0
  457. package/docs/decisions.md +94 -0
  458. package/docs/deploy.md +110 -0
  459. package/docs/design-styles.md +25 -0
  460. package/docs/figma.md +119 -0
  461. package/docs/live-hub.md +58 -0
  462. package/docs/products.md +26 -0
  463. package/docs/quality-gates.md +94 -0
  464. package/docs/release-notes.md +31 -0
  465. package/docs/storybook.md +56 -0
  466. package/docs/sync.md +48 -0
  467. package/docs/template-schema-v2.md +36 -0
  468. package/docs/vi/ci-cd.md +84 -0
  469. package/docs/vi/consuming.md +109 -0
  470. package/docs/vi/contrast-report.md +125 -0
  471. package/docs/vi/conventions.md +114 -0
  472. package/docs/vi/decisions.md +94 -0
  473. package/docs/vi/deploy.md +110 -0
  474. package/docs/vi/design-styles.md +25 -0
  475. package/docs/vi/figma.md +119 -0
  476. package/docs/vi/live-hub.md +58 -0
  477. package/docs/vi/products.md +26 -0
  478. package/docs/vi/quality-gates.md +94 -0
  479. package/docs/vi/release-notes.md +31 -0
  480. package/docs/vi/storybook.md +56 -0
  481. package/docs/vi/sync.md +48 -0
  482. package/docs/vi/template-schema-v2.md +36 -0
  483. package/docs/viewer.html +230 -0
  484. package/fonts/bevietnampro-300-latin-ext.woff2 +0 -0
  485. package/fonts/bevietnampro-300-latin.woff2 +0 -0
  486. package/fonts/bevietnampro-300-vietnamese.woff2 +0 -0
  487. package/fonts/bevietnampro-400-latin-ext.woff2 +0 -0
  488. package/fonts/bevietnampro-400-latin.woff2 +0 -0
  489. package/fonts/bevietnampro-400-vietnamese.woff2 +0 -0
  490. package/fonts/bevietnampro-400i-latin-ext.woff2 +0 -0
  491. package/fonts/bevietnampro-400i-latin.woff2 +0 -0
  492. package/fonts/bevietnampro-400i-vietnamese.woff2 +0 -0
  493. package/fonts/bevietnampro-500-latin-ext.woff2 +0 -0
  494. package/fonts/bevietnampro-500-latin.woff2 +0 -0
  495. package/fonts/bevietnampro-500-vietnamese.woff2 +0 -0
  496. package/fonts/bevietnampro-600-latin-ext.woff2 +0 -0
  497. package/fonts/bevietnampro-600-latin.woff2 +0 -0
  498. package/fonts/bevietnampro-600-vietnamese.woff2 +0 -0
  499. package/fonts/bevietnampro-600i-latin-ext.woff2 +0 -0
  500. package/fonts/bevietnampro-600i-latin.woff2 +0 -0
  501. package/fonts/bevietnampro-600i-vietnamese.woff2 +0 -0
  502. package/fonts/bevietnampro-700-latin-ext.woff2 +0 -0
  503. package/fonts/bevietnampro-700-latin.woff2 +0 -0
  504. package/fonts/bevietnampro-700-vietnamese.woff2 +0 -0
  505. package/fonts/bevietnampro-800-latin-ext.woff2 +0 -0
  506. package/fonts/bevietnampro-800-latin.woff2 +0 -0
  507. package/fonts/bevietnampro-800-vietnamese.woff2 +0 -0
  508. package/fonts/jetbrainsmono-400-cyrillic-ext.woff2 +0 -0
  509. package/fonts/jetbrainsmono-400-cyrillic.woff2 +0 -0
  510. package/fonts/jetbrainsmono-400-greek.woff2 +0 -0
  511. package/fonts/jetbrainsmono-400-latin-ext.woff2 +0 -0
  512. package/fonts/jetbrainsmono-400-latin.woff2 +0 -0
  513. package/fonts/jetbrainsmono-400-vietnamese.woff2 +0 -0
  514. package/fonts/jetbrainsmono-500-cyrillic-ext.woff2 +0 -0
  515. package/fonts/jetbrainsmono-500-cyrillic.woff2 +0 -0
  516. package/fonts/jetbrainsmono-500-greek.woff2 +0 -0
  517. package/fonts/jetbrainsmono-500-latin-ext.woff2 +0 -0
  518. package/fonts/jetbrainsmono-500-latin.woff2 +0 -0
  519. package/fonts/jetbrainsmono-500-vietnamese.woff2 +0 -0
  520. package/fonts/jetbrainsmono-700-cyrillic-ext.woff2 +0 -0
  521. package/fonts/jetbrainsmono-700-cyrillic.woff2 +0 -0
  522. package/fonts/jetbrainsmono-700-greek.woff2 +0 -0
  523. package/fonts/jetbrainsmono-700-latin-ext.woff2 +0 -0
  524. package/fonts/jetbrainsmono-700-latin.woff2 +0 -0
  525. package/fonts/jetbrainsmono-700-vietnamese.woff2 +0 -0
  526. package/guidelines/atomic-view.html +497 -0
  527. package/guidelines/brand-iconography.html +51 -0
  528. package/guidelines/brand-imagery.html +23 -0
  529. package/guidelines/brand-interaction.html +24 -0
  530. package/guidelines/brand-voice.html +24 -0
  531. package/guidelines/colors-anchors.html +20 -0
  532. package/guidelines/colors-semantic.html +19 -0
  533. package/guidelines/colors-surfaces.html +19 -0
  534. package/guidelines/colors-text.html +20 -0
  535. package/guidelines/documents.html +43 -0
  536. package/guidelines/elements-components.html +27 -0
  537. package/guidelines/elements-concept.html +48 -0
  538. package/guidelines/elements-variants.html +49 -0
  539. package/guidelines/motion-interaction.html +29 -0
  540. package/guidelines/motion.html +53 -0
  541. package/guidelines/nav-model.cjs +113 -0
  542. package/guidelines/responsive-bilingual.html +75 -0
  543. package/guidelines/rtl-preview.html +115 -0
  544. package/guidelines/spacing-breakpoints.html +20 -0
  545. package/guidelines/spacing-radius.html +19 -0
  546. package/guidelines/spacing-scale.html +21 -0
  547. package/guidelines/surfaces-aurora.html +20 -0
  548. package/guidelines/surfaces-depth.html +27 -0
  549. package/guidelines/surfaces-glass.html +21 -0
  550. package/guidelines/surfaces-shadow.html +20 -0
  551. package/guidelines/type-families.html +25 -0
  552. package/guidelines/type-lineheight.html +16 -0
  553. package/guidelines/type-scale.html +21 -0
  554. package/guidelines/type-weights.html +20 -0
  555. package/llms.txt +70 -0
  556. package/package.json +88 -0
  557. package/styles.css +39 -0
  558. package/templates/_vendor/deck-stage.js +2584 -0
  559. package/templates/ai-cluster-demo.html +46 -0
  560. package/templates/app-shell/.thumbnail +0 -0
  561. package/templates/app-shell/AppShell.dc.html +80 -0
  562. package/templates/app-shell/content-schema.json +114 -0
  563. package/templates/app-shell/ds-base.js +23 -0
  564. package/templates/app-shell/support.js +1841 -0
  565. package/templates/article/.thumbnail +0 -0
  566. package/templates/article/Article.dc.html +67 -0
  567. package/templates/article/content-schema.json +80 -0
  568. package/templates/article/ds-base.js +23 -0
  569. package/templates/article/support.js +1841 -0
  570. package/templates/auth/.thumbnail +0 -0
  571. package/templates/auth/Auth.dc.html +72 -0
  572. package/templates/auth/content-schema.json +74 -0
  573. package/templates/auth/ds-base.js +23 -0
  574. package/templates/auth/support.js +1841 -0
  575. package/templates/bod-deck/.thumbnail +0 -0
  576. package/templates/bod-deck/BodDeck.dc.html +106 -0
  577. package/templates/bod-deck/content-schema.json +171 -0
  578. package/templates/bod-deck/ds-base.js +23 -0
  579. package/templates/bod-deck/support.js +1841 -0
  580. package/templates/bod-investor-update/.thumbnail +0 -0
  581. package/templates/bod-investor-update/BodInvestorUpdate.dc.html +81 -0
  582. package/templates/bod-investor-update/content-schema.json +105 -0
  583. package/templates/bod-investor-update/ds-base.js +23 -0
  584. package/templates/bod-investor-update/support.js +1841 -0
  585. package/templates/bod-memo/.thumbnail +0 -0
  586. package/templates/bod-memo/BodMemo.dc.html +87 -0
  587. package/templates/bod-memo/ds-base.js +23 -0
  588. package/templates/bod-memo/support.js +1841 -0
  589. package/templates/bod-minutes/.thumbnail +0 -0
  590. package/templates/bod-minutes/BodMinutes.dc.html +84 -0
  591. package/templates/bod-minutes/ds-base.js +23 -0
  592. package/templates/bod-minutes/support.js +1841 -0
  593. package/templates/bod-report/.thumbnail +0 -0
  594. package/templates/bod-report/BodReport.dc.html +106 -0
  595. package/templates/bod-report/content-schema.json +29 -0
  596. package/templates/bod-report/ds-base.js +23 -0
  597. package/templates/bod-report/support.js +1841 -0
  598. package/templates/bod-resolution/.thumbnail +0 -0
  599. package/templates/bod-resolution/BodResolution.dc.html +87 -0
  600. package/templates/bod-resolution/content-schema.json +123 -0
  601. package/templates/bod-resolution/ds-base.js +23 -0
  602. package/templates/bod-resolution/support.js +1841 -0
  603. package/templates/culture-event-invite/.thumbnail +0 -0
  604. package/templates/culture-event-invite/CultureEventInvite.dc.html +73 -0
  605. package/templates/culture-event-invite/ds-base.js +23 -0
  606. package/templates/culture-event-invite/support.js +1841 -0
  607. package/templates/dashboard/.thumbnail +0 -0
  608. package/templates/dashboard/Dashboard.dc.html +120 -0
  609. package/templates/dashboard/content-schema.json +97 -0
  610. package/templates/dashboard/ds-base.js +23 -0
  611. package/templates/dashboard/support.js +1841 -0
  612. package/templates/delivery-change-note/.thumbnail +0 -0
  613. package/templates/delivery-change-note/DeliveryChangeNote.dc.html +85 -0
  614. package/templates/delivery-change-note/content-schema.json +158 -0
  615. package/templates/delivery-change-note/ds-base.js +23 -0
  616. package/templates/delivery-change-note/support.js +1841 -0
  617. package/templates/delivery-kickoff/.thumbnail +0 -0
  618. package/templates/delivery-kickoff/DeliveryKickoff.dc.html +112 -0
  619. package/templates/delivery-kickoff/content-schema.json +223 -0
  620. package/templates/delivery-kickoff/ds-base.js +23 -0
  621. package/templates/delivery-kickoff/support.js +1841 -0
  622. package/templates/delivery-qbr-deck/.thumbnail +0 -0
  623. package/templates/delivery-qbr-deck/DeliveryQbrDeck.dc.html +118 -0
  624. package/templates/delivery-qbr-deck/content-schema.json +177 -0
  625. package/templates/delivery-qbr-deck/ds-base.js +23 -0
  626. package/templates/delivery-qbr-deck/support.js +1841 -0
  627. package/templates/delivery-runbook/.thumbnail +0 -0
  628. package/templates/delivery-runbook/DeliveryRunbook.dc.html +91 -0
  629. package/templates/delivery-runbook/content-schema.json +211 -0
  630. package/templates/delivery-runbook/ds-base.js +23 -0
  631. package/templates/delivery-runbook/support.js +1841 -0
  632. package/templates/delivery-status-email/.thumbnail +0 -0
  633. package/templates/delivery-status-email/DeliveryStatusEmail.dc.html +78 -0
  634. package/templates/delivery-status-email/ds-base.js +23 -0
  635. package/templates/delivery-status-email/support.js +1841 -0
  636. package/templates/doc-style-guide/.thumbnail +0 -0
  637. package/templates/doc-style-guide/DocStyleGuide.dc.html +148 -0
  638. package/templates/doc-style-guide/ds-base.js +23 -0
  639. package/templates/doc-style-guide/support.js +1841 -0
  640. package/templates/doc-suite-index/.thumbnail +0 -0
  641. package/templates/doc-suite-index/DocSuiteIndex.dc.html +201 -0
  642. package/templates/doc-suite-index/content-schema.json +12 -0
  643. package/templates/doc-suite-index/ds-base.js +23 -0
  644. package/templates/doc-suite-index/support.js +1841 -0
  645. package/templates/doc-templates/.thumbnail +0 -0
  646. package/templates/doc-templates/DocTemplates.dc.html +118 -0
  647. package/templates/doc-templates/ds-base.js +23 -0
  648. package/templates/doc-templates/support.js +1841 -0
  649. package/templates/email/.thumbnail +0 -0
  650. package/templates/email/Email-VN-standalone.html +59 -0
  651. package/templates/email/Email.dc.html +67 -0
  652. package/templates/email/ds-base.js +23 -0
  653. package/templates/email/support.js +1841 -0
  654. package/templates/email-safe/README.md +28 -0
  655. package/templates/email-safe/email-announcement.html +88 -0
  656. package/templates/email-safe/email-dunning.html +59 -0
  657. package/templates/email-safe/email-investor.html +88 -0
  658. package/templates/email-safe/email-launch.html +88 -0
  659. package/templates/email-safe/email-newsletter.html +62 -0
  660. package/templates/email-safe/email-status.html +88 -0
  661. package/templates/email-safe/email-transactional.html +88 -0
  662. package/templates/finance-dunning-email/.thumbnail +0 -0
  663. package/templates/finance-dunning-email/FinanceDunningEmail.dc.html +58 -0
  664. package/templates/finance-dunning-email/ds-base.js +23 -0
  665. package/templates/finance-dunning-email/support.js +1841 -0
  666. package/templates/finance-expense-report/.thumbnail +0 -0
  667. package/templates/finance-expense-report/FinanceExpenseReport.dc.html +82 -0
  668. package/templates/finance-expense-report/ds-base.js +23 -0
  669. package/templates/finance-expense-report/support.js +1841 -0
  670. package/templates/finance-invoice/.thumbnail +0 -0
  671. package/templates/finance-invoice/FinanceInvoice.dc.html +92 -0
  672. package/templates/finance-invoice/ds-base.js +23 -0
  673. package/templates/finance-invoice/support.js +1841 -0
  674. package/templates/finance-quote/.thumbnail +0 -0
  675. package/templates/finance-quote/FinanceQuote.dc.html +86 -0
  676. package/templates/finance-quote/ds-base.js +23 -0
  677. package/templates/finance-quote/support.js +1841 -0
  678. package/templates/hr-announcement/.thumbnail +0 -0
  679. package/templates/hr-announcement/HrAnnouncement.dc.html +64 -0
  680. package/templates/hr-announcement/ds-base.js +23 -0
  681. package/templates/hr-announcement/support.js +1841 -0
  682. package/templates/hr-interview-kit/.thumbnail +0 -0
  683. package/templates/hr-interview-kit/HrInterviewKit.dc.html +102 -0
  684. package/templates/hr-interview-kit/ds-base.js +23 -0
  685. package/templates/hr-interview-kit/support.js +1841 -0
  686. package/templates/hr-offboarding/.thumbnail +0 -0
  687. package/templates/hr-offboarding/HrOffboarding.dc.html +86 -0
  688. package/templates/hr-offboarding/ds-base.js +23 -0
  689. package/templates/hr-offboarding/support.js +1841 -0
  690. package/templates/hr-performance-review/.thumbnail +0 -0
  691. package/templates/hr-performance-review/HrPerformanceReview.dc.html +95 -0
  692. package/templates/hr-performance-review/ds-base.js +23 -0
  693. package/templates/hr-performance-review/support.js +1841 -0
  694. package/templates/hr-pip/.thumbnail +0 -0
  695. package/templates/hr-pip/HrPip.dc.html +73 -0
  696. package/templates/hr-pip/ds-base.js +23 -0
  697. package/templates/hr-pip/support.js +1841 -0
  698. package/templates/image-slots-demo.html +57 -0
  699. package/templates/kitchen-sink.html +127 -0
  700. package/templates/legal-msa/.thumbnail +0 -0
  701. package/templates/legal-msa/LegalMsa.dc.html +85 -0
  702. package/templates/legal-msa/ds-base.js +23 -0
  703. package/templates/legal-msa/support.js +1841 -0
  704. package/templates/legal-sow/.thumbnail +0 -0
  705. package/templates/legal-sow/LegalSow.dc.html +125 -0
  706. package/templates/legal-sow/ds-base.js +23 -0
  707. package/templates/legal-sow/support.js +1841 -0
  708. package/templates/marketing-campaign-brief/.thumbnail +0 -0
  709. package/templates/marketing-campaign-brief/MarketingCampaignBrief.dc.html +86 -0
  710. package/templates/marketing-campaign-brief/content-schema.json +139 -0
  711. package/templates/marketing-campaign-brief/ds-base.js +23 -0
  712. package/templates/marketing-campaign-brief/support.js +1841 -0
  713. package/templates/marketing-case-study/.thumbnail +0 -0
  714. package/templates/marketing-case-study/MarketingCaseStudy.dc.html +82 -0
  715. package/templates/marketing-case-study/content-schema.json +103 -0
  716. package/templates/marketing-case-study/ds-base.js +23 -0
  717. package/templates/marketing-case-study/support.js +1841 -0
  718. package/templates/marketing-launch/.thumbnail +0 -0
  719. package/templates/marketing-launch/MarketingLaunch.dc.html +84 -0
  720. package/templates/marketing-launch/ds-base.js +23 -0
  721. package/templates/marketing-launch/support.js +1841 -0
  722. package/templates/marketing-newsletter/.thumbnail +0 -0
  723. package/templates/marketing-newsletter/MarketingNewsletter.dc.html +109 -0
  724. package/templates/marketing-newsletter/ds-base.js +23 -0
  725. package/templates/marketing-newsletter/support.js +1841 -0
  726. package/templates/marketing-page/.thumbnail +0 -0
  727. package/templates/marketing-page/MarketingPage.dc.html +132 -0
  728. package/templates/marketing-page/content-schema.json +33 -0
  729. package/templates/marketing-page/ds-base.js +23 -0
  730. package/templates/marketing-page/support.js +1841 -0
  731. package/templates/marketing-press-release/.thumbnail +0 -0
  732. package/templates/marketing-press-release/MarketingPressRelease.dc.html +84 -0
  733. package/templates/marketing-press-release/content-schema.json +116 -0
  734. package/templates/marketing-press-release/ds-base.js +23 -0
  735. package/templates/marketing-press-release/support.js +1841 -0
  736. package/templates/marketing-social-kit/.thumbnail +0 -0
  737. package/templates/marketing-social-kit/MarketingSocialKit.dc.html +77 -0
  738. package/templates/marketing-social-kit/content-schema.json +54 -0
  739. package/templates/marketing-social-kit/ds-base.js +23 -0
  740. package/templates/marketing-social-kit/support.js +1841 -0
  741. package/templates/playground.html +79 -0
  742. package/templates/sales-one-pager/.thumbnail +0 -0
  743. package/templates/sales-one-pager/SalesOnePager.dc.html +103 -0
  744. package/templates/sales-one-pager/content-schema.json +73 -0
  745. package/templates/sales-one-pager/ds-base.js +23 -0
  746. package/templates/sales-one-pager/support.js +1841 -0
  747. package/templates/sales-proposal-deck/.thumbnail +0 -0
  748. package/templates/sales-proposal-deck/SalesProposalDeck.dc.html +159 -0
  749. package/templates/sales-proposal-deck/content-schema.json +273 -0
  750. package/templates/sales-proposal-deck/ds-base.js +23 -0
  751. package/templates/sales-proposal-deck/support.js +1841 -0
  752. package/templates/schema/content-schema.schema.json +31 -0
  753. package/templates/settings/.thumbnail +0 -0
  754. package/templates/settings/Settings.dc.html +86 -0
  755. package/templates/settings/content-schema.json +131 -0
  756. package/templates/settings/ds-base.js +23 -0
  757. package/templates/settings/support.js +1841 -0
  758. package/templates/slide-deck/.thumbnail +0 -0
  759. package/templates/slide-deck/SlideDeck.dc.html +86 -0
  760. package/templates/slide-deck/content-schema.json +23 -0
  761. package/templates/slide-deck/ds-base.js +23 -0
  762. package/templates/slide-deck/support.js +1841 -0
  763. package/templates/team-meeting-agenda/.thumbnail +0 -0
  764. package/templates/team-meeting-agenda/TeamMeetingAgenda.dc.html +93 -0
  765. package/templates/team-meeting-agenda/ds-base.js +23 -0
  766. package/templates/team-meeting-agenda/support.js +1841 -0
  767. package/templates/tech-incident-report/.thumbnail +0 -0
  768. package/templates/tech-incident-report/TechIncidentReport.dc.html +111 -0
  769. package/templates/tech-incident-report/content-schema.json +25 -0
  770. package/templates/tech-incident-report/ds-base.js +23 -0
  771. package/templates/tech-incident-report/support.js +1841 -0
  772. package/templates/tech-release-notes/.thumbnail +0 -0
  773. package/templates/tech-release-notes/TechReleaseNotes.dc.html +79 -0
  774. package/templates/tech-release-notes/ds-base.js +23 -0
  775. package/templates/tech-release-notes/support.js +1841 -0
  776. package/templates/tech-retro/.thumbnail +0 -0
  777. package/templates/tech-retro/TechRetro.dc.html +84 -0
  778. package/templates/tech-retro/ds-base.js +23 -0
  779. package/templates/tech-retro/support.js +1841 -0
  780. package/templates/tech-rfc/.thumbnail +0 -0
  781. package/templates/tech-rfc/TechRfc.dc.html +87 -0
  782. package/templates/tech-rfc/content-schema.json +115 -0
  783. package/templates/tech-rfc/ds-base.js +23 -0
  784. package/templates/tech-rfc/support.js +1841 -0
  785. package/templates/vn-appointment-transfer/.thumbnail +0 -0
  786. package/templates/vn-appointment-transfer/VnAppointmentTransfer.dc.html +101 -0
  787. package/templates/vn-appointment-transfer/ds-base.js +23 -0
  788. package/templates/vn-appointment-transfer/support.js +1841 -0
  789. package/templates/vn-certificate-of-employment/.thumbnail +0 -0
  790. package/templates/vn-certificate-of-employment/VnCertificateOfEmployment.dc.html +84 -0
  791. package/templates/vn-certificate-of-employment/ds-base.js +23 -0
  792. package/templates/vn-certificate-of-employment/support.js +1841 -0
  793. package/templates/vn-code-of-conduct/.thumbnail +0 -0
  794. package/templates/vn-code-of-conduct/VnCodeOfConduct.dc.html +105 -0
  795. package/templates/vn-code-of-conduct/ds-base.js +23 -0
  796. package/templates/vn-code-of-conduct/support.js +1841 -0
  797. package/templates/vn-compensation-regulation/.thumbnail +0 -0
  798. package/templates/vn-compensation-regulation/VnCompensationRegulation.dc.html +122 -0
  799. package/templates/vn-compensation-regulation/ds-base.js +23 -0
  800. package/templates/vn-compensation-regulation/support.js +1841 -0
  801. package/templates/vn-contract-amendment/.thumbnail +0 -0
  802. package/templates/vn-contract-amendment/VnContractAmendment.dc.html +107 -0
  803. package/templates/vn-contract-amendment/ds-base.js +23 -0
  804. package/templates/vn-contract-amendment/support.js +1841 -0
  805. package/templates/vn-contractor-agreement/.thumbnail +0 -0
  806. package/templates/vn-contractor-agreement/VnContractorAgreement.dc.html +120 -0
  807. package/templates/vn-contractor-agreement/ds-base.js +23 -0
  808. package/templates/vn-contractor-agreement/support.js +1841 -0
  809. package/templates/vn-data-protection-policy/.thumbnail +0 -0
  810. package/templates/vn-data-protection-policy/VnDataProtectionPolicy.dc.html +116 -0
  811. package/templates/vn-data-protection-policy/ds-base.js +23 -0
  812. package/templates/vn-data-protection-policy/support.js +1841 -0
  813. package/templates/vn-disciplinary-case-file/.thumbnail +0 -0
  814. package/templates/vn-disciplinary-case-file/VnDisciplinaryCaseFile.dc.html +172 -0
  815. package/templates/vn-disciplinary-case-file/ds-base.js +23 -0
  816. package/templates/vn-disciplinary-case-file/support.js +1841 -0
  817. package/templates/vn-disciplinary-schedule/.thumbnail +0 -0
  818. package/templates/vn-disciplinary-schedule/VnDisciplinarySchedule.dc.html +91 -0
  819. package/templates/vn-disciplinary-schedule/ds-base.js +23 -0
  820. package/templates/vn-disciplinary-schedule/support.js +1841 -0
  821. package/templates/vn-employee-request/.thumbnail +0 -0
  822. package/templates/vn-employee-request/VnEmployeeRequest.dc.html +137 -0
  823. package/templates/vn-employee-request/ds-base.js +23 -0
  824. package/templates/vn-employee-request/support.js +1841 -0
  825. package/templates/vn-exit-handover/.thumbnail +0 -0
  826. package/templates/vn-exit-handover/VnExitHandover.dc.html +228 -0
  827. package/templates/vn-exit-handover/ds-base.js +23 -0
  828. package/templates/vn-exit-handover/support.js +1841 -0
  829. package/templates/vn-foreign-employee-pack/.thumbnail +0 -0
  830. package/templates/vn-foreign-employee-pack/VnForeignEmployeePack.dc.html +129 -0
  831. package/templates/vn-foreign-employee-pack/content-schema.json +22 -0
  832. package/templates/vn-foreign-employee-pack/ds-base.js +23 -0
  833. package/templates/vn-foreign-employee-pack/support.js +1841 -0
  834. package/templates/vn-grassroots-democracy/.thumbnail +0 -0
  835. package/templates/vn-grassroots-democracy/VnGrassrootsDemocracy.dc.html +117 -0
  836. package/templates/vn-grassroots-democracy/ds-base.js +23 -0
  837. package/templates/vn-grassroots-democracy/support.js +1841 -0
  838. package/templates/vn-infosec-policy/.thumbnail +0 -0
  839. package/templates/vn-infosec-policy/VnInfosecPolicy.dc.html +116 -0
  840. package/templates/vn-infosec-policy/ds-base.js +23 -0
  841. package/templates/vn-infosec-policy/support.js +1841 -0
  842. package/templates/vn-internal-labor-regulations/.thumbnail +0 -0
  843. package/templates/vn-internal-labor-regulations/VnInternalLaborRegulations.dc.html +155 -0
  844. package/templates/vn-internal-labor-regulations/ds-base.js +23 -0
  845. package/templates/vn-internal-labor-regulations/support.js +1841 -0
  846. package/templates/vn-internship-agreement/.thumbnail +0 -0
  847. package/templates/vn-internship-agreement/VnInternshipAgreement.dc.html +112 -0
  848. package/templates/vn-internship-agreement/ds-base.js +23 -0
  849. package/templates/vn-internship-agreement/support.js +1841 -0
  850. package/templates/vn-job-description/.thumbnail +0 -0
  851. package/templates/vn-job-description/VnJobDescription.dc.html +85 -0
  852. package/templates/vn-job-description/ds-base.js +23 -0
  853. package/templates/vn-job-description/support.js +1841 -0
  854. package/templates/vn-labor-contract/.thumbnail +0 -0
  855. package/templates/vn-labor-contract/VnLaborContract.dc.html +180 -0
  856. package/templates/vn-labor-contract/ds-base.js +23 -0
  857. package/templates/vn-labor-contract/support.js +1841 -0
  858. package/templates/vn-labour-management-book/.thumbnail +0 -0
  859. package/templates/vn-labour-management-book/VnLabourManagementBook.dc.html +115 -0
  860. package/templates/vn-labour-management-book/ds-base.js +23 -0
  861. package/templates/vn-labour-management-book/support.js +1841 -0
  862. package/templates/vn-mutual-nda/.thumbnail +0 -0
  863. package/templates/vn-mutual-nda/VnMutualNda.dc.html +114 -0
  864. package/templates/vn-mutual-nda/ds-base.js +23 -0
  865. package/templates/vn-mutual-nda/support.js +1841 -0
  866. package/templates/vn-ndnca-ip/.thumbnail +0 -0
  867. package/templates/vn-ndnca-ip/VnNdncaIp.dc.html +157 -0
  868. package/templates/vn-ndnca-ip/ds-base.js +23 -0
  869. package/templates/vn-ndnca-ip/support.js +1841 -0
  870. package/templates/vn-offer-letter/.thumbnail +0 -0
  871. package/templates/vn-offer-letter/VnOfferLetter.dc.html +84 -0
  872. package/templates/vn-offer-letter/ds-base.js +23 -0
  873. package/templates/vn-offer-letter/support.js +1841 -0
  874. package/templates/vn-onboarding-checklist/.thumbnail +0 -0
  875. package/templates/vn-onboarding-checklist/VnOnboardingChecklist.dc.html +98 -0
  876. package/templates/vn-onboarding-checklist/content-schema.json +12 -0
  877. package/templates/vn-onboarding-checklist/ds-base.js +23 -0
  878. package/templates/vn-onboarding-checklist/support.js +1841 -0
  879. package/templates/vn-pdpl-consent/.thumbnail +0 -0
  880. package/templates/vn-pdpl-consent/VnPdplConsent.dc.html +111 -0
  881. package/templates/vn-pdpl-consent/ds-base.js +23 -0
  882. package/templates/vn-pdpl-consent/support.js +1841 -0
  883. package/templates/vn-performance-evaluation-regulation/.thumbnail +0 -0
  884. package/templates/vn-performance-evaluation-regulation/VnPerformanceEvaluationRegulation.dc.html +135 -0
  885. package/templates/vn-performance-evaluation-regulation/ds-base.js +23 -0
  886. package/templates/vn-performance-evaluation-regulation/support.js +1841 -0
  887. package/templates/vn-performance-review-form/.thumbnail +0 -0
  888. package/templates/vn-performance-review-form/VnPerformanceReviewForm.dc.html +129 -0
  889. package/templates/vn-performance-review-form/ds-base.js +23 -0
  890. package/templates/vn-performance-review-form/support.js +1841 -0
  891. package/templates/vn-phantom-stock/.thumbnail +0 -0
  892. package/templates/vn-phantom-stock/VnPhantomStock.dc.html +184 -0
  893. package/templates/vn-phantom-stock/content-schema.json +17 -0
  894. package/templates/vn-phantom-stock/ds-base.js +23 -0
  895. package/templates/vn-phantom-stock/support.js +1841 -0
  896. package/templates/vn-probation-contract/.thumbnail +0 -0
  897. package/templates/vn-probation-contract/VnProbationContract.dc.html +126 -0
  898. package/templates/vn-probation-contract/ds-base.js +23 -0
  899. package/templates/vn-probation-contract/support.js +1841 -0
  900. package/templates/vn-receipt-acknowledgement/.thumbnail +0 -0
  901. package/templates/vn-receipt-acknowledgement/VnReceiptAcknowledgement.dc.html +106 -0
  902. package/templates/vn-receipt-acknowledgement/ds-base.js +23 -0
  903. package/templates/vn-receipt-acknowledgement/support.js +1841 -0
  904. package/templates/vn-remote-work-policy/.thumbnail +0 -0
  905. package/templates/vn-remote-work-policy/VnRemoteWorkPolicy.dc.html +103 -0
  906. package/templates/vn-remote-work-policy/ds-base.js +23 -0
  907. package/templates/vn-remote-work-policy/support.js +1841 -0
  908. package/templates/vn-resignation-letter/.thumbnail +0 -0
  909. package/templates/vn-resignation-letter/VnResignationLetter.dc.html +71 -0
  910. package/templates/vn-resignation-letter/ds-base.js +23 -0
  911. package/templates/vn-resignation-letter/support.js +1841 -0
  912. package/templates/vn-salary-scale/.thumbnail +0 -0
  913. package/templates/vn-salary-scale/VnSalaryScale.dc.html +110 -0
  914. package/templates/vn-salary-scale/ds-base.js +23 -0
  915. package/templates/vn-salary-scale/support.js +1841 -0
  916. package/templates/vn-sow-appendix/.thumbnail +0 -0
  917. package/templates/vn-sow-appendix/VnSowAppendix.dc.html +70 -0
  918. package/templates/vn-sow-appendix/ds-base.js +23 -0
  919. package/templates/vn-sow-appendix/support.js +1841 -0
  920. package/templates/vn-termination-decision/.thumbnail +0 -0
  921. package/templates/vn-termination-decision/VnTerminationDecision.dc.html +80 -0
  922. package/templates/vn-termination-decision/ds-base.js +23 -0
  923. package/templates/vn-termination-decision/support.js +1841 -0
  924. package/templates/vn-total-rewards-appendix/.thumbnail +0 -0
  925. package/templates/vn-total-rewards-appendix/VnTotalRewardsAppendix.dc.html +138 -0
  926. package/templates/vn-total-rewards-appendix/ds-base.js +23 -0
  927. package/templates/vn-total-rewards-appendix/support.js +1841 -0
  928. package/templates/vn-training-commitment/.thumbnail +0 -0
  929. package/templates/vn-training-commitment/VnTrainingCommitment.dc.html +105 -0
  930. package/templates/vn-training-commitment/ds-base.js +23 -0
  931. package/templates/vn-training-commitment/support.js +1841 -0
  932. package/templates/vn-travel-expense-policy/.thumbnail +0 -0
  933. package/templates/vn-travel-expense-policy/VnTravelExpensePolicy.dc.html +113 -0
  934. package/templates/vn-travel-expense-policy/ds-base.js +23 -0
  935. package/templates/vn-travel-expense-policy/support.js +1841 -0
  936. package/tokens/colors.css +77 -0
  937. package/tokens/component-tokens.css +46 -0
  938. package/tokens/elements.css +300 -0
  939. package/tokens/elevation.css +46 -0
  940. package/tokens/fonts.css +51 -0
  941. package/tokens/motion.css +25 -0
  942. package/tokens/native/CSTokens.kt +255 -0
  943. package/tokens/native/CSTokens.swift +257 -0
  944. package/tokens/native/cs_tokens.dart +253 -0
  945. package/tokens/provenance.json +52 -0
  946. package/tokens/spacing.css +34 -0
  947. package/tokens/tokens.dtcg.json +1222 -0
  948. package/tokens/tokens.js +436 -0
  949. package/tokens/tokens.json +434 -0
  950. package/tokens/typography.css +67 -0
  951. package/tokens/viewer.html +261 -0
  952. package/ui_kits/deck/README.md +21 -0
  953. package/ui_kits/deck/index.html +120 -0
  954. package/ui_kits/status-hub/README.md +40 -0
  955. package/ui_kits/status-hub/StatusHub.jsx +270 -0
  956. package/ui_kits/status-hub/data.js +57 -0
  957. package/ui_kits/status-hub/identity-lab.html +134 -0
  958. package/ui_kits/status-hub/index.html +24 -0
  959. package/ui_kits/status-hub/login.html +65 -0
  960. package/ui_kits/status-hub/project.html +137 -0
  961. package/ui_kits/status-hub/settings.html +88 -0
  962. package/ui_kits/status-hub/status.css +199 -0
  963. package/ui_kits/website/README.md +36 -0
  964. package/ui_kits/website/Website.jsx +167 -0
  965. package/ui_kits/website/careers.html +109 -0
  966. package/ui_kits/website/chat.html +133 -0
  967. package/ui_kits/website/copy.js +76 -0
  968. package/ui_kits/website/index.html +24 -0
  969. package/ui_kits/website/site.css +121 -0
  970. package/ui_kits/website/work.html +119 -0
@@ -0,0 +1,2584 @@
1
+ // @ds-adherence-ignore -- omelette starter scaffold (raw elements/hex/px by design)
2
+ // Copied omelette starter. Re-running copy_starter_component with this kind overwrites this file with the latest version (page content is unaffected).
3
+ /* ═══ THIS PROJECT USES DESIGN COMPONENTS (.dc.html) ═══
4
+ * Reference this stage from your <x-dc> template as an import — NEVER as a
5
+ * raw <deck-stage> tag plus a <script src> (that hides the whole deck until
6
+ * the stream finishes):
7
+ *
8
+ * <x-import component-from-global-scope="deck-stage" from="./deck-stage.js"
9
+ * width="1920" height="1080" hint-size="100%,100%">
10
+ * <section data-label="Title" style="...">…</section>
11
+ * <section data-label="Agenda" style="...">…</section>
12
+ * </x-import>
13
+ *
14
+ * Slides are inline-styled <section> siblings; do not add a stylesheet or a
15
+ * deck-stage:not(:defined) rule. The plain-HTML "Usage" block in the comment
16
+ * below does NOT apply to .dc.html templates.
17
+ */
18
+ /* BEGIN USAGE */
19
+ /**
20
+ * <deck-stage> — reusable web component for HTML decks.
21
+ *
22
+ * Handles:
23
+ * (a) speaker notes — reads <script type="application/json" id="speaker-notes">
24
+ * and posts {slideIndexChanged: N} to the parent window on nav.
25
+ * (b) keyboard navigation — ←/→, PgUp/PgDn, Space, Home/End, number keys.
26
+ * On touch devices, tapping the left/right half of the stage goes
27
+ * prev/next — taps on links, buttons and other interactive slide
28
+ * content are left alone.
29
+ * (c) press R to reset to slide 0 (with a tasteful keyboard hint).
30
+ * (d) bottom-center overlay showing slide count + hints, fades out on idle.
31
+ * (e) auto-scaling — inner canvas is a fixed design size (default 1920×1080)
32
+ * scaled with `transform: scale()` to fit the viewport, letterboxed.
33
+ * Set the `noscale` attribute to render at authored size (1:1) — the
34
+ * PPTX exporter sets this so its DOM capture sees unscaled geometry.
35
+ * (f) print — `@media print` lays every slide out as its own page at the
36
+ * design size, so the browser's Print → Save as PDF produces a clean
37
+ * one-page-per-slide PDF with no extra setup.
38
+ * (g) thumbnail rail — resizable left-hand column of per-slide thumbnails
39
+ * (static clones). Click to navigate — the clicked slide becomes the
40
+ * selected (highlighted) slide; shift-click selects a range and
41
+ * cmd/ctrl-click toggles slides in and out of the selection
42
+ * (Escape collapses it back to the current slide); ↑/↓ with a
43
+ * thumbnail focused to step between slides; Delete/Backspace with a
44
+ * thumbnail focused to delete the selection (one confirm dialog,
45
+ * one undoable operation); drag to reorder (dragging collapses a
46
+ * multi-selection); right-click for
47
+ * Skip / Move up / Move down / Duplicate / Delete — over a
48
+ * multi-selection the menu offers "Delete N slides". Drag the rail's right edge to resize;
49
+ * width persists to
50
+ * localStorage. Skipped slides carry `data-deck-skip`, are dimmed in
51
+ * the rail, omitted from prev/next navigation, and hidden at print.
52
+ * The rail is suppressed in presenting mode, in the host's Preview
53
+ * mode (ViewerMode='none'), on `noscale`, on narrow viewports
54
+ * (≤640px), and via the `no-rail` attribute. Rail mutations dispatch
55
+ * a `dc-op` CustomEvent on the element (see docs/dc-ops.md) and do
56
+ * NOT touch the DOM: the host applies the op and re-renders;
57
+ * structural rail input is locked until the host posts
58
+ * {__dc_op_ack: true, applied}.
59
+ * (h) typographic defaults — a zero-specificity stylesheet injected into
60
+ * the document gives headings `text-wrap: balance` and body text
61
+ * (p, li, blockquote, figcaption) `text-wrap: pretty`, so slides
62
+ * avoid widowed/orphaned words by default. Any text-wrap declaration
63
+ * you author on those elements wins over these defaults.
64
+ *
65
+ * Slides are HIDDEN, not unmounted. Non-active slides stay in the DOM with
66
+ * `visibility: hidden` + `opacity: 0`, so their state (videos, iframes,
67
+ * form inputs, React trees) is preserved across navigation.
68
+ *
69
+ * Lifecycle event — the component dispatches a `slidechange` CustomEvent on
70
+ * itself whenever the active slide changes (including the initial mount).
71
+ * The event bubbles and composes out of shadow DOM, so you can listen on
72
+ * the <deck-stage> element or on document:
73
+ *
74
+ * document.querySelector('deck-stage').addEventListener('slidechange', (e) => {
75
+ * e.detail.index // new 0-based index
76
+ * e.detail.previousIndex // previous index, or -1 on init
77
+ * e.detail.total // total slide count
78
+ * e.detail.slide // the new active slide element
79
+ * e.detail.previousSlide // the prior slide element, or null on init
80
+ * e.detail.reason // 'init' | 'keyboard' | 'click' | 'tap' | 'api'
81
+ * });
82
+ *
83
+ * Persistence: none at the deck level. The host app keeps the current slide
84
+ * in its own URL (?slide=) and re-delivers it via location.hash on load, so a
85
+ * bare load with no hash always starts at slide 1.
86
+ *
87
+ * Usage:
88
+ * <style>deck-stage:not(:defined){visibility:hidden}</style>
89
+ * <deck-stage width="1920" height="1080">
90
+ * <section data-label="Title">...</section>
91
+ * <section data-label="Agenda">...</section>
92
+ * </deck-stage>
93
+ * <script src="deck-stage.js"></script>
94
+ *
95
+ * The :not(:defined) rule prevents a flash of the first slide at its
96
+ * authored styles before this script runs and attaches the shadow root.
97
+ *
98
+ * Slides are the direct element children of <deck-stage>. Each slide is
99
+ * automatically tagged with:
100
+ * - data-screen-label="NN Label" (1-indexed, for comment flow)
101
+ * - data-om-validate="no_overflowing_text,no_overlapping_text,slide_sized_text"
102
+ *
103
+ * Speaker notes stay in sync because the component posts {slideIndexChanged: N}
104
+ * to the parent — just include the #speaker-notes script tag if asked for notes.
105
+ *
106
+ * Authoring guidance:
107
+ * - Write slide bodies as static HTML inside <deck-stage>, with sizing via
108
+ * CSS custom properties in a <style> block rather than JS constants.
109
+ * Static slide markup is what lets the user click a heading in edit mode
110
+ * and retype it directly; a slide rendered through <script type="text/babel">,
111
+ * React, or a loop over a JS array has to round-trip every tweak through a
112
+ * chat message instead. Reach for script-generated slides only when the
113
+ * content genuinely needs interactive behaviour static HTML can't express.
114
+ * - Do NOT set position/inset/width/height on the slide <section> elements —
115
+ * the component absolutely positions every slotted child for you.
116
+ * - Entrance animations: make the visible end-state the base style and
117
+ * animate *from* hidden, so print and reduced-motion show content.
118
+ * Gate the animation on [data-deck-active] and the motion query, e.g.
119
+ * `@media (prefers-reduced-motion:no-preference){ [data-deck-active] .x{animation:fade-in .5s both} }`.
120
+ * Avoid infinite decorative loops on slide content.
121
+ */
122
+ /* END USAGE */
123
+
124
+ (() => {
125
+ const DESIGN_W_DEFAULT = 1920;
126
+ const DESIGN_H_DEFAULT = 1080;
127
+ const OVERLAY_HIDE_MS = 1800;
128
+ const VALIDATE_ATTR = 'no_overflowing_text,no_overlapping_text,slide_sized_text';
129
+ const FINE_POINTER_MQ = matchMedia('(hover: hover) and (pointer: fine)');
130
+ const NARROW_MQ = matchMedia('(max-width: 640px)');
131
+ // Slide-authored controls that should keep a tap instead of it navigating.
132
+ const INTERACTIVE_SEL = 'a[href], button, input, select, textarea, summary, label, video[controls], audio[controls], [role="button"], [onclick], [tabindex]:not([tabindex^="-"]), [contenteditable]:not([contenteditable="false" i])';
133
+
134
+ const pad2 = (n) => String(n).padStart(2, '0');
135
+
136
+ // Label precedence: data-label → data-screen-label (number stripped) → first heading → "Slide".
137
+ const getSlideLabel = (el) => {
138
+ const explicit = el.getAttribute('data-label');
139
+ if (explicit) return explicit;
140
+
141
+ const existing = el.getAttribute('data-screen-label');
142
+ if (existing) return existing.replace(/^\s*\d+\s*/, '').trim() || existing;
143
+
144
+ const h = el.querySelector('h1, h2, h3, [data-title]');
145
+ const t = h && (h.textContent || '').trim().slice(0, 40);
146
+ if (t) return t;
147
+
148
+ return 'Slide';
149
+ };
150
+
151
+ const stylesheet = `
152
+ :host {
153
+ position: fixed;
154
+ inset: 0;
155
+ display: block;
156
+ background: #000;
157
+ color: #fff;
158
+ font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif;
159
+ overflow: hidden;
160
+ -webkit-tap-highlight-color: transparent;
161
+ }
162
+ /* connectedCallback holds this until document.fonts.ready (capped 2s) so
163
+ * the first visible paint has the deck's real typography + final rail
164
+ * layout. opacity (not visibility) so the active slide can't un-hide
165
+ * itself via the ::slotted([data-deck-active]) visibility:visible rule.
166
+ * Only the stage/rail hide — the black :host background stays, so the
167
+ * iframe doesn't flash the page's default white. */
168
+ :host([data-fonts-pending]) .stage,
169
+ :host([data-fonts-pending]) .rail { opacity: 0; pointer-events: none; }
170
+
171
+ .stage {
172
+ position: absolute;
173
+ inset: 0;
174
+ display: flex;
175
+ align-items: center;
176
+ justify-content: center;
177
+ }
178
+
179
+ .canvas {
180
+ position: relative;
181
+ transform-origin: center center;
182
+ flex-shrink: 0;
183
+ background: #fff;
184
+ will-change: transform;
185
+ }
186
+
187
+ /* Slides live in light DOM (via <slot>) so authored CSS still applies.
188
+ We absolutely position each slotted child to stack them. */
189
+ ::slotted(*) {
190
+ position: absolute !important;
191
+ inset: 0 !important;
192
+ width: 100% !important;
193
+ height: 100% !important;
194
+ box-sizing: border-box !important;
195
+ overflow: hidden;
196
+ opacity: 0;
197
+ pointer-events: none;
198
+ visibility: hidden;
199
+ }
200
+ ::slotted([data-deck-active]) {
201
+ opacity: 1;
202
+ pointer-events: auto;
203
+ visibility: visible;
204
+ }
205
+
206
+ .overlay {
207
+ position: fixed;
208
+ left: 50%;
209
+ bottom: 22px;
210
+ transform: translate(-50%, 6px) scale(0.92);
211
+ filter: blur(6px);
212
+ display: flex;
213
+ align-items: center;
214
+ gap: 4px;
215
+ padding: 4px;
216
+ background: #000;
217
+ color: #fff;
218
+ border-radius: 999px;
219
+ font-size: 12px;
220
+ font-feature-settings: "tnum" 1;
221
+ letter-spacing: 0.01em;
222
+ opacity: 0;
223
+ pointer-events: none;
224
+ transition: opacity 260ms ease, transform 260ms cubic-bezier(.2,.8,.2,1), filter 260ms ease;
225
+ transform-origin: center bottom;
226
+ z-index: 2147483000;
227
+ user-select: none;
228
+ }
229
+ .overlay[data-visible] {
230
+ opacity: 1;
231
+ pointer-events: auto;
232
+ transform: translate(-50%, 0) scale(1);
233
+ filter: blur(0);
234
+ }
235
+
236
+ .btn {
237
+ appearance: none;
238
+ -webkit-appearance: none;
239
+ background: transparent;
240
+ border: 0;
241
+ margin: 0;
242
+ padding: 0;
243
+ color: inherit;
244
+ font: inherit;
245
+ cursor: default;
246
+ display: inline-flex;
247
+ align-items: center;
248
+ justify-content: center;
249
+ height: 28px;
250
+ min-width: 28px;
251
+ border-radius: 999px;
252
+ color: rgba(255,255,255,0.72);
253
+ transition: background 140ms ease, color 140ms ease;
254
+ -webkit-tap-highlight-color: transparent;
255
+ }
256
+ .btn:hover { background: rgba(255,255,255,0.12); color: #fff; }
257
+ .btn:active { background: rgba(255,255,255,0.18); }
258
+ .btn:focus { outline: none; }
259
+ .btn:focus-visible { outline: none; }
260
+ .btn::-moz-focus-inner { border: 0; }
261
+ .btn svg { width: 14px; height: 14px; display: block; }
262
+ .btn.reset {
263
+ font-size: 11px;
264
+ font-weight: 500;
265
+ letter-spacing: 0.02em;
266
+ padding: 0 10px 0 12px;
267
+ gap: 6px;
268
+ color: rgba(255,255,255,0.72);
269
+ }
270
+ .btn.reset .kbd {
271
+ display: inline-flex;
272
+ align-items: center;
273
+ justify-content: center;
274
+ min-width: 16px;
275
+ height: 16px;
276
+ padding: 0 4px;
277
+ font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
278
+ font-size: 10px;
279
+ line-height: 1;
280
+ color: rgba(255,255,255,0.88);
281
+ background: rgba(255,255,255,0.12);
282
+ border-radius: 4px;
283
+ }
284
+
285
+ .count {
286
+ font-variant-numeric: tabular-nums;
287
+ color: #fff;
288
+ font-weight: 500;
289
+ padding: 0 8px;
290
+ min-width: 42px;
291
+ text-align: center;
292
+ font-size: 12px;
293
+ }
294
+ .count .sep { color: rgba(255,255,255,0.45); margin: 0 3px; font-weight: 400; }
295
+ .count .total { color: rgba(255,255,255,0.55); }
296
+
297
+ .divider {
298
+ width: 1px;
299
+ height: 14px;
300
+ background: rgba(255,255,255,0.18);
301
+ margin: 0 2px;
302
+ }
303
+
304
+ /* ── Thumbnail rail ──────────────────────────────────────────────────
305
+ Fixed column on the left; each thumbnail is a static deep-clone of
306
+ the light-DOM slide scaled into a 16:9 (or design-aspect) frame. The
307
+ stage re-fits around it (see _fit); hidden during present / noscale
308
+ / print so capture geometry and fullscreen output are unchanged. */
309
+ .rail {
310
+ position: fixed;
311
+ left: 0;
312
+ top: 0;
313
+ bottom: 0;
314
+ width: var(--deck-rail-w, 188px);
315
+ background: #141414;
316
+ border-right: 1px solid rgba(255,255,255,0.08);
317
+ overflow-y: auto;
318
+ overflow-x: hidden;
319
+ padding: 12px 10px;
320
+ box-sizing: border-box;
321
+ display: flex;
322
+ flex-direction: column;
323
+ gap: 12px;
324
+ z-index: 2147482500;
325
+ scrollbar-width: thin;
326
+ scrollbar-color: rgba(255,255,255,0.18) transparent;
327
+ }
328
+ .rail::-webkit-scrollbar { width: 8px; }
329
+ .rail::-webkit-scrollbar-track { background: transparent; margin: 2px; }
330
+ .rail::-webkit-scrollbar-thumb {
331
+ background: rgba(255,255,255,0.18);
332
+ border-radius: 4px;
333
+ border: 2px solid transparent;
334
+ background-clip: content-box;
335
+ }
336
+ .rail::-webkit-scrollbar-thumb:hover {
337
+ background: rgba(255,255,255,0.28);
338
+ border: 2px solid transparent;
339
+ background-clip: content-box;
340
+ }
341
+ :host([no-rail]) .rail,
342
+ :host([noscale]) .rail { display: none; }
343
+ .rail[data-presenting] { display: none; }
344
+ @media (max-width: 640px) {
345
+ .rail, .rail-resize { display: none; }
346
+ }
347
+ /* User-driven show/hide (the TweaksPanel toggle) slides instead of
348
+ popping. Transitions are gated on :host([data-rail-anim]) — set only
349
+ for the 200ms around the toggle — so window-resize and rail-width
350
+ drag (which also call _fit) don't lag behind the cursor. */
351
+ .rail[data-user-hidden] { transform: translateX(-100%); }
352
+ :host([data-rail-anim]) .rail { transition: transform 200ms cubic-bezier(.3,.7,.4,1); }
353
+ :host([data-rail-anim]) .stage { transition: left 200ms cubic-bezier(.3,.7,.4,1); }
354
+ :host([data-rail-anim]) .canvas { transition: transform 200ms cubic-bezier(.3,.7,.4,1); }
355
+ /* transition shorthand replaces rather than merges — repeat the base
356
+ .overlay opacity/transform/filter transitions so visibility changes
357
+ during the 200ms toggle window still fade instead of popping. */
358
+ :host([data-rail-anim]) .overlay {
359
+ transition: margin-left 200ms cubic-bezier(.3,.7,.4,1),
360
+ opacity 260ms ease,
361
+ transform 260ms cubic-bezier(.2,.8,.2,1),
362
+ filter 260ms ease;
363
+ }
364
+
365
+ .thumb {
366
+ position: relative;
367
+ display: flex;
368
+ align-items: flex-start;
369
+ gap: 8px;
370
+ cursor: pointer;
371
+ user-select: none;
372
+ }
373
+ .thumb .num {
374
+ width: 16px;
375
+ flex-shrink: 0;
376
+ font-size: 11px;
377
+ font-weight: 500;
378
+ text-align: right;
379
+ color: rgba(255,255,255,0.55);
380
+ padding-top: 2px;
381
+ font-variant-numeric: tabular-nums;
382
+ }
383
+ .thumb .frame {
384
+ position: relative;
385
+ flex: 1;
386
+ min-width: 0;
387
+ aspect-ratio: var(--deck-aspect);
388
+ background: #fff;
389
+ border-radius: 4px;
390
+ outline: 2px solid transparent;
391
+ outline-offset: 0;
392
+ overflow: hidden;
393
+ transition: outline-color 120ms ease;
394
+ }
395
+ .thumb:hover .frame { outline-color: rgba(255,255,255,0.25); }
396
+ .thumb { outline: none; }
397
+ .thumb:focus-visible .frame { outline-color: rgba(255,255,255,0.5); }
398
+ .thumb[data-selected] .num { color: #fff; }
399
+ .thumb[data-selected] .frame {
400
+ outline-color: rgba(217,119,87,0.65);
401
+ box-shadow: 0 0 0 4px rgba(217,119,87,0.18);
402
+ }
403
+ .thumb[data-current] .num { color: #fff; }
404
+ .thumb[data-current] .frame {
405
+ outline-color: #D97757;
406
+ box-shadow: 0 0 0 4px rgba(217,119,87,0.25);
407
+ }
408
+ .thumb[data-dragging] { opacity: 0.35; }
409
+ .thumb::before {
410
+ content: '';
411
+ position: absolute;
412
+ left: 24px;
413
+ right: 0;
414
+ height: 3px;
415
+ border-radius: 2px;
416
+ background: #D97757;
417
+ opacity: 0;
418
+ pointer-events: none;
419
+ }
420
+ .thumb[data-drop="before"]::before { top: -8px; opacity: 1; }
421
+ .thumb[data-drop="after"]::before { bottom: -8px; opacity: 1; }
422
+ .thumb[data-skip] .frame { opacity: 0.35; }
423
+ .thumb[data-skip] .frame::after {
424
+ content: 'Skipped';
425
+ position: absolute;
426
+ inset: 0;
427
+ display: flex;
428
+ align-items: center;
429
+ justify-content: center;
430
+ background: rgba(0,0,0,0.45);
431
+ color: #fff;
432
+ font-size: 10px;
433
+ font-weight: 500;
434
+ letter-spacing: 0.04em;
435
+ }
436
+
437
+ .ctxmenu {
438
+ position: fixed;
439
+ min-width: 150px;
440
+ padding: 4px;
441
+ background: #242424;
442
+ border: 1px solid rgba(255,255,255,0.12);
443
+ border-radius: 7px;
444
+ box-shadow: 0 8px 24px rgba(0,0,0,0.45);
445
+ z-index: 2147483100;
446
+ display: none;
447
+ font-size: 12px;
448
+ }
449
+ .ctxmenu[data-open] { display: block; }
450
+ .ctxmenu button {
451
+ display: block;
452
+ width: 100%;
453
+ appearance: none;
454
+ border: 0;
455
+ background: transparent;
456
+ color: #e8e8e8;
457
+ font: inherit;
458
+ text-align: left;
459
+ padding: 6px 10px;
460
+ border-radius: 4px;
461
+ cursor: pointer;
462
+ }
463
+ .ctxmenu button:hover:not(:disabled) { background: rgba(255,255,255,0.08); }
464
+ .ctxmenu button:disabled { opacity: 0.35; cursor: default; }
465
+ .ctxmenu hr {
466
+ border: 0;
467
+ border-top: 1px solid rgba(255,255,255,0.1);
468
+ margin: 4px 2px;
469
+ }
470
+
471
+ .rail-resize {
472
+ position: fixed;
473
+ left: calc(var(--deck-rail-w, 188px) - 3px);
474
+ top: 0;
475
+ bottom: 0;
476
+ width: 6px;
477
+ cursor: col-resize;
478
+ z-index: 2147482600;
479
+ touch-action: none;
480
+ }
481
+ .rail-resize:hover,
482
+ .rail-resize[data-dragging] { background: rgba(255,255,255,0.12); }
483
+ :host([no-rail]) .rail-resize,
484
+ :host([noscale]) .rail-resize,
485
+ .rail[data-presenting] + .rail-resize,
486
+ .rail[data-user-hidden] + .rail-resize { display: none; }
487
+
488
+ /* Delete-confirm popup — matches the SPA's ConfirmDialog layout
489
+ (title + message body, depressed footer with Cancel / Delete). */
490
+ .confirm-backdrop {
491
+ position: fixed;
492
+ inset: 0;
493
+ background: rgba(0,0,0,0.45);
494
+ z-index: 2147483200;
495
+ display: none;
496
+ align-items: center;
497
+ justify-content: center;
498
+ }
499
+ .confirm-backdrop[data-open] { display: flex; }
500
+ .confirm {
501
+ width: 320px;
502
+ max-width: calc(100vw - 32px);
503
+ background: #2a2a2a;
504
+ color: #e8e8e8;
505
+ border: 1px solid rgba(255,255,255,0.12);
506
+ border-radius: 12px;
507
+ box-shadow: 0 12px 32px rgba(0,0,0,0.5);
508
+ overflow: hidden;
509
+ font-family: inherit;
510
+ animation: deck-confirm-in 0.18s ease;
511
+ }
512
+ @keyframes deck-confirm-in {
513
+ from { opacity: 0; transform: scale(0.96); }
514
+ to { opacity: 1; transform: scale(1); }
515
+ }
516
+ .confirm .body { padding: 20px 20px 16px; }
517
+ .confirm .title { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
518
+ .confirm .msg { font-size: 13px; line-height: 1.5; color: rgba(255,255,255,0.65); }
519
+ .confirm .footer {
520
+ padding: 14px 20px;
521
+ background: #1f1f1f;
522
+ border-top: 1px solid rgba(255,255,255,0.08);
523
+ display: flex;
524
+ justify-content: flex-end;
525
+ gap: 8px;
526
+ }
527
+ .confirm button {
528
+ appearance: none;
529
+ font: inherit;
530
+ font-size: 13px;
531
+ font-weight: 500;
532
+ padding: 8px 16px;
533
+ border-radius: 8px;
534
+ cursor: pointer;
535
+ }
536
+ .confirm .cancel {
537
+ background: transparent;
538
+ border: 0;
539
+ color: rgba(255,255,255,0.8);
540
+ }
541
+ .confirm .cancel:hover { background: rgba(255,255,255,0.08); }
542
+ .confirm .danger {
543
+ background: #c96442;
544
+ border: 1px solid rgba(0,0,0,0.15);
545
+ color: #fff;
546
+ box-shadow: 0 1px 3px rgba(166,50,68,0.3), 0 2px 6px rgba(166,50,68,0.18);
547
+ }
548
+ .confirm .danger:hover { background: #b5563a; }
549
+
550
+ /* ── Print: one page per slide, no chrome ────────────────────────────
551
+ The screen layout stacks every slide at inset:0 inside a scaled
552
+ canvas; for print we want them in document flow at the authored
553
+ design size so the browser paginates one slide per sheet. The
554
+ @page size is set from the width/height attributes via the inline
555
+ <style id="deck-stage-print-page"> that _syncPrintPageRule appends
556
+ to the document (the @page at-rule has no effect inside shadow DOM). */
557
+ @media print {
558
+ :host {
559
+ position: static;
560
+ inset: auto;
561
+ background: none;
562
+ overflow: visible;
563
+ color: inherit;
564
+ }
565
+ .stage { position: static; display: block; }
566
+ .canvas {
567
+ transform: none !important;
568
+ width: auto !important;
569
+ height: auto !important;
570
+ background: none;
571
+ will-change: auto;
572
+ }
573
+ ::slotted(*) {
574
+ position: relative !important;
575
+ inset: auto !important;
576
+ width: var(--deck-design-w) !important;
577
+ height: var(--deck-design-h) !important;
578
+ box-sizing: border-box !important;
579
+ opacity: 1 !important;
580
+ visibility: visible !important;
581
+ pointer-events: auto;
582
+ break-after: page;
583
+ page-break-after: always;
584
+ break-inside: avoid;
585
+ overflow: hidden;
586
+ }
587
+ /* :last-child alone isn't enough once data-deck-skip hides the
588
+ trailing slide(s) — the last *visible* slide still carries
589
+ break-after:page and prints a blank sheet. _markLastVisible()
590
+ maintains data-deck-last-visible on the last non-skipped slide. */
591
+ ::slotted(*:last-child),
592
+ ::slotted([data-deck-last-visible]) {
593
+ break-after: auto;
594
+ page-break-after: auto;
595
+ }
596
+ ::slotted([data-deck-skip]) { display: none !important; }
597
+ .overlay, .rail, .rail-resize, .ctxmenu, .confirm-backdrop { display: none !important; }
598
+ }
599
+ `;
600
+
601
+ class DeckStage extends HTMLElement {
602
+ static get observedAttributes() { return ['width', 'height', 'noscale', 'no-rail']; }
603
+
604
+ constructor() {
605
+ super();
606
+ this._root = this.attachShadow({ mode: 'open' });
607
+ this._index = 0;
608
+ this._slides = [];
609
+ // Explicit multi-selection (slide elements). Empty means the
610
+ // selection is implicitly the current slide, so Delete always has
611
+ // a well-defined target while the rail has focus.
612
+ this._selected = new Set();
613
+ this._selAnchor = null;
614
+ this._notes = [];
615
+ this._hideTimer = null;
616
+ this._mouseIdleTimer = null;
617
+ this._menuIndex = -1;
618
+
619
+ this._onKey = this._onKey.bind(this);
620
+ this._onResize = this._onResize.bind(this);
621
+ this._onSlotChange = this._onSlotChange.bind(this);
622
+ this._onMouseMove = this._onMouseMove.bind(this);
623
+ this._onTap = this._onTap.bind(this);
624
+ this._onMessage = this._onMessage.bind(this);
625
+ // Capture-phase close so a click anywhere dismisses the menu, but
626
+ // ignore clicks that land inside the menu itself — otherwise the
627
+ // capture handler runs before the menu's own (bubble) handler and
628
+ // clears _menuIndex out from under it.
629
+ this._onDocClick = (e) => {
630
+ if (this._menu && e.composedPath && e.composedPath().includes(this._menu)) return;
631
+ this._closeMenu();
632
+ };
633
+ }
634
+
635
+ get designWidth() {
636
+ return parseInt(this.getAttribute('width'), 10) || DESIGN_W_DEFAULT;
637
+ }
638
+ get designHeight() {
639
+ return parseInt(this.getAttribute('height'), 10) || DESIGN_H_DEFAULT;
640
+ }
641
+
642
+ connectedCallback() {
643
+ // Presenter-view popup loads deckUrl?_snthumb=...#N for its prev/cur/
644
+ // next thumbnails — the rail has no business rendering inside those
645
+ // (wrong scale, and it offsets the stage so the thumb shows a gutter).
646
+ if (/[?&]_snthumb=/.test(location.search)) this.setAttribute('no-rail', '');
647
+ this._render();
648
+ this._loadNotes();
649
+ this._syncPrintPageRule();
650
+ this._ensureTextWrapDefaults();
651
+ window.addEventListener('keydown', this._onKey);
652
+ window.addEventListener('resize', this._onResize);
653
+ window.addEventListener('mousemove', this._onMouseMove, { passive: true });
654
+ window.addEventListener('message', this._onMessage);
655
+ window.addEventListener('click', this._onDocClick, true);
656
+ this.addEventListener('click', this._onTap);
657
+ // Print lays every slide out as its own page, so [data-deck-active]-
658
+ // gated entrance styles need the attribute on every slide (not just
659
+ // the current one) or their content prints at the hidden base style.
660
+ // The transient freeze style lands BEFORE the attributes so any
661
+ // attribute-keyed transition fires at 0s (changing transition-
662
+ // duration after a transition has started doesn't affect it).
663
+ this._onBeforePrint = () => {
664
+ this._syncPrintPageRule();
665
+ if (this._freezeStyle) this._freezeStyle.remove();
666
+ this._freezeStyle = document.createElement('style');
667
+ this._freezeStyle.textContent = '*,*::before,*::after{transition-duration:0s !important}';
668
+ document.head.appendChild(this._freezeStyle);
669
+ this._slides.forEach((s) => s.setAttribute('data-deck-active', ''));
670
+ };
671
+ this._onAfterPrint = () => {
672
+ this._applyIndex({ showOverlay: false, broadcast: false });
673
+ if (this._freezeStyle) { this._freezeStyle.remove(); this._freezeStyle = null; }
674
+ };
675
+ window.addEventListener('beforeprint', this._onBeforePrint);
676
+ window.addEventListener('afterprint', this._onAfterPrint);
677
+ // Initial collection + layout happens via slotchange, which fires on mount.
678
+ this._enableRail();
679
+ // Hold the stage hidden until webfonts are ready so the first visible
680
+ // paint has the deck's real typography — the :not(:defined) guard in
681
+ // the page HTML only covers custom-element upgrade, not font load.
682
+ // Capped so a 404'd font URL can't blank the deck indefinitely.
683
+ this.setAttribute('data-fonts-pending', '');
684
+ const reveal = () => this.removeAttribute('data-fonts-pending');
685
+ // Unconditional cap — rAF can be suspended in a hidden iframe, which
686
+ // would strand the one inside the rAF callback.
687
+ setTimeout(reveal, 2000);
688
+ // rAF first: fonts.ready is a pre-resolved promise until layout has
689
+ // resolved the slotted text's font-family and pushed a FontFace into
690
+ // 'loading'. Reading it here in connectedCallback (parse-time) would
691
+ // settle the race in a microtask before any font fetch starts.
692
+ requestAnimationFrame(() => {
693
+ Promise.race([
694
+ document.fonts ? document.fonts.ready : Promise.resolve(),
695
+ new Promise((r) => setTimeout(r, 2000)),
696
+ ]).then(reveal, reveal);
697
+ });
698
+ }
699
+
700
+ _enableRail() {
701
+ // Idempotent — older host builds still post __omelette_rail_enabled.
702
+ // no-rail guard keeps the observers/stylesheet walk off the cheap path
703
+ // for presenter-popup thumbnail iframes (up to 9 per view).
704
+ if (this._railEnabled || this.hasAttribute('no-rail')) return;
705
+ this._railEnabled = true;
706
+ // Per-viewer preference — restored alongside rail width. Default on;
707
+ // only a stored '0' (from the TweaksPanel toggle) hides it.
708
+ this._railVisible = true;
709
+ try {
710
+ if (localStorage.getItem('deck-stage.railVisible') === '0') this._railVisible = false;
711
+ } catch (e) {}
712
+ // Live thumbnail updates: watch the light-DOM slides for content
713
+ // edits and re-clone just the affected thumb(s), debounced. Ignore
714
+ // the data-deck-* / data-screen-label / data-om-validate attributes
715
+ // this component itself writes so nav doesn't trigger spurious
716
+ // refreshes — except data-deck-skip, which now arrives from the host
717
+ // re-render and is what updates the rail badge, print bookkeeping,
718
+ // and deckSkipped re-broadcast.
719
+ const OWN_ATTRS = /^data-(deck-(?!skip$)|screen-label$|om-validate$)/;
720
+ this._liveDirty = new Set();
721
+ this._liveObserver = new MutationObserver((records) => {
722
+ for (const r of records) {
723
+ if (r.type === 'attributes' && OWN_ATTRS.test(r.attributeName || '')) continue;
724
+ let n = r.target;
725
+ while (n && n.parentElement !== this) n = n.parentElement;
726
+ // Skip/unskip is handled below without re-cloning (the badge sits
727
+ // on the thumb wrapper, not the clone) — don't mark the slide
728
+ // dirty for an attr change whose only visible effect is the badge.
729
+ if (n && this._slideSet && this._slideSet.has(n)
730
+ && !(r.type === 'attributes' && r.attributeName === 'data-deck-skip')) {
731
+ this._liveDirty.add(n);
732
+ }
733
+ // Host-driven skip toggle: sync the rail badge + print + presenter
734
+ // skipped-list the way _toggleSkip used to do locally.
735
+ if (r.type === 'attributes' && r.attributeName === 'data-deck-skip'
736
+ && n && this._slideSet && this._slideSet.has(n)) {
737
+ const i = this._slides.indexOf(n);
738
+ if (this._thumbs && this._thumbs[i]) {
739
+ if (n.hasAttribute('data-deck-skip')) this._thumbs[i].thumb.setAttribute('data-skip', '');
740
+ else this._thumbs[i].thumb.removeAttribute('data-skip');
741
+ }
742
+ this._markLastVisible();
743
+ try { window.postMessage({ slideIndexChanged: this._index, deckTotal: this._slides.length, deckSkipped: this._skippedIndices() }, '*'); } catch (e) {}
744
+ }
745
+ }
746
+ if (this._liveDirty.size && !this._liveTimer) {
747
+ this._liveTimer = setTimeout(() => {
748
+ this._liveTimer = null;
749
+ this._liveDirty.forEach((s) => this._refreshThumb(s));
750
+ this._liveDirty.clear();
751
+ }, 200);
752
+ }
753
+ });
754
+ this._liveObserver.observe(this, {
755
+ subtree: true, childList: true, characterData: true, attributes: true});
756
+ // Lazy thumbnail materialization — clone the slide only when its
757
+ // frame scrolls into (or near) the rail viewport. rootMargin gives
758
+ // ~4 thumbs of pre-load so fast scrolling doesn't flash blanks.
759
+ this._railObserver = new IntersectionObserver((entries) => {
760
+ entries.forEach((e) => {
761
+ if (e.isIntersecting && e.target.__deckThumb) {
762
+ this._materialize(e.target.__deckThumb);
763
+ }
764
+ });
765
+ }, { root: this._rail, rootMargin: '400px 0px' });
766
+ // Tweaks typically change CSS vars / attrs OUTSIDE <deck-stage>
767
+ // (on <html>, <body>, a wrapper div, or a <style> tag), which
768
+ // _liveObserver can't see. Re-snapshot author CSS (constructable
769
+ // sheet is shared by reference, so one replaceSync updates every
770
+ // thumb shadow root) and re-sync each thumb host's attrs + custom
771
+ // properties. In-slide DOM mutations are _liveObserver's job.
772
+ // Debounced so slider drags don't thrash.
773
+ this._onTweakChange = () => {
774
+ clearTimeout(this._tweakTimer);
775
+ this._tweakTimer = setTimeout(() => {
776
+ this._snapshotAuthorCss();
777
+ // One getComputedStyle for the whole batch — each
778
+ // getPropertyValue read below reuses the same computed style
779
+ // as long as nothing invalidates layout between thumbs.
780
+ const cs = getComputedStyle(this);
781
+ (this._thumbs || []).forEach((t) => {
782
+ if (t.host) this._syncThumbHostAttrs(t.host, cs);
783
+ });
784
+ }, 120);
785
+ };
786
+ window.addEventListener('tweakchange', this._onTweakChange);
787
+ // Stylesheets that finish loading AFTER the snapshot below never
788
+ // reach the thumbs on their own: a still-pending <link> contributes
789
+ // nothing to document.styleSheets, and nothing re-reads it on load,
790
+ // so the live slides restyle while every clone keeps the stale
791
+ // sheet. dc-runtime's helmet mounts design-system <link>s at render
792
+ // time, so a deck-stage that connects first snapshots before that
793
+ // CSS exists. Funnel late arrivals into the same debounced resync:
794
+ // hook load/error on every current <link>, and watch <head> for
795
+ // links and styles mounted or rewritten later. Deliberately not
796
+ // rAF- or fonts.ready-driven — rAF is throttled/suspended in hidden
797
+ // iframes (thumbnail/presenter contexts), and a font-file load
798
+ // doesn't change cssRules, so it needs no resync.
799
+ this._hookedLinks = [];
800
+ this._hookSheetLoad = (el) => {
801
+ if (!el.matches || !el.matches('link[rel~="stylesheet" i]')) return;
802
+ if (this._hookedLinks.indexOf(el) !== -1) return;
803
+ this._hookedLinks.push(el);
804
+ el.addEventListener('load', this._onTweakChange);
805
+ el.addEventListener('error', this._onTweakChange);
806
+ };
807
+ document.querySelectorAll('link[rel~="stylesheet" i]')
808
+ .forEach(this._hookSheetLoad);
809
+ this._headObserver = new MutationObserver((records) => {
810
+ let resync = false;
811
+ for (const r of records) {
812
+ if (r.type === 'characterData') {
813
+ // Only <style> text is CSS — a ticking <title> shouldn't
814
+ // wake the resync forever.
815
+ const p = r.target.parentNode;
816
+ if (p && p.nodeName === 'STYLE') resync = true;
817
+ continue;
818
+ }
819
+ if (r.type === 'attributes') {
820
+ // A late rel/href rewrite turns an inert <link> into a
821
+ // stylesheet (hook it; its load fires even on cache hits);
822
+ // a media/disabled flip changes effective rules with no
823
+ // event. Resync only if this link is or ever was a
824
+ // stylesheet — favicon/preload/canonical href churn isn't
825
+ // a resync.
826
+ if (r.target.nodeName === 'LINK') {
827
+ this._hookSheetLoad(r.target);
828
+ if (this._hookedLinks.indexOf(r.target) !== -1) resync = true;
829
+ } else if (r.target.nodeName === 'STYLE') resync = true;
830
+ continue;
831
+ }
832
+ // childList: only links and styles carry CSS. A new <link> has
833
+ // no rules until it loads — hook it rather than resync now; a
834
+ // <style> mount/unmount or text-node swap takes effect
835
+ // immediately. _freezeStyle (our beforeprint helper) is skipped
836
+ // on add only — no removal-side guard: _onAfterPrint nulls the
837
+ // ref before the observer fires, so that check would be dead;
838
+ // the one debounced no-op resync per print is harmless.
839
+ if (r.target.nodeName === 'STYLE') resync = true;
840
+ for (const n of r.addedNodes) {
841
+ if (n.nodeName === 'LINK') this._hookSheetLoad(n);
842
+ else if (n.nodeName === 'STYLE' && n !== this._freezeStyle) resync = true;
843
+ }
844
+ for (const n of r.removedNodes) {
845
+ if (n.nodeName === 'LINK') {
846
+ const hi = this._hookedLinks.indexOf(n);
847
+ if (hi !== -1) {
848
+ this._hookedLinks.splice(hi, 1);
849
+ n.removeEventListener('load', this._onTweakChange);
850
+ n.removeEventListener('error', this._onTweakChange);
851
+ resync = true;
852
+ }
853
+ } else if (n.nodeName === 'STYLE') resync = true;
854
+ }
855
+ }
856
+ if (resync) this._onTweakChange();
857
+ });
858
+ this._headObserver.observe(document.head, {
859
+ childList: true, subtree: true, characterData: true,
860
+ attributes: true, attributeFilter: ['rel', 'href', 'media', 'disabled']});
861
+ this._snapshotAuthorCss();
862
+ // Re-snapshot once any still-loading stylesheet settles — it throws on
863
+ // .cssRules above and silently contributes '' → unstyled thumbs on a
864
+ // cold mount. {once:true}; routed through the debounced handler.
865
+ document.querySelectorAll('link[rel~="stylesheet"]').forEach((l) => {
866
+ try { if (l.sheet && l.sheet.cssRules) return; } catch (e) {}
867
+ l.addEventListener('load', this._onTweakChange, { once: true });
868
+ l.addEventListener('error', this._onTweakChange, { once: true });
869
+ });
870
+ if (document.fonts) document.fonts.ready.then(this._onTweakChange, this._onTweakChange);
871
+ // Build the rail now that it's enabled — slotchange already fired,
872
+ // so _renderRail's early-return skipped the initial build.
873
+ this._syncRailHidden();
874
+ this._renderRail();
875
+ this._fit();
876
+ }
877
+
878
+ /** Snapshot document stylesheets into a constructable sheet that each
879
+ * thumbnail's nested shadow root adopts — so author CSS styles the
880
+ * cloned slide content without touching this component's chrome.
881
+ * Cross-origin sheets throw on .cssRules — skip them. Re-callable:
882
+ * the existing constructable sheet is reused via replaceSync so every
883
+ * already-adopted shadow root picks up the fresh CSS without re-adopt. */
884
+ _snapshotAuthorCss() {
885
+ // :root in an adopted sheet inside a shadow root matches nothing
886
+ // (only the document root qualifies), so author rules like
887
+ // `:root[data-voice="modern"] .serif` never reach the clones.
888
+ // Rewrite :root → :host and mirror <html>'s data-*/class/lang onto
889
+ // each thumb host (see _syncThumbHostAttrs) so the same selectors
890
+ // match inside the thumbnail's shadow tree.
891
+ const authorCss = Array.from(document.styleSheets).map((sh) => {
892
+ try {
893
+ return Array.from(sh.cssRules).map((r) => r.cssText).join('\n');
894
+ } catch (e) { return ''; }
895
+ }).join('\n')
896
+ // The shadow host is featureless outside the functional :host(...)
897
+ // form, so any compound on :root — [attr], .class, #id, :pseudo —
898
+ // must become :host(<compound>) not :host<compound>. Same for the
899
+ // html type selector (Tailwind class-strategy dark mode emits
900
+ // html.dark; Pico uses html[data-theme]), which has nothing to
901
+ // match inside the thumb's shadow tree.
902
+ .replace(/:root((?:\[[^\]]*\]|[.#][-\w]+|:[-\w]+(?:\([^)]*\))?)+)/g, ':host($1)')
903
+ .replace(/:root\b/g, ':host')
904
+ .replace(/(^|[\s,>~+(}])html((?:\[[^\]]*\]|[.#][-\w]+|:[-\w]+(?:\([^)]*\))?)+)(?![-\w])/g, '$1:host($2)')
905
+ .replace(/(^|[\s,>~+(}])html(?![-\w])/g, '$1:host');
906
+ // Every custom property the author references. _syncThumbHostAttrs
907
+ // mirrors each one's *computed* value at <deck-stage> onto the
908
+ // thumb host so the live value wins over the :host default above
909
+ // regardless of which ancestor the tweak wrote to (<html>, <body>,
910
+ // a wrapper div, or the deck-stage element itself all inherit
911
+ // down to getComputedStyle(this)).
912
+ this._authorVars = new Set(authorCss.match(/--[\w-]+/g) || []);
913
+ try {
914
+ if (!this._adoptedSheet) this._adoptedSheet = new CSSStyleSheet();
915
+ this._adoptedSheet.replaceSync(authorCss);
916
+ } catch (e) {
917
+ this._adoptedSheet = null;
918
+ this._authorCss = authorCss;
919
+ }
920
+ }
921
+
922
+ _syncThumbHostAttrs(host, cs) {
923
+ const de = document.documentElement;
924
+ // setAttribute overwrites but can't delete — an attr removed from
925
+ // <html> (toggleAttribute off, classList emptied) would linger on
926
+ // the host and :host([data-*]) / :host(.foo) rules would keep
927
+ // matching. Remove stale mirrored attrs first; iterate backward
928
+ // because removeAttribute mutates the live NamedNodeMap.
929
+ for (let i = host.attributes.length - 1; i >= 0; i--) {
930
+ const n = host.attributes[i].name;
931
+ if ((n.startsWith('data-') || n === 'class' || n === 'lang')
932
+ && !de.hasAttribute(n)) {
933
+ host.removeAttribute(n);
934
+ }
935
+ }
936
+ for (const a of de.attributes) {
937
+ if (a.name.startsWith('data-') || a.name === 'class' || a.name === 'lang') {
938
+ host.setAttribute(a.name, a.value);
939
+ }
940
+ }
941
+ // The :root→:host rewrite in _snapshotAuthorCss pins each custom
942
+ // property to its stylesheet default on the thumb host, shadowing
943
+ // the live value that would otherwise inherit. Tweaks can write the
944
+ // live value on any ancestor — <html>, <body>, a wrapper div, the
945
+ // deck-stage element — so read it as the *computed* value at
946
+ // <deck-stage> (which sees the whole inheritance chain) rather than
947
+ // trying to guess which element the author wrote to. Inline on the
948
+ // host beats the :host{} rule. remove-stale covers vars dropped
949
+ // from the stylesheet between snapshots.
950
+ const vars = this._authorVars || new Set();
951
+ for (let i = host.style.length - 1; i >= 0; i--) {
952
+ const p = host.style[i];
953
+ if (p.startsWith('--') && !vars.has(p)) host.style.removeProperty(p);
954
+ }
955
+ const live = cs || getComputedStyle(this);
956
+ vars.forEach((p) => {
957
+ const v = live.getPropertyValue(p);
958
+ if (v) host.style.setProperty(p, v.trim());
959
+ else host.style.removeProperty(p);
960
+ });
961
+ }
962
+
963
+ disconnectedCallback() {
964
+ window.removeEventListener('keydown', this._onKey);
965
+ window.removeEventListener('resize', this._onResize);
966
+ window.removeEventListener('mousemove', this._onMouseMove);
967
+ window.removeEventListener('message', this._onMessage);
968
+ window.removeEventListener('click', this._onDocClick, true);
969
+ window.removeEventListener('beforeprint', this._onBeforePrint);
970
+ window.removeEventListener('afterprint', this._onAfterPrint);
971
+ if (this._freezeStyle) { this._freezeStyle.remove(); this._freezeStyle = null; }
972
+ this.removeEventListener('click', this._onTap);
973
+ if (this._hideTimer) clearTimeout(this._hideTimer);
974
+ if (this._mouseIdleTimer) clearTimeout(this._mouseIdleTimer);
975
+ if (this._liveTimer) clearTimeout(this._liveTimer);
976
+ if (this._tweakTimer) clearTimeout(this._tweakTimer);
977
+ if (this._railAnimTimer) clearTimeout(this._railAnimTimer);
978
+ if (this._scaleRaf) cancelAnimationFrame(this._scaleRaf);
979
+ if (this._liveObserver) this._liveObserver.disconnect();
980
+ if (this._railObserver) this._railObserver.disconnect();
981
+ if (this._headObserver) this._headObserver.disconnect();
982
+ (this._hookedLinks || []).forEach((l) => {
983
+ l.removeEventListener('load', this._onTweakChange);
984
+ l.removeEventListener('error', this._onTweakChange);
985
+ });
986
+ this._hookedLinks = [];
987
+ if (this._onTweakChange) window.removeEventListener('tweakchange', this._onTweakChange);
988
+ // Drop the text-wrap defaults when the last deck-stage leaves, so a
989
+ // deleted deck's typography can't restyle whatever replaces it.
990
+ // (#deck-stage-print-page keeps its existing keep-forever lifecycle.)
991
+ if (!document.querySelector('deck-stage')) {
992
+ const tw = document.getElementById('deck-stage-text-wrap');
993
+ if (tw) tw.remove();
994
+ }
995
+ }
996
+
997
+ attributeChangedCallback() {
998
+ if (this._canvas) {
999
+ this._canvas.style.width = this.designWidth + 'px';
1000
+ this._canvas.style.height = this.designHeight + 'px';
1001
+ this._canvas.style.setProperty('--deck-design-w', this.designWidth + 'px');
1002
+ this._canvas.style.setProperty('--deck-design-h', this.designHeight + 'px');
1003
+ if (this._rail) {
1004
+ this._rail.style.setProperty('--deck-aspect', this.designWidth + '/' + this.designHeight);
1005
+ }
1006
+ this._fit();
1007
+ this._scaleThumbs();
1008
+ this._syncPrintPageRule();
1009
+ }
1010
+ }
1011
+
1012
+ _render() {
1013
+ const style = document.createElement('style');
1014
+ style.textContent = stylesheet;
1015
+
1016
+ const stage = document.createElement('div');
1017
+ stage.className = 'stage';
1018
+
1019
+ const canvas = document.createElement('div');
1020
+ canvas.className = 'canvas';
1021
+ canvas.style.width = this.designWidth + 'px';
1022
+ canvas.style.height = this.designHeight + 'px';
1023
+ canvas.style.setProperty('--deck-design-w', this.designWidth + 'px');
1024
+ canvas.style.setProperty('--deck-design-h', this.designHeight + 'px');
1025
+
1026
+ const slot = document.createElement('slot');
1027
+ slot.addEventListener('slotchange', this._onSlotChange);
1028
+ canvas.appendChild(slot);
1029
+ stage.appendChild(canvas);
1030
+
1031
+ // Overlay: compact, solid black, with clickable controls.
1032
+ const overlay = document.createElement('div');
1033
+ overlay.className = 'overlay export-hidden';
1034
+ overlay.setAttribute('role', 'toolbar');
1035
+ overlay.setAttribute('aria-label', 'Deck controls');
1036
+ overlay.setAttribute('data-omelette-chrome', '');
1037
+ overlay.innerHTML = `
1038
+ <button class="btn prev" type="button" aria-label="Previous slide" title="Previous (←)">
1039
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 3L5 8l5 5"/></svg>
1040
+ </button>
1041
+ <span class="count" aria-live="polite"><span class="current">1</span><span class="sep">/</span><span class="total">1</span></span>
1042
+ <button class="btn next" type="button" aria-label="Next slide" title="Next (→)">
1043
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 3l5 5-5 5"/></svg>
1044
+ </button>
1045
+ <span class="divider"></span>
1046
+ <button class="btn reset" type="button" aria-label="Reset to first slide" title="Reset (R)">Reset<span class="kbd">R</span></button>
1047
+ `;
1048
+
1049
+ overlay.querySelector('.prev').addEventListener('click', () => this._advance(-1, 'click'));
1050
+ overlay.querySelector('.next').addEventListener('click', () => this._advance(1, 'click'));
1051
+ overlay.querySelector('.reset').addEventListener('click', () => this._go(0, 'click'));
1052
+
1053
+ // Thumbnail rail + context menu. Thumbnails are populated in
1054
+ // _renderRail() after _collectSlides().
1055
+ const rail = document.createElement('div');
1056
+ rail.className = 'rail export-hidden';
1057
+ rail.setAttribute('data-omelette-chrome', '');
1058
+ // Edit mode hooks wheel to pan the canvas; this opts the rail's own
1059
+ // scrollview out so thumbnails stay scrollable while editing.
1060
+ rail.setAttribute('data-dc-wheel-passthru', '');
1061
+ rail.style.setProperty('--deck-aspect', this.designWidth + '/' + this.designHeight);
1062
+ // Edge auto-scroll while dragging a thumb near the rail's top/bottom
1063
+ // so off-screen drop targets are reachable. Native dragover fires
1064
+ // continuously while the pointer is stationary, so a per-event nudge
1065
+ // (ramped by edge proximity) is enough — no rAF loop needed.
1066
+ rail.addEventListener('dragover', (e) => {
1067
+ if (this._dragFrom == null) return;
1068
+ const r = rail.getBoundingClientRect();
1069
+ const EDGE = 40;
1070
+ const dt = e.clientY - r.top;
1071
+ const db = r.bottom - e.clientY;
1072
+ if (dt < EDGE) rail.scrollTop -= Math.ceil((EDGE - dt) / 3);
1073
+ else if (db < EDGE) rail.scrollTop += Math.ceil((EDGE - db) / 3);
1074
+ });
1075
+
1076
+ const menu = document.createElement('div');
1077
+ menu.className = 'ctxmenu export-hidden';
1078
+ menu.setAttribute('data-omelette-chrome', '');
1079
+ menu.innerHTML = `
1080
+ <button type="button" data-act="skip">Skip slide</button>
1081
+ <button type="button" data-act="up">Move up</button>
1082
+ <button type="button" data-act="down">Move down</button>
1083
+ <button type="button" data-act="duplicate">Duplicate slide</button>
1084
+ <hr>
1085
+ <button type="button" data-act="delete">Delete slide</button>
1086
+ `;
1087
+ menu.addEventListener('click', (e) => {
1088
+ const act = e.target && e.target.getAttribute && e.target.getAttribute('data-act');
1089
+ if (!act) return;
1090
+ const i = this._menuIndex;
1091
+ const list = this._menuIndices;
1092
+ this._closeMenu();
1093
+ if (act === 'skip') this._toggleSkip(i);
1094
+ else if (act === 'up') this._moveSlide(i, i - 1);
1095
+ else if (act === 'down') this._moveSlide(i, i + 1);
1096
+ else if (act === 'duplicate') this._duplicateSlide(i);
1097
+ else if (act === 'delete') this._openConfirm(list && list.length ? list : [i]);
1098
+ });
1099
+ menu.addEventListener('contextmenu', (e) => e.preventDefault());
1100
+
1101
+ // Rail resize handle — drag to set --deck-rail-w, persisted to
1102
+ // localStorage so the width survives reloads.
1103
+ const resize = document.createElement('div');
1104
+ resize.className = 'rail-resize export-hidden';
1105
+ resize.setAttribute('data-omelette-chrome', '');
1106
+ resize.addEventListener('pointerdown', (e) => {
1107
+ e.preventDefault();
1108
+ resize.setPointerCapture(e.pointerId);
1109
+ resize.setAttribute('data-dragging', '');
1110
+ const move = (ev) => this._setRailWidth(ev.clientX);
1111
+ const up = () => {
1112
+ resize.removeEventListener('pointermove', move);
1113
+ resize.removeEventListener('pointerup', up);
1114
+ resize.removeEventListener('pointercancel', up);
1115
+ resize.removeAttribute('data-dragging');
1116
+ try { localStorage.setItem('deck-stage.railWidth', String(this._railPx)); } catch (err) {}
1117
+ };
1118
+ resize.addEventListener('pointermove', move);
1119
+ resize.addEventListener('pointerup', up);
1120
+ resize.addEventListener('pointercancel', up);
1121
+ });
1122
+
1123
+ // Delete-confirm dialog — mirrors the SPA's ConfirmDialog layout.
1124
+ const confirm = document.createElement('div');
1125
+ confirm.className = 'confirm-backdrop export-hidden';
1126
+ confirm.setAttribute('data-omelette-chrome', '');
1127
+ confirm.innerHTML = `
1128
+ <div class="confirm" role="dialog" aria-modal="true">
1129
+ <div class="body">
1130
+ <div class="title">Delete slide?</div>
1131
+ <div class="msg">This slide will be removed from the deck.</div>
1132
+ </div>
1133
+ <div class="footer">
1134
+ <button type="button" class="cancel">Cancel</button>
1135
+ <button type="button" class="danger">Delete</button>
1136
+ </div>
1137
+ </div>
1138
+ `;
1139
+ confirm.addEventListener('click', (e) => {
1140
+ if (e.target === confirm) {
1141
+ this._closeConfirm();
1142
+ this._focusCurrentThumb();
1143
+ }
1144
+ });
1145
+ confirm.querySelector('.cancel').addEventListener('click', () => {
1146
+ this._closeConfirm();
1147
+ this._focusCurrentThumb();
1148
+ });
1149
+ confirm.querySelector('.danger').addEventListener('click', () => {
1150
+ // Re-resolve at click time — the elements are the user's actual
1151
+ // selection; their indices may have shifted since confirm-open.
1152
+ const list = (this._confirmEls || [])
1153
+ .map((el) => this._slides.indexOf(el))
1154
+ .filter((i) => i >= 0);
1155
+ this._closeConfirm();
1156
+ this._deleteSlides(list);
1157
+ this._focusCurrentThumb();
1158
+ });
1159
+
1160
+ this._root.append(style, rail, resize, stage, overlay, menu, confirm);
1161
+ this._canvas = canvas;
1162
+ this._stage = stage;
1163
+ this._slot = slot;
1164
+ this._overlay = overlay;
1165
+ this._rail = rail;
1166
+ this._resize = resize;
1167
+ this._menu = menu;
1168
+ this._confirm = confirm;
1169
+ this._countEl = overlay.querySelector('.current');
1170
+ this._totalEl = overlay.querySelector('.total');
1171
+
1172
+ // Restore persisted rail width.
1173
+ let rw = 188;
1174
+ try {
1175
+ const s = localStorage.getItem('deck-stage.railWidth');
1176
+ if (s) rw = parseInt(s, 10) || rw;
1177
+ } catch (err) {}
1178
+ this._setRailWidth(rw);
1179
+ this._syncRailHidden();
1180
+ }
1181
+
1182
+ _setRailWidth(px) {
1183
+ const w = Math.max(120, Math.min(360, Math.round(px)));
1184
+ this._railPx = w;
1185
+ this.style.setProperty('--deck-rail-w', w + 'px');
1186
+ this._fit();
1187
+ // _scaleThumbs forces a sync layout (frame.offsetWidth) then writes
1188
+ // N transforms. During a resize drag this runs per-pointermove;
1189
+ // coalesce to one per frame.
1190
+ if (!this._scaleRaf) {
1191
+ this._scaleRaf = requestAnimationFrame(() => {
1192
+ this._scaleRaf = null;
1193
+ this._scaleThumbs();
1194
+ });
1195
+ }
1196
+ }
1197
+
1198
+ /** @page must live in the document stylesheet — it's a no-op inside
1199
+ * shadow DOM. (Re-)append so any author @page landing later in
1200
+ * source order can't reintroduce a margin and push each slide onto
1201
+ * two sheets; called again from beforeprint. */
1202
+ _syncPrintPageRule() {
1203
+ const id = 'deck-stage-print-page';
1204
+ let tag = document.getElementById(id);
1205
+ if (!tag) {
1206
+ tag = document.createElement('style');
1207
+ tag.id = id;
1208
+ }
1209
+ (document.body || document.head).appendChild(tag);
1210
+ tag.textContent =
1211
+ '@page { size: ' + this.designWidth + 'px ' + this.designHeight + 'px; margin: 0; } ' +
1212
+ '@media print { html, body { margin: 0 !important; padding: 0 !important; background: none !important; overflow: visible !important; height: auto !important; } ' +
1213
+ '* { -webkit-print-color-adjust: exact; print-color-adjust: exact; } ' +
1214
+ // Jump authored animations/transitions to their end state so print
1215
+ // never captures mid-entrance — pairs with the beforeprint handler
1216
+ // in connectedCallback that sets data-deck-active on every slide.
1217
+ '*, *::before, *::after { animation-delay: -99s !important; animation-duration: .001s !important; ' +
1218
+ 'animation-iteration-count: 1 !important; animation-fill-mode: both !important; ' +
1219
+ 'animation-play-state: running !important; transition-duration: 0s !important; } }';
1220
+ }
1221
+
1222
+ /** Typographic defaults for slide text: balance headings, avoid
1223
+ * widowed/orphaned words in body copy (browsers without text-wrap
1224
+ * support drop the declarations). Zero-specificity via :where() so
1225
+ * any text-wrap authored on those elements wins. Lives in the document,
1226
+ * not the shadow root, for two reasons: document rules reach the
1227
+ * slotted (light DOM) slides, and _snapshotAuthorCss copies document
1228
+ * stylesheets into each thumbnail's shadow root, so the thumbs wrap
1229
+ * the same way — a deck-stage-scoped selector would match nothing
1230
+ * there. data-omelette-injected marks the tag for the host editor
1231
+ * to strip at serialize, so it is never written back as authored
1232
+ * source. */
1233
+ _ensureTextWrapDefaults() {
1234
+ if (document.getElementById('deck-stage-text-wrap')) return;
1235
+ const tag = document.createElement('style');
1236
+ tag.id = 'deck-stage-text-wrap';
1237
+ tag.setAttribute('data-omelette-injected', '');
1238
+ tag.textContent =
1239
+ ':where(h1,h2,h3,h4,h5,h6){text-wrap:balance}' +
1240
+ ':where(p,li,blockquote,figcaption){text-wrap:pretty}';
1241
+ document.head.appendChild(tag);
1242
+ }
1243
+
1244
+ _onSlotChange() {
1245
+ // Self-mutate path already reconciled synchronously and emitted
1246
+ // slidechange; skip the async slotchange it caused.
1247
+ if (this._squelchSlotChange) { this._squelchSlotChange = false; return; }
1248
+ // Primary lock-clear is the host's __deck_rail_ack; this clears on a
1249
+ // dropped ack so the rail can't stay dead.
1250
+ this._railLock = false;
1251
+ this._collectSlides();
1252
+ this._restoreIndex();
1253
+ this._applyIndex({ showOverlay: false, broadcast: true, reason: 'init' });
1254
+ this._fit();
1255
+ // The deck just changed under any open rail surface — an open
1256
+ // confirm or menu is a question about the OLD deck (its labels and
1257
+ // counts may now lie), so close them rather than let a stale
1258
+ // answer fire. The element-held selection re-resolves, but the
1259
+ // user should re-read what they're deleting.
1260
+ if (this._confirm && this._confirm.hasAttribute('data-open')) {
1261
+ this._closeConfirm();
1262
+ // The dialog held focus (danger button); hand it back to the rail.
1263
+ this._focusCurrentThumb(true);
1264
+ }
1265
+ if (this._menu && this._menu.hasAttribute('data-open')) this._closeMenu();
1266
+ // Editor-mode deletes rebuild the rail through here; a confirmed
1267
+ // delete that started from the keyboard still owes focus to the
1268
+ // (new) current thumb.
1269
+ if (this._pendingRailRefocus) this._focusCurrentThumb(true);
1270
+ }
1271
+
1272
+ _collectSlides() {
1273
+ const assigned = this._slot.assignedElements({ flatten: true });
1274
+ this._slides = assigned.filter((el) => {
1275
+ // Skip template/style/script nodes even if someone slots them.
1276
+ const tag = el.tagName;
1277
+ return tag !== 'TEMPLATE' && tag !== 'SCRIPT' && tag !== 'STYLE';
1278
+ });
1279
+ this._slideSet = new Set(this._slides);
1280
+ // Selection is element-keyed: drop entries whose slide is gone
1281
+ // (deleted, or replaced wholesale by a host re-render).
1282
+ if (this._selected && this._selected.size) {
1283
+ this._selected.forEach((s) => {
1284
+ if (!this._slideSet.has(s)) this._selected.delete(s);
1285
+ });
1286
+ }
1287
+ if (this._selAnchor && !this._slideSet.has(this._selAnchor)) this._selAnchor = null;
1288
+
1289
+ this._slides.forEach((slide, i) => {
1290
+ const n = i + 1;
1291
+ slide.setAttribute('data-screen-label', `${pad2(n)} ${getSlideLabel(slide)}`);
1292
+
1293
+ // Validation attribute for comment flow / auto-checks.
1294
+ if (!slide.hasAttribute('data-om-validate')) {
1295
+ slide.setAttribute('data-om-validate', VALIDATE_ATTR);
1296
+ }
1297
+
1298
+ slide.setAttribute('data-deck-slide', String(i));
1299
+ });
1300
+
1301
+ if (this._totalEl) this._totalEl.textContent = String(this._slides.length || 1);
1302
+ if (this._index >= this._slides.length) this._index = Math.max(0, this._slides.length - 1);
1303
+ this._markLastVisible();
1304
+ this._renderRail();
1305
+ }
1306
+
1307
+ /** Tag the last non-skipped slide so print CSS can drop its
1308
+ * break-after (see the @media print comment above — :last-child
1309
+ * alone matches a hidden skipped slide). */
1310
+ _markLastVisible() {
1311
+ let last = null;
1312
+ this._slides.forEach((s) => {
1313
+ s.removeAttribute('data-deck-last-visible');
1314
+ if (!s.hasAttribute('data-deck-skip')) last = s;
1315
+ });
1316
+ if (last) last.setAttribute('data-deck-last-visible', '');
1317
+ }
1318
+
1319
+ _loadNotes() {
1320
+ // Per-slide data-speaker-notes is authoritative when present (attrs
1321
+ // travel with the element on reorder/dup/delete); a slide without
1322
+ // the attr falls through to the legacy #speaker-notes JSON array
1323
+ // PER SLIDE so a single attr on a JSON-authored deck doesn't blank
1324
+ // the rest.
1325
+ const tag = document.getElementById('speaker-notes');
1326
+ let json = null;
1327
+ if (tag) try {
1328
+ const p = JSON.parse(tag.textContent || '[]');
1329
+ if (Array.isArray(p)) json = p;
1330
+ } catch (e) {
1331
+ console.warn('[deck-stage] Failed to parse #speaker-notes JSON:', e);
1332
+ }
1333
+ this._notes = this._slides.map((s, i) => {
1334
+ const a = s.getAttribute('data-speaker-notes');
1335
+ return a !== null ? a : (json && typeof json[i] === 'string' ? json[i] : '');
1336
+ });
1337
+ }
1338
+
1339
+ _restoreIndex() {
1340
+ // The host's ?slide= param is delivered as a #<int> hash (1-indexed) on
1341
+ // the iframe src. No hash → slide 1; the deck itself keeps no position
1342
+ // state across loads.
1343
+ const h = (location.hash || '').match(/^#(\d+)$/);
1344
+ if (h) {
1345
+ const n = parseInt(h[1], 10) - 1;
1346
+ if (n >= 0 && n < this._slides.length) this._index = n;
1347
+ }
1348
+ }
1349
+
1350
+ _applyIndex({ showOverlay = true, broadcast = true, reason = 'init' } = {}) {
1351
+ if (!this._slides.length) return;
1352
+ const prev = this._prevIndex == null ? -1 : this._prevIndex;
1353
+ const curr = this._index;
1354
+ // Keep the iframe's own hash in sync so an in-iframe location.reload()
1355
+ // (reload banner path in viewer-handle.ts) lands on the current slide,
1356
+ // not the stale deep-link hash from initial load.
1357
+ try { history.replaceState(null, '', '#' + (curr + 1)); } catch (e) {}
1358
+ this._slides.forEach((s, i) => {
1359
+ if (i === curr) s.setAttribute('data-deck-active', '');
1360
+ else s.removeAttribute('data-deck-active');
1361
+ });
1362
+ if (this._countEl) this._countEl.textContent = String(curr + 1);
1363
+ // Follow-scroll on every navigation (init deep-link, keyboard, click,
1364
+ // tap, external goTo) — the only time we *don't* want the rail to
1365
+ // track current is after a rail-internal mutation, where _renderRail
1366
+ // has already restored the user's scroll position and yanking back to
1367
+ // current would undo it.
1368
+ this._syncRail(reason !== 'mutation');
1369
+
1370
+ if (broadcast) {
1371
+ // (1) Legacy: host-window postMessage for speaker-notes renderers.
1372
+ try { window.postMessage({ slideIndexChanged: curr, deckTotal: this._slides.length, deckSkipped: this._skippedIndices() }, '*'); } catch (e) {}
1373
+
1374
+ // (2) In-page CustomEvent on the <deck-stage> element itself.
1375
+ // Bubbles and composes out of shadow DOM so slide code can listen:
1376
+ // document.querySelector('deck-stage').addEventListener('slidechange', e => {
1377
+ // e.detail.index, e.detail.previousIndex, e.detail.total, e.detail.slide, e.detail.reason
1378
+ // });
1379
+ const detail = {
1380
+ index: curr,
1381
+ previousIndex: prev,
1382
+ total: this._slides.length,
1383
+ slide: this._slides[curr] || null,
1384
+ previousSlide: prev >= 0 ? (this._slides[prev] || null) : null,
1385
+ reason: reason, // 'init' | 'keyboard' | 'click' | 'tap' | 'api'
1386
+ };
1387
+ this.dispatchEvent(new CustomEvent('slidechange', {
1388
+ detail,
1389
+ bubbles: true,
1390
+ composed: true}));
1391
+ }
1392
+
1393
+ this._prevIndex = curr;
1394
+ if (showOverlay) this._flashOverlay();
1395
+ }
1396
+
1397
+ _flashOverlay() {
1398
+ // Host posts __omelette_presenting while in fullscreen/tab presentation
1399
+ // mode — suppress the nav footer entirely (both hover and slide-change
1400
+ // flash) so the audience sees clean slides.
1401
+ if (!this._overlay || this._presenting) return;
1402
+ this._overlay.setAttribute('data-visible', '');
1403
+ if (this._hideTimer) clearTimeout(this._hideTimer);
1404
+ this._hideTimer = setTimeout(() => {
1405
+ this._overlay.removeAttribute('data-visible');
1406
+ }, OVERLAY_HIDE_MS);
1407
+ }
1408
+
1409
+ _railWidth() {
1410
+ // State-based, no offsetWidth: the first _fit() can run before the
1411
+ // rail has had layout on some load paths, and a 0 there paints the
1412
+ // slide full-width for one frame before the post-slotchange _fit()
1413
+ // corrects it.
1414
+ if (!this._railEnabled || !this._railVisible || this.hasAttribute('no-rail')
1415
+ || this.hasAttribute('noscale') || this._presenting || this._previewMode
1416
+ || NARROW_MQ.matches) return 0;
1417
+ return this._railPx || 0;
1418
+ }
1419
+
1420
+ _fit() {
1421
+ if (!this._canvas) return;
1422
+ const stage = this._canvas.parentElement;
1423
+ // PPTX export sets noscale so the DOM capture sees authored-size
1424
+ // geometry — the scaled canvas is in shadow DOM, so the exporter's
1425
+ // resetTransformSelector can't reach .canvas.style.transform directly.
1426
+ if (this.hasAttribute('noscale')) {
1427
+ this._canvas.style.transform = 'none';
1428
+ if (stage) stage.style.left = '0';
1429
+ if (this._overlay) this._overlay.style.marginLeft = '0';
1430
+ return;
1431
+ }
1432
+ const rw = this._railWidth();
1433
+ if (stage) stage.style.left = rw + 'px';
1434
+ // Overlay is centred on the viewport via left:50% + translate(-50%);
1435
+ // marginLeft shifts the centre by rw/2 so it lands in the middle of
1436
+ // the [rw, innerWidth] stage region.
1437
+ if (this._overlay) this._overlay.style.marginLeft = (rw / 2) + 'px';
1438
+ const vw = window.innerWidth - rw;
1439
+ const vh = window.innerHeight;
1440
+ const s = Math.min(vw / this.designWidth, vh / this.designHeight);
1441
+ this._canvas.style.transform = `scale(${s})`;
1442
+ }
1443
+
1444
+ _onResize() {
1445
+ this._fit();
1446
+ // Crossing the narrow-viewport breakpoint reveals the rail — rerun the
1447
+ // thumbnail scale the same way _setRailWidth does.
1448
+ if (!this._scaleRaf) {
1449
+ this._scaleRaf = requestAnimationFrame(() => {
1450
+ this._scaleRaf = null;
1451
+ this._scaleThumbs();
1452
+ });
1453
+ }
1454
+ }
1455
+
1456
+ _onMouseMove() {
1457
+ // Keep overlay visible while mouse moves; hide after idle.
1458
+ this._flashOverlay();
1459
+ }
1460
+
1461
+ _onMessage(e) {
1462
+ const d = e.data;
1463
+ if (d && typeof d.__omelette_presenting === 'boolean') {
1464
+ this._presenting = d.__omelette_presenting;
1465
+ if (this._presenting && this._overlay) {
1466
+ this._overlay.removeAttribute('data-visible');
1467
+ if (this._hideTimer) clearTimeout(this._hideTimer);
1468
+ }
1469
+ this._syncRailHidden();
1470
+ this._closeMenu();
1471
+ this._closeConfirm();
1472
+ this._fit();
1473
+ this._scaleThumbs();
1474
+ }
1475
+ // Host's Preview segment (ViewerMode='none'): the rail's drag-reorder /
1476
+ // right-click skip-delete affordances are editing chrome, so hide it
1477
+ // while the user is just looking at the deck. Same hard-hide path as
1478
+ // presenting; independent of the user's _railVisible preference so
1479
+ // returning to Edit restores whatever they had.
1480
+ if (d && typeof d.__omelette_preview_mode === 'boolean') {
1481
+ if (d.__omelette_preview_mode === this._previewMode) return;
1482
+ this._previewMode = d.__omelette_preview_mode;
1483
+ this._syncRailHidden();
1484
+ this._closeMenu();
1485
+ this._closeConfirm();
1486
+ this._fit();
1487
+ this._scaleThumbs();
1488
+ }
1489
+ // Host has processed a dc-op; rail input is safe again. Not tied to
1490
+ // slotchange — setAttr and refusal don't fire one. On refusal,
1491
+ // revert the optimistic _index/hash adjustment so the next nav
1492
+ // starts from what's actually on screen.
1493
+ if (d && d.__dc_op_ack) {
1494
+ this._railLock = false;
1495
+ if (d.applied === false && this._indexBeforeEmit != null) {
1496
+ this._index = this._indexBeforeEmit;
1497
+ try { history.replaceState(null, '', '#' + (this._index + 1)); } catch (e) {}
1498
+ }
1499
+ this._indexBeforeEmit = null;
1500
+ // A refused op never re-renders, so slotchange won't restore the
1501
+ // keyboard flow's focus — do it here. (Applied ops refocus in
1502
+ // _onSlotChange, after the rail has been rebuilt.)
1503
+ if (d.applied === false && this._pendingRailRefocus) {
1504
+ this._focusCurrentThumb(true);
1505
+ }
1506
+ }
1507
+ // Per-viewer show/hide, driven by the TweaksPanel's auto-injected
1508
+ // "Thumbnail rail" toggle (or any author script). Independent of
1509
+ // whether the Tweaks panel itself is open — closing the panel
1510
+ // doesn't change rail visibility. Persists alongside rail width.
1511
+ if (d && d.type === '__deck_rail_visible' && typeof d.on === 'boolean') {
1512
+ if (d.on === this._railVisible) return;
1513
+ this._railVisible = d.on;
1514
+ try { localStorage.setItem('deck-stage.railVisible', d.on ? '1' : '0'); } catch (e) {}
1515
+ // Arm the transition, commit it, then flip state — otherwise the
1516
+ // browser coalesces both writes and nothing animates on show.
1517
+ this.setAttribute('data-rail-anim', '');
1518
+ void (this._rail && this._rail.offsetHeight);
1519
+ this._syncRailHidden();
1520
+ this._fit();
1521
+ this._scaleThumbs();
1522
+ clearTimeout(this._railAnimTimer);
1523
+ this._railAnimTimer = setTimeout(() => this.removeAttribute('data-rail-anim'), 220);
1524
+ }
1525
+ if (d && d.type === '__omelette_rail_enabled') this._enableRail();
1526
+ }
1527
+
1528
+ _syncRailHidden() {
1529
+ if (!this._rail) return;
1530
+ // data-presenting is the hard hide (display:none) for flag-off,
1531
+ // presentation mode, and the host's Preview segment — instant, no
1532
+ // transition. data-user-hidden is the soft hide (translateX(-100%))
1533
+ // for the viewer's rail toggle, so show/hide slides under
1534
+ // :host([data-rail-anim]).
1535
+ const hard = !this._railEnabled || this._presenting || this._previewMode;
1536
+ if (hard) this._rail.setAttribute('data-presenting', '');
1537
+ else this._rail.removeAttribute('data-presenting');
1538
+ if (!this._railVisible) this._rail.setAttribute('data-user-hidden', '');
1539
+ else this._rail.removeAttribute('data-user-hidden');
1540
+ // translateX hide leaves thumbs (tabIndex=0) in the tab order —
1541
+ // inert keeps them unfocusable while the rail is off-screen.
1542
+ this._rail.inert = hard || !this._railVisible;
1543
+ }
1544
+
1545
+ _onTap(e) {
1546
+ // Touch-only — keyboard + the overlay toolbar cover nav on desktop.
1547
+ if (FINE_POINTER_MQ.matches) return;
1548
+ // Only taps that land on the stage (slide content or letterbox); the
1549
+ // overlay / rail / menus are siblings with their own click handlers.
1550
+ const path = e.composedPath();
1551
+ if (!this._stage || !path.includes(this._stage)) return;
1552
+ // Let interactive slide content keep the tap. composedPath (not
1553
+ // e.target.closest) so we see through open shadow roots — a <button>
1554
+ // inside a slide-authored custom element retargets e.target to the
1555
+ // host but still appears in the composed path.
1556
+ if (e.defaultPrevented) return;
1557
+ for (const n of path) {
1558
+ if (n === this._stage) break;
1559
+ if (n.matches && n.matches(INTERACTIVE_SEL)) return;
1560
+ }
1561
+ e.preventDefault();
1562
+ const rw = this._railWidth();
1563
+ const mid = rw + (window.innerWidth - rw) / 2;
1564
+ this._advance(e.clientX < mid ? -1 : 1, 'tap');
1565
+ }
1566
+
1567
+ _onKey(e) {
1568
+ // Ignore when the user is typing. composedPath()[0], not e.target: a
1569
+ // window-level keydown retargets e.target to the shadow host, which
1570
+ // would miss an <input> or contenteditable inside a web component on
1571
+ // a slide (same reason _onTap uses composedPath).
1572
+ const t = (e.composedPath ? e.composedPath()[0] : e.target);
1573
+ if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;
1574
+ // Confirm dialog swallows nav keys while open; Escape cancels. Enter
1575
+ // is left to the focused button's native activation so Tab→Cancel
1576
+ // →Enter activates Cancel, not the window-level confirm path.
1577
+ if (this._confirm && this._confirm.hasAttribute('data-open')) {
1578
+ if (e.key === 'Escape') {
1579
+ this._closeConfirm();
1580
+ this._focusCurrentThumb();
1581
+ e.preventDefault();
1582
+ }
1583
+ return;
1584
+ }
1585
+ if (e.key === 'Escape' && this._menu && this._menu.hasAttribute('data-open')) {
1586
+ this._closeMenu();
1587
+ e.preventDefault();
1588
+ return;
1589
+ }
1590
+ if (e.key === 'Escape' && this._selected.size) {
1591
+ // Collapse the multi-selection back to the current slide (the
1592
+ // implicit selection), not to nothing.
1593
+ this._clearSelection();
1594
+ e.preventDefault();
1595
+ return;
1596
+ }
1597
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
1598
+
1599
+ const key = e.key;
1600
+ let handled = true;
1601
+
1602
+ if (key === 'ArrowRight' || key === 'PageDown' || key === ' ' || key === 'Spacebar') {
1603
+ this._advance(1, 'keyboard');
1604
+ } else if (key === 'ArrowLeft' || key === 'PageUp') {
1605
+ this._advance(-1, 'keyboard');
1606
+ } else if (key === 'Home') {
1607
+ this._go(0, 'keyboard');
1608
+ } else if (key === 'End') {
1609
+ this._go(this._slides.length - 1, 'keyboard');
1610
+ } else if (key === 'r' || key === 'R') {
1611
+ this._go(0, 'keyboard');
1612
+ } else if (/^[0-9]$/.test(key)) {
1613
+ // 1..9 jump to that slide; 0 jumps to 10.
1614
+ const n = key === '0' ? 9 : parseInt(key, 10) - 1;
1615
+ if (n < this._slides.length) this._go(n, 'keyboard');
1616
+ } else {
1617
+ handled = false;
1618
+ }
1619
+
1620
+ if (handled) {
1621
+ e.preventDefault();
1622
+ this._flashOverlay();
1623
+ }
1624
+ }
1625
+
1626
+ _go(i, reason = 'api') {
1627
+ // User-initiated navigation collapses a multi-selection down to
1628
+ // the (implicit) current slide, like Keynote's arrow keys. 'click'
1629
+ // handles its own selection; programmatic reasons leave it alone.
1630
+ if (reason === 'keyboard' || reason === 'tap') this._clearSelection();
1631
+ if (!this._slides.length) return;
1632
+ const clamped = Math.max(0, Math.min(this._slides.length - 1, i));
1633
+ if (clamped === this._index) {
1634
+ this._flashOverlay();
1635
+ return;
1636
+ }
1637
+ this._index = clamped;
1638
+ this._applyIndex({ showOverlay: true, broadcast: true, reason });
1639
+ }
1640
+
1641
+ /** Step forward/back skipping any slide marked data-deck-skip. Falls
1642
+ * back to _go's clamp-at-ends behaviour (flash overlay) when there's
1643
+ * nothing further in that direction. */
1644
+ _advance(dir, reason) {
1645
+ if (!this._slides.length) return;
1646
+ let i = this._index + dir;
1647
+ while (i >= 0 && i < this._slides.length && this._slides[i].hasAttribute('data-deck-skip')) {
1648
+ i += dir;
1649
+ }
1650
+ if (i < 0 || i >= this._slides.length) { this._flashOverlay(); return; }
1651
+ this._go(i, reason);
1652
+ }
1653
+
1654
+ // ── Thumbnail rail ────────────────────────────────────────────────────
1655
+ //
1656
+ // Thumbs are keyed by slide element and reused across _renderRail()
1657
+ // calls, so a reorder/delete is an O(changed) DOM shuffle instead of an
1658
+ // O(N) teardown-and-re-clone. Each thumb starts as a lightweight shell
1659
+ // (num + empty frame); the clone is materialized lazily by an
1660
+ // IntersectionObserver when the frame scrolls into (or near) view, so
1661
+ // only visible-ish slides pay the clone + image-decode cost.
1662
+
1663
+ _renderRail() {
1664
+ if (!this._rail || !this._railEnabled) { this._thumbs = []; return; }
1665
+ // FLIP: record each *materialized* thumb's top before the reconcile.
1666
+ // Off-screen (non-materialized) thumbs don't need the animation and
1667
+ // skipping their getBoundingClientRect saves a forced layout per
1668
+ // off-screen thumb on large decks.
1669
+ const prevTops = new Map();
1670
+ (this._thumbs || []).forEach(({ thumb, slide, host }) => {
1671
+ if (host) prevTops.set(slide, thumb.getBoundingClientRect().top);
1672
+ });
1673
+ const st = this._rail.scrollTop;
1674
+
1675
+ // Reconcile: reuse thumbs that already exist for a slide, create
1676
+ // shells for new slides, drop thumbs for removed slides.
1677
+ const bySlide = new Map();
1678
+ (this._thumbs || []).forEach((t) => bySlide.set(t.slide, t));
1679
+ const next = [];
1680
+ this._slides.forEach((slide) => {
1681
+ let t = bySlide.get(slide);
1682
+ if (t) bySlide.delete(slide);
1683
+ else t = this._makeThumb(slide);
1684
+ next.push(t);
1685
+ });
1686
+ // Orphans — slides removed since last render.
1687
+ bySlide.forEach((t) => {
1688
+ if (this._railObserver) this._railObserver.unobserve(t.frame);
1689
+ t.thumb.remove();
1690
+ });
1691
+ // Put thumbs into document order to match _slides. insertBefore on
1692
+ // an already-correctly-placed node is a no-op, so this is cheap
1693
+ // when nothing moved.
1694
+ next.forEach((t, i) => {
1695
+ const want = t.thumb;
1696
+ const at = this._rail.children[i];
1697
+ if (at !== want) this._rail.insertBefore(want, at || null);
1698
+ t.i = i;
1699
+ t.num.textContent = String(i + 1);
1700
+ if (t.slide.hasAttribute('data-deck-skip')) t.thumb.setAttribute('data-skip', '');
1701
+ else t.thumb.removeAttribute('data-skip');
1702
+ if (this._selected.has(t.slide)) t.thumb.setAttribute('data-selected', '');
1703
+ else t.thumb.removeAttribute('data-selected');
1704
+ });
1705
+ this._thumbs = next;
1706
+
1707
+ this._rail.scrollTop = st;
1708
+ if (prevTops.size) {
1709
+ const moved = [];
1710
+ this._thumbs.forEach(({ thumb, slide }) => {
1711
+ const old = prevTops.get(slide);
1712
+ if (old == null) return;
1713
+ const dy = old - thumb.getBoundingClientRect().top;
1714
+ if (Math.abs(dy) < 1) return;
1715
+ thumb.style.transition = 'none';
1716
+ thumb.style.transform = `translateY(${dy}px)`;
1717
+ moved.push(thumb);
1718
+ });
1719
+ if (moved.length) {
1720
+ // Commit the inverted positions before flipping the transition
1721
+ // on — otherwise the browser coalesces both style writes and
1722
+ // nothing animates.
1723
+ void this._rail.offsetHeight;
1724
+ moved.forEach((t) => {
1725
+ t.style.transition = 'transform 180ms cubic-bezier(.2,.7,.3,1)';
1726
+ t.style.transform = '';
1727
+ });
1728
+ setTimeout(() => moved.forEach((t) => { t.style.transition = ''; }), 220);
1729
+ }
1730
+ }
1731
+ requestAnimationFrame(() => this._scaleThumbs());
1732
+ this._syncRail(false);
1733
+ }
1734
+
1735
+ /** Create a lightweight thumb shell for one slide. The clone is
1736
+ * materialized later by the IntersectionObserver. Event handlers
1737
+ * look up the thumb's *current* index (via _thumbs.indexOf) so the
1738
+ * same element can be reused across reorders. */
1739
+ _makeThumb(slide) {
1740
+ const thumb = document.createElement('div');
1741
+ thumb.className = 'thumb';
1742
+ thumb.tabIndex = 0;
1743
+ const num = document.createElement('div');
1744
+ num.className = 'num';
1745
+ const frame = document.createElement('div');
1746
+ frame.className = 'frame';
1747
+ thumb.append(num, frame);
1748
+
1749
+ const entry = { thumb, num, frame, slide, clone: null, host: null, i: -1 };
1750
+ // entry.i is refreshed on every _renderRail reconcile pass, so
1751
+ // handlers read the thumb's current position without an O(N) scan.
1752
+ const idx = () => entry.i;
1753
+
1754
+ thumb.addEventListener('click', (e) => {
1755
+ const i = idx();
1756
+ const slide = this._slides[i];
1757
+ // WebKit doesn't focus a plain element on click — focus
1758
+ // explicitly so Delete/Backspace works right after selecting a
1759
+ // slide by mouse. preventScroll: _syncRail owns the rail's
1760
+ // scroll position.
1761
+ thumb.focus({ preventScroll: true });
1762
+ if (e.shiftKey || e.metaKey || e.ctrlKey) {
1763
+ // Multi-select gestures adjust the selection without
1764
+ // navigating (Keynote/Figma convention).
1765
+ e.preventDefault();
1766
+ if (e.shiftKey) {
1767
+ // Range from the anchor (last plain/cmd-clicked slide;
1768
+ // falls back to the current slide) to here, replacing any
1769
+ // previous range.
1770
+ let a = this._selAnchor ? this._slides.indexOf(this._selAnchor) : -1;
1771
+ if (a < 0) {
1772
+ a = this._index;
1773
+ this._selAnchor = this._slides[a] || null;
1774
+ }
1775
+ this._selected.clear();
1776
+ for (let j = Math.min(a, i); j <= Math.max(a, i); j++) {
1777
+ this._selected.add(this._slides[j]);
1778
+ }
1779
+ } else if (slide) {
1780
+ // Toggle. An empty explicit selection implicitly holds the
1781
+ // current slide — materialize it first so cmd-clicking a
1782
+ // second slide selects both.
1783
+ if (!this._selected.size && i !== this._index && this._slides[this._index]) {
1784
+ this._selected.add(this._slides[this._index]);
1785
+ }
1786
+ if (this._selected.has(slide)) this._selected.delete(slide);
1787
+ else {
1788
+ this._selected.add(slide);
1789
+ this._selAnchor = slide;
1790
+ }
1791
+ }
1792
+ this._syncSelection();
1793
+ return;
1794
+ }
1795
+ this._clearSelection();
1796
+ this._selAnchor = slide || null;
1797
+ this._go(i, 'click');
1798
+ });
1799
+ // ↑/↓ step through the rail when a thumb has focus. _go clamps at the
1800
+ // ends and _applyIndex→_syncRail scrolls the new current thumb into
1801
+ // view; we move focus to it (preventScroll — _syncRail already
1802
+ // scrolled) so a held key walks the whole list. stopPropagation keeps
1803
+ // this out of the window-level _onKey nav handler.
1804
+ thumb.addEventListener('keydown', (e) => {
1805
+ // Delete/Backspace with the rail focused deletes this thumb's
1806
+ // slide through the same confirm dialog as the menu item.
1807
+ // Listening on the thumb (never window-level) is what keeps
1808
+ // typing in the notes panel / slide inputs from ever landing
1809
+ // here; the target check is belt-and-braces for anything
1810
+ // focusable that ends up inside a thumb.
1811
+ if ((e.key === 'Delete' || e.key === 'Backspace') && !e.metaKey && !e.ctrlKey && !e.altKey) {
1812
+ const t = e.target;
1813
+ if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName))) return;
1814
+ e.preventDefault();
1815
+ e.stopPropagation();
1816
+ // Same refusals as the menu item: never every slide, never
1817
+ // while a prior structural op is waiting on its ack. The
1818
+ // whole-deck refusal is announced (the menu greys its item
1819
+ // out; a silently dead key reads as breakage). The rail-lock
1820
+ // refusal stays silent: it lasts one ack round-trip and
1821
+ // matches the existing single-delete behavior.
1822
+ if (this._railLock) return;
1823
+ // Explicit selection wins; otherwise the focused thumb (which
1824
+ // plain click and ↑/↓ keep equal to the current slide).
1825
+ const sel = this._selected.size ? this._selectionIndices() : [idx()];
1826
+ if (sel.length >= this._slides.length) {
1827
+ this._showNotice(sel.length === 1
1828
+ ? 'The last slide can’t be deleted.'
1829
+ : 'At least one slide has to stay — the whole deck can’t be deleted.');
1830
+ return;
1831
+ }
1832
+ this._openConfirm(sel);
1833
+ return;
1834
+ }
1835
+ if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;
1836
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
1837
+ e.preventDefault();
1838
+ e.stopPropagation();
1839
+ this._go(idx() + (e.key === 'ArrowDown' ? 1 : -1), 'keyboard');
1840
+ const cur = this._thumbs && this._thumbs[this._index];
1841
+ if (cur) cur.thumb.focus({ preventScroll: true });
1842
+ });
1843
+ thumb.addEventListener('contextmenu', (e) => {
1844
+ e.preventDefault();
1845
+ this._openMenu(idx(), e.clientX, e.clientY);
1846
+ });
1847
+ thumb.draggable = true;
1848
+ thumb.addEventListener('dragstart', (e) => {
1849
+ // v1: dragging moves ONE slide, so a multi-selection would lie
1850
+ // about what's about to move — collapse it. (Group drag would
1851
+ // instead keep it and emit a batched move.)
1852
+ this._clearSelection();
1853
+ this._dragFrom = idx();
1854
+ thumb.setAttribute('data-dragging', '');
1855
+ e.dataTransfer.effectAllowed = 'move';
1856
+ try { e.dataTransfer.setData('text/plain', String(this._dragFrom)); } catch (err) {}
1857
+ });
1858
+ thumb.addEventListener('dragend', () => {
1859
+ thumb.removeAttribute('data-dragging');
1860
+ this._clearDrop();
1861
+ this._dragFrom = null;
1862
+ });
1863
+ thumb.addEventListener('dragover', (e) => {
1864
+ if (this._dragFrom == null) return;
1865
+ e.preventDefault();
1866
+ e.dataTransfer.dropEffect = 'move';
1867
+ const r = thumb.getBoundingClientRect();
1868
+ this._setDrop(idx(), e.clientY < r.top + r.height / 2 ? 'before' : 'after');
1869
+ });
1870
+ thumb.addEventListener('drop', (e) => {
1871
+ if (this._dragFrom == null) return;
1872
+ e.preventDefault();
1873
+ const i = idx();
1874
+ const r = thumb.getBoundingClientRect();
1875
+ let to = e.clientY >= r.top + r.height / 2 ? i + 1 : i;
1876
+ if (this._dragFrom < to) to--;
1877
+ const from = this._dragFrom;
1878
+ this._clearDrop();
1879
+ this._dragFrom = null;
1880
+ if (to !== from) this._moveSlide(from, to);
1881
+ });
1882
+
1883
+ if (this._railObserver) this._railObserver.observe(frame);
1884
+ frame.__deckThumb = entry;
1885
+ return entry;
1886
+ }
1887
+
1888
+ /** Lazily build the clone for a thumb that has scrolled into view. */
1889
+ _materialize(entry) {
1890
+ if (entry.host) return;
1891
+ const dw = this.designWidth, dh = this.designHeight;
1892
+ let clone = entry.slide.cloneNode(true);
1893
+ // The clone participates in the document's flat tree, so the
1894
+ // templates' position-based CSS page counters (.slide
1895
+ // { counter-increment: page }) would count every materialized
1896
+ // thumb before the real slides — folios print offset by the
1897
+ // thumb count (slide 2 reading "7" on a five-slide deck).
1898
+ // Neutralize the counter on the clone and drop its folio pill:
1899
+ // a thumbnail's own page number is unreadable at thumb scale
1900
+ // anyway, and the real slides' numbers stay truthful.
1901
+ clone.style.counterIncrement = 'none';
1902
+ clone.querySelectorAll('.page-foot').forEach((pf) => pf.remove());
1903
+ // Canvas bitmaps don't clone — swap each cloned canvas for an <img>
1904
+ // of the live pixels. Best-effort: tainted canvases throw (left
1905
+ // as-is); zero-size are skipped; WebGL without preserveDrawingBuffer
1906
+ // reads back blank and the thumb gets a blank img (same as before).
1907
+ const liveCanvases = entry.slide.querySelectorAll('canvas');
1908
+ const cloneCanvases = clone.querySelectorAll('canvas');
1909
+ cloneCanvases.forEach((cv, i) => {
1910
+ const live = liveCanvases[i];
1911
+ if (!live || !live.width || !live.height) return;
1912
+ try {
1913
+ const img = document.createElement('img');
1914
+ img.src = live.toDataURL();
1915
+ img.alt = '';
1916
+ img.style.cssText = cv.style.cssText;
1917
+ img.className = cv.className;
1918
+ img.width = live.width;
1919
+ img.height = live.height;
1920
+ // Author CSS that sized the <canvas> via tag selector won't match
1921
+ // the <img> — pin the live canvas's laid-out box on the snapshot.
1922
+ if (live.clientWidth) {
1923
+ img.style.width = live.clientWidth + 'px';
1924
+ img.style.height = live.clientHeight + 'px';
1925
+ }
1926
+ cv.replaceWith(img);
1927
+ } catch (e) {}
1928
+ });
1929
+ // Neuter heavy media; replace <video> with its poster so the box
1930
+ // keeps a visual. <iframe>/<audio> become empty placeholders.
1931
+ // Parity with _inertify: transient top-layer UI never belongs in a
1932
+ // static thumb.
1933
+ clone.querySelectorAll('[popover], dialog').forEach((el) => el.remove());
1934
+ clone.querySelectorAll('iframe, audio, object, embed').forEach((el) => {
1935
+ el.removeAttribute('src');
1936
+ el.removeAttribute('srcdoc');
1937
+ el.removeAttribute('data');
1938
+ el.innerHTML = '';
1939
+ });
1940
+ clone.querySelectorAll('video').forEach((el) => {
1941
+ if (!el.poster) { el.removeAttribute('src'); el.innerHTML = ''; return; }
1942
+ const img = document.createElement('img');
1943
+ img.src = el.poster;
1944
+ img.alt = '';
1945
+ img.style.cssText = el.style.cssText + ';object-fit:cover;width:100%;height:100%;';
1946
+ img.className = el.className;
1947
+ el.replaceWith(img);
1948
+ });
1949
+ // Images: defer decode and let the browser pick the smallest
1950
+ // srcset candidate for the ~140px thumb. Same-URL clones reuse the
1951
+ // slide's decoded bitmap (URL-keyed cache), so the remaining cost
1952
+ // is paint/composite — lazy+async keeps that off the main thread.
1953
+ clone.querySelectorAll('img').forEach((el) => {
1954
+ el.loading = 'lazy';
1955
+ el.decoding = 'async';
1956
+ if (el.srcset) el.sizes = (this._railPx || 188) + 'px';
1957
+ });
1958
+ // Custom elements inside the slide would have their
1959
+ // connectedCallback fire when the clone is appended. Replace them
1960
+ // with inert boxes (_neuter) so a component-heavy deck doesn't run
1961
+ // N copies of each component's mount logic in the rail. Children
1962
+ // are preserved so layout-wrapper elements (<my-column><h2>…</h2>)
1963
+ // still show their authored content, and a shadow tree cloned along
1964
+ // via attachShadow({clonable:true}) (e.g. <image-slot>) moves onto
1965
+ // the box so the thumb shows the component's rendered content. The
1966
+ // querySelectorAll NodeList is static, so nested custom elements in
1967
+ // the moved subtree are still visited on later iterations.
1968
+ // querySelectorAll('*') returns descendants only — a custom-element
1969
+ // slide root (<my-slide>…</my-slide>) would slip through and upgrade
1970
+ // on append. Swap the root first.
1971
+ if (clone.tagName.includes('-')) clone = this._neuter(clone);
1972
+ clone.querySelectorAll('*').forEach((el) => {
1973
+ if (el.tagName.includes('-')) el.replaceWith(this._neuter(el));
1974
+ });
1975
+ // Strip ids only now: a defined custom element upgrades synchronously
1976
+ // during cloneNode and re-renders on attribute callbacks, so removing
1977
+ // 'id' any earlier resets components (e.g. <image-slot> falls back to
1978
+ // its author src). Post-neuter, only inert boxes and plain elements
1979
+ // remain, where the strip is just the usual duplicate-id hygiene.
1980
+ clone.removeAttribute('id');
1981
+ clone.removeAttribute('data-deck-active');
1982
+ clone.querySelectorAll('[id]').forEach((el) => el.removeAttribute('id'));
1983
+ clone.style.cssText += ';position:absolute;top:0;left:0;transform-origin:0 0;' +
1984
+ 'pointer-events:none;width:' + dw + 'px;height:' + dh + 'px;' +
1985
+ 'box-sizing:border-box;overflow:hidden;visibility:visible;opacity:1;';
1986
+ const host = document.createElement('div');
1987
+ host.style.cssText = 'position:absolute;inset:0;';
1988
+ // Clones are display-only: inert removes anything focusable inside
1989
+ // them from the tab order, so the rail's Delete/Backspace handler
1990
+ // can never see a (retargeted) key press from cloned content.
1991
+ host.inert = true;
1992
+ this._syncThumbHostAttrs(host);
1993
+ const sr = host.attachShadow({ mode: 'open' });
1994
+ if (this._adoptedSheet) sr.adoptedStyleSheets = [this._adoptedSheet];
1995
+ else {
1996
+ const st = document.createElement('style');
1997
+ st.textContent = this._authorCss || '';
1998
+ sr.appendChild(st);
1999
+ }
2000
+ sr.appendChild(clone);
2001
+ entry.frame.appendChild(host);
2002
+ entry.host = host;
2003
+ entry.clone = clone;
2004
+ if (this._thumbScale) clone.style.transform = 'scale(' + this._thumbScale + ')';
2005
+ // Once materialized the IO callback is a no-op early-return —
2006
+ // unobserve so scroll doesn't keep firing it.
2007
+ if (this._railObserver) this._railObserver.unobserve(entry.frame);
2008
+ }
2009
+
2010
+ /** Replace a cloned custom element with an inert box (see the comment
2011
+ * in _materialize). A shadow tree cloned along via {clonable:true}
2012
+ * moves onto the box, so the thumb shows the component's real content
2013
+ * with zero component logic; :host rules in the moved <style> match
2014
+ * the box, and the preserved data-* attrs keep :host([data-…])
2015
+ * selectors working. */
2016
+ _neuter(el) {
2017
+ // Adopt the shadow only when the cloned root carries renderable
2018
+ // content. A constructor-attach / connectedCallback-render component
2019
+ // clones into an empty (or style-only) slotless root — adopting that
2020
+ // would hide the light children the box is about to receive and drop
2021
+ // the placeholder chrome. Such components fall back to the plain box.
2022
+ let sr = el.shadowRoot;
2023
+ if (sr) {
2024
+ let renderable = false;
2025
+ for (let n = sr.firstElementChild; n; n = n.nextElementSibling) {
2026
+ const t = n.tagName;
2027
+ if (t !== 'STYLE' && t !== 'LINK') { renderable = true; break; }
2028
+ }
2029
+ if (!renderable) sr = null;
2030
+ }
2031
+ const box = document.createElement('div');
2032
+ box.style.cssText = (el.getAttribute('style') || '') + (sr ? '' :
2033
+ ';background:rgba(0,0,0,0.06);border:1px dashed rgba(0,0,0,0.15);');
2034
+ box.className = el.className;
2035
+ // Preserve theming/i18n hooks so [data-*] / :lang() / [dir]
2036
+ // descendant selectors still match the neutered root — but not
2037
+ // pointer-interaction transients (a mid-reframe/mid-drag re-clone
2038
+ // would render the interaction chrome statically in the thumb).
2039
+ for (const a of el.attributes) {
2040
+ const n = a.name;
2041
+ if (n === 'data-reframe' || n === 'data-panning' || n === 'data-over') continue;
2042
+ if (n.startsWith('data-') || n.startsWith('aria-') ||
2043
+ n === 'lang' || n === 'dir' || n === 'role' || n === 'title') {
2044
+ box.setAttribute(n, a.value);
2045
+ }
2046
+ }
2047
+ while (el.firstChild) box.appendChild(el.firstChild);
2048
+ if (sr) this._adoptShadow(box, sr);
2049
+ return box;
2050
+ }
2051
+
2052
+ /** Move a cloned shadow tree onto a neutered thumbnail box: attach an
2053
+ * open root on the box, carry adoptedStyleSheets, move the children,
2054
+ * then make the content inert. */
2055
+ _adoptShadow(box, sr) {
2056
+ let root;
2057
+ try { root = box.attachShadow({ mode: 'open' }); } catch (e) { return; }
2058
+ // Engine-cloned shadow roots never carry adoptedStyleSheets, but a
2059
+ // defined component's clone is upgrade-rebuilt (constructor runs
2060
+ // during cloneNode), so sheets it adopts there are present and
2061
+ // shared by reference — carry them.
2062
+ if (sr.adoptedStyleSheets && sr.adoptedStyleSheets.length) {
2063
+ try {
2064
+ root.adoptedStyleSheets = Array.prototype.slice.call(sr.adoptedStyleSheets);
2065
+ } catch (e) {}
2066
+ }
2067
+ // Clone rather than move: moving preserves listeners an upgraded
2068
+ // clone's constructor attached inside its shadow; cloning sheds
2069
+ // them, keeping thumbs free of component logic categorically.
2070
+ for (let n = sr.firstChild; n; n = n.nextSibling) {
2071
+ root.appendChild(n.cloneNode(true));
2072
+ }
2073
+ this._inertify(root);
2074
+ }
2075
+
2076
+ /** Strip anything executable from copied shadow content and apply the
2077
+ * same custom-element/media/img policy as the light-DOM clone.
2078
+ * (Canvases inside copied shadow content stay blank — there is no
2079
+ * live↔clone pairing across shadow boundaries to snapshot from.) */
2080
+ _inertify(root) {
2081
+ root.querySelectorAll('script').forEach((s) => s.remove());
2082
+ // Transient top-layer UI can never belong in a static thumb. (A
2083
+ // cloned [popover] is display:none anyway — open state doesn't
2084
+ // clone — this just makes it categorical.)
2085
+ root.querySelectorAll('[popover], dialog').forEach((el) => el.remove());
2086
+ // Same heavy-media policy as the light-DOM clone above.
2087
+ root.querySelectorAll('iframe, audio, object, embed').forEach((el) => {
2088
+ el.removeAttribute('src');
2089
+ el.removeAttribute('srcdoc');
2090
+ el.removeAttribute('data');
2091
+ el.innerHTML = '';
2092
+ });
2093
+ root.querySelectorAll('video').forEach((el) => {
2094
+ if (!el.poster) { el.removeAttribute('src'); el.innerHTML = ''; return; }
2095
+ const img = document.createElement('img');
2096
+ img.src = el.poster;
2097
+ img.alt = '';
2098
+ img.style.cssText = el.style.cssText + ';object-fit:cover;width:100%;height:100%;';
2099
+ img.className = el.className;
2100
+ el.replaceWith(img);
2101
+ });
2102
+ root.querySelectorAll('*').forEach((el) => {
2103
+ for (let i = el.attributes.length - 1; i >= 0; i--) {
2104
+ if (/^on/i.test(el.attributes[i].name)) {
2105
+ el.removeAttribute(el.attributes[i].name);
2106
+ }
2107
+ }
2108
+ });
2109
+ root.querySelectorAll('img').forEach((el) => {
2110
+ el.loading = 'lazy';
2111
+ el.decoding = 'async';
2112
+ if (el.srcset) el.sizes = (this._railPx || 188) + 'px';
2113
+ });
2114
+ // Nested custom elements inside copied shadow content would upgrade
2115
+ // on append — same treatment as the light DOM. querySelectorAll is
2116
+ // static, so boxes created mid-walk don't re-enter this loop.
2117
+ root.querySelectorAll('*').forEach((el) => {
2118
+ if (el.tagName.includes('-')) el.replaceWith(this._neuter(el));
2119
+ });
2120
+ }
2121
+
2122
+ /** Re-clone a single thumb (live-update path). No-op if the thumb
2123
+ * hasn't been materialized yet — it'll pick up current content when
2124
+ * it scrolls into view. */
2125
+ _refreshThumb(slide) {
2126
+ const entry = (this._thumbs || []).find((t) => t.slide === slide);
2127
+ if (!entry || !entry.host) return;
2128
+ entry.host.remove();
2129
+ entry.host = entry.clone = null;
2130
+ this._materialize(entry);
2131
+ }
2132
+
2133
+ _scaleThumbs() {
2134
+ if (!this._thumbs || !this._thumbs.length) return;
2135
+ // Every frame is the same width; if it reads 0 the rail is
2136
+ // display:none (noscale / no-rail / presenting / print) — leave the
2137
+ // clones as-is and re-run when the rail is revealed.
2138
+ const fw = this._thumbs[0].frame.offsetWidth;
2139
+ if (!fw) return;
2140
+ this._thumbScale = fw / this.designWidth;
2141
+ this._thumbs.forEach(({ clone }) => {
2142
+ if (clone) clone.style.transform = 'scale(' + this._thumbScale + ')';
2143
+ });
2144
+ }
2145
+
2146
+ _setDrop(i, where) {
2147
+ // dragover fires at pointer-event rate; touch only the previous
2148
+ // and new target rather than sweeping all N thumbs.
2149
+ const t = this._thumbs && this._thumbs[i];
2150
+ if (this._dropOn && this._dropOn !== t) {
2151
+ this._dropOn.thumb.removeAttribute('data-drop');
2152
+ }
2153
+ if (t) t.thumb.setAttribute('data-drop', where);
2154
+ this._dropOn = t || null;
2155
+ }
2156
+
2157
+ _clearDrop() {
2158
+ if (this._dropOn) this._dropOn.thumb.removeAttribute('data-drop');
2159
+ this._dropOn = null;
2160
+ }
2161
+
2162
+ _syncRail(follow) {
2163
+ if (!this._thumbs) return;
2164
+ this._thumbs.forEach(({ thumb }, i) => {
2165
+ if (i === this._index) {
2166
+ thumb.setAttribute('data-current', '');
2167
+ if (follow && typeof thumb.scrollIntoView === 'function') {
2168
+ thumb.scrollIntoView({ block: 'nearest' });
2169
+ }
2170
+ } else {
2171
+ thumb.removeAttribute('data-current');
2172
+ }
2173
+ });
2174
+ }
2175
+
2176
+ _openMenu(i, x, y) {
2177
+ if (!this._menu) return;
2178
+ this._menuIndex = i;
2179
+ const slide = this._slides[i];
2180
+ // Right-clicking a thumb OUTSIDE the selection collapses the
2181
+ // selection to that thumb (platform convention) — the menu then
2182
+ // always targets exactly what's highlighted.
2183
+ if (this._selected.size && slide && !this._selected.has(slide)) {
2184
+ this._selected.clear();
2185
+ this._selected.add(slide);
2186
+ this._selAnchor = slide;
2187
+ this._syncSelection();
2188
+ }
2189
+ const sel = this._selectionIndices();
2190
+ const bulk = sel.length > 1;
2191
+ this._menuIndices = bulk ? sel : [i];
2192
+ // Bulk mode offers only the one batched op that exists (delete);
2193
+ // the single-slide items address one index and stay hidden.
2194
+ this._menu.querySelectorAll('[data-act="skip"], [data-act="up"], [data-act="down"], [data-act="duplicate"], hr').forEach((el) => {
2195
+ el.style.display = bulk ? 'none' : '';
2196
+ });
2197
+ const skip = slide && slide.hasAttribute('data-deck-skip');
2198
+ this._menu.querySelector('[data-act="skip"]').textContent = skip ? 'Unskip slide' : 'Skip slide';
2199
+ this._menu.querySelector('[data-act="up"]').disabled = i <= 0;
2200
+ this._menu.querySelector('[data-act="down"]').disabled = i >= this._slides.length - 1;
2201
+ const del = this._menu.querySelector('[data-act="delete"]');
2202
+ del.textContent = bulk ? 'Delete ' + sel.length + ' slides' : 'Delete slide';
2203
+ del.disabled = bulk ? sel.length >= this._slides.length : this._slides.length <= 1;
2204
+ // Place, then clamp to viewport after it's measurable.
2205
+ this._menu.style.left = x + 'px';
2206
+ this._menu.style.top = y + 'px';
2207
+ this._menu.setAttribute('data-open', '');
2208
+ const r = this._menu.getBoundingClientRect();
2209
+ const nx = Math.min(x, window.innerWidth - r.width - 4);
2210
+ const ny = Math.min(y, window.innerHeight - r.height - 4);
2211
+ this._menu.style.left = Math.max(4, nx) + 'px';
2212
+ this._menu.style.top = Math.max(4, ny) + 'px';
2213
+ }
2214
+
2215
+ _closeMenu() {
2216
+ if (this._menu) this._menu.removeAttribute('data-open');
2217
+ this._menuIndex = -1;
2218
+ this._menuIndices = null;
2219
+ }
2220
+
2221
+ _openConfirm(sel) {
2222
+ if (!this._confirm) return;
2223
+ const list = Array.isArray(sel) ? sel : [sel];
2224
+ // Hold the slide ELEMENTS: the deck can re-render while the dialog
2225
+ // is open (collaborator/agent edit), and a frozen index list would
2226
+ // then address the wrong slides — a same-count reorder even passes
2227
+ // the host's witness guard. Elements re-resolve at danger-click.
2228
+ this._confirmEls = list.map((i) => this._slides[i]).filter(Boolean);
2229
+ this._confirm.querySelector('.title').textContent = list.length === 1
2230
+ ? 'Delete slide ' + (list[0] + 1) + '?'
2231
+ : 'Delete ' + list.length + ' slides?';
2232
+ this._confirm.querySelector('.msg').textContent = list.length === 1
2233
+ ? 'This slide will be removed from the deck.'
2234
+ : 'These slides will be removed from the deck.';
2235
+ this._confirm.setAttribute('data-open', '');
2236
+ const btn = this._confirm.querySelector('.danger');
2237
+ if (btn && btn.focus) btn.focus();
2238
+ }
2239
+
2240
+ _closeConfirm() {
2241
+ if (this._confirm) this._confirm.removeAttribute('data-open');
2242
+ this._confirmEls = null;
2243
+ }
2244
+
2245
+ /** Return focus to the current slide's thumb so the keyboard flow
2246
+ * (Delete → Enter → Delete …) survives the confirm dialog closing.
2247
+ * Without 'force', skipped while a structural op is in flight
2248
+ * (_railLock): _index is then an optimistic post-op value that
2249
+ * doesn't address the pre-op thumb list — _pendingRailRefocus stays
2250
+ * armed and the ack/slotchange paths call back with force once the
2251
+ * rail reflects the op. Skipped (and disarmed) while the rail is
2252
+ * inert (hidden / presenting). */
2253
+ _focusCurrentThumb(force) {
2254
+ if (!force && this._railLock) return;
2255
+ this._pendingRailRefocus = false;
2256
+ // Never yank focus from content the user reached meanwhile (e.g.
2257
+ // an input inside a slide during the ack round-trip) — only
2258
+ // reclaim it from the rail's own surfaces, or from nowhere.
2259
+ const ae = this._root && this._root.activeElement;
2260
+ const ours = !ae ||
2261
+ (this._rail && this._rail.contains(ae)) ||
2262
+ (this._confirm && this._confirm.contains(ae)) ||
2263
+ (this._menu && this._menu.contains(ae));
2264
+ const lightAe = document.activeElement;
2265
+ const lightOk = !lightAe || lightAe === document.body || lightAe === this;
2266
+ if (!ours || !lightOk) return;
2267
+ const cur = this._thumbs && this._thumbs[this._index];
2268
+ if (cur && this._rail && !this._rail.inert) cur.thumb.focus({ preventScroll: true });
2269
+ }
2270
+
2271
+ /** Selection as sorted slide indices. An empty explicit selection
2272
+ * means the current slide (the rail's implicit selection). */
2273
+ _selectionIndices() {
2274
+ const out = [];
2275
+ this._slides.forEach((s, i) => {
2276
+ if (this._selected.has(s)) out.push(i);
2277
+ });
2278
+ if (!out.length && this._slides[this._index]) out.push(this._index);
2279
+ return out;
2280
+ }
2281
+
2282
+ _clearSelection() {
2283
+ // Re-anchor before the early return: a plain click followed by
2284
+ // arrow/tap navigation leaves _selected empty but the anchor
2285
+ // pointing at the old slide, and a later shift-click would range
2286
+ // from there instead of the current slide.
2287
+ this._selAnchor = null;
2288
+ if (!this._selected.size) return;
2289
+ this._selected.clear();
2290
+ this._syncSelection();
2291
+ }
2292
+
2293
+ _syncSelection() {
2294
+ (this._thumbs || []).forEach((t) => {
2295
+ if (this._selected.has(t.slide)) t.thumb.setAttribute('data-selected', '');
2296
+ else t.thumb.removeAttribute('data-selected');
2297
+ });
2298
+ }
2299
+
2300
+ /** Rail mutations. When a dc-runtime is present (`window.__dcUpdate`)
2301
+ * the host owns the light DOM — handlers emit a dc-op only and the
2302
+ * host applies it (to the editor's model or to the source file) and
2303
+ * re-renders via dc-runtime; slotchange catches the rail up.
2304
+ * Structural ops lock rail input until the host acks so a rapid second
2305
+ * click can't address a stale index; setAttr/removeAttr respect the
2306
+ * lock but don't set it (indices unchanged; the host serializes).
2307
+ * `newIndex` is written to location.hash so slotchange's
2308
+ * _restoreIndex lands on the right slide.
2309
+ *
2310
+ * With NO dc-runtime (a raw .html deck), there's no re-render path,
2311
+ * so handlers self-mutate locally for an instant update and emit
2312
+ * `emitOnly: false`; the host persists to disk without
2313
+ * re-rendering over the already-mutated DOM.
2314
+ *
2315
+ * See docs/dc-ops.md for the contract. */
2316
+ /** True when the page's DC runtime reports a live template stream for
2317
+ * any component here (newer support.js bundles only — older bundles
2318
+ * lack the signal and the HOST-side gate covers those decks). Rail
2319
+ * mutations are refused for the duration: a mid-stream op addresses
2320
+ * slide indices the stream is rewriting underneath the click. */
2321
+ _streamActive() {
2322
+ try {
2323
+ return !!window.__dcUpdate &&
2324
+ typeof window.__dcStreaming === 'function' &&
2325
+ window.__dcStreaming();
2326
+ } catch (e) { return false; }
2327
+ }
2328
+
2329
+ /** Transient in-stage notice for a refused mid-stream rail op. */
2330
+ _showStreamNotice() {
2331
+ this._showNotice(
2332
+ 'Claude is still updating this deck — try again when it finishes.'
2333
+ );
2334
+ }
2335
+
2336
+ /** Transient bottom-center toast for a refused rail gesture. */
2337
+ _showNotice(text) {
2338
+ if (!this._root) return;
2339
+ let n = this._streamNotice;
2340
+ if (!n) {
2341
+ n = document.createElement('div');
2342
+ n.className = 'export-hidden';
2343
+ n.setAttribute('data-omelette-chrome', '');
2344
+ n.setAttribute('role', 'status');
2345
+ n.style.cssText =
2346
+ 'position:fixed;left:50%;bottom:24px;transform:translateX(-50%);' +
2347
+ 'background:rgba(22,22,22,.94);color:#fff;' +
2348
+ 'font:500 13px/1.4 system-ui,sans-serif;padding:8px 14px;' +
2349
+ 'border-radius:8px;z-index:2147483646;pointer-events:none;' +
2350
+ 'opacity:0;transition:opacity .15s ease';
2351
+ this._root.append(n);
2352
+ this._streamNotice = n;
2353
+ }
2354
+ n.textContent = text;
2355
+ n.style.opacity = '1';
2356
+ if (this._streamNoticeTimer) clearTimeout(this._streamNoticeTimer);
2357
+ this._streamNoticeTimer = setTimeout(() => { n.style.opacity = '0'; }, 2600);
2358
+ }
2359
+
2360
+ _emitDcOp(op, slide, lock, newIndex) {
2361
+ // Mid-stream guard: refuse the gesture outright — no lock, no
2362
+ // optimistic index change, no emit, no self-mutation (returning
2363
+ // true short-circuits every caller). The host applies the same
2364
+ // gate for decks whose committed support.js predates the signal.
2365
+ if (this._streamActive()) {
2366
+ this._showStreamNotice();
2367
+ return true;
2368
+ }
2369
+ // Slide index (template/script/style filtered — same as
2370
+ // _collectSlides). deck-stage is a filtered-index dc-op emitter;
2371
+ // the host resolves against findDeckStage().slideTids. Callers
2372
+ // already pass `to` as a slide index.
2373
+ op.at = this._slides.indexOf(slide);
2374
+ op.witness = { childCount: this._slides.length };
2375
+ // dc-runtime wraps an <x-import>-mounted component in a
2376
+ // <div class="sc-host-x" data-dc-tpl="N"> host — the stamp is on the
2377
+ // WRAPPER, not this element. closest() finds it (or this element's
2378
+ // own stamp when directly templated).
2379
+ const host = this.closest('[data-dc-tpl]');
2380
+ const tid = host && host.getAttribute('data-dc-tpl');
2381
+ op.mount = { tid: tid !== null ? parseInt(tid, 10) : null, tag: 'deck-stage' };
2382
+ op.emitOnly = !!window.__dcUpdate;
2383
+ if (op.emitOnly) {
2384
+ if (lock) this._railLock = true;
2385
+ if (newIndex != null && newIndex !== this._index) {
2386
+ this._indexBeforeEmit = this._index;
2387
+ this._index = newIndex;
2388
+ try { history.replaceState(null, '', '#' + (newIndex + 1)); } catch (e) {}
2389
+ }
2390
+ }
2391
+ this.dispatchEvent(new CustomEvent('dc-op', {
2392
+ detail: op, bubbles: true, composed: true}));
2393
+ return op.emitOnly;
2394
+ }
2395
+
2396
+ /** Delete a set of slides (pre-op indices). One slide delegates to
2397
+ * _deleteSlide — the plain 'remove' op — so single deletes keep
2398
+ * working against hosts that predate 'removeMany'. A bulk delete is
2399
+ * ONE op: one host write, one undo snapshot, and indices that all
2400
+ * address the same pre-op deck (N acked single ops would each need
2401
+ * a fresh witness). */
2402
+ _deleteSlides(list) {
2403
+ if (this._railLock || !list) return;
2404
+ const indices = [...new Set(list)]
2405
+ .filter((i) => this._slides[i])
2406
+ .sort((a, b) => a - b);
2407
+ if (!indices.length || indices.length >= this._slides.length) return;
2408
+ if (indices.length === 1) {
2409
+ this._deleteSlide(indices[0]);
2410
+ return;
2411
+ }
2412
+ // Mirrors _duplicateSlide: check the stream gate before doing any
2413
+ // work (_emitDcOp re-checks).
2414
+ if (this._streamActive()) {
2415
+ this._showStreamNotice();
2416
+ return;
2417
+ }
2418
+ const els = indices.map((i) => this._slides[i]);
2419
+ const del = new Set(indices);
2420
+ const cur = this._index;
2421
+ // New current index in post-op space: shift the kept slide left by
2422
+ // the deletions below it; if the current slide itself is deleted,
2423
+ // land on the nearest survivor (after, else before).
2424
+ const below = (n) => indices.reduce((k, x) => k + (x < n ? 1 : 0), 0);
2425
+ let ni;
2426
+ if (!del.has(cur)) {
2427
+ ni = cur - below(cur);
2428
+ } else {
2429
+ let s = -1;
2430
+ for (let j = cur + 1; j < this._slides.length; j++) {
2431
+ if (!del.has(j)) { s = j; break; }
2432
+ }
2433
+ if (s === -1) {
2434
+ for (let j = cur - 1; j >= 0; j--) {
2435
+ if (!del.has(j)) { s = j; break; }
2436
+ }
2437
+ }
2438
+ ni = s < 0 ? 0 : s - below(s);
2439
+ }
2440
+ // Emit-path deletes can't refocus until the host re-renders; arm
2441
+ // the flag at emit time (never on a refused/no-op path) so
2442
+ // ack/slotchange can finish the keyboard flow's focus hand-back.
2443
+ // The local path clears it via the caller's _focusCurrentThumb().
2444
+ this._pendingRailRefocus = true;
2445
+ if (this._emitDcOp({ op: 'removeMany', indices }, els[0], true, ni)) return;
2446
+ this._index = ni;
2447
+ this._squelchSlotChange = true;
2448
+ els.forEach((el) => el.remove());
2449
+ this._collectSlides();
2450
+ this._applyIndex({ showOverlay: true, broadcast: true, reason: 'mutation' });
2451
+ }
2452
+
2453
+ _deleteSlide(i) {
2454
+ if (this._railLock) return;
2455
+ const slide = this._slides[i];
2456
+ if (!slide || this._slides.length <= 1) return;
2457
+ const cur = this._index;
2458
+ const ni = (i < cur || (i === cur && i === this._slides.length - 1)) ? cur - 1 : cur;
2459
+ this._pendingRailRefocus = true;
2460
+ if (this._emitDcOp({ op: 'remove' }, slide, true, ni)) return;
2461
+ this._index = ni;
2462
+ this._squelchSlotChange = true;
2463
+ slide.remove();
2464
+ this._collectSlides();
2465
+ this._applyIndex({ showOverlay: true, broadcast: true, reason: 'mutation' });
2466
+ }
2467
+
2468
+ _duplicateSlide(i) {
2469
+ if (this._railLock) return;
2470
+ const slide = this._slides[i];
2471
+ if (!slide) return;
2472
+ // Mint ids + copy component state BEFORE emitting, so the op can
2473
+ // carry the id map — but never mint for an op the stream gate is
2474
+ // about to refuse (_emitDcOp re-checks; this avoids orphaned keys).
2475
+ if (this._streamActive()) {
2476
+ this._showStreamNotice();
2477
+ return;
2478
+ }
2479
+ const copy = slide.cloneNode(true);
2480
+ copy.removeAttribute('id');
2481
+ const ids = this._remintDuplicateIds(copy);
2482
+ const op = { op: 'duplicate' };
2483
+ if (ids) op.ids = ids;
2484
+ if (this._emitDcOp(op, slide, true, i + 1)) return;
2485
+ this._index = i + 1;
2486
+ this._squelchSlotChange = true;
2487
+ this.insertBefore(copy, slide.nextSibling);
2488
+ this._collectSlides();
2489
+ this._applyIndex({ showOverlay: true, broadcast: true, reason: 'mutation' });
2490
+ }
2491
+
2492
+ /** Duplicate id policy. Plain ids are stripped — two live slides must
2493
+ * not share one id. But a component that KEYS persistent state by id
2494
+ * (image-slot's sidecar photo) would silently lose that state with
2495
+ * its id. Such a component opts out of the strip by exposing a
2496
+ * static cloneSlot(fromId, isFree) that copies its stored state
2497
+ * under a fresh id of its choosing and returns that id. The old→new
2498
+ * map is returned (or null) and rides the dc-op so the host writes
2499
+ * the SAME ids into source — without that, the copy's state would
2500
+ * revert on reload (docs/dc-ops.md). */
2501
+ _remintDuplicateIds(copy) {
2502
+ const ids = {};
2503
+ let found = false;
2504
+ const used = new Set();
2505
+ const idOk = /^[A-Za-z][\w-]{0,63}$/;
2506
+ const isFree = (id) =>
2507
+ idOk.test(id) && !used.has(id) && !document.getElementById(id);
2508
+ copy.querySelectorAll('[id]').forEach((el) => {
2509
+ const tag = el.tagName.toLowerCase();
2510
+ const cls = tag.indexOf('-') >= 0 && customElements.get(tag);
2511
+ let next = null;
2512
+ if (el.id && cls && typeof cls.cloneSlot === 'function') {
2513
+ try { next = cls.cloneSlot(el.id, isFree); } catch (e) {}
2514
+ }
2515
+ // Re-checked here so a misbehaving static can't smuggle a dupe
2516
+ // or an unsafe value into the document / the emitted op.
2517
+ if (typeof next === 'string' && isFree(next)) {
2518
+ ids[el.id] = next;
2519
+ used.add(next);
2520
+ el.id = next;
2521
+ found = true;
2522
+ } else {
2523
+ el.removeAttribute('id');
2524
+ }
2525
+ });
2526
+ return found ? ids : null;
2527
+ }
2528
+
2529
+ _toggleSkip(i) {
2530
+ if (this._railLock) return;
2531
+ const slide = this._slides[i];
2532
+ if (!slide) return;
2533
+ const on = !slide.hasAttribute('data-deck-skip');
2534
+ if (this._emitDcOp(
2535
+ on ? { op: 'setAttr', attr: 'data-deck-skip', value: '' }
2536
+ : { op: 'removeAttr', attr: 'data-deck-skip' },
2537
+ slide, false
2538
+ )) return;
2539
+ if (on) slide.setAttribute('data-deck-skip', '');
2540
+ else slide.removeAttribute('data-deck-skip');
2541
+ }
2542
+
2543
+ _skippedIndices() {
2544
+ const out = [];
2545
+ for (let i = 0; i < this._slides.length; i++) {
2546
+ if (this._slides[i].hasAttribute('data-deck-skip')) out.push(i);
2547
+ }
2548
+ return out;
2549
+ }
2550
+
2551
+ _moveSlide(i, j) {
2552
+ if (this._railLock || j < 0 || j >= this._slides.length || j === i) return;
2553
+ const cur = this._index;
2554
+ const ni = cur === i ? j
2555
+ : (i < cur && j >= cur) ? cur - 1
2556
+ : (i > cur && j <= cur) ? cur + 1
2557
+ : cur;
2558
+ const slide = this._slides[i];
2559
+ if (this._emitDcOp({ op: 'move', to: j }, slide, true, ni)) return;
2560
+ const ref = j < i ? this._slides[j] : this._slides[j].nextSibling;
2561
+ this._index = ni;
2562
+ this._squelchSlotChange = true;
2563
+ this.insertBefore(slide, ref);
2564
+ this._collectSlides();
2565
+ this._applyIndex({ showOverlay: false, broadcast: true, reason: 'mutation' });
2566
+ }
2567
+
2568
+ // Public API ------------------------------------------------------------
2569
+
2570
+ /** Current slide index (0-based). */
2571
+ get index() { return this._index; }
2572
+ /** Total slide count. */
2573
+ get length() { return this._slides.length; }
2574
+ /** Programmatically navigate. */
2575
+ goTo(i) { this._go(i, 'api'); }
2576
+ next() { this._advance(1, 'api'); }
2577
+ prev() { this._advance(-1, 'api'); }
2578
+ reset() { this._go(0, 'api'); }
2579
+ }
2580
+
2581
+ if (!customElements.get('deck-stage')) {
2582
+ customElements.define('deck-stage', DeckStage);
2583
+ }
2584
+ })();