pipeup 0.4.1 → 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.
@@ -1,4 +1,4 @@
1
- export declare function levenshtein(a: string, b: string): number;
1
+ export declare const levenshtein: (a: string, b: string) => number;
2
2
  /**
3
3
  * Best approximate occurrence of `pattern` in `text` (Sellers' algorithm), or null if every
4
4
  * occurrence needs more than `maxDistance` edits.
@@ -1,4 +1,4 @@
1
- export declare const VERSION = "0.4.1";
1
+ export declare const VERSION = "0.5.1";
2
2
  export { PipeupDocument, UnsavedChangeError, type Listener, type OpenOptions } from "./document";
3
3
  export { DEFAULT_PROFILE_NAME, MemoryStore, loadOrCreateProfile, type OpStore, type Profile, } from "./storage/store";
4
4
  export { IndexedDbStore } from "./storage/indexeddb";
@@ -9,4 +9,4 @@ export { resolveAnchor, type AnchorState, type Resolved, type ResolveOptions } f
9
9
  export { labelOf, locate, type Location } from "./anchor/locate";
10
10
  export { copyAll, copyThread, formatAgo, type CopyAs, type ExportItem, type ExportMeta, } from "./export/format";
11
11
  export { ANIMALS, COLOURS, animalName, nameOf, type Animal } from "./model/animals";
12
- export type { Anchor, Comment, Thread } from "./model/types";
12
+ export type { Anchor, Comment, OpBody, SignedOp, Thread } from "./model/types";
@@ -1,5 +1,5 @@
1
- import type { Identity } from "./crypto/identity";
2
- import type { Anchor, Thread } from "./model/types";
1
+ import { type Identity } from "./crypto/identity";
2
+ import type { Anchor, SignedOp, Thread } from "./model/types";
3
3
  import type { OpStore } from "./storage/store";
4
4
  export interface OpenOptions {
5
5
  doc: string;
@@ -9,7 +9,8 @@ export interface OpenOptions {
9
9
  /** The reviewer's name; "" until they add one (they show as their animal). */
10
10
  name: string;
11
11
  }
12
- export type Listener = (threads: Thread[]) => void;
12
+ /** `added`: the ops behind the change ([] for a rename). `source`: "local", "file", an add-on's id, or "" (a rename). */
13
+ export type Listener = (threads: Thread[], added: readonly SignedOp[], source: string) => void;
13
14
  /** The change is recorded and shown, but the store refused it; it is saved with the next change. */
