@phin-tech/dottie 0.3.0 → 0.5.1

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.
package/PARITY.md CHANGED
@@ -38,6 +38,10 @@ fixture, so the gap is visible rather than silent.
38
38
  | Damaged timelines (clamped, filtered, sorted) | Supported | [edge/timeline](../fixtures/edge/timeline.dottie) | Read the way DottieKit's decoders read them |
39
39
  | Unknown kinds, fields and newer versions | Partial | [js-only/future](../fixtures/js-only/future.dottie) | Drawn as labelled placeholders, reported in `warnings`; Dottie refuses such files |
40
40
  | Text fonts | Partial | | Positions use Helvetica Neue's metrics; browsers without Helvetica Neue draw Helvetica or Arial in the same places, so runs of mixed styles may sit slightly apart |
41
- | Links between shapes and pages | Missing | | Not clickable in the player yet |
42
- | Interactivity (hover, clicks, editing) | Missing | | |
41
+ | Symbols: instances on their own clocks (loop, once, still, offset), nested, scaled and turned | Supported | [edge/symbols](../fixtures/edge/symbols.dottie) | Expanded as DottieKit expands them, with the same ids, seeds and limits ([js-only/hostile-symbols](../fixtures/js-only/hostile-symbols.dottie)) |
42
+ | Button symbols (up, over, down, hit) and clickable copies | Supported | [edge/buttons](../fixtures/edge/buttons.dottie) | As the HTML export draws them |
43
+ | Copies that play when clicked | Supported | [edge/buttons](../fixtures/edge/buttons.dottie) | `<dottie-player>` only: the HTML export is drawn ahead of time and shows them at rest |
44
+ | Clicks: play a scene, open a link, hover looks, keyboard | Supported | [edge/clicks](../fixtures/edge/clicks.dottie) | As the HTML export does; only http, https and mailto links are followed |
45
+ | Links between shapes and pages | Partial | [edge/clicks](../fixtures/edge/clicks.dottie) | A link to a shape on another page shows that page; one on the same page does nothing. The HTML export holds one page, so it has neither |
46
+ | Editing | Missing | | |
43
47
  | Ticking task boxes in notes | Missing | | Drawn, not clickable |
package/README.md CHANGED
@@ -29,8 +29,15 @@ prefer reduced motion. A still page is just the drawing.
29
29
  | `no-controls`, `no-autoplay`, `no-loop` | |
30
30
  | `label` | The accessible name; the page name by default |
31
31
 
32
- The element has `play()`, `pause()`, `nextScene()`, `prevScene()`, `currentTime` (settable), `duration`,
33
- `paused`, and fires `load`, `frame` and `error` events. Its page color comes from the drawing's theme; set
32
+ Elements the drawing makes clickable work with the mouse and the keyboard (Tab, then Enter or Space). A click plays
33
+ a named scene on to the next one, opens a link in a new tab (only `http`, `https` and `mailto`), or shows the page a
34
+ shape's link points at. Each shows its hover look under the pointer; button symbols show their over and down looks
35
+ and take clicks on their hit area, and a symbol's copy set to play when clicked plays its animation through. Every
36
+ click first fires a cancelable `action` event whose `detail` is `{element, scene | url | page, replay?}`; call
37
+ `preventDefault()` to handle it yourself.
38
+
39
+ The element has `play()`, `pause()`, `nextScene()`, `prevScene()`, `activate(elementId)`, `currentTime`
40
+ (settable), `duration`, `paused`, and fires `load`, `frame`, `action` and `error` events. Its page color comes from the drawing's theme; set
34
41
  `--dottie-radius` to round it, and style its parts with `::part(stage)` and `::part(controls)`.
35
42
 
36
43
  Without modules, `dist/dottie-player.min.js` is a classic script that registers the element and sets
package/dist/compile.js CHANGED
@@ -1,15 +1,16 @@
1
- /*! dottie.js 0.3.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
1
+ /*! dottie.js 0.5.1 | Apache-2.0 | https://github.com/phin-tech/dottie */
2
2
  var __defProp = Object.defineProperty;
3
3
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
4
4
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
5
5
 
6
6
  // src/model.ts
7
7
  var SUPPORTED_VERSION = 2;
8
- var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image"];
8
+ var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
9
9
  var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
10
10
  var KNOWN_FIELDS = {
11
11
  file: ["version", "pages", "images"],
12
- page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline"],
12
+ page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols"],
13
+ symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button"],
13
14
  layer: ["id", "name", "visible", "locked"],
14
15
  rgba: ["r", "g", "b", "a"],
15
16
  style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "gridSize", "opacity", "dashOffset"],
