@heyramzi/ui 2.11.0 → 2.11.2

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 (267) hide show
  1. package/dist/agents/helpers.js +38 -38
  2. package/dist/agents/page-state.js +49 -50
  3. package/dist/agents/vendor/color.js +146 -127
  4. package/dist/agents/vendor/draw.js +617 -508
  5. package/dist/agents/vendor/engine.js +698 -599
  6. package/dist/agents/vendor/mesh.js +653 -683
  7. package/dist/agents/vendor/plastic.js +3913 -3112
  8. package/dist/agents/vendor/presets.js +163 -164
  9. package/dist/agents/vendor/shapes.js +21 -20
  10. package/dist/agents/vendor/ticker.js +41 -42
  11. package/dist/agents/vendor/types.js +1 -0
  12. package/dist/agents/vendor/wear.js +1421 -1448
  13. package/dist/ai/confirmation/confirmation-context.svelte.js +6 -6
  14. package/dist/ai/confirmation/index.js +3 -1
  15. package/dist/ai/index.js +33 -0
  16. package/dist/ai/prompt-input/index.js +3 -1
  17. package/dist/ai/reasoning/index.js +3 -1
  18. package/dist/ai/reasoning/reasoning-context.svelte.js +6 -6
  19. package/dist/ai/suggestion/index.js +3 -1
  20. package/dist/ai/typing-dots/index.js +3 -1
  21. package/dist/animations/text-swap.js +24 -24
  22. package/dist/brand/index.js +54 -48
  23. package/dist/canvas/card/resize.js +5 -3
  24. package/dist/canvas/edges/edge-path.js +255 -242
  25. package/dist/canvas/editor/context-menu.js +39 -36
  26. package/dist/canvas/editor/dispatch.svelte.js +154 -151
  27. package/dist/canvas/editor/editor-context.js +7 -2
  28. package/dist/canvas/editor/editor-mode.js +1 -0
  29. package/dist/canvas/editor/frame.js +73 -76
  30. package/dist/canvas/editor/hand-context.js +5 -2
  31. package/dist/canvas/editor/history-core.js +163 -191
  32. package/dist/canvas/editor/keymap.js +95 -169
  33. package/dist/canvas/editor/layout-actions.js +143 -144
  34. package/dist/canvas/editor/lock.js +52 -51
  35. package/dist/canvas/editor/rebase.js +217 -240
  36. package/dist/canvas/editor/style.js +132 -137
  37. package/dist/canvas/editor/text-field-context.js +7 -2
  38. package/dist/canvas/editor/z-order.js +65 -70
  39. package/dist/canvas/headless/alignment.js +22 -16
  40. package/dist/canvas/headless/break-apart-find.js +286 -279
  41. package/dist/canvas/headless/break-apart-pieces.js +62 -58
  42. package/dist/canvas/headless/break-apart-plan.js +299 -325
  43. package/dist/canvas/headless/break-apart-style.js +75 -70
  44. package/dist/canvas/headless/break-apart.js +188 -181
  45. package/dist/canvas/headless/canvas-cleanup.js +78 -58
  46. package/dist/canvas/headless/edge-anchors.js +115 -116
  47. package/dist/canvas/headless/edge-memo.js +24 -22
  48. package/dist/canvas/headless/edge-serialization.js +17 -27
  49. package/dist/canvas/headless/figure-geometry.js +232 -264
  50. package/dist/canvas/headless/fit-viewport.js +84 -67
  51. package/dist/canvas/headless/grid.js +88 -86
  52. package/dist/canvas/headless/icon-node.js +44 -36
  53. package/dist/canvas/headless/index.js +1 -0
  54. package/dist/canvas/headless/linkify.js +17 -19
  55. package/dist/canvas/headless/pen-marks.js +101 -95
  56. package/dist/canvas/headless/sketch.js +231 -209
  57. package/dist/canvas/headless/snap-guides.js +99 -83
  58. package/dist/canvas/headless/svg-figure.js +133 -117
  59. package/dist/canvas/headless/table-layout.js +146 -107
  60. package/dist/canvas/index.js +2 -0
  61. package/dist/canvas/lod/lod-mode.svelte.js +20 -18
  62. package/dist/canvas/lod/render-all.svelte.js +36 -39
  63. package/dist/canvas/ops/arrange.js +78 -71
  64. package/dist/canvas/ops/ops.js +260 -255
  65. package/dist/canvas/pen/colors.js +37 -35
  66. package/dist/canvas/pen/eraser.js +28 -27
  67. package/dist/canvas/pen/geometry.js +120 -130
  68. package/dist/canvas/pen/index.js +1 -0
  69. package/dist/canvas/pen/ops.js +11 -11
  70. package/dist/canvas/pen/pen-session.js +150 -159
  71. package/dist/canvas/pen/plugin.js +107 -104
  72. package/dist/canvas/pen/shapes.js +73 -68
  73. package/dist/canvas/pen/stroke-path.js +32 -30
  74. package/dist/canvas/pen/types.js +21 -21
  75. package/dist/canvas/pen/ui.js +10 -8
  76. package/dist/canvas/plugin.js +46 -46
  77. package/dist/canvas/screen/glyphs.js +12 -11
  78. package/dist/canvas/screen/labels.js +22 -22
  79. package/dist/canvas/screen/parts.js +98 -111
  80. package/dist/canvas/screen/plugin.js +47 -56
  81. package/dist/canvas/screen/types.js +30 -35
  82. package/dist/canvas/screen/ui.js +7 -7
  83. package/dist/canvas/test-utils.js +14 -14
  84. package/dist/canvas/types.js +1 -0
  85. package/dist/canvas/ui/forward-wheel.js +24 -25
  86. package/dist/canvas/ui/icons.js +64 -131
  87. package/dist/canvas/ui/place-panel.js +58 -49
  88. package/dist/canvas/ui/shortcut-tip.js +14 -12
  89. package/dist/canvas/ui/tools.js +300 -385
  90. package/dist/charts/dashboard/chart.js +4 -0
  91. package/dist/charts/dashboard/data-table.js +11 -0
  92. package/dist/charts/dashboard/windows.js +8 -3
  93. package/dist/charts/edge-fade.js +10 -5
  94. package/dist/charts/hover-band.js +16 -11
  95. package/dist/charts/index.js +0 -1
  96. package/dist/charts/motion.js +11 -7
  97. package/dist/charts/primitives/chart-utils.js +12 -12
  98. package/dist/components/copy/clipboard.js +32 -32
  99. package/dist/components/copy/copied.svelte.js +31 -23
  100. package/dist/components/data-table/data-table.js +39 -39
  101. package/dist/components/docs/index.js +4 -0
  102. package/dist/components/docs/scroll-spy.svelte.js +73 -68
  103. package/dist/components/list-page/filter-section.js +4 -1
  104. package/dist/components/list-page/index.js +2 -0
  105. package/dist/components/list-page/list-table.js +7 -2
  106. package/dist/components/list-page/queue-rail.js +14 -0
  107. package/dist/components/list-page/section-host.js +4 -2
  108. package/dist/components/list-page/step.js +4 -3
  109. package/dist/components/list-page/types.js +1 -0
  110. package/dist/components/picker/trigger.js +48 -30
  111. package/dist/components/picker/types.js +1 -0
  112. package/dist/components/segmented/collapse.js +38 -27
  113. package/dist/components/segmented/index.js +8 -0
  114. package/dist/components/segmented/segmented.js +67 -42
  115. package/dist/components/star-rating-input/index.js +3 -1
  116. package/dist/components/table-view/types.js +1 -0
  117. package/dist/components/tags-input/tags.js +81 -69
  118. package/dist/components/theme/index.js +4 -0
  119. package/dist/components/toolbar/index.js +4 -0
  120. package/dist/components/toolbar/toolbar-trigger.js +14 -1
  121. package/dist/editor/anchors.js +17 -13
  122. package/dist/editor/block-id.js +52 -51
  123. package/dist/editor/block-ops.js +108 -80
  124. package/dist/editor/doc-blocks.js +155 -166
  125. package/dist/editor/extensions.js +139 -180
  126. package/dist/editor/index.js +4 -0
  127. package/dist/editor/markdown-test-helpers.js +4 -2
  128. package/dist/editor/markdown.js +420 -408
  129. package/dist/editor/serializer.js +204 -200
  130. package/dist/editor/slash-items.js +89 -101
  131. package/dist/editor/youtube-url.js +25 -20
  132. package/dist/effects/animated-beam/path-calculator.svelte.js +43 -38
  133. package/dist/effects/animated-beam/types.js +1 -0
  134. package/dist/effects/border-beam/css.js +92 -78
  135. package/dist/effects/border-beam/presets.js +1705 -1720
  136. package/dist/effects/border-beam/register-properties.js +43 -33
  137. package/dist/effects/border-beam/types.js +1 -0
  138. package/dist/effects/image-comparison/compare.js +3 -2
  139. package/dist/effects/image-reveal/types.js +1 -0
  140. package/dist/effects/magnetic/magnetic.js +58 -52
  141. package/dist/effects/reveal/observe.js +50 -40
  142. package/dist/effects/reveal/types.js +1 -0
  143. package/dist/effects/shader-gradient/color.js +36 -29
  144. package/dist/effects/shader-gradient/renderer.js +384 -295
  145. package/dist/effects/shader-gradient/shaders.js +31 -0
  146. package/dist/effects/shader-gradient/types.js +1 -0
  147. package/dist/icons/apple-symbol-exports.js +99 -124
  148. package/dist/icons/sf-symbols.js +357 -461
  149. package/dist/icons/trades.js +43 -28
  150. package/dist/inbox/index.js +1 -0
  151. package/dist/inbox/logic.js +126 -122
  152. package/dist/inbox/types.js +43 -43
  153. package/dist/lib/arrive.js +19 -12
  154. package/dist/lib/icon-types.js +1 -0
  155. package/dist/lib/initials.js +16 -10
  156. package/dist/lib/layout.js +5 -0
  157. package/dist/lib/panel.js +13 -0
  158. package/dist/lib/result.js +22 -22
  159. package/dist/lib/sliding-pill.js +59 -44
  160. package/dist/lib/theme-transition.js +43 -43
  161. package/dist/lib/tone.js +31 -24
  162. package/dist/lib/utils.js +3 -1
  163. package/dist/onboarding/define-tours.js +24 -23
  164. package/dist/onboarding/onboarding.svelte.js +16 -12
  165. package/dist/onboarding/pick.js +10 -6
  166. package/dist/onboarding/stores.js +40 -41
  167. package/dist/onboarding/wait.js +24 -23
  168. package/dist/panel/side-panel-types.js +1 -0
  169. package/dist/record/demo-data.js +459 -486
  170. package/dist/record/fields.js +129 -131
  171. package/dist/record/format.js +70 -82
  172. package/dist/record/gutter.js +1 -0
  173. package/dist/record/kinds.js +95 -105
  174. package/dist/record/logic.js +238 -219
  175. package/dist/record/types.js +49 -48
  176. package/dist/shadcn/accordion/index.js +3 -1
  177. package/dist/shadcn/alert/index.js +3 -1
  178. package/dist/shadcn/alert-dialog/index.js +3 -1
  179. package/dist/shadcn/attachment/index.js +3 -1
  180. package/dist/shadcn/avatar/index.js +3 -1
  181. package/dist/shadcn/breadcrumb/index.js +3 -1
  182. package/dist/shadcn/bubble/index.js +3 -1
  183. package/dist/shadcn/button/index.js +3 -1
  184. package/dist/shadcn/calendar/index.js +3 -1
  185. package/dist/shadcn/card/index.js +3 -1
  186. package/dist/shadcn/checkbox/index.js +3 -1
  187. package/dist/shadcn/code-block/index.js +3 -1
  188. package/dist/shadcn/collapsible/index.js +3 -1
  189. package/dist/shadcn/command/index.js +4 -1
  190. package/dist/shadcn/context-menu/index.js +3 -1
  191. package/dist/shadcn/dialog/index.js +3 -1
  192. package/dist/shadcn/dropdown-menu/menu-item.js +40 -20
  193. package/dist/shadcn/empty/index.js +3 -1
  194. package/dist/shadcn/field/index.js +3 -1
  195. package/dist/shadcn/hooks/highlighter.js +77 -75
  196. package/dist/shadcn/hooks/is-mobile.svelte.js +3 -3
  197. package/dist/shadcn/hooks/use-highlighted-code.svelte.js +62 -62
  198. package/dist/shadcn/input/index.js +3 -1
  199. package/dist/shadcn/kbd/index.js +3 -1
  200. package/dist/shadcn/label/index.js +3 -1
  201. package/dist/shadcn/marker/index.js +3 -1
  202. package/dist/shadcn/message/index.js +3 -1
  203. package/dist/shadcn/message-scroller/index.js +3 -1
  204. package/dist/shadcn/popover/index.js +3 -1
  205. package/dist/shadcn/progress/index.js +3 -1
  206. package/dist/shadcn/progress/tone.js +8 -7
  207. package/dist/shadcn/radio-group/index.js +3 -1
  208. package/dist/shadcn/scroll-area/index.js +3 -1
  209. package/dist/shadcn/select/index.js +3 -1
  210. package/dist/shadcn/separator/index.js +3 -1
  211. package/dist/shadcn/sheet/index.js +3 -1
  212. package/dist/shadcn/sidebar/context.svelte.js +42 -28
  213. package/dist/shadcn/sidebar/index.js +3 -1
  214. package/dist/shadcn/skeleton/index.js +3 -1
  215. package/dist/shadcn/slider/index.js +3 -1
  216. package/dist/shadcn/switch/index.js +3 -1
  217. package/dist/shadcn/table/index.js +3 -1
  218. package/dist/shadcn/tabs/index.js +3 -1
  219. package/dist/shadcn/textarea/index.js +3 -1
  220. package/dist/shadcn/toggle/index.js +3 -1
  221. package/dist/shadcn/toggle-group/index.js +3 -1
  222. package/dist/shadcn/tooltip/context.js +7 -2
  223. package/dist/shadcn/tooltip/index.js +3 -1
  224. package/dist/shadcn/trace/index.js +3 -1
  225. package/dist/slides/bridges.js +65 -59
  226. package/dist/slides/deck-controller.svelte.js +210 -207
  227. package/dist/slides/export/capture.js +66 -71
  228. package/dist/slides/export/ffmpeg.js +76 -72
  229. package/dist/slides/export/index.js +3 -0
  230. package/dist/slides/export/launch.js +9 -6
  231. package/dist/slides/export/pdf.js +60 -54
  232. package/dist/slides/export/types.js +1 -0
  233. package/dist/slides/labels.js +24 -24
  234. package/dist/slides/layout.js +39 -41
  235. package/dist/slides/model.js +9 -7
  236. package/dist/slides/navigation.js +78 -73
  237. package/dist/slides/types.js +2 -2
  238. package/dist/status/define-statuses.js +18 -14
  239. package/dist/status/status-types.js +1 -0
  240. package/dist/test-setup.js +24 -20
  241. package/dist/thread/logic.js +73 -77
  242. package/dist/thread/types.js +21 -21
  243. package/dist/work/board-order.js +94 -90
  244. package/dist/work/dates.js +16 -9
  245. package/dist/work/detail/detail-types.js +18 -18
  246. package/dist/work/docs/edited.js +32 -29
  247. package/dist/work/docs/index.js +4 -0
  248. package/dist/work/docs/labels.js +34 -34
  249. package/dist/work/docs/menu.js +113 -106
  250. package/dist/work/docs/outline.js +39 -29
  251. package/dist/work/gantt/logic.js +211 -235
  252. package/dist/work/inbox/inbox-logic.js +71 -75
  253. package/dist/work/inbox/inbox-statuses.js +75 -72
  254. package/dist/work/inbox/inbox-types.js +1 -0
  255. package/dist/work/kanban-drag.js +27 -22
  256. package/dist/work/kanban-types.js +1 -0
  257. package/dist/work/presets/sales.js +36 -32
  258. package/dist/work/reorder.js +9 -8
  259. package/dist/work/task-types.js +1 -0
  260. package/dist/work/time/duration.js +51 -46
  261. package/dist/work/time/time-types.js +23 -23
  262. package/dist/work/use-kanban-dnd.svelte.js +126 -125
  263. package/package.json +3 -1
  264. package/src/components/list-page/master-detail.svelte +3 -1
  265. package/src/components/list-page/rail-row.svelte +4 -4
  266. package/src/components/tags-input/tags-input.svelte +2 -0
  267. package/src/editor/markdown.ts +30 -35
