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.
- package/LICENSE +373 -0
- package/README.md +261 -2
- package/dist/astro/Deck.astro +68 -0
- package/dist/astro/host.d.ts +20 -0
- package/dist/astro/host.js +170 -0
- package/dist/astro/legacy.d.ts +22 -0
- package/dist/astro/legacy.js +42 -0
- package/dist/core/deck.d.ts +2 -0
- package/dist/core/deck.js +598 -0
- package/dist/core/dom.d.ts +8 -0
- package/dist/core/dom.js +22 -0
- package/dist/core/gestures.d.ts +6 -0
- package/dist/core/gestures.js +24 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +2 -0
- package/dist/core/labels.d.ts +2 -0
- package/dist/core/labels.js +20 -0
- package/dist/core/model.d.ts +37 -0
- package/dist/core/model.js +51 -0
- package/dist/core/navigator.d.ts +6 -0
- package/dist/core/navigator.js +12 -0
- package/dist/core/notes.d.ts +11 -0
- package/dist/core/notes.js +34 -0
- package/dist/core/overview.d.ts +11 -0
- package/dist/core/overview.js +104 -0
- package/dist/core/rail.d.ts +8 -0
- package/dist/core/rail.js +26 -0
- package/dist/core/stops.d.ts +17 -0
- package/dist/core/stops.js +40 -0
- package/dist/core/transitions.d.ts +9 -0
- package/dist/core/transitions.js +37 -0
- package/dist/core/types.d.ts +82 -0
- package/dist/core/types.js +1 -0
- package/dist/rehype/hast.d.ts +26 -0
- package/dist/rehype/hast.js +48 -0
- package/dist/rehype/index.d.ts +12 -0
- package/dist/rehype/index.js +221 -0
- package/dist/rehype/page.d.ts +2 -0
- package/dist/rehype/page.js +84 -0
- package/dist/rehype/reading.d.ts +11 -0
- package/dist/rehype/reading.js +9 -0
- package/dist/rehype/titles.d.ts +19 -0
- package/dist/rehype/titles.js +63 -0
- package/dist/remark/index.d.ts +20 -0
- package/dist/remark/index.js +117 -0
- package/dist/remote/clock.d.ts +35 -0
- package/dist/remote/clock.js +48 -0
- package/dist/remote/guest.d.ts +41 -0
- package/dist/remote/guest.js +223 -0
- package/dist/remote/index.d.ts +18 -0
- package/dist/remote/index.js +11 -0
- package/dist/remote/ink.d.ts +11 -0
- package/dist/remote/ink.js +74 -0
- package/dist/remote/labels.d.ts +72 -0
- package/dist/remote/labels.js +68 -0
- package/dist/remote/links.d.ts +2 -0
- package/dist/remote/links.js +12 -0
- package/dist/remote/overlay.d.ts +11 -0
- package/dist/remote/overlay.js +63 -0
- package/dist/remote/pairing.d.ts +40 -0
- package/dist/remote/pairing.js +97 -0
- package/dist/remote/panel.d.ts +38 -0
- package/dist/remote/panel.js +131 -0
- package/dist/remote/phone.d.ts +10 -0
- package/dist/remote/phone.js +407 -0
- package/dist/remote/presenter.d.ts +2 -0
- package/dist/remote/presenter.js +126 -0
- package/dist/remote/protocol.d.ts +106 -0
- package/dist/remote/protocol.js +123 -0
- package/dist/remote/qr.d.ts +8 -0
- package/dist/remote/qr.js +266 -0
- package/dist/remote/sensor.d.ts +9 -0
- package/dist/remote/sensor.js +50 -0
- package/dist/remote/session.d.ts +39 -0
- package/dist/remote/session.js +287 -0
- package/dist/remote/store.d.ts +9 -0
- package/dist/remote/store.js +27 -0
- package/dist/remote/style.d.ts +2 -0
- package/dist/remote/style.js +131 -0
- package/dist/remote/tilt.d.ts +78 -0
- package/dist/remote/tilt.js +148 -0
- package/dist/remote/transport.d.ts +35 -0
- package/dist/remote/transport.js +162 -0
- package/dist/remote/view.d.ts +48 -0
- package/dist/remote/view.js +119 -0
- package/dist/shared/markers.d.ts +15 -0
- package/dist/shared/markers.js +15 -0
- package/dist/style.css +629 -0
- package/docs/CONTRACT.md +178 -0
- package/docs/PROTOCOL.md +99 -0
- package/package.json +89 -4
- package/styles/base.css +3 -0
- package/styles/deck.css +151 -0
- package/styles/index.css +3 -0
package/docs/CONTRACT.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# HTML contract
|
|
2
|
+
|
|
3
|
+
This is the HTML that `contour-deck/rehype` (or any other producer) writes and that the core reads.
|
|
4
|
+
The core recognises `data-ct-*` attributes only, never tag or class names, so page content may hold any HTML, including its own `<section>` elements.
|
|
5
|
+
A change to this file goes in its own commit whose message starts with `contract:`.
|
|
6
|
+
|
|
7
|
+
## Example
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<div data-ct-deck data-title="大綱優先的簡報">
|
|
11
|
+
<section data-ct-block data-title="三、翻頁怎麼運作" data-short="三" data-minutes="5">
|
|
12
|
+
<section data-ct-slide id="s-4-6" data-title="長頁怎麼停靠?" data-legacy="11" data-legacy-v="0">
|
|
13
|
+
<section data-ct-page data-ms="9800">
|
|
14
|
+
<h2>長頁怎麼停靠?</h2>
|
|
15
|
+
<ul>
|
|
16
|
+
<li data-ct-step data-ms="2300">…</li>
|
|
17
|
+
<li data-ct-step data-ms="1900">…</li>
|
|
18
|
+
</ul>
|
|
19
|
+
<aside data-ct-notes><p>…講稿…</p></aside>
|
|
20
|
+
</section>
|
|
21
|
+
<section data-ct-page>…</section>
|
|
22
|
+
</section>
|
|
23
|
+
</section>
|
|
24
|
+
<section data-ct-block data-kind="appendix" data-title="附錄 A|結構與命名" data-short="A">
|
|
25
|
+
…
|
|
26
|
+
</section>
|
|
27
|
+
</div>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Nesting
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
[data-ct-deck] exactly one per deck
|
|
34
|
+
[data-ct-block] 1 or more, element children of the deck
|
|
35
|
+
[data-ct-slide] 1 or more, element children of a block
|
|
36
|
+
[data-ct-page] 1 or more, element children of a slide
|
|
37
|
+
content any HTML
|
|
38
|
+
[data-ct-step] 0 or more, anywhere inside the page content
|
|
39
|
+
[data-ct-notes] 0 or more, element children of the page
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- Each level holds only the level below it: a deck has no element child other than blocks, a block none other than slides, a slide none other than pages.
|
|
43
|
+
Anything that belongs to the document but not to a slide (footnotes, ESM nodes) stays outside the deck element.
|
|
44
|
+
- A deck, block, slide, or page never appears inside page content, and decks do not nest.
|
|
45
|
+
- A deck without block markers still has one block that wraps every slide.
|
|
46
|
+
- Source order is presentation order at every level.
|
|
47
|
+
- Tag names are free. `section` is recommended for block, slide, and page, and `aside` for notes, so the page reads as a document without JavaScript.
|
|
48
|
+
|
|
49
|
+
## Attributes the producer writes
|
|
50
|
+
|
|
51
|
+
| Element | Attribute | Required | Value |
|
|
52
|
+
| --- | --- | --- | --- |
|
|
53
|
+
| deck | `data-ct-deck` | yes | no value |
|
|
54
|
+
| deck | `data-title` | no | Deck title as plain text, shown in the bar over the outline. Default: `document.title`. |
|
|
55
|
+
| block | `data-ct-block` | yes | no value |
|
|
56
|
+
| block | `data-title` | yes | Block title as plain text, for example `三、翻頁怎麼運作`. A deck with one unnamed block uses the deck title. |
|
|
57
|
+
| block | `data-short` | no | Label of at most 3 characters for the locator rail and thumbnail numbers, for example `三`. Default: the block's 1-based ordinal. |
|
|
58
|
+
| block | `data-minutes` | no | Planned minutes, a positive number (`5`, `1.5`). Omit when unknown. |
|
|
59
|
+
| block | `data-kind` | no | One token. The core knows `appendix`; any other value is carried but has no behaviour. |
|
|
60
|
+
| slide | `data-ct-slide` | yes | no value |
|
|
61
|
+
| slide | `data-title` | yes | Slide title as plain text: its first heading, or a fallback the producer chooses. |
|
|
62
|
+
| slide | `id` | yes | Unique in the document. The author's id when given, otherwise `s-<b>-<s>` with the 1-based block and slide ordinals. |
|
|
63
|
+
| slide | `data-legacy` | no | The slide's old reveal.js horizontal index: the 0-based `h` of `?view=slides&slide=h.v`. Several slides split from one old vertical stack share it. |
|
|
64
|
+
| slide | `data-legacy-v` | no | The old 0-based `v` of the slide's first page; page `k` (0-based) of the slide was old `h.(v+k)`. Absent means `0`. |
|
|
65
|
+
| page | `data-ct-page` | yes | no value |
|
|
66
|
+
| page | `data-ms` | no | Reading time of the page without its steps, in whole milliseconds (autoplay). |
|
|
67
|
+
| step | `data-ct-step` | yes | no value |
|
|
68
|
+
| step | `data-ms` | no | Reading time of the step, in whole milliseconds (autoplay). |
|
|
69
|
+
| notes | `data-ct-notes` | yes | no value |
|
|
70
|
+
| content | `data-ct-sk` | no | Thumbnail hint on an element child of a page: `text`, `list`, `table`, `grid`, `flow`, `nums`, `quote`, or `figure`. See [Thumbnails](#thumbnails). |
|
|
71
|
+
|
|
72
|
+
`data-title`, `data-short`, `data-minutes`, `data-kind`, `data-legacy`, `data-legacy-v`, and `data-ms` have meaning only on the elements listed here.
|
|
73
|
+
|
|
74
|
+
### Blocks
|
|
75
|
+
|
|
76
|
+
- `::block{title="…" minutes=5 kind=appendix}` starts a block and supplies `data-title`, `data-minutes`, and `data-kind`.
|
|
77
|
+
- Without `::block`, a slide that opens with a kicker heading (`####`) starts a block whose title is the heading text.
|
|
78
|
+
- With neither, the whole deck is one block.
|
|
79
|
+
- `data-kind="appendix"` marks a block the linear advance never enters: it is reached from the outline, the locator rail, or a deep link.
|
|
80
|
+
|
|
81
|
+
### Steps
|
|
82
|
+
|
|
83
|
+
- Every child a `:::build` reveals one at a time carries `data-ct-step`: list items, table body rows, or other blocks, by the rule `rehype-slides.ts` uses for fragments.
|
|
84
|
+
- The reveal order is document order within the page.
|
|
85
|
+
- A step is never inside `[data-ct-notes]`.
|
|
86
|
+
- Without JavaScript every step is visible.
|
|
87
|
+
|
|
88
|
+
### Notes
|
|
89
|
+
|
|
90
|
+
- `:::summary` becomes `<aside data-ct-notes>` in the page where it was written.
|
|
91
|
+
- A page with several notes elements reads them in document order.
|
|
92
|
+
- The stylesheet hides notes in the document and on the stage; the notes panel and the notes view show their HTML.
|
|
93
|
+
|
|
94
|
+
### Thumbnails
|
|
95
|
+
|
|
96
|
+
The outline draws a schematic of each slide's first page from the first two content elements (element children of the page that are not notes and not `h1`–`h6`).
|
|
97
|
+
The type of each comes from `data-ct-sk` when present, otherwise from the element itself:
|
|
98
|
+
|
|
99
|
+
| Type | Detected without a hint |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `list` | `ul`, `ol` |
|
|
102
|
+
| `table` | `table`, or an element whose only element child is a `table` |
|
|
103
|
+
| `quote` | `blockquote` |
|
|
104
|
+
| `figure` | `figure`, `img`, `picture`, `svg`, `canvas`, `video`, `iframe` |
|
|
105
|
+
| `grid` | a `div` with two or more element children |
|
|
106
|
+
| `text` | anything else |
|
|
107
|
+
|
|
108
|
+
`flow` and `nums` are drawn only from a hint.
|
|
109
|
+
|
|
110
|
+
## What the core adds at run time
|
|
111
|
+
|
|
112
|
+
The producer never writes these.
|
|
113
|
+
The core removes all of them in `destroy()`.
|
|
114
|
+
|
|
115
|
+
| Name | On | Meaning |
|
|
116
|
+
| --- | --- | --- |
|
|
117
|
+
| `data-ct-on` | step | The step is revealed. |
|
|
118
|
+
| `data-ct-active` | block | The block is the one on the stage. |
|
|
119
|
+
| `data-ct-current` | page | The page is the current one on the stage. It replaces reveal.js `.present` as the hook for entrance animations (`--i`). |
|
|
120
|
+
| `data-ct-open` | `<html>` | A deck is open. The stylesheet uses it to stop the document behind from scrolling. |
|
|
121
|
+
| `class="ct-…"` | elements the core creates | The app shell around the deck. The core adds no class to contract elements. |
|
|
122
|
+
|
|
123
|
+
Every other `data-ct-*` name is reserved for later versions, except the [host attributes](#host-attributes).
|
|
124
|
+
|
|
125
|
+
`contour-deck/remote`, when it loads, adds one `<style data-ct-remote>` at the start of `<head>` and leaves it there; its elements (`.ct-rp`, `.ct-laser`, `.ct-ink`, and the `.ct-app` of the phone and presenter views) go with the session or the view that made them.
|
|
126
|
+
|
|
127
|
+
The core moves the deck element into its shell (`.ct-app .ct-stage`) while the deck is open and puts it back in `destroy()`; it does not clone it.
|
|
128
|
+
The shell is inserted where the deck element was, so the deck keeps its ancestors and the styles it inherits from them.
|
|
129
|
+
|
|
130
|
+
### Events on page elements
|
|
131
|
+
|
|
132
|
+
| Event | When | `detail` |
|
|
133
|
+
| --- | --- | --- |
|
|
134
|
+
| `ct:enter` | The page becomes the current page on the stage. | `{ block, slide, page }`, 1-based |
|
|
135
|
+
| `ct:leave` | The page stops being current: another page, the outline, or `destroy()`. | same |
|
|
136
|
+
|
|
137
|
+
Both bubble, so a listener on the deck or the document sees them.
|
|
138
|
+
|
|
139
|
+
## Host attributes
|
|
140
|
+
|
|
141
|
+
`contour-deck/host` (the script `Deck.astro` runs) reads these from the host page.
|
|
142
|
+
The core does not read them, and a host that calls `createDeck` itself needs none.
|
|
143
|
+
|
|
144
|
+
| Attribute | On | Meaning |
|
|
145
|
+
| --- | --- | --- |
|
|
146
|
+
| `data-ct-host` | the wrapper of one deck element | The element the host script mounts. The deck may sit at any depth inside it. |
|
|
147
|
+
| `data-ct-doc-class` | the wrapper | Space-separated classes the wrapper carries only while the deck is closed; the script removes them on opening and restores them on closing. |
|
|
148
|
+
| `data-ct-paging` | the wrapper | `snap` or `free`: the `paging` option of the core. |
|
|
149
|
+
| `data-ct-overview` | the wrapper | `flat` or `3d`: the `overview` option of the core. |
|
|
150
|
+
| `data-ct-legacy` | the wrapper | Accept old reveal.js links: `?view=slides&slide=h.v` is rewritten to `?view=deck&at=…` through `data-legacy` and `data-legacy-v`. |
|
|
151
|
+
| `data-ct-start` | any control in the page | A click opens the deck; with the value `notes`, its notes view. Inside a wrapper it opens that deck, elsewhere the first deck of the page. |
|
|
152
|
+
| `data-ct-remote` | the wrapper | The deck has the remote button and the `R` key (`contour-deck/remote`, loaded on first use). |
|
|
153
|
+
| `data-ct-relay` | the wrapper | Base URL of the relay (`wss://…`) for the phone remote; implies `data-ct-remote`. See [PROTOCOL.md](PROTOCOL.md). |
|
|
154
|
+
|
|
155
|
+
## Deep links
|
|
156
|
+
|
|
157
|
+
The core owns two query parameters while a deck is open, and removes both in `destroy()`.
|
|
158
|
+
|
|
159
|
+
| Parameter | Value | Meaning |
|
|
160
|
+
| --- | --- | --- |
|
|
161
|
+
| `view` | `deck` | The deck is open. The host decides from this parameter whether to call `createDeck`. |
|
|
162
|
+
| `view` | `notes` | The deck is open in the notes view (speaker notes as a scrolling script). |
|
|
163
|
+
| `at` | `b.s` or `b.s.p` | 1-based ordinals: block in the deck, slide in the block, page in the slide. `4.4.2` is the second page of the fourth slide of the fourth block. |
|
|
164
|
+
| `at` | an element id | The slide, page, or block with that id, or the one that contains the element with that id. |
|
|
165
|
+
|
|
166
|
+
`contour-deck/remote` owns two more values of `view`, on pages that are views of a deck open elsewhere; the host loads the module when it sees them.
|
|
167
|
+
|
|
168
|
+
| Parameter | Value | Meaning |
|
|
169
|
+
| --- | --- | --- |
|
|
170
|
+
| `view` | `remote` | The phone remote of the first deck of the page. |
|
|
171
|
+
| `view` | `presenter` | The presenter window, in the same browser as the deck. |
|
|
172
|
+
| `room` | 26 characters of Crockford base32 | The room both sides meet in: the relay room of the phone, the `BroadcastChannel` of the presenter window. |
|
|
173
|
+
|
|
174
|
+
The links to these views keep every other query parameter of the deck's page and drop `at` and the fragment.
|
|
175
|
+
|
|
176
|
+
Without `at` the deck opens on the outline.
|
|
177
|
+
The position is in the query string, not the fragment, because a restricted page on the blog drops the fragment on redirect.
|
|
178
|
+
The slide `id` also works as a plain document anchor (`#s-4-6`) when the deck is closed.
|
package/docs/PROTOCOL.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Remote protocol
|
|
2
|
+
|
|
3
|
+
How a deck (the **host**) and phones (**guests**) talk through the relay. The relay is a per-room forwarder; it decides who may hold the host seat, and the host decides who may control the deck. Version 1.
|
|
4
|
+
|
|
5
|
+
Version 1 changed before its first release (contour-deck 0.1.0 has no remote): the host seat needs a key, the relay removes a guest the host denied or revoked, it limits requests per client address, and `state` lost `block`. Nothing that spoke the earlier draft was published, so the version stays 1.
|
|
6
|
+
|
|
7
|
+
## Transport
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
wss://<relay>/v1/room/<room>?role=host&key=<key>
|
|
11
|
+
wss://<relay>/v1/room/<room>?role=guest
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- `key`: the host key, 26 characters of Crockford base32 (128 random bits from `crypto.getRandomValues`). The host keeps it in `sessionStorage` and sends it to the relay only.
|
|
15
|
+
- `room`: the first 26 characters of the Crockford base32 encoding of SHA-256 of the key (its UTF-8 bytes, upper case), bits taken most significant first. The QR and every link carry the room, never the key. Anything that is not 26 Crockford characters gets HTTP 400.
|
|
16
|
+
- Test vector: key `0123456789ABCDEFGHJKMNPQRS` opens room `YQV9WCK1VFXD4486E3JKK6CYND`.
|
|
17
|
+
- Frames are UTF-8 JSON text, at most **2048 bytes**. A larger or binary frame closes the socket with `1009`.
|
|
18
|
+
- Every message has a string field `t` (its type). The relay reads only `t`, `to`, and the size; it never stores or logs message bodies.
|
|
19
|
+
|
|
20
|
+
## Relay rules
|
|
21
|
+
|
|
22
|
+
One Durable Object per room (`idFromName(room)`).
|
|
23
|
+
|
|
24
|
+
| Event | Relay does |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| host opens without a key, or with a key whose room is not this room | HTTP 403 before the upgrade |
|
|
27
|
+
| socket opens | sends `{ "t": "hello", "id": "<8 chars>", "role": "host" \| "guest" }` to it. `id` is assigned by the relay. |
|
|
28
|
+
| host opens while a host is connected | the new host takes the seat: the old host is closed with `4009`, its guests with `4000`; the room stays |
|
|
29
|
+
| guest opens with no host connected | closes it with `4004` |
|
|
30
|
+
| guest opens when 4 guests are connected | closes it with `4029` |
|
|
31
|
+
| guest opens / closes | sends `{ "t": "join", "id" }` / `{ "t": "leave", "id" }` to the host |
|
|
32
|
+
| host sends a message with `to` | forwards it to that guest only (unknown `to` is dropped) |
|
|
33
|
+
| host sends `deny` or `revoke` with `to` | forwards it, then closes that guest with `4003` (the guest leaves; the host gets `leave`) |
|
|
34
|
+
| host sends a message without `to` | forwards it to every guest |
|
|
35
|
+
| guest sends a message | forwards it to the host only, with `from` set to the guest's id (overwriting any `from` the guest sent) |
|
|
36
|
+
| host closes | closes every guest with `4000`; the room is gone |
|
|
37
|
+
| no message in the room for 2 hours | closes everyone with `4000` |
|
|
38
|
+
| a socket exceeds 60 messages/s (burst 120) | extra messages are dropped; above 10 s of sustained excess, closes with `1008` |
|
|
39
|
+
| request `Origin` not in `ALLOWED_ORIGINS` | HTTP 403 before the upgrade |
|
|
40
|
+
| more than 60 requests to room paths a minute from one client address | HTTP 429 before anything else is checked |
|
|
41
|
+
|
|
42
|
+
`ALLOWED_ORIGINS` is a comma-separated Worker variable (e.g. `https://www.wayneh.tw,https://wayne930242.github.io,http://localhost:4321`). Guests never reach other guests.
|
|
43
|
+
|
|
44
|
+
Only the key holder can be host, so only the deck's own tab receives what guests send, the resume token included. A host that reconnects while the relay still counts its old connection as open takes the seat back at once. Of two tabs with the same key (a duplicated tab copies `sessionStorage`), the newer one holds the seat; the older one is closed with `4009` and does not reconnect by itself.
|
|
45
|
+
|
|
46
|
+
`GET /v1/health` answers `200 ok` (no upgrade) for monitoring.
|
|
47
|
+
|
|
48
|
+
## Application messages
|
|
49
|
+
|
|
50
|
+
The relay does not interpret these, except that a `deny` or `revoke` with `to` removes that guest. Coordinates are normalised to the stage viewport: `x`, `y` in `[0, 1]`, origin at the top left.
|
|
51
|
+
|
|
52
|
+
### Pairing
|
|
53
|
+
|
|
54
|
+
1. Host opens the room with its key and shows a QR for `<page URL>?view=remote&room=<room>`.
|
|
55
|
+
2. Guest connects; host gets `join` and sends `{ "t": "pair", "to": id, "code": "1234" }` — a fresh 4-digit code per guest.
|
|
56
|
+
3. Guest shows the code large. Host shows "a phone asks for control: code 1234 — allow / deny".
|
|
57
|
+
4. Presenter compares the two screens and answers:
|
|
58
|
+
- allow → `{ "t": "grant", "to": id, "token": "<22 chars>" }` (random, kept by the guest in `sessionStorage` for this room)
|
|
59
|
+
- deny → `{ "t": "deny", "to": id }`; the relay then closes that guest with `4003`, so a denied guest cannot keep a place
|
|
60
|
+
5. A reconnecting guest sends `{ "t": "resume", "token" }`; if it matches a token the host granted in this room, the host grants again without asking.
|
|
61
|
+
6. The host can take control back: `{ "t": "revoke", "to": id }`, which also removes the guest from the room (`4003`). At most one guest holds control; granting a second revokes the first.
|
|
62
|
+
|
|
63
|
+
A guest that is not granted gets `state` messages only if the host chooses (default: none), and its commands are ignored. A projected QR scanned by the audience therefore only produces a request the presenter denies.
|
|
64
|
+
|
|
65
|
+
### Host → granted guest
|
|
66
|
+
|
|
67
|
+
| Message | Fields |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `state` | `mode` (`overview` \| `stage`), `at` (`b.s.p`, 1-based), `step`, `steps`, `elapsed` (s), `total` (s or `null`), `blockElapsed` (s), `blockMinutes` (or `null`), `ratio` (optional: width over height of the stage) |
|
|
70
|
+
|
|
71
|
+
Sent on every position change and every 15 s. No text of the page crosses the relay: the phone has the same page and reads the notes and the block title from its own DOM, at the position `at` names.
|
|
72
|
+
|
|
73
|
+
### Granted guest → host
|
|
74
|
+
|
|
75
|
+
| Message | Fields | Host does |
|
|
76
|
+
| --- | --- | --- |
|
|
77
|
+
| `cmd` | `c`: `next` \| `prev` \| `slideNext` \| `slidePrev` \| `pageNext` \| `pagePrev` \| `overview` \| `open`; `at` with `open` | same as the keyboard |
|
|
78
|
+
| `ptr` | `x`, `y`, `on` (bool) | shows or hides the laser pointer |
|
|
79
|
+
| `ink` | `s` (stroke id, number), `pts` ([x, y, x, y, …], at most 64 points), `end` (bool, optional) | draws the stroke on the overlay |
|
|
80
|
+
| `ink-clear` | — | clears the overlay |
|
|
81
|
+
| `ping` | — | answers `{ "t": "pong", "to": from }` |
|
|
82
|
+
|
|
83
|
+
Ink is cleared when the page changes. The host rate-limits `ptr` rendering to one per animation frame.
|
|
84
|
+
|
|
85
|
+
## Same machine
|
|
86
|
+
|
|
87
|
+
The presenter window on the same computer uses `BroadcastChannel("contour-deck:<room>")` with the same messages, without `pair` (same browser, trusted).
|
|
88
|
+
|
|
89
|
+
## Close codes
|
|
90
|
+
|
|
91
|
+
| Code | Meaning |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| `4000` | room closed (host left or idle, or a new host took the seat) |
|
|
94
|
+
| `4003` | removed by the host (after `deny` or `revoke`); the client does not reconnect by itself |
|
|
95
|
+
| `4004` | no host in this room |
|
|
96
|
+
| `4009` | host replaced: a newer connection with the same key holds the host seat; the client does not reconnect by itself |
|
|
97
|
+
| `4029` | room full |
|
|
98
|
+
| `1008` | rate limit |
|
|
99
|
+
| `1009` | message too large |
|
package/package.json
CHANGED
|
@@ -1,6 +1,91 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "contour-deck",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Outline-first slide decks on real DOM: blocks, slides, pages.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"slides",
|
|
7
|
+
"presentation",
|
|
8
|
+
"astro-component"
|
|
9
|
+
],
|
|
10
|
+
"license": "MPL-2.0",
|
|
11
|
+
"author": "洪偉 <me@wayneh.tw>",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/wayne930242/contour-deck.git"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/wayne930242/contour-deck#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/wayne930242/contour-deck/issues"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"sideEffects": [
|
|
22
|
+
"*.css"
|
|
23
|
+
],
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/core/index.d.ts",
|
|
27
|
+
"default": "./dist/core/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./remark": {
|
|
30
|
+
"types": "./dist/remark/index.d.ts",
|
|
31
|
+
"default": "./dist/remark/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./rehype": {
|
|
34
|
+
"types": "./dist/rehype/index.d.ts",
|
|
35
|
+
"default": "./dist/rehype/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./remote": {
|
|
38
|
+
"types": "./dist/remote/index.d.ts",
|
|
39
|
+
"default": "./dist/remote/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./astro": "./dist/astro/Deck.astro",
|
|
42
|
+
"./host": {
|
|
43
|
+
"types": "./dist/astro/host.d.ts",
|
|
44
|
+
"default": "./dist/astro/host.js"
|
|
45
|
+
},
|
|
46
|
+
"./style.css": "./dist/style.css"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"LICENSE",
|
|
50
|
+
"dist",
|
|
51
|
+
"docs/CONTRACT.md",
|
|
52
|
+
"docs/PROTOCOL.md",
|
|
53
|
+
"styles"
|
|
54
|
+
],
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=22.12"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "node scripts/build.mjs",
|
|
60
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
61
|
+
"test": "vitest run --passWithNoTests",
|
|
62
|
+
"test:browser": "node scripts/build.mjs && playwright test --pass-with-no-tests",
|
|
63
|
+
"size": "node scripts/size.mjs",
|
|
64
|
+
"demo": "node scripts/build.mjs && node scripts/serve.mjs",
|
|
65
|
+
"fixtures": "node scripts/build.mjs && node scripts/fixtures.mjs",
|
|
66
|
+
"demo:decks": "node scripts/build.mjs && node scripts/demo-decks.mjs",
|
|
67
|
+
"media": "node scripts/build.mjs && node scripts/media.mjs"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@playwright/test": "1.65.0",
|
|
71
|
+
"@types/node": "^26.6.6",
|
|
72
|
+
"esbuild": "0.28.2",
|
|
73
|
+
"jsqr": "1.4.0",
|
|
74
|
+
"rehype-stringify": "^10.0.1",
|
|
75
|
+
"remark-cjk-friendly": "^2.3.4",
|
|
76
|
+
"remark-directive": "^4.0.0",
|
|
77
|
+
"remark-gfm": "^4.0.1",
|
|
78
|
+
"remark-parse": "^11.0.0",
|
|
79
|
+
"remark-rehype": "^11.1.2",
|
|
80
|
+
"typescript": "7.0.2",
|
|
81
|
+
"unified": "^11.0.5",
|
|
82
|
+
"vfile": "^6.0.3",
|
|
83
|
+
"vite": "^8.3.8",
|
|
84
|
+
"vitest": "5.1.0"
|
|
85
|
+
},
|
|
86
|
+
"publishConfig": {
|
|
87
|
+
"access": "public",
|
|
88
|
+
"provenance": true
|
|
89
|
+
},
|
|
90
|
+
"packageManager": "pnpm@12.9.1"
|
|
91
|
+
}
|
package/styles/base.css
ADDED
package/styles/deck.css
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/* The open deck. Theme: set the --ct-* of this first rule on .ct-app; the defaults follow the site's own variables. */
|
|
2
|
+
:where(.ct-app) {
|
|
3
|
+
--ct-bg: var(--background, Canvas);
|
|
4
|
+
--ct-fg: var(--foreground, CanvasText);
|
|
5
|
+
--ct-accent: var(--accent, #e3311d);
|
|
6
|
+
--ct-accent-fg: var(--accent-foreground, #fff);
|
|
7
|
+
--ct-border: var(--border, color-mix(in srgb, var(--ct-fg) 24%, var(--ct-bg)));
|
|
8
|
+
--ct-muted: var(--muted-foreground, color-mix(in srgb, var(--ct-fg) 62%, var(--ct-bg)));
|
|
9
|
+
--ct-measure: 62rem;
|
|
10
|
+
--ct-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
11
|
+
}
|
|
12
|
+
.ct-app {
|
|
13
|
+
--ct-panel: color-mix(in srgb, var(--ct-fg) 4%, var(--ct-bg));
|
|
14
|
+
--ct-tint: color-mix(in srgb, var(--ct-accent) 10%, var(--ct-bg));
|
|
15
|
+
--ct-ease: cubic-bezier(.22, 1, .36, 1);
|
|
16
|
+
--ct-notes-w: min(24rem, 38vw);
|
|
17
|
+
--ct-lift: 12px;
|
|
18
|
+
position: fixed; inset: 0; z-index: 1000;
|
|
19
|
+
display: grid; grid-template-rows: 3rem minmax(0, 1fr);
|
|
20
|
+
overflow: hidden; background: var(--ct-bg); color: var(--ct-fg); line-height: 1.5;
|
|
21
|
+
}
|
|
22
|
+
:where(.ct-app, .ct-app *) { box-sizing: border-box; }
|
|
23
|
+
.ct-app :focus-visible { outline: 2px dashed var(--ct-accent); outline-offset: 2px; }
|
|
24
|
+
.ct-app :is(.ct-open, .ct-thumb, .ct-rl, .ct-rc, .ct-sh) { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; text-align: left; }
|
|
25
|
+
.ct-live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
26
|
+
.ct-meta { font: .66rem/1 var(--ct-mono); color: var(--ct-muted); white-space: nowrap; }
|
|
27
|
+
.ct-badge { font: 650 .62rem/1 var(--ct-mono); letter-spacing: .1em; padding: .28rem .4rem; background: color-mix(in srgb, var(--ct-fg) 10%, var(--ct-bg)); color: var(--ct-muted); }
|
|
28
|
+
|
|
29
|
+
/* ---- bar ---- */
|
|
30
|
+
.ct-bar { display: flex; align-items: center; gap: .6rem; padding: 0 .7rem; min-width: 0; background: var(--ct-panel); }
|
|
31
|
+
.ct-btn { font: 650 .68rem/1 var(--ct-mono); letter-spacing: .08em; padding: .5rem .6rem; border: 1px solid var(--ct-border); background: var(--ct-bg); color: inherit; cursor: pointer; white-space: nowrap; min-height: 2rem; }
|
|
32
|
+
.ct-btn:hover { background: var(--ct-tint); }
|
|
33
|
+
.ct-btn[aria-pressed="true"] { background: var(--ct-fg); color: var(--ct-bg); border-color: var(--ct-fg); }
|
|
34
|
+
.ct-app[data-ct-mode="overview"] :is([data-ct-act="overview"], [data-ct-act="notes"]),
|
|
35
|
+
.ct-app:not([data-ct-mode="overview"]) :is([data-ct-act="3d"], [data-ct-act="script"]),
|
|
36
|
+
.ct-app[data-ct-mode="notes"] [data-ct-act="notes"] { display: none; }
|
|
37
|
+
.ct-title { font-size: .8rem; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 16rem; }
|
|
38
|
+
|
|
39
|
+
/* locator rail: one column per slide, one pip per page */
|
|
40
|
+
.ct-rail { position: relative; flex: 1; min-width: 0; height: 100%; display: flex; align-items: flex-start; justify-content: center; gap: .7rem; padding-top: .55rem; overflow: hidden; }
|
|
41
|
+
.ct-rg { display: flex; align-items: flex-start; gap: 3px; }
|
|
42
|
+
.ct-app .ct-rl { font: 650 .62rem/11px var(--ct-mono); color: var(--ct-muted); padding-right: .2rem; }
|
|
43
|
+
.ct-app .ct-rg[data-cur] .ct-rl { color: var(--ct-fg); }
|
|
44
|
+
.ct-app .ct-rc { display: flex; flex-direction: column; gap: 2px; padding-bottom: 8px; }
|
|
45
|
+
.ct-rc i { display: block; width: 12px; height: 5px; background: var(--ct-border); }
|
|
46
|
+
.ct-rg[data-cur] i { background: var(--ct-muted); }
|
|
47
|
+
.ct-rg[data-kind="appendix"] i { opacity: .55; }
|
|
48
|
+
.ct-rm { position: absolute; left: 0; top: 0; width: 12px; height: 5px; background: var(--ct-accent); outline: 2px solid color-mix(in srgb, var(--ct-accent) 35%, transparent); pointer-events: none; transition: transform .34s var(--ct-ease); }
|
|
49
|
+
|
|
50
|
+
.ct-vp { position: relative; overflow: hidden; perspective: 1700px; }
|
|
51
|
+
|
|
52
|
+
/* ---- outline ---- */
|
|
53
|
+
.ct-ov { position: absolute; inset: 0; overflow: auto; padding: 1.1rem; }
|
|
54
|
+
.ct-plane { position: relative; display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 19rem), 1fr)); gap: 1rem; max-width: 86rem; margin: 0 auto; transform-style: preserve-3d; transition: transform .7s var(--ct-ease); }
|
|
55
|
+
.ct-tile { border: 1px solid var(--ct-border); background: var(--ct-bg); padding: .7rem .8rem .9rem; transform-style: preserve-3d; min-width: 0; }
|
|
56
|
+
.ct-tile[data-kind="appendix"] { background: var(--ct-panel); }
|
|
57
|
+
@media (min-width: 900px) { .ct-tile[data-wide] { grid-column: span 2; } }
|
|
58
|
+
.ct-app .ct-open { display: flex; align-items: baseline; gap: .5rem; width: 100%; padding: .1rem 0; }
|
|
59
|
+
.ct-open:hover .ct-tt { text-decoration: underline; text-underline-offset: .2em; }
|
|
60
|
+
.ct-tt { font-size: .95rem; font-weight: 800; }
|
|
61
|
+
.ct-open .ct-meta { margin-left: auto; }
|
|
62
|
+
.ct-strip { list-style: none; display: flex; flex-wrap: wrap; gap: 1.5rem .8rem; margin: .7rem 0 0; padding: 0; transform-style: preserve-3d; }
|
|
63
|
+
.ct-strip li { transform-style: preserve-3d; margin: 0; }
|
|
64
|
+
.ct-app .ct-thumb { position: relative; display: block; width: 7.6rem; aspect-ratio: 16 / 11; transform-style: preserve-3d; margin: 0 calc((var(--n) - 1) * 3px) calc((var(--n) - 1) * 3px) 0; transition: transform .25s var(--ct-ease); }
|
|
65
|
+
.ct-layer, .ct-face { position: absolute; inset: 0; border: 1px solid var(--ct-border); background: var(--ct-bg); transition: transform .7s var(--ct-ease), background-color .2s; }
|
|
66
|
+
.ct-layer { transform: translate(calc(var(--k) * 3px), calc(var(--k) * 3px)); }
|
|
67
|
+
.ct-face { padding: .35rem .4rem; display: flex; flex-direction: column; gap: .2rem; overflow: hidden; }
|
|
68
|
+
.ct-thumb:is(:hover, :focus-visible) .ct-face { background: var(--ct-tint); }
|
|
69
|
+
.ct-app .ct-thumb:focus-visible { outline: none; }
|
|
70
|
+
.ct-num { font: .56rem/1 var(--ct-mono); color: var(--ct-muted); display: flex; justify-content: space-between; }
|
|
71
|
+
.ct-t { font-size: .64rem; line-height: 1.25; font-weight: 750; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
72
|
+
.ct-sk { margin-top: auto; display: flex; flex-direction: column; gap: 3px; }
|
|
73
|
+
.ct-g { display: block; height: 7px; }
|
|
74
|
+
.ct-g-text, .ct-g-list { width: 92%; background: repeating-linear-gradient(var(--ct-border) 0 2px, transparent 2px 5px); }
|
|
75
|
+
.ct-g-list { width: 66%; margin-left: 6px; height: calc(var(--c) * 5px - 3px); }
|
|
76
|
+
.ct-g-table { height: 13px; border: 1px solid var(--ct-border); background: linear-gradient(90deg, transparent 33%, var(--ct-border) 33% calc(33% + 1px), transparent 0 66%, var(--ct-border) 66% calc(66% + 1px), transparent 0), linear-gradient(transparent 6px, var(--ct-border) 6px 7px, transparent 0); }
|
|
77
|
+
.ct-g-figure { height: 13px; width: 60%; border: 1px solid var(--ct-border); background: linear-gradient(to top right, transparent calc(50% - .5px), var(--ct-border) 0 calc(50% + .5px), transparent 0); }
|
|
78
|
+
.ct-g-grid, .ct-g-flow, .ct-g-nums { display: flex; gap: 3px; height: 12px; }
|
|
79
|
+
.ct-g-grid b, .ct-g-flow b { flex: 1; border: 1px solid var(--ct-border); }
|
|
80
|
+
.ct-g-flow { gap: 6px; background: linear-gradient(var(--ct-border), var(--ct-border)) center / 100% 1px no-repeat; }
|
|
81
|
+
.ct-g-flow b { background: var(--ct-bg); }
|
|
82
|
+
.ct-g-nums b { width: 16px; background: color-mix(in srgb, var(--ct-accent) 55%, var(--ct-bg)); }
|
|
83
|
+
.ct-g-quote { background: var(--ct-tint); height: 9px; }
|
|
84
|
+
.ct-ring { position: absolute; left: 0; top: 0; pointer-events: none; outline: 2px solid var(--ct-accent); outline-offset: 3px; z-index: 3; transition: left .38s var(--ct-ease), top .38s var(--ct-ease), width .38s var(--ct-ease), height .38s var(--ct-ease), transform .7s var(--ct-ease); }
|
|
85
|
+
.ct-ring::after { content: attr(data-label); position: absolute; left: -5px; top: calc(100% + 5px); font: 650 .56rem/1 var(--ct-mono); letter-spacing: .08em; padding: .2rem .3rem; background: var(--ct-accent); color: var(--ct-accent-fg); white-space: nowrap; }
|
|
86
|
+
|
|
87
|
+
/* 3D structure: the plane tilts and every slide becomes a stack as high as its page count */
|
|
88
|
+
.ct-app[data-ct-3d] .ct-ov { overflow: hidden; }
|
|
89
|
+
.ct-app[data-ct-3d] .ct-plane { transform: translateY(-4%) rotateX(52deg) rotateZ(-24deg) scale(.86); }
|
|
90
|
+
.ct-app[data-ct-3d] .ct-layer { transform: translateZ(calc((var(--n) - 1 - var(--k)) * var(--ct-lift))); }
|
|
91
|
+
.ct-app[data-ct-3d] .ct-face { transform: translateZ(calc((var(--n) - 1) * var(--ct-lift))); }
|
|
92
|
+
.ct-app[data-ct-3d] .ct-thumb:is(:hover, :focus-visible) { transform: translateZ(22px); }
|
|
93
|
+
.ct-app[data-ct-3d] .ct-ring { transform: translateZ(calc((var(--n) - 1) * var(--ct-lift) + 1px)); }
|
|
94
|
+
|
|
95
|
+
/* ---- stage: one block is a left-right track; each slide scrolls by pages ---- */
|
|
96
|
+
.ct-stage { position: absolute; inset: 0; background: var(--ct-bg); border: 1px solid var(--ct-border); transform-origin: 0 0; z-index: 2; overflow: hidden; outline: none; container-type: size; }
|
|
97
|
+
.ct-app[data-ct-panel] .ct-stage { right: var(--ct-notes-w); }
|
|
98
|
+
.ct-app:not([data-ct-mode="stage"]) :is(.ct-stage, .ct-notes), .ct-app:not([data-ct-mode="overview"]) .ct-ov { visibility: hidden; }
|
|
99
|
+
.ct-app[data-ct-busy] :is(.ct-stage, .ct-ov) { visibility: visible; }
|
|
100
|
+
/* Three classes deep, so a host's document-view rules for the same elements lose here. */
|
|
101
|
+
.ct-app .ct-stage :is([data-ct-deck], [data-ct-block], [data-ct-slide], [data-ct-page]) { max-width: none; margin: 0; border: 0; }
|
|
102
|
+
.ct-app .ct-stage :is([data-ct-deck], [data-ct-block], [data-ct-slide]) { height: 100%; padding: 0; }
|
|
103
|
+
.ct-stage [data-ct-block] { display: none; }
|
|
104
|
+
.ct-stage [data-ct-block][data-ct-active] { display: flex; overflow: auto hidden; scroll-snap-type: x mandatory; overscroll-behavior: contain; scrollbar-width: none; }
|
|
105
|
+
.ct-stage [data-ct-slide] { position: relative; flex: 0 0 100%; overflow-y: auto; scroll-snap-align: start; scroll-snap-stop: always; scroll-snap-type: y mandatory; overscroll-behavior-y: contain; scrollbar-width: none; }
|
|
106
|
+
.ct-stage :is([data-ct-block], [data-ct-slide])::-webkit-scrollbar { display: none; }
|
|
107
|
+
/* The side padding keeps the content in a centred column of --ct-measure, so the host's own margins and widths on the content stay as written. */
|
|
108
|
+
.ct-app .ct-stage [data-ct-page] { min-height: 100%; scroll-snap-align: start; scroll-snap-stop: always; display: flex; flex-direction: column; justify-content: center; padding: clamp(1.4rem, 5cqh, 3.2rem) max(clamp(1.1rem, 6cqw, 5.5rem), (100cqw - var(--ct-measure)) / 2); font-size: clamp(1rem, min(2.05cqw, 3.7cqh), 3rem); }
|
|
109
|
+
.ct-app[data-ct-paging="free"] [data-ct-slide] { scroll-snap-type: y proximity; }
|
|
110
|
+
.ct-app[data-ct-paging="free"] [data-ct-page] { scroll-snap-stop: normal; }
|
|
111
|
+
.ct-stage [data-ct-step] { transition: opacity .45s var(--ct-ease), transform .45s var(--ct-ease); }
|
|
112
|
+
.ct-stage [data-ct-step]:not([data-ct-on]) { opacity: 0; transform: translateY(.45em); }
|
|
113
|
+
.ct-stage tr[data-ct-step]:not([data-ct-on]) { visibility: collapse; }
|
|
114
|
+
|
|
115
|
+
.ct-pager { display: flex; flex-direction: column; gap: 5px; position: absolute; right: .55rem; top: 50%; transform: translateY(-50%); z-index: 4; }
|
|
116
|
+
.ct-pager i { display: block; width: 5px; height: 14px; background: var(--ct-border); }
|
|
117
|
+
.ct-pager i[data-on] { background: linear-gradient(var(--ct-accent) calc(var(--f, 1) * 100%), color-mix(in srgb, var(--ct-accent) 30%, var(--ct-bg)) 0); }
|
|
118
|
+
.ct-ctl { display: flex; gap: .35rem; position: absolute; right: 1.4rem; bottom: .6rem; z-index: 4; }
|
|
119
|
+
.ct-ctl .ct-btn { min-width: 2.4rem; min-height: 2.4rem; font-size: .9rem; }
|
|
120
|
+
|
|
121
|
+
/* ---- notes panel ---- */
|
|
122
|
+
.ct-notes { position: absolute; top: 0; right: 0; bottom: 0; width: var(--ct-notes-w); background: var(--ct-bg); border: 1px solid var(--ct-border); padding: .9rem 1rem; overflow: auto; z-index: 6; }
|
|
123
|
+
.ct-app:not([data-ct-panel]) .ct-notes { visibility: hidden; }
|
|
124
|
+
.ct-notes h2 { font: 650 .66rem/1 var(--ct-mono); letter-spacing: .12em; margin: 0 0 .8rem; color: var(--ct-muted); }
|
|
125
|
+
.ct-nb { font-size: 1rem; line-height: 1.75; }
|
|
126
|
+
|
|
127
|
+
/* ---- notes view: the whole script, scrolled by hand ---- */
|
|
128
|
+
.ct-script { display: none; position: absolute; inset: 0; overflow: auto; padding: 0 1rem 40vh; background: var(--ct-bg); z-index: 7; outline: none; }
|
|
129
|
+
.ct-app[data-ct-mode="notes"] .ct-script { display: block; }
|
|
130
|
+
.ct-sb { max-width: 40rem; margin: 0 auto; }
|
|
131
|
+
.ct-sbh { position: sticky; top: 0; z-index: 1; display: flex; align-items: baseline; gap: .5rem; margin: 0; padding: .7rem 0 .5rem; font-size: .95rem; font-weight: 800; background: var(--ct-bg); }
|
|
132
|
+
.ct-se { scroll-margin-top: 2.6rem; padding-bottom: 1rem; }
|
|
133
|
+
.ct-app .ct-sh { display: flex; flex-direction: column; gap: .3rem; width: 100%; padding: .55rem .7rem; background: var(--ct-panel); font-size: 1rem; line-height: 1.3; }
|
|
134
|
+
.ct-sn { padding: .5rem .7rem 0; font-size: 1.12rem; line-height: 1.8; }
|
|
135
|
+
.ct-sn p, .ct-nb p { margin: 0 0 .8em; }
|
|
136
|
+
|
|
137
|
+
@media (max-width: 760px) {
|
|
138
|
+
.ct-app { --ct-notes-w: 100%; }
|
|
139
|
+
.ct-rail { gap: .35rem; }
|
|
140
|
+
.ct-rg { gap: 2px; }
|
|
141
|
+
.ct-rl, .ct-title { display: none; }
|
|
142
|
+
.ct-rc i, .ct-rm { width: 6px; height: 4px; }
|
|
143
|
+
.ct-app[data-ct-panel] .ct-stage { right: 0; bottom: 42%; }
|
|
144
|
+
.ct-notes { top: 58%; }
|
|
145
|
+
/* room for the step buttons under the last line */
|
|
146
|
+
.ct-app .ct-stage [data-ct-page] { padding-bottom: 4.2rem; }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@media (prefers-reduced-motion: reduce) {
|
|
150
|
+
.ct-app :is(.ct-plane, .ct-layer, .ct-face, .ct-thumb, .ct-ring, .ct-rm, [data-ct-step]) { transition-duration: .01ms !important; }
|
|
151
|
+
}
|
package/styles/index.css
ADDED