@@ -33,6 +34,11 @@ var KNOWN_FIELDS = {
33
34
  "rotation",
34
35
  "link",
35
36
  "anchor",
37
+ "click",
38
+ "hover",
39
+ "symbol",
40
+ "playback",
41
+ "offset",
36
42
  "image",
37
43
  "reveal"
38
44
  ],
@@ -47,7 +53,9 @@ var KNOWN_FIELDS = {
47
53
  follow: ["path", "start", "end", "ease", "reverse", "orient", "laps"],
48
54
  repeat: ["start", "end", "count"],
49
55
  cameraKey: ["t", "x", "y", "width", "height", "ease"],
50
- marker: ["t", "name"]
56
+ marker: ["t", "name"],
57
+ click: ["scene", "url"],
58
+ hover: ["stroke", "fill", "opacity"]
51
59
  };
52
60
  var MAX_DURATION = 3600;
53
61
  var MAX_MARCH = 1e4;
@@ -68,6 +76,11 @@ function clampTime(t) {
68
76
  function clampFPS(f) {
69
77
  return Number.isFinite(f) ? Math.min(Math.max(f, 1), 60) : 12;
70
78
  }
79
+ function allowedURL(url) {
80
+ const u = url.trim();
81
+ if (!/^(https?:\/\/|mailto:)/i.test(u) || [...u].length <= 7 || /[\u0000-\u001f\u007f-\u009f]/.test(u)) return null;
82
+ return u;
83
+ }
71
84
  function rgba(v) {
72
85
  if (!isObj(v)) return void 0;
73
86
  const r = num(v.r), g = num(v.g), b = num(v.b);
@@ -182,10 +195,61 @@ var Reader = class {
182
195
  rotation: num(v.rotation) ?? null,
183
196
  link: uuid(v.link) ?? null,
184
197
  anchor: str(v.anchor) ?? null,
198
+ click: this.click(v.click),
199
+ hover: this.hover(v.hover),
200
+ symbol: uuid(v.symbol) ?? null,
201
+ playback: this.playback(v.playback),
202
+ offset: num(v.offset) ?? null,
185
203
  image: this.image(v.image, images),
186
204
  reveal: num(v.reveal) ?? null
187
205
  };
188
206
  }
207
+ click(v) {
208
+ if (!isObj(v)) return null;
209
+ this.fields(v, "click");
210
+ const url = str(v.url);
211
+ const safe = url === void 0 ? null : allowedURL(url);
212
+ if (url !== void 0 && safe === null) this.warn("Skipped a click link that isn't http, https or mailto");
213
+ return { scene: str(v.scene) ?? null, url: safe };
214
+ }
215
+ playback(v) {
216
+ const s = str(v);
217
+ if (s === void 0) return null;
218
+ return ["loop", "once", "still", "click"].includes(s) ? s : "loop";
219
+ }
220
+ symbol(v, images) {
221
+ if (!isObj(v)) return null;
222
+ this.fields(v, "symbol");
223
+ const id = uuid(v.id);
224
+ if (!id) {
225
+ this.warn("Skipped a symbol without an id");
226
+ return null;
227
+ }
228
+ const size = (x) => {
229
+ const n = num(x);
230
+ return Math.max(1, n ?? 100);
231
+ };
232
+ const layers = (Array.isArray(v.layers) ? v.layers : []).filter(isObj).flatMap((l) => {
233
+ const lid = uuid(l.id);
234
+ return lid ? [{ id: lid, name: str(l.name) ?? "", visible: bool(l.visible) ?? true, locked: bool(l.locked) ?? false }] : [];
235
+ });
236
+ if (layers.length === 0) layers.push({ id: `${id}-LAYER`, name: "Layer 1", visible: true, locked: false });
237
+ return {
238
+ id,
239
+ name: str(v.name) ?? "Symbol",
240
+ width: size(v.width),
241
+ height: size(v.height),
242
+ layers,
243
+ elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
244
+ timeline: this.timeline(v.timeline),
245
+ button: bool(v.button) ?? null
246
+ };
247
+ }
248
+ hover(v) {
249
+ if (!isObj(v)) return null;
250
+ this.fields(v, "hover");
251
+ return { stroke: rgba(v.stroke) ?? null, fill: rgba(v.fill) ?? null, opacity: num(v.opacity) ?? null };
252
+ }
189
253
  ease(v) {
190
254
  const s = str(v);
191
255
  if (s === void 0) return null;
@@ -278,7 +342,8 @@ var Reader = class {
278
342
  layers,
279
343
  elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
280
344
  theme: str(v.theme) ?? null,
281
- timeline: this.timeline(v.timeline)
345
+ timeline: this.timeline(v.timeline),
346
+ symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null
282
347
  };
283
348
  }
284
349
  };