@@ -3,219 +3,222 @@ import { fitBox, gridBoxes } from "./layout.js";
3
3
  import { keyAction, nextPosition, ordinalOf, prevPosition, progressOf, settle, visibleIndexes } from "./navigation.js";
4
4
  import { resolveLabels } from "./labels.js";
5
5
  import { DEFAULT_CANVAS } from "./types.js";
6
+ /* With several decks on a page, keys belong to the one the reader last touched. */
6
7
  const live = new Set();
7
8
  let active = null;
8
- const claimKeys = (deck)=>{
9
- active = deck;
9
+ const claimKeys = (deck) => {
10
+ active = deck;
10
11
  };
12
+ /** Height the controls bar takes at the bottom of the overview, in px. */
11
13
  const CONTROLS_ROOM = 56;
14
+ /** How long the overview morph flag stays up, a little past the longest stagger. */
12
15
  const MORPH_MS = 900;
13
- const clamp = (value, min, max)=>Math.min(max, Math.max(min, value));
16
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
14
17
  function isEditable(el) {
15
- return el.isContentEditable || [
16
- "INPUT",
17
- "TEXTAREA",
18
- "SELECT"
19
- ].includes(el.tagName);
18
+ return el.isContentEditable || [
19
+ "INPUT",
20
+ "TEXTAREA",
21
+ "SELECT"
22
+ ].includes(el.tagName);
20
23
  }
21
- const byDocumentOrder = (a, b)=>a.el && b.el ? a.el.compareDocumentPosition(b.el) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1 : 0;
24
+ const byDocumentOrder = (a, b) => a.el && b.el ? a.el.compareDocumentPosition(b.el) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1 : 0;
22
25
  export class DeckController {
23
- entries = $state.raw([]);
24
- view = $state("slide");
25
- cursor = $state(0);
26
- direction = $state(1);
27
- morphing = $state(false);
28
- size = $state({
29
- w: 0,
30
- h: 0
31
- });
32
- options;
33
- #listeners = new Set();
34
- #timer;
35
- constructor(options){
36
- this.options = options;
37
- }
38
- canvas = $derived(this.options.canvas ?? DEFAULT_CANVAS);
39
- labels = $derived(resolveLabels(this.options.labels));
40
- slides = $derived(this.entries.map((entry)=>({
41
- steps: this.options.fragments === false ? 0 : entry.steps,
42
- hidden: entry.hidden
43
- })));
44
- visible = $derived(visibleIndexes(this.slides));
45
- position = $derived(settle(this.slides, {
46
- index: this.options.index,
47
- step: this.options.step
48
- }));
49
- ordinal = $derived(ordinalOf(this.slides, this.position.index));
50
- progress = $derived(progressOf(this.slides, this.position));
51
- current = $derived(this.entries[this.position.index]);
52
- upcoming = $derived(this.visible.find((i)=>i > this.position.index) ?? null);
53
- overview = $derived(this.view === "overview");
54
- revealAll = $derived(this.overview || this.options.fragments === false);
55
- atStart = $derived(prevPosition(this.slides, this.position, this.options.loop) === null);
56
- atEnd = $derived(nextPosition(this.slides, this.position, this.options.loop) === null);
57
- fit = $derived(fitBox(this.size, this.canvas));
58
- grid = $derived(gridBoxes({
59
- w: this.size.w,
60
- h: Math.max(0, this.size.h - (this.options.controls === false ? 0 : CONTROLS_ROOM))
61
- }, this.canvas, this.visible.length));
62
- boxOf(index) {
63
- if (!this.overview) return this.fit;
64
- return this.grid.boxes[this.visible.indexOf(index)];
65
- }
66
- register(entry) {
67
- untrack(()=>{
68
- this.entries = [
69
- ...this.entries,
70
- entry
71
- ].sort(byDocumentOrder);
72
- });
73
- return ()=>untrack(()=>{
74
- this.entries = this.entries.filter((candidate)=>candidate !== entry);
75
- this.#write(this.position);
76
- });
77
- }
78
- listen(listener) {
79
- this.#listeners.add(listener);
80
- return ()=>this.#listeners.delete(listener);
81
- }
82
- activate() {
83
- claimKeys(this);
84
- }
85
- mount() {
86
- live.add(this);
87
- claimKeys(this);
88
- return ()=>{
89
- live.delete(this);
90
- if (active === this) active = null;
91
- clearTimeout(this.#timer);
92
- };
93
- }
94
- get listening() {
95
- if (this.options.keyboard === false) return false;
96
- const eligible = [
97
- ...live
98
- ].filter((deck)=>deck.options.keyboard !== false);
99
- return eligible.length <= 1 || active === this;
100
- }
101
- #write(position) {
102
- this.options.index = position.index;
103
- this.options.step = position.step;
104
- }
105
- commit(target, source = "local") {
106
- if (!target) return;
107
- const from = this.position;
108
- const to = settle(this.slides, target);
109
- if (to.index === from.index && to.step === from.step) return;
110
- this.direction = to.index >= from.index ? 1 : -1;
111
- const apply = ()=>this.#write(to);
112
- if (to.index !== from.index && this.options.transition) this.options.transition(apply);
113
- else apply();
114
- for (const listener of this.#listeners)listener(to, source);
115
- }
116
- next() {
117
- this.commit(nextPosition(this.slides, this.position, this.options.loop));
118
- }
119
- prev() {
120
- this.commit(prevPosition(this.slides, this.position, this.options.loop));
121
- }
122
- go(index, step = 0, source = "local") {
123
- this.commit({
124
- index,
125
- step
126
- }, source);
127
- }
128
- toggleOverview(force) {
129
- const open = force ?? !this.overview;
130
- if (open === this.overview) return;
131
- if (open) this.cursor = Math.max(0, this.visible.indexOf(this.position.index));
132
- this.view = open ? "overview" : "slide";
133
- this.#morph();
134
- }
135
- select(index) {
136
- this.toggleOverview(false);
137
- const keep = index === this.position.index;
138
- this.go(index, keep ? this.position.step : 0);
139
- }
140
- #morph() {
141
- this.morphing = true;
142
- clearTimeout(this.#timer);
143
- this.#timer = setTimeout(()=>this.morphing = false, MORPH_MS);
144
- }
145
- #moveCursor(to) {
146
- this.cursor = clamp(to, 0, this.visible.length - 1);
147
- }
148
- handleKey(event) {
149
- if (event.defaultPrevented || !this.listening || event.altKey || event.ctrlKey || event.metaKey) return null;
150
- const target = event.target instanceof HTMLElement ? event.target : null;
151
- if (target && isEditable(target)) return null;
152
- const action = keyAction(event.key, event.shiftKey);
153
- if (!action) return null;
154
- if ((event.key === " " || event.key === "Enter") && target?.closest("button, a, summary")) return null;
155
- const handled = this.overview ? this.#overviewKey(action) : this.#slideKey(action);
156
- if (!handled) return null;
157
- event.preventDefault();
158
- return action;
159
- }
160
- #slideKey(action) {
161
- switch(action){
162
- case "next":
163
- this.next();
164
- return true;
165
- case "prev":
166
- this.prev();
167
- return true;
168
- case "first":
169
- this.go(this.visible[0] ?? 0);
170
- return true;
171
- case "last":
172
- this.go(this.visible[this.visible.length - 1] ?? 0);
173
- return true;
174
- case "overview":
175
- this.toggleOverview(true);
176
- return true;
177
- case "fullscreen":
178
- return true;
179
- case "presenter":
180
- return !!this.options.presenter;
181
- default:
182
- return false;
183
- }
184
- }
185
- #overviewKey(action) {
186
- const cols = this.grid.cols;
187
- switch(action){
188
- case "next":
189
- this.#moveCursor(this.cursor + 1);
190
- return true;
191
- case "prev":
192
- this.#moveCursor(this.cursor - 1);
193
- return true;
194
- case "down":
195
- this.#moveCursor(this.cursor + cols);
196
- return true;
197
- case "up":
198
- this.#moveCursor(this.cursor - cols);
199
- return true;
200
- case "first":
201
- this.#moveCursor(0);
202
- return true;
203
- case "last":
204
- this.#moveCursor(this.visible.length - 1);
205
- return true;
206
- case "select":
207
- this.select(this.visible[this.cursor] ?? this.position.index);
208
- return true;
209
- case "close":
210
- case "overview":
211
- this.toggleOverview(false);
212
- return true;
213
- case "fullscreen":
214
- return true;
215
- case "presenter":
216
- return !!this.options.presenter;
217
- default:
218
- return false;
219
- }
220
- }
26
+ entries = $state.raw([]);
27
+ view = $state("slide");
28
+ /** The highlighted thumbnail in the overview, as an index among the visible slides. */
29
+ cursor = $state(0);
30
+ direction = $state(1);
31
+ morphing = $state(false);
32
+ size = $state({
33
+ w: 0,
34
+ h: 0
35
+ });
36
+ options;
37
+ #listeners = new Set();
38
+ #timer;
39
+ constructor(options) {
40
+ this.options = options;
41
+ }
42
+ canvas = $derived(this.options.canvas ?? DEFAULT_CANVAS);
43
+ labels = $derived(resolveLabels(this.options.labels));
44
+ slides = $derived(this.entries.map((entry) => ({
45
+ steps: this.options.fragments === false ? 0 : entry.steps,
46
+ hidden: entry.hidden
47
+ })));
48
+ visible = $derived(visibleIndexes(this.slides));
49
+ position = $derived(settle(this.slides, {
50
+ index: this.options.index,
51
+ step: this.options.step
52
+ }));
53
+ ordinal = $derived(ordinalOf(this.slides, this.position.index));
54
+ progress = $derived(progressOf(this.slides, this.position));
55
+ current = $derived(this.entries[this.position.index]);
56
+ /** The slide after this one, for a presenter's preview. Null on the last. */
57
+ upcoming = $derived(this.visible.find((i) => i > this.position.index) ?? null);
58
+ overview = $derived(this.view === "overview");
59
+ /** True when every fragment shows: in the overview, or on a deck that doesn't stage reveals. */
60
+ revealAll = $derived(this.overview || this.options.fragments === false);
61
+ atStart = $derived(prevPosition(this.slides, this.position, this.options.loop) === null);
62
+ atEnd = $derived(nextPosition(this.slides, this.position, this.options.loop) === null);
63
+ fit = $derived(fitBox(this.size, this.canvas));
64
+ grid = $derived(gridBoxes({
65
+ w: this.size.w,
66
+ h: Math.max(0, this.size.h - (this.options.controls === false ? 0 : CONTROLS_ROOM))
67
+ }, this.canvas, this.visible.length));
68
+ /** Where slide `index` sits right now: centred to fit, or in its overview cell. */
69
+ boxOf(index) {
70
+ if (!this.overview) return this.fit;
71
+ return this.grid.boxes[this.visible.indexOf(index)];
72
+ }
73
+ register(entry) {
74
+ untrack(() => {
75
+ this.entries = [...this.entries, entry].sort(byDocumentOrder);
76
+ });
77
+ return () => untrack(() => {
78
+ this.entries = this.entries.filter((candidate) => candidate !== entry);
79
+ this.#write(this.position);
80
+ });
81
+ }
82
+ listen(listener) {
83
+ this.#listeners.add(listener);
84
+ return () => this.#listeners.delete(listener);
85
+ }
86
+ /** Marks this deck as the one that owns the keys. Mounting does it too. */
87
+ activate() {
88
+ claimKeys(this);
89
+ }
90
+ mount() {
91
+ live.add(this);
92
+ claimKeys(this);
93
+ return () => {
94
+ live.delete(this);
95
+ if (active === this) active = null;
96
+ clearTimeout(this.#timer);
97
+ };
98
+ }
99
+ get listening() {
100
+ if (this.options.keyboard === false) return false;
101
+ const eligible = [...live].filter((deck) => deck.options.keyboard !== false);
102
+ return eligible.length <= 1 || active === this;
103
+ }
104
+ #write(position) {
105
+ this.options.index = position.index;
106
+ this.options.step = position.step;
107
+ }
108
+ commit(target, source = "local") {
109
+ if (!target) return;
110
+ const from = this.position;
111
+ const to = settle(this.slides, target);
112
+ if (to.index === from.index && to.step === from.step) return;
113
+ this.direction = to.index >= from.index ? 1 : -1;
114
+ const apply = () => this.#write(to);
115
+ if (to.index !== from.index && this.options.transition) this.options.transition(apply);
116
+ else apply();
117
+ for (const listener of this.#listeners) listener(to, source);
118
+ }
119
+ next() {
120
+ this.commit(nextPosition(this.slides, this.position, this.options.loop));
121
+ }
122
+ prev() {
123
+ this.commit(prevPosition(this.slides, this.position, this.options.loop));
124
+ }
125
+ go(index, step = 0, source = "local") {
126
+ this.commit({
127
+ index,
128
+ step
129
+ }, source);
130
+ }
131
+ toggleOverview(force) {
132
+ const open = force ?? !this.overview;
133
+ if (open === this.overview) return;
134
+ if (open) this.cursor = Math.max(0, this.visible.indexOf(this.position.index));
135
+ this.view = open ? "overview" : "slide";
136
+ this.#morph();
137
+ }
138
+ /** Leaves the overview on slide `index`. Picking the current slide keeps its step. */
139
+ select(index) {
140
+ this.toggleOverview(false);
141
+ const keep = index === this.position.index;
142
+ this.go(index, keep ? this.position.step : 0);
143
+ }
144
+ #morph() {
145
+ this.morphing = true;
146
+ clearTimeout(this.#timer);
147
+ this.#timer = setTimeout(() => this.morphing = false, MORPH_MS);
148
+ }
149
+ #moveCursor(to) {
150
+ this.cursor = clamp(to, 0, this.visible.length - 1);
151
+ }
152
+ /**
153
+ * Runs the deck's own keys. Returns the action it took, or null when the key wasn't its.
154
+ * `fullscreen` and `presenter` are returned untouched, since they need the page.
155
+ */
156
+ handleKey(event) {
157
+ if (event.defaultPrevented || !this.listening || event.altKey || event.ctrlKey || event.metaKey) return null;
158
+ const target = event.target instanceof HTMLElement ? event.target : null;
159
+ if (target && isEditable(target)) return null;
160
+ const action = keyAction(event.key, event.shiftKey);
161
+ if (!action) return null;
162
+ /* Space and Enter belong to a focused control, not to the deck. */
163
+ if ((event.key === " " || event.key === "Enter") && target?.closest("button, a, summary")) return null;
164
+ const handled = this.overview ? this.#overviewKey(action) : this.#slideKey(action);
165
+ if (!handled) return null;
166
+ event.preventDefault();
167
+ return action;
168
+ }
169
+ #slideKey(action) {
170
+ switch (action) {
171
+ case "next":
172
+ this.next();
173
+ return true;
174
+ case "prev":
175
+ this.prev();
176
+ return true;
177
+ case "first":
178
+ this.go(this.visible[0] ?? 0);
179
+ return true;
180
+ case "last":
181
+ this.go(this.visible[this.visible.length - 1] ?? 0);
182
+ return true;
183
+ case "overview":
184
+ this.toggleOverview(true);
185
+ return true;
186
+ case "fullscreen": return true;
187
+ case "presenter": return !!this.options.presenter;
188
+ default: return false;
189
+ }
190
+ }
191
+ #overviewKey(action) {
192
+ const cols = this.grid.cols;
193
+ switch (action) {
194
+ case "next":
195
+ this.#moveCursor(this.cursor + 1);
196
+ return true;
197
+ case "prev":
198
+ this.#moveCursor(this.cursor - 1);
199
+ return true;
200
+ case "down":
201
+ this.#moveCursor(this.cursor + cols);
202
+ return true;
203
+ case "up":
204
+ this.#moveCursor(this.cursor - cols);
205
+ return true;
206
+ case "first":
207
+ this.#moveCursor(0);
208
+ return true;
209
+ case "last":
210
+ this.#moveCursor(this.visible.length - 1);
211
+ return true;
212
+ case "select":
213
+ this.select(this.visible[this.cursor] ?? this.position.index);
214
+ return true;
215
+ case "close":
216
+ case "overview":
217
+ this.toggleOverview(false);
218
+ return true;
219
+ case "fullscreen": return true;
220
+ case "presenter": return !!this.options.presenter;
221
+ default: return false;
222
+ }
223
+ }
221
224
  }
