@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.
- package/dist/backdrop/Backdrop.cjs +420 -0
- package/dist/backdrop/Backdrop.d.ts +53 -0
- package/dist/backdrop/Backdrop.d.ts.map +1 -0
- package/dist/backdrop/Backdrop.js +418 -0
- package/dist/backdrop/Backdrop.js.map +1 -0
- package/dist/backdrop/index.cjs +50 -0
- package/dist/backdrop/index.d.ts +29 -0
- package/dist/backdrop/index.d.ts.map +1 -0
- package/dist/backdrop/index.js +29 -0
- package/dist/backdrop/index.js.map +1 -0
- package/dist/backdrop/scene.cjs +439 -0
- package/dist/backdrop/scene.d.ts +235 -0
- package/dist/backdrop/scene.d.ts.map +1 -0
- package/dist/backdrop/scene.js +423 -0
- package/dist/backdrop/scene.js.map +1 -0
- package/dist/chat/Chat.cjs +23 -1
- package/dist/chat/Chat.d.ts +6 -3
- package/dist/chat/Chat.d.ts.map +1 -1
- package/dist/chat/Chat.js +23 -1
- package/dist/chat/Chat.js.map +1 -1
- package/dist/chat/Inspector.cjs +51 -0
- package/dist/chat/Inspector.d.ts +49 -0
- package/dist/chat/Inspector.d.ts.map +1 -0
- package/dist/chat/Inspector.js +49 -0
- package/dist/chat/Inspector.js.map +1 -0
- package/dist/chat/Message.cjs +3 -1
- package/dist/chat/Message.d.ts +3 -1
- package/dist/chat/Message.d.ts.map +1 -1
- package/dist/chat/Message.js +3 -1
- package/dist/chat/Message.js.map +1 -1
- package/dist/chat/Parts.cjs +159 -0
- package/dist/chat/Parts.d.ts +186 -0
- package/dist/chat/Parts.d.ts.map +1 -0
- package/dist/chat/Parts.js +153 -0
- package/dist/chat/Parts.js.map +1 -0
- package/dist/chat/Persona.cjs +34 -0
- package/dist/chat/Persona.d.ts +37 -0
- package/dist/chat/Persona.d.ts.map +1 -0
- package/dist/chat/Persona.js +30 -0
- package/dist/chat/Persona.js.map +1 -0
- package/dist/chat/Preview.cjs +42 -0
- package/dist/chat/Preview.d.ts +36 -0
- package/dist/chat/Preview.d.ts.map +1 -0
- package/dist/chat/Preview.js +40 -0
- package/dist/chat/Preview.js.map +1 -0
- package/dist/chat/directive.cjs +272 -0
- package/dist/chat/directive.d.ts +167 -0
- package/dist/chat/directive.d.ts.map +1 -0
- package/dist/chat/directive.js +266 -0
- package/dist/chat/directive.js.map +1 -0
- package/dist/chat/index.cjs +59 -4
- package/dist/chat/index.d.ts +31 -3
- package/dist/chat/index.d.ts.map +1 -1
- package/dist/chat/index.js +31 -3
- package/dist/chat/index.js.map +1 -1
- package/dist/gallery.html +4 -4
- package/dist/styles.css +3 -3
- package/dist/theme.css +3 -3
- 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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"}
|