@@ -318,6 +383,19 @@ function parseDottie(source) {
318
383
  return { version, pages, warnings: reader.warnings };
319
384
  }
320
385
 
386
+ // src/clicks.ts
387
+ var CLICK_STYLE = `
388
+ .dottie-a { cursor: pointer; outline: none; }
389
+ .dottie-a .dottie-h { display: none; }
390
+ .dottie-a:hover .dottie-h, .dottie-a.dottie-hover .dottie-h, .dottie-a:focus-visible .dottie-h { display: inline; }
391
+ .dottie-a:hover .dottie-n, .dottie-a.dottie-hover .dottie-n, .dottie-a:focus-visible .dottie-n { display: none; }
392
+ .dottie-a:focus-visible { filter: drop-shadow(0 0 3px var(--dottie-focus, #3b82f6)); }
393
+ .dottie-a:not(.dottie-b):active, .dottie-a.dottie-press:not(.dottie-b) { transform: translate(1px, 1px); }
394
+ .dottie-a .dottie-d { display: none; }
395
+ .dottie-b:active .dottie-n, .dottie-b:active .dottie-h, .dottie-b.dottie-press .dottie-n, .dottie-b.dottie-press .dottie-h { display: none; }
396
+ .dottie-b:active .dottie-d, .dottie-b.dottie-press .dottie-d { display: inline; }
397
+ `;
398
+
321
399
  // src/player.ts
