contour-deck 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +261 -2
  3. package/dist/astro/Deck.astro +68 -0
  4. package/dist/astro/host.d.ts +20 -0
  5. package/dist/astro/host.js +170 -0
  6. package/dist/astro/legacy.d.ts +22 -0
  7. package/dist/astro/legacy.js +42 -0
  8. package/dist/core/deck.d.ts +2 -0
  9. package/dist/core/deck.js +598 -0
  10. package/dist/core/dom.d.ts +8 -0
  11. package/dist/core/dom.js +22 -0
  12. package/dist/core/gestures.d.ts +6 -0
  13. package/dist/core/gestures.js +24 -0
  14. package/dist/core/index.d.ts +3 -0
  15. package/dist/core/index.js +2 -0
  16. package/dist/core/labels.d.ts +2 -0
  17. package/dist/core/labels.js +20 -0
  18. package/dist/core/model.d.ts +37 -0
  19. package/dist/core/model.js +51 -0
  20. package/dist/core/navigator.d.ts +6 -0
  21. package/dist/core/navigator.js +12 -0
  22. package/dist/core/notes.d.ts +11 -0
  23. package/dist/core/notes.js +34 -0
  24. package/dist/core/overview.d.ts +11 -0
  25. package/dist/core/overview.js +104 -0
  26. package/dist/core/rail.d.ts +8 -0
  27. package/dist/core/rail.js +26 -0
  28. package/dist/core/stops.d.ts +17 -0
  29. package/dist/core/stops.js +40 -0
  30. package/dist/core/transitions.d.ts +9 -0
  31. package/dist/core/transitions.js +37 -0
  32. package/dist/core/types.d.ts +82 -0
  33. package/dist/core/types.js +1 -0
  34. package/dist/rehype/hast.d.ts +26 -0
  35. package/dist/rehype/hast.js +48 -0
  36. package/dist/rehype/index.d.ts +12 -0
  37. package/dist/rehype/index.js +221 -0
  38. package/dist/rehype/page.d.ts +2 -0
  39. package/dist/rehype/page.js +84 -0
  40. package/dist/rehype/reading.d.ts +11 -0
  41. package/dist/rehype/reading.js +9 -0
  42. package/dist/rehype/titles.d.ts +19 -0
  43. package/dist/rehype/titles.js +63 -0
  44. package/dist/remark/index.d.ts +20 -0
  45. package/dist/remark/index.js +117 -0
  46. package/dist/remote/clock.d.ts +35 -0
  47. package/dist/remote/clock.js +48 -0
  48. package/dist/remote/guest.d.ts +41 -0
  49. package/dist/remote/guest.js +223 -0
  50. package/dist/remote/index.d.ts +18 -0
  51. package/dist/remote/index.js +11 -0
  52. package/dist/remote/ink.d.ts +11 -0
  53. package/dist/remote/ink.js +74 -0
  54. package/dist/remote/labels.d.ts +72 -0
  55. package/dist/remote/labels.js +68 -0
  56. package/dist/remote/links.d.ts +2 -0
  57. package/dist/remote/links.js +12 -0
  58. package/dist/remote/overlay.d.ts +11 -0
  59. package/dist/remote/overlay.js +63 -0
  60. package/dist/remote/pairing.d.ts +40 -0
  61. package/dist/remote/pairing.js +97 -0
  62. package/dist/remote/panel.d.ts +38 -0
  63. package/dist/remote/panel.js +131 -0
  64. package/dist/remote/phone.d.ts +10 -0
  65. package/dist/remote/phone.js +407 -0
  66. package/dist/remote/presenter.d.ts +2 -0
  67. package/dist/remote/presenter.js +126 -0
  68. package/dist/remote/protocol.d.ts +106 -0
  69. package/dist/remote/protocol.js +123 -0
  70. package/dist/remote/qr.d.ts +8 -0
  71. package/dist/remote/qr.js +266 -0
  72. package/dist/remote/sensor.d.ts +9 -0
  73. package/dist/remote/sensor.js +50 -0
  74. package/dist/remote/session.d.ts +39 -0
  75. package/dist/remote/session.js +287 -0
  76. package/dist/remote/store.d.ts +9 -0
  77. package/dist/remote/store.js +27 -0
  78. package/dist/remote/style.d.ts +2 -0
  79. package/dist/remote/style.js +131 -0
  80. package/dist/remote/tilt.d.ts +78 -0
  81. package/dist/remote/tilt.js +148 -0
  82. package/dist/remote/transport.d.ts +35 -0
  83. package/dist/remote/transport.js +162 -0
  84. package/dist/remote/view.d.ts +48 -0
  85. package/dist/remote/view.js +119 -0
  86. package/dist/shared/markers.d.ts +15 -0
  87. package/dist/shared/markers.js +15 -0
  88. package/dist/style.css +629 -0
  89. package/docs/CONTRACT.md +178 -0
  90. package/docs/PROTOCOL.md +99 -0
  91. package/package.json +89 -4
  92. package/styles/base.css +3 -0
  93. package/styles/deck.css +151 -0
  94. package/styles/index.css +3 -0
