@heyramzi/ui 1.54.2 → 2.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 (361) hide show
  1. package/COMPONENTS.md +3 -2
  2. package/README.md +34 -9
  3. package/package.json +198 -6
  4. package/src/ai/generation-progress/GenerationProgress.svelte +12 -12
  5. package/src/ambient.d.ts +8 -0
  6. package/src/brand/brand.test.ts +67 -0
  7. package/src/brand/index.ts +137 -0
  8. package/src/canvas/README.md +236 -0
  9. package/src/canvas/card/card-frame.svelte +61 -0
  10. package/src/canvas/card/card.css +39 -0
  11. package/src/canvas/card/resize-handles.svelte +119 -0
  12. package/src/canvas/card/resize.ts +33 -0
  13. package/src/canvas/chrome/chrome-zoom.svelte +120 -0
  14. package/src/canvas/chrome/chrome.css +231 -0
  15. package/src/canvas/chrome/lock-badge.svelte +18 -0
  16. package/src/canvas/chrome/side-handles.svelte +30 -0
  17. package/src/canvas/chrome/size-chip.svelte +45 -0
  18. package/src/canvas/chrome/snap-guides.svelte +69 -0
  19. package/src/canvas/components/icons/ToolIcon.svelte +30 -0
  20. package/src/canvas/components/shared/svg/PreviewSvg.svelte +67 -0
  21. package/src/canvas/components/shared/svg/ShapeOutline.svelte +76 -0
  22. package/src/canvas/components/shared/svg/StrokeNode.svelte +69 -0
  23. package/src/canvas/components/shared/svg/TextRule.svelte +37 -0
  24. package/src/canvas/edges/bend-handle.svelte +76 -0
  25. package/src/canvas/edges/edge-gradient.svelte +30 -0
  26. package/src/canvas/edges/edge-path.test.ts +58 -0
  27. package/src/canvas/edges/edge-path.ts +293 -0
  28. package/src/canvas/edges/gradient-edge.svelte +43 -0
  29. package/src/canvas/editor/context-menu.test.ts +65 -0
  30. package/src/canvas/editor/context-menu.ts +48 -0
  31. package/src/canvas/editor/dispatch.svelte.test.ts +243 -0
  32. package/src/canvas/editor/dispatch.svelte.ts +205 -0
  33. package/src/canvas/editor/editor-context.ts +62 -0
  34. package/src/canvas/editor/editor-mode.ts +67 -0
  35. package/src/canvas/editor/frame.ts +98 -0
  36. package/src/canvas/editor/hand-context.ts +15 -0
  37. package/src/canvas/editor/history-core.test.ts +239 -0
  38. package/src/canvas/editor/history-core.ts +247 -0
  39. package/src/canvas/editor/keymap.test.ts +161 -0
  40. package/src/canvas/editor/keymap.ts +151 -0
  41. package/src/canvas/editor/layout-actions.test.ts +156 -0
  42. package/src/canvas/editor/layout-actions.ts +219 -0
  43. package/src/canvas/editor/lock.test.ts +68 -0
  44. package/src/canvas/editor/lock.ts +87 -0
  45. package/src/canvas/editor/rebase.test.ts +208 -0
  46. package/src/canvas/editor/rebase.ts +272 -0
  47. package/src/canvas/editor/style.ts +184 -0
  48. package/src/canvas/editor/text-field-context.ts +41 -0
  49. package/src/canvas/editor/z-order.test.ts +75 -0
  50. package/src/canvas/editor/z-order.ts +87 -0
  51. package/src/canvas/headless/alignment.test.ts +59 -0
  52. package/src/canvas/headless/alignment.ts +23 -0
  53. package/src/canvas/headless/break-apart-find.ts +418 -0
  54. package/src/canvas/headless/break-apart-pieces.ts +104 -0
  55. package/src/canvas/headless/break-apart-plan.ts +439 -0
  56. package/src/canvas/headless/break-apart-style.ts +106 -0
  57. package/src/canvas/headless/break-apart.ts +244 -0
  58. package/src/canvas/headless/canvas-cleanup.test.ts +181 -0
  59. package/src/canvas/headless/canvas-cleanup.ts +102 -0
  60. package/src/canvas/headless/edge-anchors.test.ts +70 -0
  61. package/src/canvas/headless/edge-anchors.ts +143 -0
  62. package/src/canvas/headless/edge-memo.test.ts +58 -0
  63. package/src/canvas/headless/edge-memo.ts +32 -0
  64. package/src/canvas/headless/edge-serialization.test.ts +55 -0
  65. package/src/canvas/headless/edge-serialization.ts +32 -0
  66. package/src/canvas/headless/figure-geometry.ts +293 -0
  67. package/src/canvas/headless/fit-viewport.test.ts +85 -0
  68. package/src/canvas/headless/fit-viewport.ts +137 -0
  69. package/src/canvas/headless/grid.test.ts +34 -0
  70. package/src/canvas/headless/grid.ts +94 -0
  71. package/src/canvas/headless/icon-node.test.ts +23 -0
  72. package/src/canvas/headless/icon-node.ts +66 -0
  73. package/src/canvas/headless/index.ts +9 -0
  74. package/src/canvas/headless/linkify.test.ts +23 -0
  75. package/src/canvas/headless/linkify.ts +25 -0
  76. package/src/canvas/headless/pen-marks.ts +133 -0
  77. package/src/canvas/headless/sketch.test.ts +99 -0
  78. package/src/canvas/headless/sketch.ts +302 -0
  79. package/src/canvas/headless/snap-guides.test.ts +61 -0
  80. package/src/canvas/headless/snap-guides.ts +146 -0
  81. package/src/canvas/headless/svg-figure.test.ts +49 -0
  82. package/src/canvas/headless/svg-figure.ts +176 -0
  83. package/src/canvas/headless/table-layout.test.ts +158 -0
  84. package/src/canvas/headless/table-layout.ts +309 -0
  85. package/src/canvas/index.ts +35 -0
  86. package/src/canvas/lod/lod-mode.svelte.ts +30 -0
  87. package/src/canvas/lod/lod-switch.svelte +140 -0
  88. package/src/canvas/lod/render-all.svelte.ts +52 -0
  89. package/src/canvas/ops/arrange.ts +108 -0
  90. package/src/canvas/ops/ops.test.ts +98 -0
  91. package/src/canvas/ops/ops.ts +347 -0
  92. package/src/canvas/pen/PenField.svelte +128 -0
  93. package/src/canvas/pen/PenField.test.ts +87 -0
  94. package/src/canvas/pen/PenOverlay.svelte +172 -0
  95. package/src/canvas/pen/ShapeNode.svelte +64 -0
  96. package/src/canvas/pen/TextNode.svelte +97 -0
  97. package/src/canvas/pen/colors.ts +28 -0
  98. package/src/canvas/pen/eraser.ts +46 -0
  99. package/src/canvas/pen/geometry.ts +148 -0
  100. package/src/canvas/pen/host-field.test.svelte +13 -0
  101. package/src/canvas/pen/index.ts +40 -0
  102. package/src/canvas/pen/ops.ts +16 -0
  103. package/src/canvas/pen/pen-session.ts +177 -0
  104. package/src/canvas/pen/pen.css +149 -0
  105. package/src/canvas/pen/plugin.ts +139 -0
  106. package/src/canvas/pen/shapes.ts +104 -0
  107. package/src/canvas/pen/stroke-path.ts +54 -0
  108. package/src/canvas/pen/types.ts +96 -0
  109. package/src/canvas/pen/ui.ts +26 -0
  110. package/src/canvas/plugin.test.ts +45 -0
  111. package/src/canvas/plugin.ts +159 -0
  112. package/src/canvas/screen/GlyphIcon.svelte +25 -0
  113. package/src/canvas/screen/PartView.svelte +176 -0
  114. package/src/canvas/screen/ScreenBody.svelte +142 -0
  115. package/src/canvas/screen/ScreenCard.svelte +32 -0
  116. package/src/canvas/screen/glyphs/button.svg +1 -0
  117. package/src/canvas/screen/glyphs/close.svg +1 -0
  118. package/src/canvas/screen/glyphs/down.svg +1 -0
  119. package/src/canvas/screen/glyphs/header.svg +1 -0
  120. package/src/canvas/screen/glyphs/hills.svg +1 -0
  121. package/src/canvas/screen/glyphs/image.svg +1 -0
  122. package/src/canvas/screen/glyphs/input.svg +1 -0
  123. package/src/canvas/screen/glyphs/list.svg +1 -0
  124. package/src/canvas/screen/glyphs/modal.svg +1 -0
  125. package/src/canvas/screen/glyphs/plus.svg +1 -0
  126. package/src/canvas/screen/glyphs/up.svg +1 -0
  127. package/src/canvas/screen/glyphs.ts +40 -0
  128. package/src/canvas/screen/labels.ts +41 -0
  129. package/src/canvas/screen/parts.ts +164 -0
  130. package/src/canvas/screen/plugin.ts +67 -0
  131. package/src/canvas/screen/screen.css +593 -0
  132. package/src/canvas/screen/types.ts +47 -0
  133. package/src/canvas/screen/ui.ts +13 -0
  134. package/src/canvas/test-utils.ts +18 -0
  135. package/src/canvas/types.ts +53 -0
  136. package/src/canvas/ui/CanvasToolbar.svelte +169 -0
  137. package/src/canvas/ui/FrameLayer.svelte +116 -0
  138. package/src/canvas/ui/PenLayer.svelte +39 -0
  139. package/src/canvas/ui/StylePanel.svelte +268 -0
  140. package/src/canvas/ui/canvas-grid.svelte +156 -0
  141. package/src/canvas/ui/canvas-ui.css +350 -0
  142. package/src/canvas/ui/forward-wheel.ts +29 -0
  143. package/src/canvas/ui/icons.ts +124 -0
  144. package/src/canvas/ui/place-panel.ts +80 -0
  145. package/src/canvas/ui/shortcut-tip.test.ts +16 -0
  146. package/src/canvas/ui/shortcut-tip.ts +17 -0
  147. package/src/canvas/ui/tools.ts +146 -0
  148. package/src/charts/README.md +104 -0
  149. package/src/charts/ambient.d.ts +4 -0
  150. package/src/charts/animated-value.css +7 -0
  151. package/src/charts/animated-value.svelte +110 -0
  152. package/src/charts/area-chart.css +29 -0
  153. package/src/charts/area-chart.svelte +204 -0
  154. package/src/charts/bar-chart.css +119 -0
  155. package/src/charts/bar-chart.svelte +275 -0
  156. package/src/charts/charts.css +201 -0
  157. package/src/charts/crosshair.svelte +35 -0
  158. package/src/charts/dashboard/chart.ts +5 -0
  159. package/src/charts/dashboard/dashboard-empty.svelte +55 -0
  160. package/src/charts/dashboard/dashboard-section.svelte +58 -0
  161. package/src/charts/dashboard/data-panel.svelte +81 -0
  162. package/src/charts/dashboard/data-sort-head.svelte +102 -0
  163. package/src/charts/dashboard/data-table.svelte +278 -0
  164. package/src/charts/dashboard/data-table.ts +26 -0
  165. package/src/charts/dashboard/index.ts +25 -0
  166. package/src/charts/dashboard/panel-grid.svelte +43 -0
  167. package/src/charts/dashboard/period-filter.svelte +53 -0
  168. package/src/charts/dashboard/rating-chip.svelte +35 -0
  169. package/src/charts/dashboard/stat-band.svelte +47 -0
  170. package/src/charts/dashboard/stat-tile.svelte +176 -0
  171. package/src/charts/dashboard/windows.ts +8 -0
  172. package/src/charts/donut-chart.css +56 -0
  173. package/src/charts/donut-chart.svelte +158 -0
  174. package/src/charts/edge-fade.svelte +43 -0
  175. package/src/charts/edge-fade.ts +16 -0
  176. package/src/charts/figure.css +100 -0
  177. package/src/charts/figure.svelte +122 -0
  178. package/src/charts/gauge-chart.css +94 -0
  179. package/src/charts/gauge-chart.svelte +302 -0
  180. package/src/charts/hover-band.test.ts +30 -0
  181. package/src/charts/hover-band.ts +21 -0
  182. package/src/charts/hover-focus.svelte +85 -0
  183. package/src/charts/index.ts +31 -0
  184. package/src/charts/legend.css +136 -0
  185. package/src/charts/legend.svelte +112 -0
  186. package/src/charts/line-chart.css +26 -0
  187. package/src/charts/line-chart.svelte +256 -0
  188. package/src/charts/motion.ts +19 -0
  189. package/src/charts/number-chart.css +179 -0
  190. package/src/charts/number-chart.svelte +187 -0
  191. package/src/charts/primitives/chart-container.svelte +57 -0
  192. package/src/charts/primitives/chart-style.svelte +36 -0
  193. package/src/charts/primitives/chart-tooltip.svelte +161 -0
  194. package/src/charts/primitives/chart-utils.ts +54 -0
  195. package/src/charts/primitives/index.ts +11 -0
  196. package/src/charts/reference-line.css +18 -0
  197. package/src/charts/reference-line.svelte +31 -0
  198. package/src/charts/reveal.svelte +61 -0
  199. package/src/charts/ring-chart.css +104 -0
  200. package/src/charts/ring-chart.svelte +166 -0
  201. package/src/charts/sparkline.css +45 -0
  202. package/src/charts/sparkline.svelte +141 -0
  203. package/src/charts/stat-card.css +105 -0
  204. package/src/charts/stat-card.svelte +267 -0
  205. package/src/charts/time-series-chart.css +28 -0
  206. package/src/charts/time-series-chart.svelte +179 -0
  207. package/src/charts/trend-pill.css +38 -0
  208. package/src/charts/trend-pill.svelte +48 -0
  209. package/src/components/copy/copy-button.svelte +1 -1
  210. package/src/components/docs/doc-page.svelte +1 -1
  211. package/src/components/docs/on-this-page.svelte +2 -2
  212. package/src/components/list-page/empty-state.svelte +1 -1
  213. package/src/components/list-page/filter-popover.svelte +1 -1
  214. package/src/components/list-page/section-label.svelte +1 -1
  215. package/src/components/segmented/segment.svelte +4 -4
  216. package/src/components/theme/theme-provider.svelte +6 -4
  217. package/src/components/toolbar/index.ts +5 -0
  218. package/src/components/toolbar/toolbar.svelte +168 -0
  219. package/src/components/toolbar/toolbar.test.ts +66 -0
  220. package/src/editor/README.md +58 -0
  221. package/src/editor/ambient.d.ts +1 -0
  222. package/src/editor/block-id.ts +77 -0
  223. package/src/editor/block-ops.test.ts +237 -0
  224. package/src/editor/block-ops.ts +157 -0
  225. package/src/editor/content.css +418 -0
  226. package/src/editor/editor-toolbar.svelte +303 -0
  227. package/src/editor/editor.svelte +462 -0
  228. package/src/editor/extensions.ts +145 -0
  229. package/src/editor/image-toolbar.svelte +123 -0
  230. package/src/editor/index.ts +17 -0
  231. package/src/editor/inline-editor.svelte +261 -0
  232. package/src/editor/markdown-escaping.test.ts +158 -0
  233. package/src/editor/markdown-marks.test.ts +89 -0
  234. package/src/editor/markdown-test-helpers.ts +13 -0
  235. package/src/editor/markdown-youtube.test.ts +93 -0
  236. package/src/editor/markdown.test.ts +250 -0
  237. package/src/editor/markdown.ts +511 -0
  238. package/src/editor/renderer.svelte +92 -0
  239. package/src/editor/serializer.test.ts +133 -0
  240. package/src/editor/serializer.ts +370 -0
  241. package/src/editor/slash-items.ts +115 -0
  242. package/src/editor/slash-menu.svelte +159 -0
  243. package/src/editor/youtube-url.ts +35 -0
  244. package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +13 -13
  245. package/src/lib/result.ts +19 -0
  246. package/src/onboarding/README.md +120 -0
  247. package/src/onboarding/define-tours.test.ts +50 -0
  248. package/src/onboarding/define-tours.ts +31 -0
  249. package/src/onboarding/demo.svelte +86 -0
  250. package/src/onboarding/index.ts +12 -0
  251. package/src/onboarding/onboarding-provider.svelte +120 -0
  252. package/src/onboarding/onboarding.svelte.ts +24 -0
  253. package/src/onboarding/pick.test.ts +79 -0
  254. package/src/onboarding/pick.ts +36 -0
  255. package/src/onboarding/stores.test.ts +103 -0
  256. package/src/onboarding/stores.ts +58 -0
  257. package/src/onboarding/tour-card.svelte +70 -0
  258. package/src/onboarding/tour-runner.svelte +197 -0
  259. package/src/onboarding/types.ts +34 -0
  260. package/src/onboarding/wait.ts +29 -0
  261. package/src/shadcn/command/command-input.svelte +1 -1
  262. package/src/slides/Controls.svelte +138 -0
  263. package/src/slides/Deck.svelte +271 -0
  264. package/src/slides/Fragment.svelte +71 -0
  265. package/src/slides/Overview.svelte +113 -0
  266. package/src/slides/Presenter.svelte +292 -0
  267. package/src/slides/README.md +89 -0
  268. package/src/slides/Slide.svelte +179 -0
  269. package/src/slides/SlideList.svelte +19 -0
  270. package/src/slides/ambient.d.ts +1 -0
  271. package/src/slides/bridges.ts +69 -0
  272. package/src/slides/deck-controller.svelte.ts +292 -0
  273. package/src/slides/demo-slides.svelte +358 -0
  274. package/src/slides/demo.svelte +176 -0
  275. package/src/slides/export/capture.ts +113 -0
  276. package/src/slides/export/ffmpeg.ts +120 -0
  277. package/src/slides/export/index.ts +9 -0
  278. package/src/slides/export/launch.ts +10 -0
  279. package/src/slides/export/pdf.ts +152 -0
  280. package/src/slides/export/types.ts +48 -0
  281. package/src/slides/index.ts +19 -0
  282. package/src/slides/labels.ts +48 -0
  283. package/src/slides/layout.ts +51 -0
  284. package/src/slides/model.ts +47 -0
  285. package/src/slides/navigation.ts +100 -0
  286. package/src/slides/theme.css +205 -0
  287. package/src/slides/types.ts +86 -0
  288. package/src/tasks/README.md +312 -0
  289. package/src/tasks/ambient.d.ts +3 -0
  290. package/src/tasks/board-order.ts +137 -0
  291. package/src/tasks/dates.ts +31 -0
  292. package/src/tasks/define-statuses.ts +32 -0
  293. package/src/tasks/demo.svelte +389 -0
  294. package/src/tasks/detail/detail-demo.svelte +186 -0
  295. package/src/tasks/detail/detail-types.ts +54 -0
  296. package/src/tasks/detail/task-chip.svelte +28 -0
  297. package/src/tasks/detail/task-detail.svelte +315 -0
  298. package/src/tasks/detail/task-property.svelte +17 -0
  299. package/src/tasks/detail/task-thread.svelte +258 -0
  300. package/src/tasks/gantt/gantt-bar.svelte +243 -0
  301. package/src/tasks/gantt/gantt-grid.svelte +94 -0
  302. package/src/tasks/gantt/gantt-group-row.svelte +55 -0
  303. package/src/tasks/gantt/gantt-header.svelte +71 -0
  304. package/src/tasks/gantt/gantt-task-row.svelte +186 -0
  305. package/src/tasks/gantt/gantt.css +22 -0
  306. package/src/tasks/gantt/gantt.test.ts +77 -0
  307. package/src/tasks/gantt/logic.test.ts +163 -0
  308. package/src/tasks/gantt/logic.ts +248 -0
  309. package/src/tasks/inbox/inbox-card.svelte +283 -0
  310. package/src/tasks/inbox/inbox-draft.svelte +84 -0
  311. package/src/tasks/inbox/inbox-logic.ts +55 -0
  312. package/src/tasks/inbox/inbox-statuses.ts +48 -0
  313. package/src/tasks/inbox/inbox-trace.svelte +45 -0
  314. package/src/tasks/inbox/inbox-types.ts +108 -0
  315. package/src/tasks/inbox/inbox.svelte +203 -0
  316. package/src/tasks/inbox/index.ts +23 -0
  317. package/src/tasks/index.ts +85 -0
  318. package/src/tasks/kanban-board.svelte +561 -0
  319. package/src/tasks/kanban-column.svelte +117 -0
  320. package/src/tasks/kanban-drag.ts +38 -0
  321. package/src/tasks/kanban-types.ts +65 -0
  322. package/src/tasks/presets/sales.ts +16 -0
  323. package/src/tasks/record/demo-data.ts +286 -0
  324. package/src/tasks/record/field-section.svelte +53 -0
  325. package/src/tasks/record/fields.ts +142 -0
  326. package/src/tasks/record/format.ts +87 -0
  327. package/src/tasks/record/gutter.ts +2 -0
  328. package/src/tasks/record/index.ts +54 -0
  329. package/src/tasks/record/info-list.svelte +316 -0
  330. package/src/tasks/record/kinds.ts +42 -0
  331. package/src/tasks/record/logic.ts +324 -0
  332. package/src/tasks/record/next-up.svelte +70 -0
  333. package/src/tasks/record/project-list.svelte +133 -0
  334. package/src/tasks/record/record-demo.svelte +324 -0
  335. package/src/tasks/record/record-header.svelte +70 -0
  336. package/src/tasks/record/record-shell.svelte +138 -0
  337. package/src/tasks/record/record-stats.svelte +48 -0
  338. package/src/tasks/record/record-timeline.svelte +231 -0
  339. package/src/tasks/record/record.test.ts +350 -0
  340. package/src/tasks/record/steps-rail.svelte +474 -0
  341. package/src/tasks/record/types.ts +280 -0
  342. package/src/tasks/record/where-we-are.svelte +121 -0
  343. package/src/tasks/reorder.ts +19 -0
  344. package/src/tasks/side-panel-types.ts +16 -0
  345. package/src/tasks/side-panel.svelte +193 -0
  346. package/src/tasks/status-badge.svelte +63 -0
  347. package/src/tasks/status-dot.svelte +228 -0
  348. package/src/tasks/status-icon.svelte +30 -0
  349. package/src/tasks/status-types.ts +40 -0
  350. package/src/tasks/task-gantt.svelte +230 -0
  351. package/src/tasks/task-list.svelte +470 -0
  352. package/src/tasks/task-types.ts +33 -0
  353. package/src/tasks/time/duration.ts +69 -0
  354. package/src/tasks/time/time-chip.svelte +62 -0
  355. package/src/tasks/time/time-dot.svelte +58 -0
  356. package/src/tasks/time/time-ring.svelte +74 -0
  357. package/src/tasks/time/time-tracker.svelte +565 -0
  358. package/src/tasks/time/time-types.ts +74 -0
  359. package/src/tasks/use-kanban-dnd.svelte.ts +173 -0
  360. package/src/tokens/base.css +5 -5
  361. package/src/tokens/palette.css +115 -120
