@hanzo/ui 8.27.3 → 8.27.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/backdrop/Backdrop.cjs +420 -0
  2. package/dist/backdrop/Backdrop.d.ts +53 -0
  3. package/dist/backdrop/Backdrop.d.ts.map +1 -0
  4. package/dist/backdrop/Backdrop.js +418 -0
  5. package/dist/backdrop/Backdrop.js.map +1 -0
  6. package/dist/backdrop/index.cjs +50 -0
  7. package/dist/backdrop/index.d.ts +29 -0
  8. package/dist/backdrop/index.d.ts.map +1 -0
  9. package/dist/backdrop/index.js +29 -0
  10. package/dist/backdrop/index.js.map +1 -0
  11. package/dist/backdrop/scene.cjs +439 -0
  12. package/dist/backdrop/scene.d.ts +235 -0
  13. package/dist/backdrop/scene.d.ts.map +1 -0
  14. package/dist/backdrop/scene.js +423 -0
  15. package/dist/backdrop/scene.js.map +1 -0
  16. package/dist/chat/Chat.cjs +23 -1
  17. package/dist/chat/Chat.d.ts +6 -3
  18. package/dist/chat/Chat.d.ts.map +1 -1
  19. package/dist/chat/Chat.js +23 -1
  20. package/dist/chat/Chat.js.map +1 -1
  21. package/dist/chat/Inspector.cjs +51 -0
  22. package/dist/chat/Inspector.d.ts +49 -0
  23. package/dist/chat/Inspector.d.ts.map +1 -0
  24. package/dist/chat/Inspector.js +49 -0
  25. package/dist/chat/Inspector.js.map +1 -0
  26. package/dist/chat/Message.cjs +3 -1
  27. package/dist/chat/Message.d.ts +3 -1
  28. package/dist/chat/Message.d.ts.map +1 -1
  29. package/dist/chat/Message.js +3 -1
  30. package/dist/chat/Message.js.map +1 -1
  31. package/dist/chat/Parts.cjs +159 -0
  32. package/dist/chat/Parts.d.ts +186 -0
  33. package/dist/chat/Parts.d.ts.map +1 -0
  34. package/dist/chat/Parts.js +153 -0
  35. package/dist/chat/Parts.js.map +1 -0
  36. package/dist/chat/Persona.cjs +34 -0
  37. package/dist/chat/Persona.d.ts +37 -0
  38. package/dist/chat/Persona.d.ts.map +1 -0
  39. package/dist/chat/Persona.js +30 -0
  40. package/dist/chat/Persona.js.map +1 -0
  41. package/dist/chat/Preview.cjs +42 -0
  42. package/dist/chat/Preview.d.ts +36 -0
  43. package/dist/chat/Preview.d.ts.map +1 -0
  44. package/dist/chat/Preview.js +40 -0
  45. package/dist/chat/Preview.js.map +1 -0
  46. package/dist/chat/directive.cjs +272 -0
  47. package/dist/chat/directive.d.ts +167 -0
  48. package/dist/chat/directive.d.ts.map +1 -0
  49. package/dist/chat/directive.js +266 -0
  50. package/dist/chat/directive.js.map +1 -0
  51. package/dist/chat/index.cjs +59 -4
  52. package/dist/chat/index.d.ts +31 -3
  53. package/dist/chat/index.d.ts.map +1 -1
  54. package/dist/chat/index.js +31 -3
  55. package/dist/chat/index.js.map +1 -1
  56. package/dist/gallery.html +4 -4
  57. package/dist/styles.css +3 -3
  58. package/dist/theme.css +3 -3
  59. package/package.json +7 -1