322
400
  function pageIndex(file, spec) {
323
401
  const s = (spec ?? "").trim();
@@ -327,6 +405,22 @@ function pageIndex(file, spec) {
327
405
  const n = Number(s);
328
406
  return Number.isInteger(n) && n >= 1 && n <= file.pages.length ? n - 1 : 0;
329
407
  }
408
+ var STYLE = `
409
+ :host { display: block; position: relative; color: var(--dottie-ink); background: var(--dottie-page);
410
+ font: 14px -apple-system, system-ui, sans-serif; border-radius: var(--dottie-radius, 0); }
411
+ :host([hidden]) { display: none; }
412
+ .stage { display: block; width: 100%; height: auto; }
413
+ .stage svg { display: block; width: 100%; height: auto; margin: 0 auto; }
414
+ .bar { display: flex; align-items: center; gap: 12px; padding: 8px 12px 12px; max-width: 560px; margin: 0 auto; box-sizing: border-box; }
415
+ .bar[hidden] { display: none; }
416
+ button { font: inherit; color: inherit; background: none; border: 1.5px solid currentColor; border-radius: 6px;
417
+ width: 34px; height: 30px; cursor: pointer; flex: none; padding: 0; }
418
+ button:focus-visible, input:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
419
+ input[type=range] { flex: 1; min-width: 60px; accent-color: currentColor; }
420
+ output { font-variant-numeric: tabular-nums; min-width: 4.5em; text-align: right; }
421
+ .scene { min-width: 0; flex: 0 1 auto; text-align: center; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
422
+ .error { padding: 16px; font-family: ui-monospace, Menlo, monospace; white-space: pre-wrap; }
423
+ ${CLICK_STYLE}`;
330
424
 
331
425
  // src/compile.ts
332
426
  var LOADER = `;(function () {
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- /*! dottie.js 0.3.0 | Apache-2.0 | https://github.com/phin-tech/dottie */
2
+ /*! dottie.js 0.5.1 | Apache-2.0 | https://github.com/phin-tech/dottie */
3
3
  var __defProp = Object.defineProperty;
4
4
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
5
5
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
@@ -10,11 +10,12 @@ import { basename } from "node:path";
10
10
 
11
11
  // src/model.ts
12
12
  var SUPPORTED_VERSION = 2;
13
- var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image"];
13
+ var KINDS = ["rectangle", "ellipse", "diamond", "text", "connector", "freehand", "path", "note", "image", "symbol"];
14
14
  var EASES = ["linear", "easeIn", "easeOut", "easeInOut", "hold", "spring", "bounce", "elastic"];
15
15
  var KNOWN_FIELDS = {
16
16
  file: ["version", "pages", "images"],
17
- page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline"],
17
+ page: ["id", "name", "layers", "elements", "theme", "guidesX", "guidesY", "guidesLocked", "timeline", "symbols"],
18
+ symbol: ["id", "name", "width", "height", "layers", "elements", "timeline", "button"],
18
19
  layer: ["id", "name", "visible", "locked"],
19
20
  rgba: ["r", "g", "b", "a"],
20
21
  style: ["stroke", "fill", "fillStyle", "strokeWidth", "dash", "roughness", "bowing", "fontSize", "gridSize", "opacity", "dashOffset"],
@@ -38,6 +39,11 @@ var KNOWN_FIELDS = {
38
39
  "rotation",
39
40
  "link",
40
41
  "anchor",
42
+ "click",
43
+ "hover",
44
+ "symbol",
45
+ "playback",
46
+ "offset",
41
47
  "image",
42
48
  "reveal"
43
49
  ],
@@ -52,7 +58,9 @@ var KNOWN_FIELDS = {
52
58
  follow: ["path", "start", "end", "ease", "reverse", "orient", "laps"],
53
59
  repeat: ["start", "end", "count"],
54
60
  cameraKey: ["t", "x", "y", "width", "height", "ease"],
55
- marker: ["t", "name"]
61
+ marker: ["t", "name"],
62
+ click: ["scene", "url"],
63
+ hover: ["stroke", "fill", "opacity"]
56
64
  };
57
65
  var MAX_DURATION = 3600;
58
66
  var MAX_MARCH = 1e4;
@@ -73,6 +81,11 @@ function clampTime(t) {
73
81
  function clampFPS(f) {
74
82
  return Number.isFinite(f) ? Math.min(Math.max(f, 1), 60) : 12;
75
83
  }
84
+ function allowedURL(url) {
85
+ const u = url.trim();
86
+ if (!/^(https?:\/\/|mailto:)/i.test(u) || [...u].length <= 7 || /[\u0000-\u001f\u007f-\u009f]/.test(u)) return null;
87
+ return u;
88
+ }
76
89
  function rgba(v) {
77
90
  if (!isObj(v)) return void 0;
78
91
  const r = num(v.r), g = num(v.g), b = num(v.b);
@@ -187,10 +200,61 @@ var Reader = class {
187
200
  rotation: num(v.rotation) ?? null,
188
201
  link: uuid(v.link) ?? null,
189
202
  anchor: str(v.anchor) ?? null,
203
+ click: this.click(v.click),
204
+ hover: this.hover(v.hover),
205
+ symbol: uuid(v.symbol) ?? null,
206
+ playback: this.playback(v.playback),
207
+ offset: num(v.offset) ?? null,
190
208
  image: this.image(v.image, images),
191
209
  reveal: num(v.reveal) ?? null
192
210
  };
193
211
  }
212
+ click(v) {
213
+ if (!isObj(v)) return null;
214
+ this.fields(v, "click");
215
+ const url = str(v.url);
216
+ const safe = url === void 0 ? null : allowedURL(url);
217
+ if (url !== void 0 && safe === null) this.warn("Skipped a click link that isn't http, https or mailto");
218
+ return { scene: str(v.scene) ?? null, url: safe };
219
+ }
220
+ playback(v) {
221
+ const s = str(v);
222
+ if (s === void 0) return null;
223
+ return ["loop", "once", "still", "click"].includes(s) ? s : "loop";
224
+ }
225
+ symbol(v, images) {
226
+ if (!isObj(v)) return null;
227
+ this.fields(v, "symbol");
228
+ const id = uuid(v.id);
229
+ if (!id) {
230
+ this.warn("Skipped a symbol without an id");
231
+ return null;
232
+ }
233
+ const size = (x) => {
234
+ const n = num(x);
235
+ return Math.max(1, n ?? 100);
236
+ };
237
+ const layers = (Array.isArray(v.layers) ? v.layers : []).filter(isObj).flatMap((l) => {
238
+ const lid = uuid(l.id);
239
+ return lid ? [{ id: lid, name: str(l.name) ?? "", visible: bool(l.visible) ?? true, locked: bool(l.locked) ?? false }] : [];
240
+ });
241
+ if (layers.length === 0) layers.push({ id: `${id}-LAYER`, name: "Layer 1", visible: true, locked: false });
242
+ return {
243
+ id,
244
+ name: str(v.name) ?? "Symbol",
245
+ width: size(v.width),
246
+ height: size(v.height),
247
+ layers,
248
+ elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
249
+ timeline: this.timeline(v.timeline),
250
+ button: bool(v.button) ?? null
251
+ };
252
+ }
253
+ hover(v) {
254
+ if (!isObj(v)) return null;
255
+ this.fields(v, "hover");
256
+ return { stroke: rgba(v.stroke) ?? null, fill: rgba(v.fill) ?? null, opacity: num(v.opacity) ?? null };
257
+ }
194
258
  ease(v) {
195
259
  const s = str(v);
196
260
  if (s === void 0) return null;
@@ -283,7 +347,8 @@ var Reader = class {
283
347
  layers,
284
348
  elements: (Array.isArray(v.elements) ? v.elements : []).map((e) => this.element(e, images)).filter((e) => e !== null),
285
349
  theme: str(v.theme) ?? null,
286
- timeline: this.timeline(v.timeline)
350
+ timeline: this.timeline(v.timeline),
351
+ symbols: Array.isArray(v.symbols) ? v.symbols.map((x) => this.symbol(x, images)).filter((x) => x !== null) : null
287
352
  };
288
353
  }
289
354
  };
@@ -323,6 +388,19 @@ function parseDottie(source2) {
323
388
  return { version, pages, warnings: reader.warnings };
324
389
  }
325
390
 
391
+ // src/clicks.ts
392
+ var CLICK_STYLE = `
393
+ .dottie-a { cursor: pointer; outline: none; }
394
+ .dottie-a .dottie-h { display: none; }
395
+ .dottie-a:hover .dottie-h, .dottie-a.dottie-hover .dottie-h, .dottie-a:focus-visible .dottie-h { display: inline; }
396
+ .dottie-a:hover .dottie-n, .dottie-a.dottie-hover .dottie-n, .dottie-a:focus-visible .dottie-n { display: none; }
397
+ .dottie-a:focus-visible { filter: drop-shadow(0 0 3px var(--dottie-focus, #3b82f6)); }
398
+ .dottie-a:not(.dottie-b):active, .dottie-a.dottie-press:not(.dottie-b) { transform: translate(1px, 1px); }
399
+ .dottie-a .dottie-d { display: none; }
400
+ .dottie-b:active .dottie-n, .dottie-b:active .dottie-h, .dottie-b.dottie-press .dottie-n, .dottie-b.dottie-press .dottie-h { display: none; }
401
+ .dottie-b:active .dottie-d, .dottie-b.dottie-press .dottie-d { display: inline; }
402
+ `;
403
+
326
404
  // src/player.ts
327
405
  function pageIndex(file, spec) {
328
406
  const s = (spec ?? "").trim();
@@ -332,6 +410,22 @@ function pageIndex(file, spec) {
332
410
  const n = Number(s);
333
411
  return Number.isInteger(n) && n >= 1 && n <= file.pages.length ? n - 1 : 0;
334
412
  }
413
+ var STYLE = `
414
+ :host { display: block; position: relative; color: var(--dottie-ink); background: var(--dottie-page);
415
+ font: 14px -apple-system, system-ui, sans-serif; border-radius: var(--dottie-radius, 0); }
416
+ :host([hidden]) { display: none; }
417
+ .stage { display: block; width: 100%; height: auto; }
418
+ .stage svg { display: block; width: 100%; height: auto; margin: 0 auto; }
419
+ .bar { display: flex; align-items: center; gap: 12px; padding: 8px 12px 12px; max-width: 560px; margin: 0 auto; box-sizing: border-box; }
420
+ .bar[hidden] { display: none; }
421
+ button { font: inherit; color: inherit; background: none; border: 1.5px solid currentColor; border-radius: 6px;
422
+ width: 34px; height: 30px; cursor: pointer; flex: none; padding: 0; }
423
+ button:focus-visible, input:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
424
+ input[type=range] { flex: 1; min-width: 60px; accent-color: currentColor; }
425
+ output { font-variant-numeric: tabular-nums; min-width: 4.5em; text-align: right; }
426
+ .scene { min-width: 0; flex: 0 1 auto; text-align: center; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
427
+ .error { padding: 16px; font-family: ui-monospace, Menlo, monospace; white-space: pre-wrap; }
428
+ ${CLICK_STYLE}`;
335
429
 
336
430
  // src/compile.ts
337
431
  var LOADER = `;(function () {
@@ -393,7 +487,7 @@ function compile(source2, runtime, options2 = {}) {
393
487
  }
394
488
 
395
489
  // src/index.ts
396
- var VERSION = "0.3.0";
490
+ var VERSION = "0.5.1";
397
491
 
398
492
  // src/cli.ts
399
493
  var usage = `dottie-compile ${VERSION}: a Dottie drawing as one script that plays it on any page.