@@ -1,81 +1,76 @@
1
+ // Capture a page, or one element of it, to PNG at a canvas size and device scale.
1
2
  import { mkdir, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
- export const DEFAULT_WAITS = [
4
- "networkidle",
5
- "load"
6
- ];
4
+ /**
5
+ * `networkidle` never fires on a page that keeps polling, like a chat widget, so a retry
6
+ * falls back to `load`, which always fires.
7
+ */
8
+ export const DEFAULT_WAITS = ["networkidle", "load"];
9
+ /** Navigates, retrying with the next wait condition when one times out. */
7
10
  export async function gotoSettled(page, url, options = {}) {
8
- const waits = options.waits ?? DEFAULT_WAITS;
9
- const timeout = options.timeout ?? 45_000;
10
- let failure;
11
- for (const waitUntil of waits){
12
- try {
13
- return await page.goto(url, {
14
- waitUntil,
15
- timeout
16
- });
17
- } catch (error) {
18
- failure = error;
19
- }
20
- }
21
- throw failure ?? new Error("gotoSettled needs at least one wait condition.");
11
+ const waits = options.waits ?? DEFAULT_WAITS;
12
+ const timeout = options.timeout ?? 45e3;
13
+ let failure;
14
+ for (const waitUntil of waits) {
15
+ try {
16
+ return await page.goto(url, {
17
+ waitUntil,
18
+ timeout
19
+ });
20
+ } catch (error) {
21
+ failure = error;
22
+ }
23
+ }
24
+ throw failure ?? new Error("gotoSettled needs at least one wait condition.");
22
25
  }
26
+ /** Opens a page at the canvas size, captures it to PNG and closes the page. */
23
27
  export async function capturePage(browser, source, options) {
24
- const page = await browser.newPage({
25
- viewport: options.size,
26
- deviceScaleFactor: options.scale ?? 1
27
- });
28
- try {
29
- if ("url" in source) await gotoSettled(page, source.url, options.goto);
30
- else await page.setContent(source.html, {
31
- waitUntil: "load"
32
- });
33
- if (options.style) await page.addStyleTag({
34
- content: options.style
35
- });
36
- if (options.wait) await page.waitForTimeout(options.wait);
37
- const { selector } = options;
38
- let bytes;
39
- if (selector) {
40
- const target = page.locator(selector).first();
41
- await target.waitFor({
42
- state: "visible"
43
- });
44
- bytes = await target.screenshot({
45
- type: "png"
46
- });
47
- } else {
48
- bytes = await page.screenshot({
49
- type: "png",
50
- fullPage: options.fullPage
51
- });
52
- }
53
- if (options.path) {
54
- await mkdir(path.dirname(options.path), {
55
- recursive: true
56
- });
57
- await writeFile(options.path, bytes);
58
- }
59
- return bytes;
60
- } finally{
61
- await page.close();
62
- }
28
+ const page = await browser.newPage({
29
+ viewport: options.size,
30
+ deviceScaleFactor: options.scale ?? 1
31
+ });
32
+ try {
33
+ if ("url" in source) await gotoSettled(page, source.url, options.goto);
34
+ else await page.setContent(source.html, { waitUntil: "load" });
35
+ if (options.style) await page.addStyleTag({ content: options.style });
36
+ if (options.wait) await page.waitForTimeout(options.wait);
37
+ const { selector } = options;
38
+ let bytes;
39
+ if (selector) {
40
+ const target = page.locator(selector).first();
41
+ await target.waitFor({ state: "visible" });
42
+ bytes = await target.screenshot({ type: "png" });
43
+ } else {
44
+ bytes = await page.screenshot({
45
+ type: "png",
46
+ fullPage: options.fullPage
47
+ });
48
+ }
49
+ if (options.path) {
50
+ await mkdir(path.dirname(options.path), { recursive: true });
51
+ await writeFile(options.path, bytes);
52
+ }
53
+ return bytes;
54
+ } finally {
55
+ await page.close();
56
+ }
63
57
  }
58
+ /** `01.png`, `02.png` ...: the file name for the nth capture, one-based, zero-padded. */
64
59
  export function frameName(index, digits = 2, extension = "png") {
65
- return `${String(index + 1).padStart(digits, "0")}.${extension}`;
60
+ return `${String(index + 1).padStart(digits, "0")}.${extension}`;
66
61
  }
62
+ /**
63
+ * Screenshots every element a locator matches into `dir`, one PNG each, and
64
+ * returns the paths in order. The elements are the pages of a deck.
65
+ */
67
66
  export async function captureEach(elements, dir, options = {}) {
68
- await mkdir(dir, {
69
- recursive: true
70
- });
71
- const count = await elements.count();
72
- const files = [];
73
- for(let index = 0; index < count; index++){
74
- const file = path.join(dir, frameName(index, options.digits));
75
- await elements.nth(index).screenshot({
76
- path: file
77
- });
78
- files.push(file);
79
- }
80
- return files;
67
+ await mkdir(dir, { recursive: true });
68
+ const count = await elements.count();
69
+ const files = [];
70
+ for (let index = 0; index < count; index++) {
71
+ const file = path.join(dir, frameName(index, options.digits));
72
+ await elements.nth(index).screenshot({ path: file });
73
+ files.push(file);
74
+ }
75
+ return files;
81
76
  }