@@ -0,0 +1,439 @@
1
+ "use strict";
2
+ /**
3
+ * What paints behind a conversation, and the pure rules that read it.
4
+ *
5
+ * No React and no imports: every rule about what can play, what a link means and
6
+ * what a typed command does is decidable from data alone, so it is decidable in
7
+ * a test without rendering anything — and a surface can read a stored value
8
+ * before it has mounted anything.
9
+ *
10
+ * ONE switch. `mode: 'off'` is the only way to say "no backdrop"; there is
11
+ * deliberately no enable flag beside it, because a pair of them makes two states
12
+ * (off + video, on + off) that paint the same nothing, and that ambiguity is
13
+ * where a bug grows.
14
+ *
15
+ * WHAT CANNOT PLAY, AND WHY: only YouTube and Twitch are embeddable. Every DRM
16
+ * service — Netflix and its neighbours — publishes no embed, wraps playback in
17
+ * Widevine and forbids framing in its terms. There is no key and no clever
18
+ * iframe that changes that, so such a link is classed `other` and NEVER becomes
19
+ * a frame. A menu can offer it as a link that opens in a tab; anything else
20
+ * would be pretending.
21
+ *
22
+ * No provider password is ever asked for. A members-only or age-gated stream
23
+ * plays if — and only if — the viewer is already signed in to that provider in
24
+ * this browser, because the embed rides their own cookies. Collecting those
25
+ * credentials is not a feature this is missing; it is one it refuses.
26
+ */
27
+ Object.defineProperty(exports, "__esModule", { value: true });
28
+ exports.HOSTS = exports.PRESETS = exports.DWELL = exports.BLANK = void 0;
29
+ exports.web = web;
30
+ exports.media = media;
31
+ exports.files = files;
32
+ exports.provider = provider;
33
+ exports.videoId = videoId;
34
+ exports.channel = channel;
35
+ exports.link = link;
36
+ exports.playable = playable;
37
+ exports.youtube = youtube;
38
+ exports.knobs = knobs;
39
+ exports.twitch = twitch;
40
+ exports.merge = merge;
41
+ exports.scene = scene;
42
+ exports.command = command;
43
+ /** A blank canvas — the value a surface starts from and folds changes onto.
44
+ * `off`, because a component library must not begin streaming video on its
45
+ * own; a surface that wants footage says so. */
46
+ exports.BLANK = {
47
+ mode: 'off',
48
+ photo: '',
49
+ video: '',
50
+ playlist: [],
51
+ clips: [],
52
+ loop: true,
53
+ sound: false,
54
+ };
55
+ /** How long a live stream holds the canvas before the list moves on. A Twitch
56
+ * channel never ends, so unlike a video it cannot announce its turn is up. */
57
+ exports.DWELL = 5 * 60 * 1000;
58
+ /**
59
+ * The longest URL that will be kept. Browsers stopped being the limit long ago;
60
+ * the limit that matters is whatever storage a surface writes this to, one quota
61
+ * shared by every preference it holds. A URL is a string a visitor can paste, so
62
+ * without a ceiling one pasted megabyte fills that quota and the NEXT write of
63
+ * any unrelated setting is the one that throws. 2048 is what every server and
64
+ * proxy has accepted since forever, and far past any real link.
65
+ */
66
+ const LONGEST = 2048;
67
+ /** The longest list. Same quota, same reasoning: appending is a loop a visitor
68
+ * can run, so it needs an end. Well past any list anyone watches. */
69
+ const MOST = 64;
70
+ /**
71
+ * A short list to choose from without knowing a link — offered, never a default
72
+ * (`BLANK` is the default, and it is off). Short by intent: the set someone can
73
+ * read in one glance, with the whole rest of the web one paste away.
74
+ */
75
+ exports.PRESETS = [
76
+ { id: 'cetaceans', label: 'Cetaceans', url: 'https://www.youtube.com/watch?v=UY5YH6B4A9o' },
77
+ { id: 'reef', label: 'Reef', url: 'https://www.youtube.com/watch?v=6lZ3CookYNg' },
78
+ { id: 'deep', label: 'Deep water', url: 'https://www.youtube.com/watch?v=7Exxd2ievAw' },
79
+ ];
80
+ /**
81
+ * The media hosts a file may be loaded from besides the page's own origin.
82
+ *
83
+ * The Hanzo store is the default because it is where our surfaces keep footage.
84
+ * A brand serving from somewhere else passes its own list — every function here
85
+ * that reads a URL takes one — and whatever it passes must also be what its
86
+ * `img-src` / `media-src` policy allows, or the element is refused by the
87
+ * browser and paints nothing with no error anywhere.
88
+ */
89
+ exports.HOSTS = ['s3.hanzo.ai', 's3-api.hanzo.ai'];
90
+ /**
91
+ * The URL if it is one worth loading, else ''. Anything that is not http(s) —
92
+ * `javascript:`, `data:`, `blob:` — is refused here, at the boundary, so no
93
+ * caller downstream has to remember to check.
94
+ */
95
+ function web(value) {
96
+ if (typeof value !== 'string' || value.trim() === '' || value.length > LONGEST)
97
+ return '';
98
+ try {
99
+ const url = new URL(value.trim());
100
+ return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : '';
101
+ }
102
+ catch {
103
+ return '';
104
+ }
105
+ }
106
+ const named = (url) => {
107
+ try {
108
+ return new URL(url).hostname.replace(/^www\./, '').toLowerCase();
109
+ }
110
+ catch {
111
+ return '';
112
+ }
113
+ };
114
+ /** Any absolute URL resolves against this, so a relative path can be told from
115
+ * one that only looks relative. Never fetched; it is a ruler, not a place. */
116
+ const HERE = 'https://backdrop.invalid';
117
+ /**
118
+ * A media file that may be painted or played, else ''.
119
+ *
120
+ * Narrower than `web`, and it is the one rule here that is not about what plays.
121
+ * An `<img>` or a `<video src>` fires a GET the moment it renders, with no click
122
+ * and no script, and a STORED one refires on every load afterwards — reporting
123
+ * the viewer's address to whoever owns the host, forever. So a file is ours: a
124
+ * path on this origin, kept RELATIVE because a surface is served under several
125
+ * brands' domains and a stored absolute origin would be wrong on all but one, or
126
+ * a file on a host the caller allows.
127
+ *
128
+ * A URL the browser's own policy would refuse is dropped HERE instead, where it
129
+ * can be answered honestly: a blocked element never fires `load`, so allowing it
130
+ * would mean a blank canvas and nothing anywhere saying why.
131
+ *
132
+ * Backslashes are why this parses rather than matches: `/\evil.example/x.png`
133
+ * looks like a path and resolves to a different host.
134
+ *
135
+ * A photo and a clip are the same question asked about two elements, so they are
136
+ * one function. Two copies would be two places to get the backslash wrong.
137
+ */
138
+ function media(value, hosts = exports.HOSTS) {
139
+ if (typeof value !== 'string' || value.trim() === '' || value.length > LONGEST)
140
+ return '';
141
+ try {
142
+ const url = new URL(value.trim(), HERE);
143
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
144
+ return '';
145
+ if (url.origin === HERE)
146
+ return url.pathname + url.search;
147
+ return hosts.includes(named(url.href)) ? url.href : '';
148
+ }
149
+ catch {
150
+ return '';
151
+ }
152
+ }
153
+ /**
154
+ * A list of files from an array, or from a comma- or space-separated string —
155
+ * whichever a configuration value supplies. Every entry passes through `media`,
156
+ * so an unusable one is dropped rather than trusted, and the whole is capped for
157
+ * the same quota reason a playlist is.
158
+ */
159
+ function files(value, hosts = exports.HOSTS) {
160
+ const raw = Array.isArray(value)
161
+ ? value.map((v) => (typeof v === 'string' ? v : ''))
162
+ : typeof value === 'string'
163
+ ? value.split(/[\s,]+/)
164
+ : [];
165
+ const out = [];
166
+ for (const entry of raw) {
167
+ const ok = media(entry, hosts);
168
+ if (ok && !out.includes(ok))
169
+ out.push(ok);
170
+ if (out.length >= MOST)
171
+ break;
172
+ }
173
+ return out;
174
+ }
175
+ /** Who serves this link, decided by host alone. */
176
+ function provider(url) {
177
+ const name = named(url);
178
+ if (name === 'youtube.com' || name === 'm.youtube.com' || name === 'music.youtube.com') {
179
+ return 'youtube';
180
+ }
181
+ if (name === 'youtu.be')
182
+ return 'youtube';
183
+ if (name === 'twitch.tv' || name === 'm.twitch.tv' || name === 'player.twitch.tv') {
184
+ return 'twitch';
185
+ }
186
+ return 'other';
187
+ }
188
+ /** A YouTube video id is exactly 11 of these. */
189
+ const ID = /^[\w-]{11}$/;
190
+ /**
191
+ * The video id inside a YouTube URL, or '' if there is not one. Accepts the
192
+ * forms people actually paste — watch links, share links, shorts, embeds — and a
193
+ * bare id, which is what the address bar leaves you with often enough.
194
+ */
195
+ function videoId(value) {
196
+ const raw = value.trim();
197
+ if (ID.test(raw))
198
+ return raw;
199
+ const url = web(raw);
200
+ if (!url || provider(url) !== 'youtube')
201
+ return '';
202
+ const { pathname, searchParams } = new URL(url);
203
+ const query = searchParams.get('v') ?? '';
204
+ if (ID.test(query))
205
+ return query;
206
+ // youtu.be/ID, /embed/ID, /shorts/ID, /live/ID — the id is the last segment.
207
+ const last = pathname.split('/').filter(Boolean).pop() ?? '';
208
+ return ID.test(last) ? last : '';
209
+ }
210
+ /**
211
+ * What to ask the Twitch player for: a live `channel`, or a recorded `video`.
212
+ * Empty `name` means the URL names neither.
213
+ *
214
+ * A CLIP names neither. `twitch.tv/<channel>/clip/<slug>` is a few seconds
215
+ * somebody cut out, and the player this builds cannot ask for one — that embed
216
+ * is a different host with a different parameter. Falling back to the channel
217
+ * would put a LIVE STREAM on the canvas in place of the clip that was asked for:
218
+ * not the thing, and not obviously not the thing. So it is unplayable, and a
219
+ * menu says so with a link out. Refusing what we cannot serve beats serving
220
+ * something else.
221
+ */
222
+ function channel(value) {
223
+ const none = { key: 'channel', name: '' };
224
+ const url = web(value);
225
+ if (!url || provider(url) !== 'twitch')
226
+ return none;
227
+ const parsed = new URL(url);
228
+ const parts = parsed.pathname.split('/').filter(Boolean);
229
+ const direct = parsed.searchParams.get('channel') ?? parsed.searchParams.get('video') ?? '';
230
+ if (direct)
231
+ return { key: parsed.searchParams.get('video') ? 'video' : 'channel', name: direct };
232
+ if (parts[0] === 'videos' && parts[1])
233
+ return { key: 'video', name: parts[1] };
234
+ if (parts.includes('clip'))
235
+ return none;
236
+ return { key: 'channel', name: parts[0] ?? '' };
237
+ }
238
+ /** A playlist entry from a pasted URL, or null if it is not a URL at all. */
239
+ function link(value) {
240
+ const url = web(value);
241
+ return url ? { url, provider: provider(url) } : null;
242
+ }
243
+ /** Whether this can actually go on the canvas. `other` never can. */
244
+ function playable(entry) {
245
+ if (entry.provider === 'youtube')
246
+ return videoId(entry.url) !== '';
247
+ if (entry.provider === 'twitch')
248
+ return channel(entry.url).name !== '';
249
+ return false;
250
+ }
251
+ /** Every YouTube player param that keeps the chrome out of frame. */
252
+ const PLAYER = [
253
+ 'autoplay=1',
254
+ 'mute=1',
255
+ 'controls=0',
256
+ 'rel=0',
257
+ 'playsinline=1',
258
+ 'disablekb=1',
259
+ 'fs=0',
260
+ 'iv_load_policy=3',
261
+ 'vq=hd1080',
262
+ // The listening handshake this enables is what lets the canvas wait for a real
263
+ // "playing" report before revealing anything.
264
+ 'enablejsapi=1',
265
+ ].join('&');
266
+ /**
267
+ * The embed URL for one or more YouTube videos.
268
+ *
269
+ * www.youtube.com, not youtube-nocookie: the nocookie host answers embeds with
270
+ * "video player configuration error" (153) in current Chrome.
271
+ *
272
+ * `loop` needs `playlist` even for a single video — that is YouTube's rule, not
273
+ * ours: a lone video with `loop=1` and no list plays once and stops.
274
+ */
275
+ function youtube(ids, loop, origin, start) {
276
+ const [first, ...rest] = ids;
277
+ const params = [PLAYER, `origin=${encodeURIComponent(origin)}`];
278
+ if (loop) {
279
+ params.push('loop=1', `playlist=${(rest.length ? rest : [first]).join(',')}`);
280
+ }
281
+ else if (rest.length) {
282
+ params.push(`playlist=${rest.join(',')}`);
283
+ }
284
+ // Where the video opens, in seconds. Native `start=` on the embed, so it is
285
+ // honoured before the first frame rather than seeked to after.
286
+ if (start && start > 0)
287
+ params.push(`start=${Math.floor(start)}`);
288
+ return `https://www.youtube.com/embed/${first}?${params.join('&')}`;
289
+ }
290
+ /**
291
+ * Optional knobs a brand carries in the SAME video URL, so one configuration
292
+ * value tunes all of them with no rebuild:
293
+ * `start` / `t` seconds to open on
294
+ * `rate` playback speed (YouTube quantises to its allowed steps —
295
+ * 0.25/0.5/0.75/1/…, so 0.42 plays at the nearest, 0.5)
296
+ * `zoom` cover-crop scale, to push a corner watermark off-frame
297
+ * Absent keys return undefined and the caller keeps its own default.
298
+ */
299
+ function knobs(value) {
300
+ const url = web(value);
301
+ if (!url)
302
+ return {};
303
+ const q = new URL(url).searchParams;
304
+ const num = (k) => {
305
+ const v = q.get(k);
306
+ if (v == null)
307
+ return undefined;
308
+ const n = Number(v);
309
+ return Number.isFinite(n) ? n : undefined;
310
+ };
311
+ return { start: num('start') ?? num('t'), rate: num('rate'), zoom: num('zoom') };
312
+ }
313
+ /**
314
+ * The embed URL for a Twitch channel or VOD. `parent` is mandatory — Twitch
315
+ * refuses to frame for a host it was not told about — and must be the bare
316
+ * hostname, no scheme, no port.
317
+ */
318
+ function twitch(value, parent) {
319
+ const { key, name } = channel(value);
320
+ return (`https://player.twitch.tv/?${key}=${encodeURIComponent(name)}` +
321
+ `&parent=${encodeURIComponent(parent)}&muted=true&autoplay=true&controls=false`);
322
+ }
323
+ const MODES = ['off', 'photo', 'video', 'playlist', 'clips'];
324
+ /**
325
+ * Fold an untrusted description of a backdrop onto the current one.
326
+ *
327
+ * This is the ONE gate, and it is deliberately total: anything it cannot make
328
+ * sense of is dropped and the current value kept, so no caller can push a scene
329
+ * into a shape the canvas has to defend against later. Everything a surface
330
+ * takes in comes through here — a settings panel, a card a model can propose,
331
+ * and the read back out of storage on the next load, which matters because bytes
332
+ * in storage are not ours and a rule enforced only on the way in lasts exactly
333
+ * until the page is refreshed.
334
+ */
335
+ function merge(current, change, hosts = exports.HOSTS) {
336
+ if (typeof change !== 'object' || change === null)
337
+ return current;
338
+ const next = { ...current };
339
+ const given = change;
340
+ if (MODES.includes(given.mode))
341
+ next.mode = given.mode;
342
+ if (typeof given.loop === 'boolean')
343
+ next.loop = given.loop;
344
+ if (typeof given.sound === 'boolean')
345
+ next.sound = given.sound;
346
+ const photo = media(given.photo, hosts);
347
+ if (photo)
348
+ next.photo = photo;
349
+ if (given.video !== undefined && videoId(String(given.video))) {
350
+ next.video = String(given.video).trim();
351
+ }
352
+ if (Array.isArray(given.playlist)) {
353
+ // The provider is recomputed from the URL every time rather than believed: a
354
+ // caller claiming `provider: 'youtube'` for a netflix.com link must not be
355
+ // able to talk the canvas into framing it.
356
+ next.playlist = given.playlist
357
+ .map((entry) => link(typeof entry === 'string' ? entry : String(entry?.url ?? '')))
358
+ .filter((entry) => entry !== null)
359
+ .slice(0, MOST);
360
+ }
361
+ // Re-validated on the way in AND on the way out, the same as the playlist, so
362
+ // a file the media policy would refuse cannot reach a `<video src>` by having
363
+ // been written to storage under an older rule.
364
+ if (given.clips !== undefined)
365
+ next.clips = files(given.clips, hosts);
366
+ return next;
367
+ }
368
+ /**
369
+ * What a brand's single configuration string names.
370
+ *
371
+ * A list of direct video files — comma- or space-separated, or any value naming
372
+ * a media file the surface serves — opens on `clips`; anything else stays the
373
+ * YouTube video it has always been, so an existing single-URL value does not
374
+ * change meaning.
375
+ *
376
+ * Pure, and here rather than in a store, because "what does this value mean" is
377
+ * a rule about data, decidable in a test with no browser.
378
+ */
379
+ function scene(value, hosts = exports.HOSTS) {
380
+ const list = files(value, hosts);
381
+ if (list.length > 0)
382
+ return { mode: 'clips', clips: list, video: '' };
383
+ return { mode: 'video', clips: [], video: value };
384
+ }
385
+ /**
386
+ * `/background` or `/bg`, and then a space or the end of the line — not merely a
387
+ * word boundary, which `/background-image …` also satisfies. A command is one of
388
+ * exactly two words; anything else is a message and is sent as one.
389
+ */
390
+ const VERB = /^\/(?:background|bg)(?:\s|$)/i;
391
+ /**
392
+ * Read a `/background` (or `/bg`) line typed into a composer.
393
+ *
394
+ * Answers the scene it asks for, or null when the line is not one of these
395
+ * commands or names something unusable — null meaning "not handled", so the
396
+ * caller sends it on as an ordinary message and the viewer can see for
397
+ * themselves that nothing was applied.
398
+ *
399
+ * /bg off stop painting anything
400
+ * /bg photo <url> a still image
401
+ * /bg video <url> one YouTube video
402
+ * /bg add <url> append to the playlist and switch to it
403
+ * /bg loop on|off repeat at the end
404
+ *
405
+ * There is no `/bg <url>` that guesses: guessing is a second way to say what
406
+ * `photo` and `video` already say exactly, and the guess is wrong precisely when
407
+ * the URL is unusual — which is when being wrong costs the most.
408
+ */
409
+ function command(input, current, hosts = exports.HOSTS) {
410
+ const line = input.trim();
411
+ if (!VERB.test(line))
412
+ return null;
413
+ const [, verb = '', ...rest] = line.split(/\s+/);
414
+ const argument = rest.join(' ').trim();
415
+ switch (verb.toLowerCase()) {
416
+ case 'off':
417
+ return { ...current, mode: 'off' };
418
+ case 'photo': {
419
+ const url = media(argument, hosts);
420
+ return url ? { ...current, mode: 'photo', photo: url } : null;
421
+ }
422
+ case 'video':
423
+ return videoId(argument) ? { ...current, mode: 'video', video: argument.trim() } : null;
424
+ case 'add': {
425
+ // Through `merge` so the list has ONE keeper of its length, rather than a
426
+ // second append here that forgets what the first one enforces.
427
+ const entry = link(argument);
428
+ return entry && playable(entry)
429
+ ? merge(current, { mode: 'playlist', playlist: [...current.playlist, entry] }, hosts)
430
+ : null;
431
+ }
432
+ case 'loop': {
433
+ const on = argument.toLowerCase();
434
+ return on === 'on' || on === 'off' ? { ...current, loop: on === 'on' } : null;
435
+ }
436
+ default:
437
+ return null;
438
+ }
439
+ }
@@ -0,0 +1,235 @@
1
+ /**
2
+ * What paints behind a conversation, and the pure rules that read it.
3
+ *
4
+ * No React and no imports: every rule about what can play, what a link means and
5
+ * what a typed command does is decidable from data alone, so it is decidable in
6
+ * a test without rendering anything — and a surface can read a stored value
7
+ * before it has mounted anything.
8
+ *
9
+ * ONE switch. `mode: 'off'` is the only way to say "no backdrop"; there is
10
+ * deliberately no enable flag beside it, because a pair of them makes two states
11
+ * (off + video, on + off) that paint the same nothing, and that ambiguity is
12
+ * where a bug grows.
13
+ *
14
+ * WHAT CANNOT PLAY, AND WHY: only YouTube and Twitch are embeddable. Every DRM
15
+ * service — Netflix and its neighbours — publishes no embed, wraps playback in
16
+ * Widevine and forbids framing in its terms. There is no key and no clever
17
+ * iframe that changes that, so such a link is classed `other` and NEVER becomes
18
+ * a frame. A menu can offer it as a link that opens in a tab; anything else
19
+ * would be pretending.
20
+ *
21
+ * No provider password is ever asked for. A members-only or age-gated stream
22
+ * plays if — and only if — the viewer is already signed in to that provider in
23
+ * this browser, because the embed rides their own cookies. Collecting those
24
+ * credentials is not a feature this is missing; it is one it refuses.
25
+ */
26
+ /** Who serves a link. `other` means "we cannot play this", not "unknown". */
27
+ export type Provider = 'youtube' | 'twitch' | 'other';
28
+ /**
29
+ * What the backdrop is painting. `off` is the master switch.
30
+ *
31
+ * `clips` is OURS where `video` and `playlist` are third-party frames: a list of
32
+ * direct video files served from a host we allow, played as a preloaded
33
+ * crossfade with nothing revealed until a frame is really painted. It is the one
34
+ * mode that is a `<video>` this code controls rather than a frame it can only
35
+ * ask, which is why an emotion-driven scene (see `Persona`) is built on it.
36
+ */
37
+ export type Mode = 'off' | 'photo' | 'video' | 'playlist' | 'clips';
38
+ /** One entry of a playlist: what was pasted, and who serves it. */
39
+ export interface Link {
40
+ url: string;
41
+ provider: Provider;
42
+ }
43
+ /** What is on the canvas. Every field, so a scene is one value to store, merge
44
+ * and pass — never a bag of independent settings that can disagree. */
45
+ export interface Scene {
46
+ mode: Mode;
47
+ /** Image URL. Empty means unset. */
48
+ photo: string;
49
+ /** YouTube URL (or bare id). Empty means unset. */
50
+ video: string;
51
+ playlist: Link[];
52
+ /** Direct video files, played as a preloaded crossfade. Empty means none. */
53
+ clips: string[];
54
+ /** Repeat at the end — the single video, or the whole list. */
55
+ loop: boolean;
56
+ /**
57
+ * Play the backdrop's audio. OFF by default, and that default is the point: a
58
+ * page that starts making noise on its own is a page people close, and this
59
+ * one is scenery behind someone's reading.
60
+ *
61
+ * It is not a URL parameter. The embed must start MUTED or it does not start
62
+ * at all — every current browser refuses autoplay with sound — so `mute=1`
63
+ * stays in the player params and the sound is turned on afterwards, through
64
+ * the API, once the player reports it is actually playing. Turning this on IS
65
+ * the gesture that permits it: a setting a person just toggled is a user
66
+ * activation, which is what the autoplay policy asks for.
67
+ */
68
+ sound: boolean;
69
+ }
70
+ /** A blank canvas — the value a surface starts from and folds changes onto.
71
+ * `off`, because a component library must not begin streaming video on its
72
+ * own; a surface that wants footage says so. */
73
+ export declare const BLANK: Scene;
74
+ /** How long a live stream holds the canvas before the list moves on. A Twitch
75
+ * channel never ends, so unlike a video it cannot announce its turn is up. */
76
+ export declare const DWELL: number;
77
+ /** A backdrop offered by name, for someone who does not have a URL in mind. */
78
+ export interface Preset {
79
+ id: string;
80
+ label: string;
81
+ url: string;
82
+ }
83
+ /**
84
+ * A short list to choose from without knowing a link — offered, never a default
85
+ * (`BLANK` is the default, and it is off). Short by intent: the set someone can
86
+ * read in one glance, with the whole rest of the web one paste away.
87
+ */
88
+ export declare const PRESETS: readonly Preset[];
89
+ /**
90
+ * The media hosts a file may be loaded from besides the page's own origin.
91
+ *
92
+ * The Hanzo store is the default because it is where our surfaces keep footage.
93
+ * A brand serving from somewhere else passes its own list — every function here
94
+ * that reads a URL takes one — and whatever it passes must also be what its
95
+ * `img-src` / `media-src` policy allows, or the element is refused by the
96
+ * browser and paints nothing with no error anywhere.
97
+ */
98
+ export declare const HOSTS: readonly string[];
99
+ /**
100
+ * The URL if it is one worth loading, else ''. Anything that is not http(s) —
101
+ * `javascript:`, `data:`, `blob:` — is refused here, at the boundary, so no
102
+ * caller downstream has to remember to check.
103
+ */
104
+ export declare function web(value: unknown): string;
105
+ /**
106
+ * A media file that may be painted or played, else ''.
107
+ *
108
+ * Narrower than `web`, and it is the one rule here that is not about what plays.
109
+ * An `<img>` or a `<video src>` fires a GET the moment it renders, with no click
110
+ * and no script, and a STORED one refires on every load afterwards — reporting
111
+ * the viewer's address to whoever owns the host, forever. So a file is ours: a
112
+ * path on this origin, kept RELATIVE because a surface is served under several
113
+ * brands' domains and a stored absolute origin would be wrong on all but one, or
114
+ * a file on a host the caller allows.
115
+ *
116
+ * A URL the browser's own policy would refuse is dropped HERE instead, where it
117
+ * can be answered honestly: a blocked element never fires `load`, so allowing it
118
+ * would mean a blank canvas and nothing anywhere saying why.
119
+ *
120
+ * Backslashes are why this parses rather than matches: `/\evil.example/x.png`
121
+ * looks like a path and resolves to a different host.
122
+ *
123
+ * A photo and a clip are the same question asked about two elements, so they are
124
+ * one function. Two copies would be two places to get the backslash wrong.
125
+ */
126
+ export declare function media(value: unknown, hosts?: readonly string[]): string;
127
+ /**
128
+ * A list of files from an array, or from a comma- or space-separated string —
129
+ * whichever a configuration value supplies. Every entry passes through `media`,
130
+ * so an unusable one is dropped rather than trusted, and the whole is capped for
131
+ * the same quota reason a playlist is.
132
+ */
133
+ export declare function files(value: unknown, hosts?: readonly string[]): string[];
134
+ /** Who serves this link, decided by host alone. */
135
+ export declare function provider(url: string): Provider;
136
+ /**
137
+ * The video id inside a YouTube URL, or '' if there is not one. Accepts the
138
+ * forms people actually paste — watch links, share links, shorts, embeds — and a
139
+ * bare id, which is what the address bar leaves you with often enough.
140
+ */
141
+ export declare function videoId(value: string): string;
142
+ /**
143
+ * What to ask the Twitch player for: a live `channel`, or a recorded `video`.
144
+ * Empty `name` means the URL names neither.
145
+ *
146
+ * A CLIP names neither. `twitch.tv/<channel>/clip/<slug>` is a few seconds
147
+ * somebody cut out, and the player this builds cannot ask for one — that embed
148
+ * is a different host with a different parameter. Falling back to the channel
149
+ * would put a LIVE STREAM on the canvas in place of the clip that was asked for:
150
+ * not the thing, and not obviously not the thing. So it is unplayable, and a
151
+ * menu says so with a link out. Refusing what we cannot serve beats serving
152
+ * something else.
153
+ */
154
+ export declare function channel(value: string): {
155
+ key: 'channel' | 'video';
156
+ name: string;
157
+ };
158
+ /** A playlist entry from a pasted URL, or null if it is not a URL at all. */
159
+ export declare function link(value: string): Link | null;
160
+ /** Whether this can actually go on the canvas. `other` never can. */
161
+ export declare function playable(entry: Link): boolean;
162
+ /**
163
+ * The embed URL for one or more YouTube videos.
164
+ *
165
+ * www.youtube.com, not youtube-nocookie: the nocookie host answers embeds with
166
+ * "video player configuration error" (153) in current Chrome.
167
+ *
168
+ * `loop` needs `playlist` even for a single video — that is YouTube's rule, not
169
+ * ours: a lone video with `loop=1` and no list plays once and stops.
170
+ */
171
+ export declare function youtube(ids: string[], loop: boolean, origin: string, start?: number): string;
172
+ /**
173
+ * Optional knobs a brand carries in the SAME video URL, so one configuration
174
+ * value tunes all of them with no rebuild:
175
+ * `start` / `t` seconds to open on
176
+ * `rate` playback speed (YouTube quantises to its allowed steps —
177
+ * 0.25/0.5/0.75/1/…, so 0.42 plays at the nearest, 0.5)
178
+ * `zoom` cover-crop scale, to push a corner watermark off-frame
179
+ * Absent keys return undefined and the caller keeps its own default.
180
+ */
181
+ export declare function knobs(value: string): {
182
+ start?: number;
183
+ rate?: number;
184
+ zoom?: number;
185
+ };
186
+ /**
187
+ * The embed URL for a Twitch channel or VOD. `parent` is mandatory — Twitch
188
+ * refuses to frame for a host it was not told about — and must be the bare
189
+ * hostname, no scheme, no port.
190
+ */
191
+ export declare function twitch(value: string, parent: string): string;
192
+ /**
193
+ * Fold an untrusted description of a backdrop onto the current one.
194
+ *
195
+ * This is the ONE gate, and it is deliberately total: anything it cannot make
196
+ * sense of is dropped and the current value kept, so no caller can push a scene
197
+ * into a shape the canvas has to defend against later. Everything a surface
198
+ * takes in comes through here — a settings panel, a card a model can propose,
199
+ * and the read back out of storage on the next load, which matters because bytes
200
+ * in storage are not ours and a rule enforced only on the way in lasts exactly
201
+ * until the page is refreshed.
202
+ */
203
+ export declare function merge(current: Scene, change: unknown, hosts?: readonly string[]): Scene;
204
+ /**
205
+ * What a brand's single configuration string names.
206
+ *
207
+ * A list of direct video files — comma- or space-separated, or any value naming
208
+ * a media file the surface serves — opens on `clips`; anything else stays the
209
+ * YouTube video it has always been, so an existing single-URL value does not
210
+ * change meaning.
211
+ *
212
+ * Pure, and here rather than in a store, because "what does this value mean" is
213
+ * a rule about data, decidable in a test with no browser.
214
+ */
215
+ export declare function scene(value: string, hosts?: readonly string[]): Pick<Scene, 'mode' | 'video' | 'clips'>;
216
+ /**
217
+ * Read a `/background` (or `/bg`) line typed into a composer.
218
+ *
219
+ * Answers the scene it asks for, or null when the line is not one of these
220
+ * commands or names something unusable — null meaning "not handled", so the
221
+ * caller sends it on as an ordinary message and the viewer can see for
222
+ * themselves that nothing was applied.
223
+ *
224
+ * /bg off stop painting anything
225
+ * /bg photo <url> a still image
226
+ * /bg video <url> one YouTube video
227
+ * /bg add <url> append to the playlist and switch to it
228
+ * /bg loop on|off repeat at the end
229
+ *
230
+ * There is no `/bg <url>` that guesses: guessing is a second way to say what
231
+ * `photo` and `video` already say exactly, and the guess is wrong precisely when
232
+ * the URL is unusual — which is when being wrong costs the most.
233
+ */
234
+ export declare function command(input: string, current: Scene, hosts?: readonly string[]): Scene | null;
235
+ //# sourceMappingURL=scene.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene.d.ts","sourceRoot":"","sources":["../../src/backdrop/scene.ts"],"names":[],"mappings":"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"}