14
15
  export declare class UnsavedChangeError extends Error {
15
16
  readonly id: string;
@@ -38,6 +39,8 @@ export declare class PipeupDocument {
38
39
  /** Names new comments, and this reviewer's earlier unnamed ones, which listeners hear about. */
39
40
  set name(value: string);
40
41
  threads(): readonly Thread[];
42
+ /** Every verified op this browser holds, as {body, sig}. A new array each call; don't change the ops. */
43
+ ops(): readonly SignedOp[];
41
44
  onChange(fn: Listener): () => void;
42
45
  comment(anchor: Anchor, text: string): Promise<string>;
43
46
  reply(parentId: string, text: string): Promise<string>;
@@ -45,6 +48,11 @@ export declare class PipeupDocument {
45
48
  remove(commentId: string): Promise<void>;
46
49
  resolve(threadId: string): Promise<void>;
47
50
  reopen(threadId: string): Promise<void>;
51
+ /**
52
+ * Signs `data` for `purpose` ("<add-on id>/<name>") with this reviewer's identity, as base64url. The signed
53
+ * text starts "pipeup:", never "{", so it can never verify as an op.
54
+ */
55
+ sign(purpose: string, data: string): Promise<string>;
48
56
  /** Saves any changes the store has not yet accepted. Rejects while the store still fails. */
49
57
  flush(): Promise<void>;
50
58
  /** Everything this browser knows about the document, sealed when the page has a key. */
@@ -54,6 +62,11 @@ export declare class PipeupDocument {
54
62
  * Throws UnsavedChangeError (id of the first added change) if they were applied but the store refused them.
55
63
  */
56
64
  importFile(json: string): Promise<number>;
65
+ /**
66
+ * Verifies and merges ops from elsewhere, exactly as a feedback file's are; resolves to how many were new.
67
+ * Listeners hear once per call, only if something was added. More than 5,000 ops rejects with a RangeError.
68
+ */
69
+ merge(ops: readonly unknown[], source: string): Promise<number>;
57
70
  /** Runs one change at a time, in call order; a failed call does not stop later ones. */
58
71
  private enqueue;
59
72
  /** Records a change and returns its id. */
@@ -1,3 +1,4 @@
1
1
  export * from "./core";
2
2
  export { mount, type MountOptions, type PipeupInstance } from "./ui/mount";
3
+ export { ADDON_API, addons, use, type AddonDocument, type AddonHost, type AddonInfo, type Capability, type ComposerHandle, type ComposerTool, type Dictation, type ItemHandle, type MenuItem, type NetworkUse, type Off, type PanelHandle, type PanelOptions, type PipeupAddon, type Status, type Teardown, type UiSnapshot, } from "./ui/addons";
3
4
  export { onReveal, setViewState, type SlideHook, type ViewState } from "./ui/here";
@@ -2,6 +2,8 @@ import { type Identity } from "../crypto/identity";
2
2
  import type { OpBody, SignedOp } from "./types";
3
3
  /** Lengths are UTF-16 code units (JavaScript string length). */
4
4
  export declare const MAX_TEXT = 10000;
5
+ /** The longest an op may be, as JSON, so one op can never be a way to fill someone's storage. */
6
+ export declare const MAX_OP: number;
5
7
  /** Far beyond any real log, far below where clock arithmetic could overflow. */
6
8
  export declare const MAX_CLOCK: number;
7
9
  /** Latest valid `at` in ms (about the year 6400); new ops stay at it rather than fail. */
@@ -0,0 +1,229 @@
1
+ import type { Listener, PipeupDocument } from "../document";
2
+ import type { SignedOp, Thread } from "../model/types";
3
+ import { type ComposerTool } from "./composer";
4
+ import type { Ctx } from "./context";
5
+ import type { ViewState } from "./here";
6
+ /** Bumped only for breaking changes to the add-on object, the host, AddonDocument or the data slots. */
7
+ export declare const ADDON_API = 1;
8
+ export type Teardown = () => void;
9
+ export type Off = () => void;
10
+ export type Capability = "sync" | "menu" | "notify" | "note" | "composer" | "panel" | "styles" | "status" | "overlay" | "here" | "sign";
11
+ export interface NetworkUse {
12
+ when: "never" | "after-consent" | "page-configured";
13
+ /** Hosts; "page" is an address the page names. */
14
+ to: readonly string[];
15
+ /** One plain sentence: what leaves the device, and who sees what. */
16
+ says: string;
17
+ }
18
+ export interface AddonInfo {
19
+ id: string;
20
+ version: string;
21
+ network: NetworkUse;
22
+ /** waiting: registered, Pipeup not mounted yet (or unmounted). */
23
+ state: "waiting" | "on" | "off" | "failed";
24
+ /** Why it is off or failed, in words. */
25
+ reason?: string;
26
+ }
27
+ export interface PipeupAddon {
28
+ /** /^[a-z][a-z0-9-]{1,23}$/; also the source of everything it merges. */
29
+ readonly id: string;
30
+ /** The add-on API it was written for; must equal ADDON_API while 0.x. */
31
+ readonly api: number;
32
+ readonly version: string;
33
+ /** Slots it can't work without. A missing one keeps it off, with a reason. */
34
+ readonly needs: readonly Capability[];
35
+ /** What it sends where: for people, docs and `pipeup check`. */
36
+ readonly network: NetworkUse;
37
+ /** Once per mount. May return a teardown for what the add-on itself opened. */
38
+ setup(host: AddonHost): void | Teardown | Promise<void | Teardown>;
39
+ }
40
+ export type { Listener };
41
+ /** What add-ons see of the document: read only. Ops from elsewhere go in through `host.merge`. */
42
+ export interface AddonDocument {
43
+ readonly id: string;
44
+ /** This reviewer's public key: share sends only ops with author === me. */
45
+ readonly me: string;
46
+ /** This reviewer's name; "" until they add one (they show as their animal). */
47
+ readonly name: string;
48
+ threads(): readonly Thread[];
49
+ ops(): readonly SignedOp[];
50
+ onChange(fn: Listener): Off;
51
+ }
52
+ export interface MenuItem {
53
+ id: string;
54
+ /** 24-unit SVG path data, drawn by Pipeup; never markup. */
55
+ icon: readonly string[];
56
+ /** Short: the menu is 290 px wide and truncates. */
57
+ label(): string;
58
+ /** Tooltip and aria-description. */
59
+ hint(): string;
60
+ /** Present: a switch row that keeps the menu open. */
61
+ checked?(): boolean;
62
+ /** Short end text, such as "3". */
63
+ count?(): string;
64
+ select(): void | Promise<void>;
65
+ }
66
+ export interface ItemHandle {
67
+ update(): void;
68
+ remove(): void;
69
+ }
70
+ export interface PanelOptions {
71
+ label: string;
72
+ onClose?(): void;
73
+ }
74
+ export interface PanelHandle {
75
+ close(): void;
76
+ }
77
+ export interface Status {
78
+ /** Plain words: "Live with 2 others". */
79
+ text: string;
80
+ people?: readonly {
81
+ key: string;
82
+ name: string;
83
+ view?: ViewState;
84
+ }[];
85
+ }
86
+ export interface UiSnapshot {
87
+ /** Comments are showing. */
88
+ shown: boolean;
89
+ commentMode: boolean;
90
+ /** All comments is open. */
91
+ allOpen: boolean;
92
+ dark: boolean;
93
+ view: ViewState | undefined;
94
+ /** null: not writing; "": the new-comment box; else that thread's reply line. */
95
+ writing: string | null;
96
+ }
97
+ export type { ComposerHandle, ComposerTool, Dictation } from "./composer";
98
+ export interface AddonHost {
99
+ readonly api: number;
100
+ /** The core's version, for diagnostics only. */
101
+ readonly version: string;
102
+ readonly document: AddonDocument;
103
+ /** The area people comment on. */
104
+ readonly root: Element;
105
+ /** This browser can't keep comments (they are held in memory). */
106
+ readonly ephemeral: boolean;
107
+ /** Aborted at teardown: pass it to fetch, timers and sockets. */
108
+ readonly signal: AbortSignal;
109
+ has(c: Capability): boolean;
110
+ /** Says the add-on can't work here (no speech engine, nothing configured): it stays off, with this reason. */
111
+ off(reason: string): void;
112
+ /** Verifies and merges ops from elsewhere, as a feedback file's are; the source is this add-on's id. */
113
+ merge(ops: readonly unknown[]): Promise<number>;
114
+ addMenuItem(item: MenuItem): ItemHandle;
115
+ notify(text: string): void;
116
+ announce(text: string): void;
117
+ setComposerNote(text: string | null): void;
118
+ addComposerTool(tool: ComposerTool): Off;
119
+ openPanel(content: Node, options: PanelOptions): PanelHandle;
120
+ addStyles(css: string): Off;
121
+ setStatus(status: Status | null): void;
122
+ overlay(): HTMLElement;
123
+ onFrame(fn: () => void): Off;
124
+ /** What a comment on `at` would save. */
125
+ where(at?: Node): ViewState | undefined;
126
+ /** The rule threads follow: this view is where the reviewer is. */
127
+ isHere(view?: ViewState): boolean;
128
+ /** Goes to `view`; true once there. */
129
+ go(view: ViewState): Promise<boolean>;
130
+ /** Called at once, then on every change. */
131
+ onUi(fn: (ui: UiSnapshot) => void): Off;
132
+ avatar(key: string, name: string): HTMLElement;
133
+ sign(name: string, data: string): Promise<string>;
134
+ }
135
+ /**
136
+ * Everything add-ons put on the page, held by the app (not by the views) so it survives layout rebuilds. Each
137
+ * thing belongs to one add-on and is removed with it.
138
+ */
139
+ export declare class Surface {
140
+ readonly ctx: Ctx;
141
+ private readonly shadow;
142
+ private readonly after;
143
+ /** Bumped when the set of menu rows changes. */
144
+ rev: number;
145
+ readonly menuRows: MenuItem[];
146
+ readonly notes: Map<string, string>;
147
+ readonly statuses: Map<string, Status>;
148
+ private readonly toolList;
149
+ private readonly frames;
150
+ private readonly uis;
151
+ private readonly subs;
152
+ private readonly overlays;
153
+ private readonly pending;
154
+ /** How to take away each thing an add-on added, so everything one add-on added goes with it. */
155
+ private readonly undo;
156
+ private remote;
157
+ private last;
158
+ private timer;
159
+ private uiKey;
160
+ constructor(ctx: Ctx, shadow: ShadowRoot, after: Node);
161
+ subscribe(fn: (structure: boolean) => void): Off;
162
+ private changed;
163
+ /** Remembers how to remove something an add-on added; returns the add-on's own `Off` for it. */
164
+ private own;
165
+ /** The add-ons' statuses, one line. */
166
+ statusText(): string;
167
+ rowFor(id: string, item: MenuItem): ItemHandle;
168
+ /** Sets (or, with null, removes) one add-on's note or status. */
169
+ setIn<T>(map: Map<string, T>, id: string, value: T | null): void;
170
+ addTool(id: string, tool: ComposerTool): Off;
171
+ /** Comments are closed: only the control shows, so a notice waits for the reviewer to open Pipeup. */
172
+ private closed;
173
+ notice(id: string, text: string): void;
174
+ /** Shows the latest notice kept while comments were closed. Called when the menu or comments open. */
175
+ flush(): void;
176
+ /** A reviewer is writing: a Pipeup line has words or focus. */
177
+ private isWriting;
178
+ /**
179
+ * An add-on's own words (state changes such as "Listening") are heard at once, while comments are showing:
180
+ * they answer something the reviewer just did, so they don't wait for them to stop writing.
181
+ */
182
+ addSay(text: string): void;
183
+ /** Other people's new comments are announced as one coalesced sentence. */
184
+ heard(added: readonly SignedOp[], source: string): void;
185
+ private pump;
186
+ ovFor(id: string): HTMLElement;
187
+ addSheet(id: string, css: string): Off;
188
+ /** Adds a callback to one of the sets; the add-on's `Off` takes it away. */
189
+ addTo<T>(id: string, set: Set<T>, fn: T): Off;
190
+ onTick(id: string, fn: () => void): Off;
191
+ onSnap(id: string, fn: (ui: UiSnapshot) => void): Off;
192
+ uiState(): UiSnapshot;
193
+ /** Runs once per animation frame, after the views; a callback that throws is removed. */
194
+ frame(): void;
195
+ private call;
196
+ /** A side panel: the rest of Pipeup's UI is inert while it is open; Escape closes it before anything else. */
197
+ panel(id: string, content: Node, o: PanelOptions): PanelHandle;
198
+ /** Removes everything one add-on added. */
199
+ clear(id: string): void;
200
+ destroy(): void;
201
+ }
202
+ interface Live {
203
+ surface: Surface;
204
+ doc: PipeupDocument;
205
+ root: Element;
206
+ ephemeral: boolean;
207
+ version: string;
208
+ active: boolean;
209
+ }
210
+ /**
211
+ * A Pipeup already on the page when this copy loaded (the classic build assigns its global only when it ends, so
212
+ * this is read at load): the first copy owns the registry, the queue and the mount; this one hands over to it.
213
+ */
214
+ export declare const firstCore: {
215
+ VERSION: string;
216
+ use(a: PipeupAddon): void;
217
+ addons(): readonly AddonInfo[];
218
+ mount(o?: unknown): Promise<unknown>;
219
+ } | null;
220
+ export declare function addons(): readonly AddonInfo[];
221
+ /** Registers an add-on: it sets up now if Pipeup is mounted, else when it mounts. */
222
+ export declare function use(addon: PipeupAddon): void;
223
+ /**
224
+ * Takes the add-ons scripts queued before Pipeup loaded, and makes later pushes register at once. Idempotent;
225
+ * returns false when another core owns the queue.
226
+ */
227
+ export declare function drainQueue(): boolean;
228
+ /** Sets up every waiting add-on for this mount, in registration order. Returns the function that tears them down. */
229
+ export declare function attachAddons(o: Omit<Live, "active">): () => void;
@@ -1,5 +1,4 @@
1
1
  import type { Ctx, View } from "./context";
2
- import { type Look } from "./pick";
3
2
  /** Page controls Enter or Space would activate. */
4
3
  export declare const ACTIVATES = "a,button,summary,[role=button],[role=link],[role=switch],[role=tab],[role=checkbox],input[type=checkbox],input[type=radio],input[type=submit],input[type=button],input[type=reset]";
5
4
  /** The keyboard's hint: shown as a toast, and read with the block cursor's first landing. */
@@ -25,4 +24,4 @@ export interface CommentMode extends View {
25
24
  * Known limits: the page's CSS `:hover` still applies; page listeners on window in the capture phase that were
26
25
  * added before comment mode started still hear events; Escape still reaches the page's own handlers.
27
26
  */
28
- export declare function createCommentMode(ctx: Ctx, look?: Look): CommentMode;
27
+ export declare function createCommentMode(ctx: Ctx): CommentMode;
@@ -1,5 +1,8 @@
1
1
  export interface ComposerOptions {
2
2
  label: string;
3
+ /** A new comment, or a reply in `thread()`. */
4
+ kind?: "comment" | "reply";
5
+ thread?: () => string;
3
6
  /** Something to show before the line instead, such as the writer's avatar. */
4
7
  before?: Node;
5
8
  onSend(text: string): Promise<void> | void;
@@ -9,7 +12,48 @@ export interface Composer {
9
12
  element: HTMLElement;
10
13
  input: HTMLTextAreaElement;
11
14
  focus(): void;
12
- clear(): void;
13
15
  }
16
+ /** One live insertion at the caret: words in progress, then final words. */
17
+ export interface Dictation {
18
+ /** Replace the words in progress. */
19
+ update(text: string): void;
20
+ /** Make them final; the next update starts after them. */
21
+ commit(text: string): void;
22
+ /** Remove the words in progress. */
23
+ cancel(): void;
24
+ }
25
+ export interface ComposerHandle {
26
+ readonly kind: "comment" | "reply";
27
+ readonly thread: string | null;
28
+ text(): string;
29
+ focus(): void;
30
+ /** The box is still on screen. */
31
+ connected(): boolean;
32
+ /** aria-pressed and the button's look. */
33
+ setPressed(on: boolean): void;
34
+ dictate(): Dictation;
35
+ /** Send, cancel, clear. Poll `connected()` for a box that is removed. */
36
+ onEnd(fn: () => void): () => void;
37
+ }
38
+ export interface ComposerTool {
39
+ id: string;
40
+ /** 24-unit SVG path data. */
41
+ icon: readonly string[];
42
+ /** Tooltip and aria-label. */
43
+ label: string;
44
+ /** false: no button at all. */
45
+ available?(): boolean;
46
+ press(handle: ComposerHandle): void;
47
+ }
48
+ /** What add-ons put in writing lines; set by the app while add-ons are mounted. */
49
+ export interface ComposerSource {
50
+ tools(): ComposerTool[];
51
+ note(): string | null;
52
+ }
53
+ export declare const setComposerSource: (s: ComposerSource | null) => void;
54
+ /** The one quiet line add-ons may show above the new-comment box. */
55
+ export declare const composerNote: () => string | null;
56
+ /** Sent to a writing line or a new-comment box when what add-ons put in it changes. */
57
+ export declare const SOURCE_CHANGED = "pu-source";
14
58
  /** A writing line: no placeholder, Enter sends, Shift+Enter breaks, Esc cancels, a paper plane appears with text. */
15
59
  export declare function composer(o: ComposerOptions): Composer;
@@ -2,6 +2,7 @@ import type { Resolved } from "../anchor/resolve";
2
2
  import type { PipeupDocument } from "../document";
3
3
  import type { CopyAs, ExportItem } from "../export/format";
4
4
  import type { Anchor, Thread } from "../model/types";
5
+ import type { Surface } from "./addons";
5
6
  import type { DraftBox } from "./draft-view";
6
7
  import type { Here } from "./here";
7
8
  import type { ThreadActions } from "./thread-view";
@@ -60,6 +61,8 @@ export interface Ctx {
60
61
  readonly menu: MenuActions;
61
62
  /** Where the reviewer is: the slide, or the page's own view. */
62
63
  readonly here: Here;
64
+ /** What add-ons put on the page. */
65
+ readonly addons: Surface;
63
66
  /** The thread lives on another slide or view, or the page hides its content: nothing of it shows. */
64
67
  elsewhere(t: Thread): boolean;
65
68
  /** Sorts threads into here and elsewhere again now, re-rendering if that changed anything. */
@@ -5,6 +5,8 @@ export declare function h<K extends keyof HTMLElementTagNameMap>(tag: K, props?:
5
5
  export declare function inert(el: HTMLElement, on: boolean): void;
6
6
  /** Puts `els` in this order before `ref` (at the end without one) in `parent`, moving only what is out of place and keeping focus; true when any moved. */
7
7
  export declare function reorder(parent: Element, els: readonly Element[], ref?: Node | null): boolean;
8
+ /** The page's selection when it holds words (not collapsed, not only spaces). */
9
+ export declare const selected: () => Selection | null;
8
10
  export declare function clip(text: string, n: number): string;
9
11
  /** Sizes a fixed overlay (outline, pulse, mark) to a box, with padding around it. */
10
12
  export declare function fit(el: HTMLElement, r: {
@@ -28,6 +28,8 @@ export declare function stackColumn(items: StackItem[], activeId: string | null,
28
28
  */
29
29
  export declare const PANEL = 320;
30
30
  export declare const POPOVER = 300;
31
+ /** A popover's width: roomier where the window has the space (All comments keeps its own width). */
32
+ export declare const popover: () => number;
31
33
  /** On screens this narrow or less, All comments takes the whole width (it gets .full). */
32
34
  export declare const NARROW = 480;
33
35
  export declare const narrow: () => boolean;
@@ -34,7 +34,7 @@ export declare function pickBlock(from: Element, root: Element, look: Look): Ele
34
34
  /** The next block out from `el` with a different box (wrappers the same size are skipped). */
35
35
  export declare function parentBlock(el: Element, root: Element, look: Look): Element | null;
36
36
  /** The real page: boxes from layout, "drawn" and "grouped" from computed styles. */
37
- export declare function pageLook(win?: Window): Look;
37
+ export declare function pageLook(): Look;
38
38
  /** A block in the keyboard's tree: the block around it, and its level (0 with no block inside it). */
39
39
  export interface Block {
40
40
  el: Element;
@@ -2,24 +2,10 @@
2
2
  export declare const SHORTCUT: {
3
3
  /** The physical key (`KeyboardEvent.code`): Option changes the typed character on a Mac. */
4
4
  readonly code: "KeyC";
5
- /** The modifiers that must be down, and only those (Shift, Alt/Option, Command, Ctrl). */
6
- readonly mods: {
7
- readonly shift: true;
8
- readonly alt: true;
9
- readonly meta: false;
10
- readonly ctrl: false;
11
- };
12
- readonly mac: {
13
- readonly label: "⇧⌥C";
14
- readonly aria: "Shift+Alt+C";
15
- };
16
- readonly other: {
17
- readonly label: "Shift+Alt+C";
18
- readonly aria: "Shift+Alt+C";
19
- };
20
5
  };
6
+ /** The key's label: symbols on Apple platforms, words elsewhere. */
21
7
  export declare const SHORTCUT_LABEL: string;
22
- export declare const SHORTCUT_ARIA: string;
8
+ export declare const SHORTCUT_ARIA = "Shift+Alt+C";
23
9
  /**
24
10
  * Shift+Option+C (Shift+Alt+C): never a key repeat, Command or Ctrl, or AltGr (which types characters).
25
11
  * Callers must not claim it inside text fields: on a Mac it types a character there.
@@ -1,2 +1,2 @@
1
1
  /** Pipeup's stylesheet, scoped to its shadow root. Light by default; `.dark` follows a dark page. */
2
- export declare const STYLES = "\n/* Inherited page styles are reset on .layer below; custom properties pass through all:initial. */\n:host{all:initial}\n*{box-sizing:border-box}\nbutton{-webkit-appearance:none;appearance:none;font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer}\nsvg{display:block}\n.layer{all:initial;display:block;position:fixed;inset:0;pointer-events:none;font:13px/1.5 var(--pu-font,system-ui,sans-serif);color:var(--k);\n--k:#2C2C2A;--mu:#5F5E5A;--fa:#888780;--ln:#E5E3DA;--ru:#ECEAE2;--so:#FAFAF7;--su:#FFFFFF;--ho:#F1EFE8;\n--as:color-mix(in srgb,var(--pu-accent,#534AB7) 12%,transparent);\n--sh:0 8px 28px rgba(44,44,42,.10);\n--in:.26s;--mv:.34s;--out:.2s;--pl:1.2s;\n--eo:cubic-bezier(.22,.61,.36,1);--eio:cubic-bezier(.4,0,.2,1);--ei:cubic-bezier(.4,0,1,1)}\n.layer.dark{--k:#ECEBE6;--mu:#B4B2A9;--fa:#8D8B84;--ln:#3A3936;--ru:#34332F;--so:#22221F;--su:#1B1B19;--ho:#2C2C29;--sh:0 8px 28px rgba(0,0,0,.45)}\n.layer.hidden .bub,.layer.hidden .col,.layer.hidden .pop,.layer.hidden .tip,.layer.hidden .mark,.layer.hidden .selbar{opacity:0!important;pointer-events:none!important}\n\n.tx{font-size:14px;line-height:1.5;color:var(--k);white-space:pre-wrap;overflow-wrap:anywhere}\n.tx.del,.who,.rest,.ctx{color:var(--fa)}\n.who,.rest{font-size:12px}\n/* Long names are cut short on one line, never wrapped. */\n.who{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.rft>.who{display:block}\n.ctx[hidden]{display:none}\n.ctx{font-size:12px;margin-bottom:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.clamp{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}\n.tx a{color:var(--pu-accent);text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:2px}\n\n/* Footers: details take no space until hovered, then ease open beneath the words. */\n.ft,.rft{display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows var(--in) var(--eo),opacity var(--in) var(--eo)}\n.ft>*,.rft>*{overflow:hidden;min-height:0;display:flex;align-items:center}\n.root:hover>.ft,.root:focus-within>.ft,.it:hover>.rft,.it:focus-within>.rft{grid-template-rows:1fr;opacity:1}\n.ft.has-rest{grid-template-rows:1fr;opacity:1}\n.ft.has-rest>*{grid-area:1/1}\n.ft .hover{opacity:0;transition:opacity var(--in) var(--eo)}\n.ft .rest{transition:opacity var(--in) var(--eo)}\n.root:hover>.ft .hover,.root:focus-within>.ft .hover,.ft:not(.has-rest) .hover{opacity:1}\n.root:hover>.ft .rest,.root:focus-within>.ft .rest{opacity:0}\n.acts{margin-left:6px;display:flex;flex:none}\n.ib{width:24px;height:22px;display:grid;place-items:center;border-radius:6px;color:var(--mu);transition:background-color .2s var(--eo),color .2s var(--eo)}\n.ib:hover,.ib:focus-visible{background:var(--ho);color:var(--k);outline:none}\n\n/* Replies: one level in, oldest first; the reply line ends the thread at the same indent. */\n.rps,.rbox{padding-left:14px}\n.rps .it{padding-top:6px}\n.more{display:grid;grid-template-rows:1fr;transition:grid-template-rows var(--mv) var(--eio)}\n.more>div{overflow:hidden;min-height:0}\n.th:not(.on) .more{grid-template-rows:0fr}\n\n.row{display:flex;align-items:center;gap:8px;padding:8px 0 2px;cursor:text}\n.input{flex:1;min-width:0;height:27px;display:block;border:0;border-bottom:1px solid var(--ln);outline:none;resize:none;overflow:hidden;\nfont:inherit;font-size:14px;line-height:1.5;padding:3px 0;color:var(--k);background:transparent;transition:border-color var(--in) var(--eo)}\n.row:hover .input,.input:hover{border-bottom-color:var(--mu)}\n.input:focus{border-bottom-color:var(--pu-accent)}\n.ed{display:flex;gap:10px;align-items:center;min-height:34px;padding:6px 10px}\n.ed .input{font-size:13px;height:22px}\n\n/* Avatars: a squircle with the writer's animal (or initial) on a soft colour. Each comment keeps a fixed area\nat its top right for one, so its words are the same width with or without it. */\n.root,.it{position:relative;padding-right:30px}\n.av{display:grid;place-items:center;width:22px;height:22px;border-radius:32%;\nfont-size:12px;font-weight:600;line-height:1;opacity:0;transform:scale(.85);transition:opacity var(--in) var(--eo),transform var(--in) var(--eo)}\n.av.in{opacity:1;transform:none}\n.av svg{width:80%;height:80%}\n.root>.av,.it>.av{position:absolute;top:0;right:0}\n.it>.av{top:6px}\n/* Ten colours (.c0 Red to .c9 Grey, as COLOURS): a soft tint and a dark drawing, swapped round on dark pages. */\n.av{--av-l:92%;--av-d:30%;--s:60%;background:hsl(var(--h) var(--s) var(--av-l));color:hsl(var(--h) var(--s) var(--av-d))}\n.dark .av{--av-l:20%;--av-d:78%}\n.c0{--h:0}.c1{--h:25}.c2{--h:50}.c3{--h:130}.c4{--h:175}.c5{--h:215}.c6{--h:270}.c7{--h:330}.c8{--h:28;--s:35%}.c9{--h:220;--s:8%}\n.me{flex:none;display:grid}\n.me>.av{grid-area:1/1;width:26px;height:26px;font-size:13px}\n/* Who you are, under the comment line; a name field eases open in its place. */\n.you{display:grid;padding:0 34px;min-height:24px;font-size:12px;color:var(--fa)}\n.you>*{grid-area:1/1;display:flex;align-items:center;min-width:0;transition:opacity var(--in) var(--eo),transform var(--in) var(--eo)}\n.say{white-space:pre;overflow:hidden}\n.nm{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.say button{flex:none}\n.say button{color:var(--mu);text-decoration:underline;text-decoration-color:var(--ln);text-underline-offset:2px;transition:color .2s var(--eo)}\n.say button:hover,.say button:focus-visible{color:var(--pu-accent);outline:none}\n.nf{opacity:0;transform:translateY(-4px);pointer-events:none}\n.nf .input{font-size:13px;height:24px}\n.you.naming .nf{opacity:1;transform:none;pointer-events:auto}\n.you.naming .say{opacity:0;pointer-events:none}\n.send{width:26px;height:26px;border-radius:50%;display:grid;place-items:center;color:var(--pu-accent);flex:none;opacity:0;pointer-events:none;\ntransition:opacity var(--in) var(--eo),background-color .2s var(--eo)}\n.send.show{opacity:1;pointer-events:auto}\n.send:hover{background:var(--as)}\n\n/* Bubbles, tips, popovers, marks, pulses. */\n.bub{position:fixed;left:0;top:0;width:20px;height:20px;margin:-20px 0 0 -2px;border-radius:50% 50% 50% 0;background:var(--pu-accent);\nborder:2px solid var(--su);box-shadow:0 1px 4px rgba(0,0,0,.2);pointer-events:auto;opacity:0;transform:translateY(-6px) scale(.9);transform-origin:2px 20px;\ntransition:opacity var(--out) var(--ei),transform var(--out) var(--ei)}\n.bub.in{opacity:1;transform:none;transition:opacity var(--in) var(--eo),transform var(--in) var(--eo)}\n.bub.in:hover,.bub.on{transform:scale(1.18)}\n.bub.done{background:var(--fa)}\n.bub.ghost{pointer-events:none}\n.tip,.pop,.menu,.all,.toast,.selbar{background:var(--su);border:1px solid var(--ln);box-shadow:var(--sh)}\n.tip{position:fixed;max-width:260px;border-radius:10px;padding:7px 10px;opacity:0;transform:translateY(3px);\ntransition:opacity .22s var(--eo),transform .26s var(--eo)}\n.tip.show{opacity:1;transform:none;pointer-events:auto;cursor:pointer;\ntransition:opacity .22s var(--eo),transform .26s var(--eo),left var(--mv) var(--eio),top var(--mv) var(--eio)}\n.pop{position:fixed;width:var(--pu-pop);border-radius:12px;padding:10px 12px 8px;pointer-events:none;opacity:0;transform:translateY(6px) scale(.98);\ntransition:opacity var(--out) var(--ei),transform var(--out) var(--ei)}\n.pop.show{opacity:1;transform:none;pointer-events:auto;transition:opacity var(--in) var(--eo),transform var(--mv) var(--eo)}\n.pop.show.glide{transition:opacity var(--in) var(--eo),transform var(--mv) var(--eo),left var(--mv) var(--eio),top var(--mv) var(--eio)}\n.mark{position:fixed;border:2px solid color-mix(in srgb,var(--pu-accent) 50%,transparent);background:var(--as);border-radius:8px;opacity:0;\ntransition:opacity .3s var(--eo)}\n.mark.show{opacity:1}\n.pulse{position:fixed;border:2px solid var(--pu-accent);border-radius:8px;background:var(--as);animation:pu-pulse var(--pl) var(--eio) forwards}\n@keyframes pu-pulse{0%{opacity:0;transform:scale(.985)}35%{opacity:.85;transform:scale(1)}100%{opacity:0;transform:scale(1.02)}}\n\n/* Document column. */\n.col{position:fixed;left:0;top:0;pointer-events:none;transition:opacity var(--in) var(--eo)}\n.th{position:absolute;left:0;right:0;top:0;padding:4px 4px 4px 12px;pointer-events:auto;cursor:pointer;box-shadow:inset 2px 0 0 transparent;\ntransition:transform var(--mv) var(--eio),box-shadow var(--in) var(--eo),opacity var(--in) var(--eo)}\n.th.hot{box-shadow:inset 2px 0 0 var(--ln)}\n.th.on{box-shadow:inset 2px 0 0 var(--pu-accent);cursor:default}\n.th.dim{opacity:.45}\n.th.resolved:not(.on):not(.hot){opacity:.6}\n.th.lost .ctx{white-space:normal}\n.th.wait{opacity:0!important;pointer-events:none}\n.th.out{opacity:0!important;pointer-events:none;transition:opacity var(--out) var(--ei)}\n/* A closed thread's button has no box of its own: the thread shows its focus. */\n.th:has(.opn:focus-visible){box-shadow:inset 2px 0 0 var(--ln);outline:2px solid var(--pu-accent);outline-offset:2px;border-radius:4px}\n\n/* Selection bar. */\n.selbar{position:fixed;display:flex;border-radius:10px;padding:2px;opacity:0;transform:translateY(4px);pointer-events:none;\ntransition:opacity var(--in) var(--eo),transform .3s var(--eo)}\n.selbar.show{opacity:1;transform:none;pointer-events:auto}\n.selbar button{width:32px;height:30px;display:grid;place-items:center;border-radius:8px;color:var(--pu-accent);transition:background-color .2s var(--eo)}\n.selbar button:hover,.selbar button:focus-visible{background:var(--as);outline:none}\n\n/* Comment mode's outline: glides between blocks; solid accent once chosen. */\n.pick{position:fixed;left:0;top:0;border:2px solid color-mix(in srgb,var(--pu-accent) 70%,transparent);background:color-mix(in srgb,var(--pu-accent) 6%,transparent);\nborder-radius:8px;pointer-events:none;opacity:0;transition:opacity var(--out) var(--ei),border-color var(--out) var(--ei),border-width var(--out) var(--ei),box-shadow var(--out) var(--ei)}\n.pick.show{opacity:1;transition-duration:var(--in);transition-timing-function:var(--eo)}\n.pick.show.glide{transition-property:all;transition-duration:var(--mv);transition-timing-function:var(--eio)}\n.pick.on{border-color:var(--pu-accent)}\n/* The block cursor: its focus is drawn on the outline (3px accent with a surface halo, 3:1 on any page); the\nmarkers that hold focus are invisible. */\n.pick.kf{border:3px solid var(--pu-accent);box-shadow:0 0 0 2px var(--su)}\n.km{position:fixed;left:0;top:0;pointer-events:none;outline:none}\n.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n.namebar{position:fixed;left:0;top:0;display:flex;align-items:center;gap:2px;padding:2px 2px 2px 10px;border-radius:10px;font-size:12px;color:var(--mu);\nwhite-space:nowrap;background:var(--su);border:1px solid var(--ln);box-shadow:var(--sh);pointer-events:none;opacity:0;transform:translateY(4px);\ntransition:opacity var(--out) var(--ei),transform var(--out) var(--ei)}\n.namebar.show{opacity:1;transform:none;pointer-events:auto;transition:opacity var(--in) var(--eo),transform .3s var(--eo)}\n.namebar.show.glide{transition-property:all;transition-duration:var(--mv);transition-timing-function:var(--eio)}\n.namebar .lbl{margin-right:6px;max-width:260px;overflow:hidden;text-overflow:ellipsis}\n.namebar .nb{padding:4px;display:grid;place-items:center;border-radius:7px;transition:background-color .2s var(--eo),color .2s var(--eo)}\n.namebar .nb:hover,.namebar .nb:focus-visible{background:var(--ho);color:var(--k);outline:none}\n.namebar .nb[hidden]{display:none}\n\n/* The comment control: one round button; the open count sits inside a comment bubble. Nothing about it\ngrows or slides: its layers and numbers cross-fade in place. */\n.launch{position:fixed;right:20px;bottom:20px;pointer-events:auto}\n.launch .mode{position:relative;display:grid;place-items:center;width:46px;height:46px;border-radius:50%;background:var(--su);border:1px solid var(--ln);\nbox-shadow:var(--sh);transition:background-color .3s var(--eo),color .3s var(--eo)}\n.mode>*,.cnt>*{grid-area:1/1}\n.mode>*{display:grid;place-items:center;transition:opacity var(--in) var(--eo)}\n.cnt,.has>.plus{opacity:0}\n.has>.cnt{opacity:1}\n.n{font-size:12px;font-weight:500;line-height:1;margin:-1.4px 0 0 1.4px;font-variant-numeric:tabular-nums;opacity:0;transition:opacity var(--in) var(--eo)}\n.n.s{font-size:9.5px;letter-spacing:-.04em}\n.n.on{opacity:1}\n.launch .mode:hover,.launch .mode[aria-expanded=true]{background:var(--ho)}\n.launch .mode.on{background:var(--pu-accent);color:#fff}\n/* Open threads on other slides or views: a small dot at the button's top right, easing in and out. */\n.mode>.dot{position:absolute;top:3px;right:3px;width:9px;height:9px;border-radius:50%;background:var(--pu-accent);border:2px solid var(--su);opacity:0}\n.mode.else>.dot{opacity:1}\n/* The tooltip eases in after a moment, above the button, and never moves it. */\n.ttip{position:absolute;right:0;bottom:54px;white-space:nowrap;border-radius:8px;padding:4px 9px;font-size:12px;background:var(--su);border:1px solid var(--ln);\nbox-shadow:var(--sh);pointer-events:none;opacity:0;transform:translateY(3px);transition:opacity var(--out) var(--ei),transform var(--out) var(--ei)}\n.ttip small{margin-left:8px}\n.ttip small,.mi .kc{font-size:12px;color:var(--fa)}\n.mode:hover+.ttip,.mode:focus-visible+.ttip{opacity:1;transform:none;transition:opacity var(--in) var(--eo) .5s,transform var(--in) var(--eo) .5s}\n.launch .mode[aria-expanded=true]+.ttip{opacity:0;transition:opacity var(--out) var(--ei)}\n@media (hover:none){.ttip,.tt{display:none}}\n.menu{position:fixed;right:20px;bottom:76px;width:290px;border-radius:12px;padding:4px;pointer-events:none;opacity:0;transform:translateY(6px);\ntransition:opacity var(--out) var(--ei),transform var(--out) var(--ei)}\n.menu.show{opacity:1;transform:none;pointer-events:auto;transition:opacity var(--in) var(--eo),transform var(--mv) var(--eo)}\n/* Menu rows: one line each; the explanation is a tooltip beside the menu that eases in after a moment. */\n.mi{position:relative;display:flex;gap:10px;align-items:center;width:100%;min-height:34px;text-align:left;padding:6px 10px;border-radius:8px;transition:background-color .2s var(--eo)}\n.mi:hover,.mi:focus-visible{background:var(--so);outline:none}\n.mi svg{color:var(--mu);flex:none}\n.mi small{display:block;color:var(--fa);font-size:12px}\n.lb{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.ma{flex:none;display:grid;margin:-2px 0}\n.ma .av{width:20px;height:20px;font-size:11px}\n.mi .av svg{color:inherit}\n.sep{height:1px;margin:4px 10px;background:var(--ru)}\n.tt{position:absolute;right:calc(100% + 14px);top:50%;translate:0 -50%;width:max-content;max-width:220px;padding:4px 9px;border-radius:8px;font-size:12px;line-height:1.4;\ncolor:var(--mu);background:var(--su);border:1px solid var(--ln);box-shadow:var(--sh);pointer-events:none;opacity:0;transform:translateX(3px);\ntransition:opacity var(--out) var(--ei),transform var(--out) var(--ei)}\n.mi:hover>.tt,.mi:focus-visible>.tt{opacity:1;transform:none;transition:opacity var(--in) var(--eo) .5s,transform var(--in) var(--eo) .5s}\n@media (max-width:600px){.tt{right:8px;top:auto;bottom:100%;translate:none;transform:translateY(3px)}}\n/* Switches: a track and a knob that slides (cross-fades with reduced motion). */\n.sw{position:relative;flex:none;width:26px;height:16px;border-radius:8px;background:var(--ln);transition:background-color var(--mv) var(--eio)}\n.sw::before,.sw::after{content:\"\";position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);\ntransition:transform var(--mv) var(--eio),opacity var(--mv) var(--eio)}\n.sw::before{opacity:0;transform:translateX(10px)}\n[aria-checked=true]>.sw{background:var(--pu-accent)}\n[aria-checked=true]>.sw::after{transform:translateX(10px)}\n/* All comments: a full-height panel on the right that slides in (fades, with reduced motion). */\n.all{position:fixed;top:0;right:0;bottom:0;width:var(--pu-panel);display:flex;flex-direction:column;border-width:0 0 0 1px;opacity:0;transform:translateX(100%);\ntransition:transform var(--out) var(--ei),opacity var(--out) var(--ei)}\n.all.show{opacity:1;transform:none;pointer-events:auto;transition:transform var(--mv) var(--eo),opacity var(--in) var(--eo)}\n.all.full{width:100%}\n.hd{display:flex;align-items:center;gap:8px;padding:12px 10px 8px 16px;font-weight:600}\n.pn{font-weight:400;color:var(--fa)}\n.hs{margin-left:auto;display:flex;align-items:center;gap:6px;padding:3px 4px 3px 6px;border-radius:6px;font-size:12px;font-weight:400;color:var(--mu);\ntransition:background-color .2s var(--eo)}\n.hs:hover,.hs:focus-visible{background:var(--ho);outline:none}\n.list{flex:1;overflow-y:auto;overscroll-behavior:contain;padding:4px}\n.all .mi{align-items:flex-start}\n.all .mi>.av{flex:none}\n.all .mi>span:last-child{flex:1;min-width:0}\n.all .mi small{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.all .mi .av svg{margin:0}\n.all .mi.done{opacity:.6}\n.layer.listing .col{opacity:0;transition:opacity var(--out) var(--ei)}\n.sec{padding:8px 10px 2px;font-size:12px;color:var(--fa)}\n.sec b{margin-left:6px;font-weight:500;color:var(--pu-accent)}\n.toast{position:fixed;left:50%;bottom:28px;border-radius:10px;padding:8px 14px;font-size:13px;opacity:0;transform:translate(-50%,6px);\ntransition:opacity .3s var(--eo),transform .36s var(--eo)}\n.toast.show{opacity:1;transform:translate(-50%,0)}\n\n@media (forced-colors: active){\n.pick.kf{border:3px solid Highlight}\n.layer :is(.ib,.mi,.nb,.hs,.selbar button,.say button):focus-visible{outline:2px solid Highlight}\n}\n\n@media (prefers-reduced-motion: reduce){\n.bub,.bub.in,.bub.in:hover,.bub.on,.tip,.pop,.selbar,.menu,.all,.ttip,.tt,.toast{transform:none!important}\n.sw::after{transform:none!important;transition-property:opacity}\n[aria-checked=true]>.sw::after{opacity:0}\n[aria-checked=true]>.sw::before{opacity:1}\n.th{transition-property:opacity,box-shadow}\n.tip.show{transition-property:opacity}\n.pick,.pick.show{transition:opacity .16s ease-in-out}\n.all,.all.show{transition-property:opacity}\n.namebar,.namebar.show{transform:none!important;transition:opacity .16s ease-in-out}\n.av,.nf{transform:none!important}\n@keyframes pu-pulse{0%{opacity:0}35%{opacity:.85}100%{opacity:0}}\n}\n";
2
+ export declare const STYLES = "\n/* Inherited page styles are reset on .layer below; custom properties pass through all:initial. */\n:host{all:initial}\n*{box-sizing:border-box}\nbutton{appearance:none;font:inherit;color:inherit;background:none;border:0;padding:0;cursor:pointer}\nsvg{display:block}\n.layer{all:initial;display:block;position:fixed;inset:0;pointer-events:none;font:13px/1.5 var(--pu-font,system-ui,sans-serif);color:var(--k);\n--k:#2C2C2A;--mu:#5F5E5A;--fa:#888780;--ln:#E5E3DA;--ru:#ECEAE2;--so:#FAFAF7;--su:#FFFFFF;--ho:#F1EFE8;\n--as:color-mix(in srgb,var(--pu-accent,#534AB7) 12%,transparent);\n--sh:0 8px 28px rgba(44,44,42,.10);\n--in:.26s;--mv:.34s;--out:.2s;--pl:1.2s;\n--eo:cubic-bezier(.22,.61,.36,1);--eio:cubic-bezier(.4,0,.2,1);--ei:cubic-bezier(.4,0,1,1);\n--pu-text:var(--k);--pu-muted:var(--mu);--pu-faint:var(--fa);--pu-line:var(--ln);--pu-surface:var(--su);--pu-soft:var(--so);--pu-hover:var(--ho);--pu-shadow:var(--sh);--pu-ease:var(--eo)}\n.layer.dark{--k:#ECEBE6;--mu:#B4B2A9;--fa:#8D8B84;--ln:#3A3936;--ru:#34332F;--so:#22221F;--su:#1B1B19;--ho:#2C2C29;--sh:0 8px 28px rgba(0,0,0,.45)}\n.layer.hidden .bub,.layer.hidden .col,.layer.hidden .pop,.layer.hidden .tip,.layer.hidden .mark,.layer.hidden .selbar{opacity:0!important;pointer-events:none!important}\n\n.tx{font-size:14px;line-height:1.5;color:var(--k);white-space:pre-wrap;overflow-wrap:anywhere}\n.tx.del,.who,.rest,.ctx{color:var(--fa)}\n.who,.rest{font-size:12px}\n/* Long names are cut short on one line, never wrapped. */\n.who{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.rft>.who{display:block}\n.ctx[hidden]{display:none}\n.ctx{font-size:12px;margin-bottom:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.clamp{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}\n.tx a{color:var(--pu-accent);text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:2px}\n\n/* Footers: details take no space until hovered, then ease open beneath the words. */\n.ft,.rft{display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows var(--in) var(--eo),opacity var(--in) var(--eo)}\n.ft>*,.rft>*{overflow:hidden;min-height:0;display:flex;align-items:center}\n.root:hover>.ft,.root:focus-within>.ft,.it:hover>.rft,.it:focus-within>.rft{grid-template-rows:1fr;opacity:1}\n.ft.has-rest{grid-template-rows:1fr;opacity:1}\n.ft.has-rest>*{grid-area:1/1}\n.ft .hover{opacity:0;transition:opacity var(--in) var(--eo)}\n.ft .rest{transition:opacity var(--in) var(--eo)}\n.root:hover>.ft .hover,.root:focus-within>.ft .hover,.ft:not(.has-rest) .hover{opacity:1}\n.root:hover>.ft .rest,.root:focus-within>.ft .rest{opacity:0}\n.acts{margin-left:6px;display:flex;flex:none}\n.ib{width:24px;height:22px;display:grid;place-items:center;border-radius:6px;color:var(--mu);transition:background-color .2s var(--eo),color .2s var(--eo)}\n.ib:hover,.ib:focus-visible{background:var(--ho);color:var(--k);outline:none}\n\n/* Replies: one level in, oldest first; the reply line ends the thread at the same indent. */\n.rps,.rbox{padding-left:14px}\n.rps .it{padding-top:6px}\n.more{display:grid;grid-template-rows:1fr;transition:grid-template-rows var(--mv) var(--eio)}\n.more>div{overflow:hidden;min-height:0}\n.th:not(.on) .more{grid-template-rows:0fr}\n\n.row{display:flex;align-items:center;gap:8px;padding:8px 0 2px;cursor:text}\n.input{flex:1;min-width:0;height:27px;display:block;border:0;border-bottom:1px solid var(--ln);outline:none;resize:none;overflow:hidden;\nfont:inherit;font-size:14px;line-height:1.5;padding:3px 0;color:var(--k);background:transparent;transition:border-color var(--in) var(--eo)}\n.row:hover .input,.input:hover{border-bottom-color:var(--mu)}\n.input:focus{border-bottom-color:var(--pu-accent)}\n.ed{display:flex;gap:10px;align-items:center;min-height:34px;padding:6px 10px}\n.ed .input{font-size:13px;height:22px}\n\n/* Avatars: a squircle with the writer's animal (or initial) on a soft colour. Each comment keeps a fixed area\nat its top right for one, so its words are the same width with or without it. */\n.root,.it{position:relative;padding-right:30px}\n.av{display:grid;place-items:center;width:22px;height:22px;border-radius:32%;\nfont-size:12px;font-weight:600;line-height:1;opacity:0;transform:scale(.85);transition:opacity var(--in) var(--eo),transform var(--in) var(--eo)}\n.av.in{opacity:1;transform:none}\n.av svg{width:80%;height:80%}\n.root>.av,.it>.av{position:absolute;top:0;right:0}\n.it>.av{top:6px}\n/* Ten colours (.c0 Red to .c9 Grey, as COLOURS): a soft tint and a dark drawing, swapped round on dark pages. */\n.av{--av-l:92%;--av-d:30%;--s:60%;background:hsl(var(--h) var(--s) var(--av-l));color:hsl(var(--h) var(--s) var(--av-d))}\n.dark .av{--av-l:20%;--av-d:78%}\n.c0{--h:0}.c1{--h:25}.c2{--h:50}.c3{--h:130}.c4{--h:175}.c5{--h:215}.c6{--h:270}.c7{--h:330}.c8{--h:28;--s:35%}.c9{--h:220;--s:8%}\n.me{flex:none;display:grid}\n.me>.av{grid-area:1/1;width:26px;height:26px;font-size:13px}\n/* Who you are, under the comment line; a name field eases open in its place. */\n.you{display:grid;padding:0 34px;min-height:24px;font-size:12px;color:var(--fa)}\n.you>*{grid-area:1/1;display:flex;align-items:center;min-width:0;transition:opacity var(--in) var(--eo),transform var(--in) var(--eo)}\n.say{white-space:pre;overflow:hidden}\n.nm{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.say button{flex:none}\n.say button{color:var(--mu);text-decoration:underline;text-decoration-color:var(--ln);text-underline-offset:2px;transition:color .2s var(--eo)}\n.say button:hover,.say button:focus-visible{color:var(--pu-accent);outline:none}\n.nf{opacity:0;transform:translateY(-4px);pointer-events:none}\n.nf .input{font-size:13px;height:24px}\n.you.naming .nf{opacity:1;transform:none;pointer-events:auto}\n.you.naming .say{opacity:0;pointer-events:none}\n.send{width:26px;height:26px;border-radius:50%;display:grid;place-items:center;color:var(--pu-accent);flex:none;opacity:0;pointer-events:none;\ntransition:opacity var(--in) var(--eo),background-color .2s var(--eo)}\n.send.show{opacity:1;pointer-events:auto}\n.send:hover{background:var(--as)}\n\n/* Bubbles, tips, popovers, marks, pulses. */\n.bub{position:fixed;left:0;top:0;width:20px;height:20px;margin:-20px 0 0 -2px;border-radius:50% 50% 50% 0;background:var(--pu-accent);\nborder:2px solid var(--su);box-shadow:0 1px 4px rgba(0,0,0,.2);pointer-events:auto;opacity:0;transform:translateY(-6px) scale(.9);transform-origin:2px 20px;\ntransition:opacity var(--out) var(--ei),transform var(--out) var(--ei)}\n.bub.in{opacity:1;transform:none;transition:opacity var(--in) var(--eo),transform var(--in) var(--eo)}\n.bub.in:hover,.bub.on{transform:scale(1.18)}\n.bub.done{background:var(--fa)}\n.bub.ghost{pointer-events:none}\n.tip,.pop,.menu,.all,.xp,.toast,.selbar,.namebar,.ttip,.tt,.mode{background:var(--su);border:1px solid var(--ln);box-shadow:var(--sh)}\n.tip{position:fixed;max-width:260px;border-radius:10px;padding:7px 10px;opacity:0;transform:translateY(3px);\ntransition:opacity .22s var(--eo),transform .26s var(--eo)}\n.tip.show{opacity:1;transform:none;pointer-events:auto;cursor:pointer;\ntransition:opacity .22s var(--eo),transform .26s var(--eo),left var(--mv) var(--eio),top var(--mv) var(--eio)}\n.pop{position:fixed;width:var(--pu-pop);border-radius:12px;padding:10px 12px 8px;pointer-events:none;opacity:0;transform:translateY(6px) scale(.98);\ntransition:opacity var(--out) var(--ei),transform var(--out) var(--ei)}\n.pop.show{opacity:1;transform:none;pointer-events:auto;transition:opacity var(--in) var(--eo),transform var(--mv) var(--eo)}\n.pop.show.glide{transition:opacity var(--in) var(--eo),transform var(--mv) var(--eo),left var(--mv) var(--eio),top var(--mv) var(--eio)}\n.mark{position:fixed;border:2px solid color-mix(in srgb,var(--pu-accent) 50%,transparent);background:var(--as);border-radius:8px;opacity:0;\ntransition:opacity .3s var(--eo)}\n.mark.show{opacity:1}\n.pulse{position:fixed;border:2px solid var(--pu-accent);border-radius:8px;background:var(--as);animation:pu-pulse var(--pl) var(--eio) forwards}\n@keyframes pu-pulse{0%{opacity:0;transform:scale(.985)}35%{opacity:.85;transform:scale(1)}100%{opacity:0;transform:scale(1.02)}}\n\n/* Document column. */\n.col{position:fixed;left:0;top:0;pointer-events:none;transition:opacity var(--in) var(--eo)}\n.th{position:absolute;left:0;right:0;top:0;padding:4px 4px 4px 12px;pointer-events:auto;cursor:pointer;box-shadow:inset 2px 0 0 transparent;\ntransition:transform var(--mv) var(--eio),box-shadow var(--in) var(--eo),opacity var(--in) var(--eo)}\n.th.hot{box-shadow:inset 2px 0 0 var(--ln)}\n.th.on{box-shadow:inset 2px 0 0 var(--pu-accent);cursor:default}\n.th.dim{opacity:.45}\n.th.resolved:not(.on):not(.hot){opacity:.6}\n.th.lost .ctx{white-space:normal}\n.th.wait{opacity:0!important;pointer-events:none}\n.th.out{opacity:0!important;pointer-events:none;transition:opacity var(--out) var(--ei)}\n/* A closed thread's button has no box of its own: the thread shows its focus. */\n.th:has(.opn:focus-visible){box-shadow:inset 2px 0 0 var(--ln);outline:2px solid var(--pu-accent);outline-offset:2px;border-radius:4px}\n\n/* Selection bar. */\n.selbar{position:fixed;display:flex;border-radius:10px;padding:2px;opacity:0;transform:translateY(4px);pointer-events:none;\ntransition:opacity var(--in) var(--eo),transform .3s var(--eo)}\n.selbar.show{opacity:1;transform:none;pointer-events:auto}\n.selbar button{width:32px;height:30px;display:grid;place-items:center;border-radius:8px;color:var(--pu-accent);transition:background-color .2s var(--eo)}\n.selbar button:hover,.selbar button:focus-visible{background:var(--as);outline:none}\n\n/* Comment mode's outline: glides between blocks; solid accent once chosen. */\n.pick{position:fixed;left:0;top:0;border:2px solid color-mix(in srgb,var(--pu-accent) 70%,transparent);background:color-mix(in srgb,var(--pu-accent) 6%,transparent);\nborder-radius:8px;pointer-events:none;opacity:0;transition:opacity var(--out) var(--ei),border-color var(--out) var(--ei),border-width var(--out) var(--ei),box-shadow var(--out) var(--ei)}\n.pick.show{opacity:1;transition-duration:var(--in);transition-timing-function:var(--eo)}\n.pick.show.glide{transition-property:all;transition-duration:var(--mv);transition-timing-function:var(--eio)}\n.pick.on{border-color:var(--pu-accent)}\n/* The block cursor: its focus is drawn on the outline (3px accent with a surface halo, 3:1 on any page); the\nmarkers that hold focus are invisible. */\n.pick.kf{border:3px solid var(--pu-accent);box-shadow:0 0 0 2px var(--su)}\n.km{position:fixed;left:0;top:0;pointer-events:none;outline:none}\n.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n.namebar{position:fixed;left:0;top:0;display:flex;align-items:center;gap:2px;padding:2px 2px 2px 10px;border-radius:10px;font-size:12px;color:var(--mu);\nwhite-space:nowrap;pointer-events:none;opacity:0;transform:translateY(4px);\ntransition:opacity var(--out) var(--ei),transform var(--out) var(--ei)}\n.namebar.show{opacity:1;transform:none;pointer-events:auto;transition:opacity var(--in) var(--eo),transform .3s var(--eo)}\n.namebar.show.glide{transition-property:all;transition-duration:var(--mv);transition-timing-function:var(--eio)}\n.namebar .lbl{margin-right:6px;max-width:260px;overflow:hidden;text-overflow:ellipsis}\n.namebar .nb{padding:4px;display:grid;place-items:center;border-radius:7px;transition:background-color .2s var(--eo),color .2s var(--eo)}\n.namebar .nb:hover,.namebar .nb:focus-visible{background:var(--ho);color:var(--k);outline:none}\n.namebar .nb[hidden]{display:none}\n\n/* The comment control: one round button; the open count sits inside a comment bubble. Nothing about it\ngrows or slides: its layers and numbers cross-fade in place. */\n.launch{position:fixed;right:20px;bottom:20px;pointer-events:auto}\n.launch .mode{position:relative;display:grid;place-items:center;width:46px;height:46px;border-radius:50%;transition:background-color .3s var(--eo),color .3s var(--eo)}\n.mode>*,.cnt>*{grid-area:1/1}\n.mode>*{display:grid;place-items:center;transition:opacity var(--in) var(--eo)}\n.cnt,.has>.plus{opacity:0}\n.has>.cnt{opacity:1}\n.n{font-size:12px;font-weight:500;line-height:1;margin:-1.4px 0 0 1.4px;font-variant-numeric:tabular-nums;opacity:0;transition:opacity var(--in) var(--eo)}\n.n.s{font-size:9.5px;letter-spacing:-.04em}\n.n.on{opacity:1}\n.launch .mode:hover,.launch .mode[aria-expanded=true]{background:var(--ho)}\n.launch .mode.on{background:var(--pu-accent);color:var(--pu-on,#fff)}\n/* Open threads on other slides or views: a small dot at the button's top right, easing in and out. */\n.mode>.dot{position:absolute;top:3px;right:3px;width:9px;height:9px;border-radius:50%;background:var(--pu-accent);border:2px solid var(--su);opacity:0}\n.mode.else>.dot{opacity:1}\n/* The tooltip eases in after a moment, above the button, and never moves it. */\n.ttip{position:absolute;right:0;bottom:54px;white-space:nowrap;border-radius:8px;padding:4px 9px;font-size:12px;pointer-events:none;opacity:0;transform:translateY(3px);transition:opacity var(--out) var(--ei),transform var(--out) var(--ei)}\n.ttip small{margin-left:8px}\n.ttip small,.mi .kc{font-size:12px;color:var(--fa)}\n.mode:hover+.ttip,.mode:focus-visible+.ttip{opacity:1;transform:none;transition:opacity var(--in) var(--eo) .5s,transform var(--in) var(--eo) .5s}\n.launch .mode[aria-expanded=true]+.ttip{opacity:0;transition:opacity var(--out) var(--ei)}\n@media (hover:none){.ttip,.tt{display:none}}\n.menu{position:fixed;right:20px;bottom:76px;width:290px;border-radius:12px;padding:4px;pointer-events:none;opacity:0;transform:translateY(6px);\ntransition:opacity var(--out) var(--ei),transform var(--out) var(--ei)}\n.menu.show{opacity:1;transform:none;pointer-events:auto;transition:opacity var(--in) var(--eo),transform var(--mv) var(--eo)}\n/* Menu rows: one line each; the explanation is a tooltip beside the menu that eases in after a moment. */\n.mi{position:relative;display:flex;gap:10px;align-items:center;width:100%;min-height:34px;text-align:left;padding:6px 10px;border-radius:8px;transition:background-color .2s var(--eo)}\n.mi:hover,.mi:focus-visible{background:var(--so);outline:none}\n.mi svg{color:var(--mu);flex:none}\n.mi small{display:block;color:var(--fa);font-size:12px}\n.lb{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.ma{flex:none;display:grid;margin:-2px 0}\n.ma .av{width:20px;height:20px;font-size:11px}\n.mi .av svg{color:inherit}\n.sep{height:1px;margin:4px 10px;background:var(--ru)}\n.tt{position:absolute;right:calc(100% + 14px);top:50%;translate:0 -50%;width:max-content;max-width:220px;padding:4px 9px;border-radius:8px;font-size:12px;line-height:1.4;\ncolor:var(--mu);pointer-events:none;opacity:0;transform:translateX(3px);\ntransition:opacity var(--out) var(--ei),transform var(--out) var(--ei)}\n.mi:hover>.tt,.mi:focus-visible>.tt{opacity:1;transform:none;transition:opacity var(--in) var(--eo) .5s,transform var(--in) var(--eo) .5s}\n@media (max-width:600px){.tt{right:8px;top:auto;bottom:100%;translate:none;transform:translateY(3px)}}\n/* Switches: a track and a knob that slides (cross-fades with reduced motion). */\n.sw{position:relative;flex:none;width:26px;height:16px;border-radius:8px;background:var(--ln);transition:background-color var(--mv) var(--eio)}\n.sw::before,.sw::after{content:\"\";position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.25);\ntransition:transform var(--mv) var(--eio),opacity var(--mv) var(--eio)}\n.sw::before{opacity:0;transform:translateX(10px)}\n[aria-checked=true]>.sw{background:var(--pu-accent)}\n[aria-checked=true]>.sw::after{transform:translateX(10px)}\n.tools{display:flex;flex:none;gap:2px}\n.tools[hidden]{display:none}\n.tool[aria-pressed=true]{background:var(--as);color:var(--pu-accent)}\n.note{display:block;padding:0 0 4px;font-size:12px;color:var(--fa)}\n.note[hidden]{display:none}\n.ov{position:absolute;inset:0;pointer-events:none;overflow:hidden}\n/* An add-on's panel (.xp) is the same full-height panel as All comments (.all), with its own body. */\n.xb{flex:1;overflow-y:auto;overscroll-behavior:contain;padding:4px 16px 16px;font-size:13px}\n.xb p{margin:0 0 10px}\n/* All comments: a full-height panel on the right that slides in (fades, with reduced motion). */\n.all,.xp{position:fixed;top:0;right:0;bottom:0;width:var(--pu-panel);display:flex;flex-direction:column;border-width:0 0 0 1px;opacity:0;transform:translateX(100%);\ntransition:transform var(--out) var(--ei),opacity var(--out) var(--ei)}\n.all.show,.xp.show{opacity:1;transform:none;pointer-events:auto;transition:transform var(--mv) var(--eo),opacity var(--in) var(--eo)}\n.all.full{width:100%}\n.hd{display:flex;align-items:center;gap:8px;padding:12px 10px 8px 16px;font-weight:600}\n.pn{font-weight:400;color:var(--fa)}\n.hs{margin-left:auto;display:flex;align-items:center;gap:6px;padding:3px 4px 3px 6px;border-radius:6px;font-size:12px;font-weight:400;color:var(--mu);\ntransition:background-color .2s var(--eo)}\n.hs:hover,.hs:focus-visible{background:var(--ho);outline:none}\n.list{flex:1;overflow-y:auto;overscroll-behavior:contain;padding:4px}\n.all .mi{align-items:flex-start}\n.all .mi>.av{flex:none}\n.all .mi>span:last-child{flex:1;min-width:0}\n.all .mi small{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.all .mi.done{opacity:.6}\n.layer.listing .col{opacity:0;transition:opacity var(--out) var(--ei)}\n.sec{padding:8px 10px 2px;font-size:12px;color:var(--fa)}\n.sec b{margin-left:6px;font-weight:500;color:var(--pu-accent)}\n.toast{position:fixed;left:50%;bottom:28px;border-radius:10px;padding:8px 14px;font-size:13px;opacity:0;transform:translate(-50%,6px);\ntransition:opacity .3s var(--eo),transform .36s var(--eo)}\n.toast.show{opacity:1;transform:translate(-50%,0)}\n\n@media (forced-colors: active){\n.pick.kf{border:3px solid Highlight}\n.layer :is(.ib,.mi,.nb,.hs,.selbar button,.say button,.xb button):focus-visible{outline:2px solid Highlight}\n}\n\n@media (prefers-reduced-motion: reduce){\n.bub,.bub.in,.bub.in:hover,.bub.on,.tip,.pop,.selbar,.menu,.all,.xp,.ttip,.tt,.toast{transform:none!important}\n.sw::after{transform:none!important;transition-property:opacity}\n[aria-checked=true]>.sw::after{opacity:0}\n[aria-checked=true]>.sw::before{opacity:1}\n.th{transition-property:opacity,box-shadow}\n.tip.show{transition-property:opacity}\n.pick,.pick.show{transition:opacity .16s ease-in-out}\n.all,.all.show,.xp,.xp.show{transition-property:opacity}\n.namebar,.namebar.show{transform:none!important;transition:opacity .16s ease-in-out}\n.av,.nf{transform:none!important}\n@keyframes pu-pulse{0%{opacity:0}35%{opacity:.85}100%{opacity:0}}\n}\n";
@@ -2,6 +2,8 @@ export interface Theme {
2
2
  font: string;
3
3
  accent: string;
4
4
  dark: boolean;
5
+ /** The text colour that reads on the accent: white, or near black on a pale accent. */
6
+ on: string;
5
7
  }
6
8
  export declare function parseRgb(value: string): {
7
9
  r: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pipeup",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Comments and feedback right on any HTML page: documents, slide decks and whole sites.",
5
5
  "keywords": [
6
6
  "comments",