@@ -0,0 +1,68 @@
1
+ ---
2
+ /**
3
+ * The Astro host of a Contour deck. Put the rendered Markdown (the output of `contour-deck/rehype`)
4
+ * in the slot; without JavaScript it stays a document.
5
+ *
6
+ * <Deck docClass="prose" legacy><Content /></Deck>
7
+ * <button data-ct-start>START</button>
8
+ * <a href="?view=notes" data-ct-start="notes">Notes</a>
9
+ *
10
+ * `?view=deck` and `?view=notes` open the deck on load; so does any `[data-ct-start]` control.
11
+ * With `remote` or `relay`, the deck has a remote (R): `?view=remote` is the phone, `?view=presenter` the presenter window.
12
+ *
13
+ * The build copies this file to dist/astro/, and the import paths are the ones that hold there:
14
+ * ../style.css is dist/style.css, ./host.js the compiled src/astro/host.ts.
15
+ */
16
+ import "../style.css";
17
+
18
+ interface Props {
19
+ /** Classes of the wrapper in both views. */
20
+ class?: string;
21
+ /**
22
+ * Classes the wrapper carries only while the deck is closed, such as a prose class.
23
+ * The open deck stays inside the wrapper, so these would otherwise restyle the stage and the shell.
24
+ */
25
+ docClass?: string;
26
+ /** `snap`: one wheel gesture moves one stop. `free`: native scrolling. Default `snap`. */
27
+ paging?: "snap" | "free";
28
+ /** How the outline looks on entry. Default `flat`. */
29
+ overview?: "flat" | "3d";
30
+ /** Accept old reveal.js links, `?view=slides&slide=h.v`, through `data-legacy`. */
31
+ legacy?: boolean;
32
+ /** The remote button and the `R` key: presenter window and notes link. Implied by `relay`. */
33
+ remote?: boolean;
34
+ /** Base URL of the relay (`wss://…`), so a phone can pair and control the deck. See docs/PROTOCOL.md. */
35
+ relay?: string;
36
+ }
37
+
38
+ const { class: className, docClass, paging, overview, legacy = false, remote = false, relay } = Astro.props;
39
+ ---
40
+
41
+ <div
42
+ data-ct-host
43
+ class:list={[className, docClass]}
44
+ data-ct-doc-class={docClass}
45
+ data-ct-paging={paging}
46
+ data-ct-overview={overview}
47
+ data-ct-legacy={legacy ? "" : undefined}
48
+ data-ct-remote={remote ? "" : undefined}
49
+ data-ct-relay={relay}
50
+ >
51
+ <slot />
52
+ </div>
53
+
54
+ <script>
55
+ import { mountAll, type Host } from "./host.js";
56
+
57
+ let hosts: Host[] = mountAll();
58
+
59
+ // Under Astro's client router this module runs once; each later page mounts on page-load,
60
+ // and an open deck is put back before the old page is swapped out.
61
+ document.addEventListener("astro:page-load", () => {
62
+ hosts = mountAll();
63
+ });
64
+ document.addEventListener("astro:before-swap", () => {
65
+ for (const host of hosts) host.unmount();
66
+ hosts = [];
67
+ });
68
+ </script>
@@ -0,0 +1,20 @@
1
+ import type { Deck, DeckOptions } from "../core/index.js";
2
+ import type { RemoteOptions } from "../remote/index.js";
3
+ /**
4
+ * Options of the remote for every deck of the page, such as a QR encoder the site already ships:
5
+ * `configureRemote({ qr: text => QRCode.toString(text, { type: "svg" }) })`.
6
+ * A `data-ct-relay` on a wrapper wins over `relay` given here.
7
+ */
8
+ export declare function configureRemote(options: RemoteOptions): void;
9
+ export interface Host {
10
+ /** The open deck, or `null` while the page shows the document. */
11
+ readonly deck: Deck | null;
12
+ open(options?: Pick<DeckOptions, "view" | "at">): void;
13
+ close(): void;
14
+ /** Closes the deck and removes every listener. */
15
+ unmount(): void;
16
+ }
17
+ /** Wires one `[data-ct-host]`. Mounting the same element twice returns the first host. */
18
+ export declare function mount(host: HTMLElement): Host | null;
19
+ /** Mounts every `[data-ct-host]` in the document that is not mounted yet. */
20
+ export declare function mountAll(): Host[];
@@ -0,0 +1,170 @@
1
+ /**
2
+ * What a host page does around the core, written once: open the deck when the URL or a control asks
3
+ * for it, close it on exit, and send old reveal.js links to their new position.
4
+ * `Deck.astro` calls this; a plain HTML host can call it too.
5
+ *
6
+ * | Host markup | Meaning |
7
+ * | ---------------------------------------- | ------------------------------------------------------------ |
8
+ * | `[data-ct-host]` | wrapper of one `[data-ct-deck]` |
9
+ * | `data-ct-doc-class="prose"` | classes the wrapper carries only while the deck is closed |
10
+ * | `data-ct-paging`, `data-ct-overview` | the `paging` and `overview` options of the core |
11
+ * | `data-ct-legacy` | accept `?view=slides&slide=h.v` |
12
+ * | `[data-ct-start]`, `[data-ct-start="notes"]` | a control that opens the deck or its notes view: inside a host for that deck, elsewhere for the first deck of the page |
13
+ * | `data-ct-remote`, `data-ct-relay="wss://…"` | the remote button and the `R` key; with a relay URL a phone can pair (docs/PROTOCOL.md) |
14
+ *
15
+ * The remote (`contour-deck/remote`) is loaded on first use: on `R`, on `?view=remote` (the phone),
16
+ * and on `?view=presenter` (the presenter window).
17
+ */
18
+ import { createDeck } from "../core/index.js";
19
+ import { hostKey } from "../remote/store.js";
20
+ import { legacyAt, parseLegacy, readLegacy } from "./legacy.js";
21
+ let remoteDefaults = {};
22
+ /**
23
+ * Options of the remote for every deck of the page, such as a QR encoder the site already ships:
24
+ * `configureRemote({ qr: text => QRCode.toString(text, { type: "svg" }) })`.
25
+ * A `data-ct-relay` on a wrapper wins over `relay` given here.
26
+ */
27
+ export function configureRemote(options) {
28
+ remoteDefaults = { ...remoteDefaults, ...options };
29
+ }
30
+ const mounted = new WeakMap();
31
+ function stored(key) {
32
+ try {
33
+ return sessionStorage.getItem(key) !== null;
34
+ }
35
+ catch {
36
+ return false;
37
+ }
38
+ }
39
+ function optionsOf(host) {
40
+ const { ctPaging, ctOverview } = host.dataset;
41
+ return {
42
+ ...(ctPaging === "free" || ctPaging === "snap" ? { paging: ctPaging } : {}),
43
+ ...(ctOverview === "3d" || ctOverview === "flat" ? { overview: ctOverview } : {}),
44
+ };
45
+ }
46
+ /**
47
+ * Rewrites `?view=slides&slide=h.v` to `?view=deck&at=b.s.p` in place and returns nothing:
48
+ * the core then reads its own parameters. An old link without a match opens the outline.
49
+ */
50
+ function adoptLegacyUrl(root) {
51
+ const url = new URL(location.href);
52
+ if (url.searchParams.get("view") !== "slides")
53
+ return;
54
+ const old = parseLegacy(url.searchParams.get("slide") ?? "0");
55
+ const at = old && legacyAt(readLegacy(root), old.h, old.v);
56
+ url.searchParams.delete("slide");
57
+ url.searchParams.set("view", "deck");
58
+ if (at)
59
+ url.searchParams.set("at", at);
60
+ history.replaceState(history.state, "", url);
61
+ }
62
+ /** Wires one `[data-ct-host]`. Mounting the same element twice returns the first host. */
63
+ export function mount(host) {
64
+ const known = mounted.get(host);
65
+ if (known)
66
+ return known;
67
+ const root = host.querySelector("[data-ct-deck]");
68
+ if (!root)
69
+ return null;
70
+ const docClasses = (host.dataset.ctDocClass ?? "").split(/\s+/).filter(Boolean);
71
+ const abort = new AbortController();
72
+ let deck = null;
73
+ let view = null;
74
+ let opener = null;
75
+ const relayUrl = host.dataset.ctRelay;
76
+ const hasRemote = "ctRemote" in host.dataset || !!relayUrl;
77
+ const remoteOptions = () => ({ ...remoteDefaults, ...(relayUrl ? { relay: relayUrl } : {}) });
78
+ /** Starts the remote session of the open deck, or returns the one it has; `null` when the deck closed while the module loaded. */
79
+ const remoteOf = async (open) => {
80
+ const { remoteHost } = await import("../remote/session.js");
81
+ return deck === open ? remoteHost(open, root, remoteOptions()) : null;
82
+ };
83
+ function close() {
84
+ view?.destroy();
85
+ view = null;
86
+ if (!deck)
87
+ return void host.classList.add(...docClasses);
88
+ deck.destroy();
89
+ deck = null;
90
+ host.classList.add(...docClasses);
91
+ opener?.focus({ preventScroll: true });
92
+ opener = null;
93
+ }
94
+ function open(options = {}) {
95
+ if (deck)
96
+ return;
97
+ // Document-view classes (a prose class, say) would restyle the shell and the stage, so they wait outside.
98
+ host.classList.remove(...docClasses);
99
+ try {
100
+ deck = createDeck(root, {
101
+ ...optionsOf(host),
102
+ ...(hasRemote ? { remote: (open) => void remoteOf(open).then(remote => remote?.toggle()) } : {}),
103
+ ...options,
104
+ });
105
+ }
106
+ catch (error) {
107
+ host.classList.add(...docClasses);
108
+ throw error;
109
+ }
110
+ deck.on("exit", close);
111
+ // A remote session of this tab (a paired phone, a running clock) continues after a reload.
112
+ if (hasRemote && stored(hostKey()))
113
+ void remoteOf(deck);
114
+ }
115
+ /** The phone remote or the presenter window: this page as a view of a deck open elsewhere. */
116
+ async function openView(kind) {
117
+ host.classList.remove(...docClasses);
118
+ const made = kind === "remote"
119
+ ? (await import("../remote/phone.js")).remoteView(root, remoteOptions())
120
+ : (await import("../remote/presenter.js")).presenterView(root, remoteOptions());
121
+ if (abort.signal.aborted)
122
+ made.destroy();
123
+ else
124
+ view = made;
125
+ }
126
+ // In the capture phase: a client-side router that listens on the document would otherwise
127
+ // take a start link as a navigation before this handler sees it.
128
+ document.addEventListener("click", event => {
129
+ const control = event.target.closest?.("[data-ct-start]");
130
+ if (!control || event.defaultPrevented)
131
+ return;
132
+ // A modified click on a start link opens it in a new tab or window, as any link.
133
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
134
+ return;
135
+ // A control inside a host opens that deck; one elsewhere opens the first deck of the page.
136
+ if ((control.closest("[data-ct-host]") ?? document.querySelector("[data-ct-host]")) !== host)
137
+ return;
138
+ event.preventDefault();
139
+ opener = control;
140
+ open({ view: control.dataset.ctStart === "notes" ? "notes" : "deck" });
141
+ }, { capture: true, signal: abort.signal });
142
+ if ("ctLegacy" in host.dataset)
143
+ adoptLegacyUrl(root);
144
+ const asked = new URLSearchParams(location.search).get("view");
145
+ if (asked === "deck" || asked === "notes")
146
+ open();
147
+ // The remote views follow the first deck of the page.
148
+ else if ((asked === "remote" || asked === "presenter") && document.querySelector("[data-ct-host]") === host)
149
+ void openView(asked);
150
+ const api = {
151
+ get deck() {
152
+ return deck;
153
+ },
154
+ open,
155
+ close,
156
+ unmount() {
157
+ close();
158
+ abort.abort();
159
+ mounted.delete(host);
160
+ },
161
+ };
162
+ mounted.set(host, api);
163
+ return api;
164
+ }
165
+ /** Mounts every `[data-ct-host]` in the document that is not mounted yet. */
166
+ export function mountAll() {
167
+ return Array.from(document.querySelectorAll("[data-ct-host]"))
168
+ .map(mount)
169
+ .filter((host) => host !== null);
170
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Old reveal.js links (`?view=slides&slide=h.v`) to a Contour position, read from the
3
+ * `data-legacy` and `data-legacy-v` attributes of docs/CONTRACT.md.
4
+ */
5
+ /** One slide as the old deck knew it: its horizontal index, the vertical index of its first page, its page count. */
6
+ export interface LegacySlide {
7
+ legacy: number;
8
+ legacyV: number;
9
+ pages: number;
10
+ }
11
+ /** `"11"` or `"11.2"` to the old indexes; `null` when the text is not an old slide parameter. */
12
+ export declare function parseLegacy(slide: string): {
13
+ h: number;
14
+ v: number;
15
+ } | null;
16
+ /**
17
+ * The `?at=` value (1-based `b.s.p`) of the old slide `h.v`. `blocks[b][s]` describes slide `s` of block `b`.
18
+ * A `v` past the end of the old stack lands on its last page; an `h` the deck never had gives `null`.
19
+ */
20
+ export declare function legacyAt(blocks: LegacySlide[][], h: number, v: number): string | null;
21
+ /** Reads the legacy table of a deck element. Slides without `data-legacy` never match. */
22
+ export declare function readLegacy(root: Element): LegacySlide[][];
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Old reveal.js links (`?view=slides&slide=h.v`) to a Contour position, read from the
3
+ * `data-legacy` and `data-legacy-v` attributes of docs/CONTRACT.md.
4
+ */
5
+ /** `"11"` or `"11.2"` to the old indexes; `null` when the text is not an old slide parameter. */
6
+ export function parseLegacy(slide) {
7
+ const match = /^(\d+)(?:\.(\d+))?$/.exec(slide.trim());
8
+ return match ? { h: Number(match[1]), v: Number(match[2] ?? 0) } : null;
9
+ }
10
+ /**
11
+ * The `?at=` value (1-based `b.s.p`) of the old slide `h.v`. `blocks[b][s]` describes slide `s` of block `b`.
12
+ * A `v` past the end of the old stack lands on its last page; an `h` the deck never had gives `null`.
13
+ */
14
+ export function legacyAt(blocks, h, v) {
15
+ let found;
16
+ for (const [b, slides] of blocks.entries()) {
17
+ for (const [s, slide] of slides.entries()) {
18
+ if (slide.legacy !== h)
19
+ continue;
20
+ // Slides split from one old stack share `h`; the one that holds `v` starts at or before it.
21
+ if (!found || (slide.legacyV <= v && slide.legacyV >= found.slide.legacyV))
22
+ found = { b, s, slide };
23
+ }
24
+ }
25
+ if (!found)
26
+ return null;
27
+ const page = Math.min(Math.max(v - found.slide.legacyV, 0), found.slide.pages - 1);
28
+ return `${found.b + 1}.${found.s + 1}.${page + 1}`;
29
+ }
30
+ /** Reads the legacy table of a deck element. Slides without `data-legacy` never match. */
31
+ export function readLegacy(root) {
32
+ const childrenWith = (el, attribute) => Array.from(el.children).filter(child => child.hasAttribute(attribute));
33
+ const index = (el, attribute, missing) => {
34
+ const value = el.getAttribute(attribute);
35
+ return value !== null && /^\d+$/.test(value) ? Number(value) : missing;
36
+ };
37
+ return childrenWith(root, "data-ct-block").map(block => childrenWith(block, "data-ct-slide").map(slide => ({
38
+ legacy: index(slide, "data-legacy", -1),
39
+ legacyV: index(slide, "data-legacy-v", 0),
40
+ pages: childrenWith(slide, "data-ct-page").length,
41
+ })));
42
+ }
@@ -0,0 +1,2 @@
1
+ import type { Deck, DeckOptions } from "./types.js";
2
+ export declare function createDeck(root: HTMLElement, options?: DeckOptions): Deck;