@@ -0,0 +1,292 @@
1
+ import { untrack } from 'svelte';
2
+ import { fitBox, gridBoxes, type Box, type Size } from './layout.js';
3
+ import {
4
+ keyAction,
5
+ nextPosition,
6
+ ordinalOf,
7
+ prevPosition,
8
+ progressOf,
9
+ settle,
10
+ visibleIndexes,
11
+ type KeyAction,
12
+ type Position,
13
+ type SlideState,
14
+ } from './navigation.js';
15
+ import { resolveLabels, type DeckLabels } from './labels.js';
16
+ import { DEFAULT_CANVAS, type Canvas, type SlideNotes } from './types.js';
17
+
18
+ /** What a mounted Slide tells the deck. Properties are getters over the slide's own props. */
19
+ export type SlideEntry = {
20
+ readonly id: symbol;
21
+ readonly steps: number;
22
+ readonly hidden: boolean;
23
+ readonly label?: string;
24
+ readonly notes?: SlideNotes;
25
+ readonly el?: HTMLElement | null;
26
+ };
27
+
28
+ /** `remote` is a move that came from another window or frame, so it must not be echoed back. */
29
+ export type Source = 'local' | 'remote';
30
+ export type Listener = (position: Position, source: Source) => void;
31
+
32
+ /** `index` and `step` are read and written through, so a Deck can back them with its bindable props. */
33
+ export type ControllerOptions = {
34
+ index: number;
35
+ step: number;
36
+ loop?: boolean;
37
+ fragments?: boolean;
38
+ keyboard?: boolean;
39
+ /** Whether the controls bar is showing, so the overview grid leaves room for it. */
40
+ controls?: boolean;
41
+ /** Whether a presenter view is configured, so the `p` key has something to open. */
42
+ presenter?: boolean;
43
+ canvas?: Canvas;
44
+ labels?: Partial<DeckLabels>;
45
+ /** Wraps a position change so a View Transition can snapshot around it. */
46
+ transition?: (update: () => void) => void;
47
+ };
48
+
49
+ /* With several decks on a page, keys belong to the one the reader last touched. */
50
+ const live = new Set<DeckController>();
51
+ let active: DeckController | null = null;
52
+ const claimKeys = (deck: DeckController) => {
53
+ active = deck;
54
+ };
55
+
56
+ /** Height the controls bar takes at the bottom of the overview, in px. */
57
+ const CONTROLS_ROOM = 56;
58
+
59
+ /** How long the overview morph flag stays up, a little past the longest stagger. */
60
+ const MORPH_MS = 900;
61
+
62
+ const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
63
+
64
+ function isEditable(el: HTMLElement) {
65
+ return el.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName);
66
+ }
67
+
68
+ const byDocumentOrder = (a: SlideEntry, b: SlideEntry) =>
69
+ a.el && b.el ? (a.el.compareDocumentPosition(b.el) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1) : 0;
70
+
71
+ export class DeckController {
72
+ entries = $state.raw<SlideEntry[]>([]);
73
+ view = $state<'slide' | 'overview'>('slide');
74
+ /** The highlighted thumbnail in the overview, as an index among the visible slides. */
75
+ cursor = $state(0);
76
+ direction = $state<1 | -1>(1);
77
+ morphing = $state(false);
78
+ size = $state<Size>({ w: 0, h: 0 });
79
+
80
+ readonly options!: ControllerOptions;
81
+ #listeners = new Set<Listener>();
82
+ #timer: ReturnType<typeof setTimeout> | undefined;
83
+
84
+ constructor(options: ControllerOptions) {
85
+ this.options = options;
86
+ }
87
+
88
+ canvas = $derived(this.options.canvas ?? DEFAULT_CANVAS);
89
+ labels = $derived(resolveLabels(this.options.labels));
90
+ slides = $derived<SlideState[]>(
91
+ this.entries.map((entry) => ({ steps: this.options.fragments === false ? 0 : entry.steps, hidden: entry.hidden })),
92
+ );
93
+ visible = $derived(visibleIndexes(this.slides));
94
+ position = $derived(settle(this.slides, { index: this.options.index, step: this.options.step }));
95
+ ordinal = $derived(ordinalOf(this.slides, this.position.index));
96
+ progress = $derived(progressOf(this.slides, this.position));
97
+ current = $derived(this.entries[this.position.index]);
98
+ /** The slide after this one, for a presenter's preview. Null on the last. */
99
+ upcoming = $derived(this.visible.find((i) => i > this.position.index) ?? null);
100
+ overview = $derived(this.view === 'overview');
101
+ /** True when every fragment shows: in the overview, or on a deck that doesn't stage reveals. */
102
+ revealAll = $derived(this.overview || this.options.fragments === false);
103
+ atStart = $derived(prevPosition(this.slides, this.position, this.options.loop) === null);
104
+ atEnd = $derived(nextPosition(this.slides, this.position, this.options.loop) === null);
105
+ fit = $derived(fitBox(this.size, this.canvas));
106
+ grid = $derived(
107
+ gridBoxes(
108
+ { w: this.size.w, h: Math.max(0, this.size.h - (this.options.controls === false ? 0 : CONTROLS_ROOM)) },
109
+ this.canvas,
110
+ this.visible.length,
111
+ ),
112
+ );
113
+
114
+ /** Where slide `index` sits right now: centred to fit, or in its overview cell. */
115
+ boxOf(index: number): Box | undefined {
116
+ if (!this.overview) return this.fit;
117
+ return this.grid.boxes[this.visible.indexOf(index)];
118
+ }
119
+
120
+ register(entry: SlideEntry): () => void {
121
+ untrack(() => {
122
+ this.entries = [...this.entries, entry].sort(byDocumentOrder);
123
+ });
124
+ return () =>
125
+ untrack(() => {
126
+ this.entries = this.entries.filter((candidate) => candidate !== entry);
127
+ this.#write(this.position);
128
+ });
129
+ }
130
+
131
+ listen(listener: Listener): () => void {
132
+ this.#listeners.add(listener);
133
+ return () => this.#listeners.delete(listener);
134
+ }
135
+
136
+ /** Marks this deck as the one that owns the keys. Mounting does it too. */
137
+ activate() {
138
+ claimKeys(this);
139
+ }
140
+
141
+ mount(): () => void {
142
+ live.add(this);
143
+ claimKeys(this);
144
+ return () => {
145
+ live.delete(this);
146
+ if (active === this) active = null;
147
+ clearTimeout(this.#timer);
148
+ };
149
+ }
150
+
151
+ get listening() {
152
+ if (this.options.keyboard === false) return false;
153
+ const eligible = [...live].filter((deck) => deck.options.keyboard !== false);
154
+ return eligible.length <= 1 || active === this;
155
+ }
156
+
157
+ #write(position: Position) {
158
+ this.options.index = position.index;
159
+ this.options.step = position.step;
160
+ }
161
+
162
+ commit(target: Position | null, source: Source = 'local') {
163
+ if (!target) return;
164
+ const from = this.position;
165
+ const to = settle(this.slides, target);
166
+ if (to.index === from.index && to.step === from.step) return;
167
+ this.direction = to.index >= from.index ? 1 : -1;
168
+ const apply = () => this.#write(to);
169
+ if (to.index !== from.index && this.options.transition) this.options.transition(apply);
170
+ else apply();
171
+ for (const listener of this.#listeners) listener(to, source);
172
+ }
173
+
174
+ next() {
175
+ this.commit(nextPosition(this.slides, this.position, this.options.loop));
176
+ }
177
+
178
+ prev() {
179
+ this.commit(prevPosition(this.slides, this.position, this.options.loop));
180
+ }
181
+
182
+ go(index: number, step = 0, source: Source = 'local') {
183
+ this.commit({ index, step }, source);
184
+ }
185
+
186
+ toggleOverview(force?: boolean) {
187
+ const open = force ?? !this.overview;
188
+ if (open === this.overview) return;
189
+ if (open) this.cursor = Math.max(0, this.visible.indexOf(this.position.index));
190
+ this.view = open ? 'overview' : 'slide';
191
+ this.#morph();
192
+ }
193
+
194
+ /** Leaves the overview on slide `index`. Picking the current slide keeps its step. */
195
+ select(index: number) {
196
+ this.toggleOverview(false);
197
+ const keep = index === this.position.index;
198
+ this.go(index, keep ? this.position.step : 0);
199
+ }
200
+
201
+ #morph() {
202
+ this.morphing = true;
203
+ clearTimeout(this.#timer);
204
+ this.#timer = setTimeout(() => (this.morphing = false), MORPH_MS);
205
+ }
206
+
207
+ #moveCursor(to: number) {
208
+ this.cursor = clamp(to, 0, this.visible.length - 1);
209
+ }
210
+
211
+ /**
212
+ * Runs the deck's own keys. Returns the action it took, or null when the key wasn't its.
213
+ * `fullscreen` and `presenter` are returned untouched, since they need the page.
214
+ */
215
+ handleKey(event: KeyboardEvent): KeyAction | null {
216
+ if (event.defaultPrevented || !this.listening || event.altKey || event.ctrlKey || event.metaKey) return null;
217
+ const target = event.target instanceof HTMLElement ? event.target : null;
218
+ if (target && isEditable(target)) return null;
219
+ const action = keyAction(event.key, event.shiftKey);
220
+ if (!action) return null;
221
+ /* Space and Enter belong to a focused control, not to the deck. */
222
+ if ((event.key === ' ' || event.key === 'Enter') && target?.closest('button, a, summary')) return null;
223
+
224
+ const handled = this.overview ? this.#overviewKey(action) : this.#slideKey(action);
225
+ if (!handled) return null;
226
+ event.preventDefault();
227
+ return action;
228
+ }
229
+
230
+ #slideKey(action: KeyAction): boolean {
231
+ switch (action) {
232
+ case 'next':
233
+ this.next();
234
+ return true;
235
+ case 'prev':
236
+ this.prev();
237
+ return true;
238
+ case 'first':
239
+ this.go(this.visible[0] ?? 0);
240
+ return true;
241
+ case 'last':
242
+ this.go(this.visible[this.visible.length - 1] ?? 0);
243
+ return true;
244
+ case 'overview':
245
+ this.toggleOverview(true);
246
+ return true;
247
+ case 'fullscreen':
248
+ return true;
249
+ case 'presenter':
250
+ return !!this.options.presenter;
251
+ default:
252
+ return false;
253
+ }
254
+ }
255
+
256
+ #overviewKey(action: KeyAction): boolean {
257
+ const cols = this.grid.cols;
258
+ switch (action) {
259
+ case 'next':
260
+ this.#moveCursor(this.cursor + 1);
261
+ return true;
262
+ case 'prev':
263
+ this.#moveCursor(this.cursor - 1);
264
+ return true;
265
+ case 'down':
266
+ this.#moveCursor(this.cursor + cols);
267
+ return true;
268
+ case 'up':
269
+ this.#moveCursor(this.cursor - cols);
270
+ return true;
271
+ case 'first':
272
+ this.#moveCursor(0);
273
+ return true;
274
+ case 'last':
275
+ this.#moveCursor(this.visible.length - 1);
276
+ return true;
277
+ case 'select':
278
+ this.select(this.visible[this.cursor] ?? this.position.index);
279
+ return true;
280
+ case 'close':
281
+ case 'overview':
282
+ this.toggleOverview(false);
283
+ return true;
284
+ case 'fullscreen':
285
+ return true;
286
+ case 'presenter':
287
+ return !!this.options.presenter;
288
+ default:
289
+ return false;
290
+ }
291
+ }
292
+ }
@@ -0,0 +1,358 @@
1
+ <script lang="ts" module>
2
+ /** The demo's type pairing. A deck sets these two variables and every slide follows. */
3
+ export const demoStyle =
4
+ "--deck-font:'Schibsted Grotesk',ui-sans-serif,system-ui,sans-serif;--deck-font-display:'Bricolage Grotesque','Schibsted Grotesk',ui-sans-serif,sans-serif;";
5
+ </script>
6
+
7
+ <script lang="ts">
8
+ import Fragment from './Fragment.svelte';
9
+ import Slide from './Slide.svelte';
10
+
11
+ const keys = [
12
+ { does: 'Next', keys: ['→', 'Space', 'PgDn'] },
13
+ { does: 'Back', keys: ['←', 'Shift Space', 'PgUp'] },
14
+ { does: 'First or last', keys: ['Home', 'End'] },
15
+ { does: 'Overview', keys: ['O'] },
16
+ { does: 'Fullscreen', keys: ['F'] },
17
+ { does: 'Presenter', keys: ['P'] },
18
+ ];
19
+ </script>
20
+
21
+ <svelte:head>
22
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
23
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous" />
24
+ <link
25
+ rel="stylesheet"
26
+ href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,600;12..96,800&family=Schibsted+Grotesk:wght@400;500;600&display=swap"
27
+ />
28
+ </svelte:head>
29
+
30
+ <Slide
31
+ align="bare"
32
+ label="Cover"
33
+ notes="Open with the promise: design once at 960 by 540 and it fits anything. Resize the window while you say it, the slide keeps its shape."
34
+ >
35
+ <div class="cover">
36
+ <div class="cover__text">
37
+ <p class="deck-label">@heyramzi/ui/slides</p>
38
+ <h1 class="deck-display">One canvas, any screen.</h1>
39
+ <p class="deck-lead">Design in 960 by 540 pixels. The deck scales to fit a phone, a laptop or a projector.</p>
40
+ </div>
41
+ <svg class="cover__fit" viewBox="0 0 280 204" role="img" aria-label="The same slide fitted into a phone, a wide window and a screen">
42
+ <rect x="6" y="6" width="96" height="190" rx="14" class="frame" />
43
+ <rect x="12" y="75" width="84" height="47.25" rx="3" class="slide-shape" />
44
+ <rect x="126" y="6" width="148" height="83.25" rx="8" class="frame" />
45
+ <rect x="131" y="11" width="138" height="73.25" rx="3" class="slide-shape" />
46
+ <rect x="126" y="126" width="148" height="70" rx="8" class="frame" />
47
+ <rect x="151.6" y="131" width="96.8" height="60" rx="3" class="slide-shape" />
48
+ </svg>
49
+ <p class="deck-caption cover__hint">Right arrow or space to go on.</p>
50
+ </div>
51
+ </Slide>
52
+
53
+ <Slide
54
+ align="bare"
55
+ label="Reveals"
56
+ notes="Press the arrow slowly and let each row land. The last step has two rows, so you can see them arrive one after the other."
57
+ >
58
+ <div class="split split--rows">
59
+ <div class="split__head">
60
+ <p class="deck-label">Reveals</p>
61
+ <h2 class="deck-title">Press once and one thing appears.</h2>
62
+ </div>
63
+ <ol class="rows">
64
+ <Fragment as="li" step={1} class="row">
65
+ <h3 class="deck-heading">Walk it by hand</h3>
66
+ <p class="deck-body">An arrow press shows one step. After the last, the next slide.</p>
67
+ </Fragment>
68
+ <Fragment as="li" step={2} class="row">
69
+ <h3 class="deck-heading">Back undoes a step</h3>
70
+ <p class="deck-body">Going back lands on the earlier slide fully shown.</p>
71
+ </Fragment>
72
+ <Fragment as="li" step={3} class="row">
73
+ <h3 class="deck-heading">Shared steps stagger</h3>
74
+ <p class="deck-body">Rows that share a step arrive one after another.</p>
75
+ </Fragment>
76
+ <Fragment as="li" step={3} class="row">
77
+ <h3 class="deck-heading">Print shows them all</h3>
78
+ <p class="deck-body">A PDF has no keyboard. It shows every step.</p>
79
+ </Fragment>
80
+ </ol>
81
+ </div>
82
+ </Slide>
83
+
84
+ <Slide
85
+ align="bare"
86
+ label="Keys"
87
+ notes="Press O now. The slides zoom out into a grid and you can click one to jump. Escape or O brings you back."
88
+ >
89
+ <div class="split">
90
+ <div class="split__head">
91
+ <p class="deck-label">Keys</p>
92
+ <h2 class="deck-title">Press <kbd class="key key--big">O</kbd> to see the whole deck.</h2>
93
+ <p class="deck-body">Click a slide to jump to it. Escape closes the grid.</p>
94
+ </div>
95
+ <dl class="keys">
96
+ {#each keys as row (row.does)}
97
+ <div class="keys__row">
98
+ <dt class="deck-body">{row.does}</dt>
99
+ <dd>
100
+ {#each row.keys as key (key)}<kbd class="key">{key}</kbd>{/each}
101
+ </dd>
102
+ </div>
103
+ {/each}
104
+ </dl>
105
+ </div>
106
+ </Slide>
107
+
108
+ <Slide
109
+ align="bare"
110
+ label="Themes"
111
+ notes="Switch the theme from the toolbar above the deck while this slide is up. Nothing else changes, because every colour is a variable."
112
+ >
113
+ <div class="split">
114
+ <div class="split__head">
115
+ <p class="deck-label">Themes</p>
116
+ <h2 class="deck-title">Retheme with variables.</h2>
117
+ <p class="deck-body">Ink, paper or the system setting. The package brings no brand of its own.</p>
118
+ </div>
119
+ <div class="theme">
120
+ <pre class="code"><code>.deck &#123;
121
+ --deck-accent: #ff4d8d;
122
+ --deck-font-display: 'Space Grotesk';
123
+ --deck-pad: 72px;
124
+ &#125;</code></pre>
125
+ <div class="swatch" role="img" aria-label="Ground, text and accent in proportion">
126
+ <span class="swatch__ground">Ground</span>
127
+ <span class="swatch__text">Text</span>
128
+ <span class="swatch__accent"></span>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </Slide>
133
+
134
+ <Slide
135
+ align="bare"
136
+ label="Data"
137
+ notes="This is the mode the apps use: slides are JSON, and a registry says how each type draws itself. Validation is optional and brings no dependency."
138
+ >
139
+ <div class="split">
140
+ <div class="split__head">
141
+ <p class="deck-label">Data</p>
142
+ <h2 class="deck-title">Or write the deck as data.</h2>
143
+ <p class="deck-body">A registry maps each slide type to a layout. Bring your own schema if you want one.</p>
144
+ </div>
145
+ <div class="theme">
146
+ <pre class="code"><code>const cover = defineSlideType(&#123;
147
+ type: 'cover',
148
+ label: 'Cover',
149
+ layout: CoverLayout,
150
+ &#125;);
151
+
152
+ &lt;Deck &#123;...deck&#125; &#123;registry&#125; /&gt;</code></pre>
153
+ <pre class="code"><code>&#123; "id": "q3", "title": "Q3 review",
154
+ "canvas": &#123; "w": 960, "h": 540 &#125;,
155
+ "slides": [&#123; "id": "a", "type": "cover",
156
+ "content": &#123; "title": "Q3" &#125; &#125;] &#125;</code></pre>
157
+ </div>
158
+ </div>
159
+ </Slide>
160
+
161
+ <Slide
162
+ align="bare"
163
+ label="Presenter"
164
+ notes="Open the presenter view from the toolbar. Move on here and watch the other window follow. Then print the page: one canvas per page, every step shown."
165
+ >
166
+ <div class="close">
167
+ <p class="deck-label">Presenter view and print</p>
168
+ <h2 class="deck-title">Notes in one window, the audience in another.</h2>
169
+ <p class="deck-lead">Both windows follow the same slide. Print the page and every canvas gets its own page, steps shown.</p>
170
+ </div>
171
+ </Slide>
172
+
173
+ <style>
174
+ .cover,
175
+ .split,
176
+ .close {
177
+ position: relative;
178
+ box-sizing: border-box;
179
+ height: 100%;
180
+ padding: var(--deck-pad);
181
+ }
182
+
183
+ /* Cover: copy on the left, the fit diagram on the right. */
184
+ .cover {
185
+ display: grid;
186
+ grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr);
187
+ gap: 48px;
188
+ align-items: center;
189
+ }
190
+ .cover__text {
191
+ display: grid;
192
+ gap: 22px;
193
+ justify-items: start;
194
+ }
195
+ .cover__text .deck-display {
196
+ max-width: 9ch;
197
+ }
198
+ .cover__text .deck-lead {
199
+ max-width: 24em;
200
+ }
201
+ .cover__fit {
202
+ width: 100%;
203
+ max-height: 100%;
204
+ overflow: visible;
205
+ }
206
+ .frame {
207
+ fill: var(--deck-surface);
208
+ stroke: var(--deck-line);
209
+ stroke-width: 1.5;
210
+ }
211
+ .slide-shape {
212
+ fill: var(--deck-accent);
213
+ }
214
+ .cover__hint {
215
+ position: absolute;
216
+ bottom: 40px;
217
+ left: var(--deck-pad);
218
+ }
219
+
220
+ /* Split: a heading column and a content column. */
221
+ .split {
222
+ display: grid;
223
+ grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
224
+ gap: 56px;
225
+ align-items: center;
226
+ }
227
+ .split--rows {
228
+ grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
229
+ }
230
+ .split__head {
231
+ display: grid;
232
+ gap: 18px;
233
+ justify-items: start;
234
+ }
235
+ .split__head .deck-title {
236
+ max-width: 11ch;
237
+ }
238
+ .split__head .deck-body {
239
+ max-width: 22em;
240
+ }
241
+
242
+ .rows {
243
+ margin: 0;
244
+ padding: 0;
245
+ list-style: none;
246
+ }
247
+ .rows :global(.row) {
248
+ display: grid;
249
+ gap: 4px;
250
+ padding: 15px 0;
251
+ border-top: 1px solid var(--deck-line);
252
+ }
253
+ .rows :global(.deck-heading) {
254
+ font-size: 23px;
255
+ }
256
+ .rows :global(.deck-body) {
257
+ font-size: 16px;
258
+ }
259
+ .rows :global(.row:last-child) {
260
+ border-bottom: 1px solid var(--deck-line);
261
+ }
262
+
263
+ .keys {
264
+ margin: 0;
265
+ border-top: 1px solid var(--deck-line);
266
+ }
267
+ .keys__row {
268
+ display: flex;
269
+ align-items: center;
270
+ justify-content: space-between;
271
+ padding: 12px 0;
272
+ border-bottom: 1px solid var(--deck-line);
273
+ }
274
+ .keys dd {
275
+ display: flex;
276
+ gap: 6px;
277
+ margin: 0;
278
+ }
279
+ .key {
280
+ display: inline-grid;
281
+ place-items: center;
282
+ min-width: 30px;
283
+ height: 28px;
284
+ padding: 0 9px;
285
+ font: 600 13px/1 var(--deck-font);
286
+ color: var(--deck-fg);
287
+ background: var(--deck-surface);
288
+ border: 1px solid var(--deck-line);
289
+ border-bottom-width: 2px;
290
+ border-radius: 7px;
291
+ }
292
+ .key--big {
293
+ min-width: 0;
294
+ height: 1.05em;
295
+ padding: 0 0.28em;
296
+ margin: 0 0.08em;
297
+ font: inherit;
298
+ font-size: 0.85em;
299
+ color: var(--deck-on-accent);
300
+ background: var(--deck-accent);
301
+ border-color: var(--deck-accent);
302
+ vertical-align: 0.04em;
303
+ }
304
+
305
+ .theme {
306
+ display: grid;
307
+ gap: 16px;
308
+ }
309
+ .code {
310
+ margin: 0;
311
+ padding: 20px 22px;
312
+ overflow: hidden;
313
+ font: 500 14px/1.6 ui-monospace, 'SF Mono', Menlo, monospace;
314
+ color: var(--deck-fg);
315
+ background: var(--deck-surface);
316
+ border: 1px solid var(--deck-line);
317
+ border-radius: 12px;
318
+ }
319
+ .swatch {
320
+ display: grid;
321
+ grid-template-columns: 6fr 3fr 1fr;
322
+ height: 76px;
323
+ overflow: hidden;
324
+ font: 600 13px/1 var(--deck-font);
325
+ border: 1px solid var(--deck-line);
326
+ border-radius: 12px;
327
+ }
328
+ .swatch span {
329
+ display: flex;
330
+ align-items: flex-end;
331
+ padding: 12px 14px;
332
+ }
333
+ .swatch__ground {
334
+ background: var(--deck-bg);
335
+ color: var(--deck-muted);
336
+ }
337
+ .swatch__text {
338
+ background: var(--deck-fg);
339
+ color: var(--deck-bg);
340
+ }
341
+ .swatch__accent {
342
+ background: var(--deck-accent);
343
+ }
344
+
345
+ .close {
346
+ display: grid;
347
+ align-content: center;
348
+ justify-items: start;
349
+ gap: 22px;
350
+ }
351
+ .close .deck-title {
352
+ max-width: 14em;
353
+ font-size: 60px;
354
+ }
355
+ .close .deck-lead {
356
+ max-width: 26em;
357
+ }
358
+ </style>