@hanzo/ui 8.27.3 → 8.27.5
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/backends/gui/select.cjs +38 -27
- package/dist/backends/gui/select.d.ts +14 -5
- package/dist/backends/gui/select.d.ts.map +1 -1
- package/dist/backends/gui/select.js +42 -31
- package/dist/backends/gui/select.js.map +1 -1
- package/dist/backends/gui/tooltip.cjs +22 -3
- package/dist/backends/gui/tooltip.d.ts +12 -4
- package/dist/backends/gui/tooltip.d.ts.map +1 -1
- package/dist/backends/gui/tooltip.js +22 -3
- package/dist/backends/gui/tooltip.js.map +1 -1
- 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,423 @@
|
|
|
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
|
+
/** A blank canvas — the value a surface starts from and folds changes onto.
|
|
27
|
+
* `off`, because a component library must not begin streaming video on its
|
|
28
|
+
* own; a surface that wants footage says so. */
|
|
29
|
+
export const BLANK = {
|
|
30
|
+
mode: 'off',
|
|
31
|
+
photo: '',
|
|
32
|
+
video: '',
|
|
33
|
+
playlist: [],
|
|
34
|
+
clips: [],
|
|
35
|
+
loop: true,
|
|
36
|
+
sound: false,
|
|
37
|
+
};
|
|
38
|
+
/** How long a live stream holds the canvas before the list moves on. A Twitch
|
|
39
|
+
* channel never ends, so unlike a video it cannot announce its turn is up. */
|
|
40
|
+
export const DWELL = 5 * 60 * 1000;
|
|
41
|
+
/**
|
|
42
|
+
* The longest URL that will be kept. Browsers stopped being the limit long ago;
|
|
43
|
+
* the limit that matters is whatever storage a surface writes this to, one quota
|
|
44
|
+
* shared by every preference it holds. A URL is a string a visitor can paste, so
|
|
45
|
+
* without a ceiling one pasted megabyte fills that quota and the NEXT write of
|
|
46
|
+
* any unrelated setting is the one that throws. 2048 is what every server and
|
|
47
|
+
* proxy has accepted since forever, and far past any real link.
|
|
48
|
+
*/
|
|
49
|
+
const LONGEST = 2048;
|
|
50
|
+
/** The longest list. Same quota, same reasoning: appending is a loop a visitor
|
|
51
|
+
* can run, so it needs an end. Well past any list anyone watches. */
|
|
52
|
+
const MOST = 64;
|
|
53
|
+
/**
|
|
54
|
+
* A short list to choose from without knowing a link — offered, never a default
|
|
55
|
+
* (`BLANK` is the default, and it is off). Short by intent: the set someone can
|
|
56
|
+
* read in one glance, with the whole rest of the web one paste away.
|
|
57
|
+
*/
|
|
58
|
+
export const PRESETS = [
|
|
59
|
+
{ id: 'cetaceans', label: 'Cetaceans', url: 'https://www.youtube.com/watch?v=UY5YH6B4A9o' },
|
|
60
|
+
{ id: 'reef', label: 'Reef', url: 'https://www.youtube.com/watch?v=6lZ3CookYNg' },
|
|
61
|
+
{ id: 'deep', label: 'Deep water', url: 'https://www.youtube.com/watch?v=7Exxd2ievAw' },
|
|
62
|
+
];
|
|
63
|
+
/**
|
|
64
|
+
* The media hosts a file may be loaded from besides the page's own origin.
|
|
65
|
+
*
|
|
66
|
+
* The Hanzo store is the default because it is where our surfaces keep footage.
|
|
67
|
+
* A brand serving from somewhere else passes its own list — every function here
|
|
68
|
+
* that reads a URL takes one — and whatever it passes must also be what its
|
|
69
|
+
* `img-src` / `media-src` policy allows, or the element is refused by the
|
|
70
|
+
* browser and paints nothing with no error anywhere.
|
|
71
|
+
*/
|
|
72
|
+
export const HOSTS = ['s3.hanzo.ai', 's3-api.hanzo.ai'];
|
|
73
|
+
/**
|
|
74
|
+
* The URL if it is one worth loading, else ''. Anything that is not http(s) —
|
|
75
|
+
* `javascript:`, `data:`, `blob:` — is refused here, at the boundary, so no
|
|
76
|
+
* caller downstream has to remember to check.
|
|
77
|
+
*/
|
|
78
|
+
export function web(value) {
|
|
79
|
+
if (typeof value !== 'string' || value.trim() === '' || value.length > LONGEST)
|
|
80
|
+
return '';
|
|
81
|
+
try {
|
|
82
|
+
const url = new URL(value.trim());
|
|
83
|
+
return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : '';
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const named = (url) => {
|
|
90
|
+
try {
|
|
91
|
+
return new URL(url).hostname.replace(/^www\./, '').toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return '';
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
/** Any absolute URL resolves against this, so a relative path can be told from
|
|
98
|
+
* one that only looks relative. Never fetched; it is a ruler, not a place. */
|
|
99
|
+
const HERE = 'https://backdrop.invalid';
|
|
100
|
+
/**
|
|
101
|
+
* A media file that may be painted or played, else ''.
|
|
102
|
+
*
|
|
103
|
+
* Narrower than `web`, and it is the one rule here that is not about what plays.
|
|
104
|
+
* An `<img>` or a `<video src>` fires a GET the moment it renders, with no click
|
|
105
|
+
* and no script, and a STORED one refires on every load afterwards — reporting
|
|
106
|
+
* the viewer's address to whoever owns the host, forever. So a file is ours: a
|
|
107
|
+
* path on this origin, kept RELATIVE because a surface is served under several
|
|
108
|
+
* brands' domains and a stored absolute origin would be wrong on all but one, or
|
|
109
|
+
* a file on a host the caller allows.
|
|
110
|
+
*
|
|
111
|
+
* A URL the browser's own policy would refuse is dropped HERE instead, where it
|
|
112
|
+
* can be answered honestly: a blocked element never fires `load`, so allowing it
|
|
113
|
+
* would mean a blank canvas and nothing anywhere saying why.
|
|
114
|
+
*
|
|
115
|
+
* Backslashes are why this parses rather than matches: `/\evil.example/x.png`
|
|
116
|
+
* looks like a path and resolves to a different host.
|
|
117
|
+
*
|
|
118
|
+
* A photo and a clip are the same question asked about two elements, so they are
|
|
119
|
+
* one function. Two copies would be two places to get the backslash wrong.
|
|
120
|
+
*/
|
|
121
|
+
export function media(value, hosts = HOSTS) {
|
|
122
|
+
if (typeof value !== 'string' || value.trim() === '' || value.length > LONGEST)
|
|
123
|
+
return '';
|
|
124
|
+
try {
|
|
125
|
+
const url = new URL(value.trim(), HERE);
|
|
126
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
127
|
+
return '';
|
|
128
|
+
if (url.origin === HERE)
|
|
129
|
+
return url.pathname + url.search;
|
|
130
|
+
return hosts.includes(named(url.href)) ? url.href : '';
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* A list of files from an array, or from a comma- or space-separated string —
|
|
138
|
+
* whichever a configuration value supplies. Every entry passes through `media`,
|
|
139
|
+
* so an unusable one is dropped rather than trusted, and the whole is capped for
|
|
140
|
+
* the same quota reason a playlist is.
|
|
141
|
+
*/
|
|
142
|
+
export function files(value, hosts = HOSTS) {
|
|
143
|
+
const raw = Array.isArray(value)
|
|
144
|
+
? value.map((v) => (typeof v === 'string' ? v : ''))
|
|
145
|
+
: typeof value === 'string'
|
|
146
|
+
? value.split(/[\s,]+/)
|
|
147
|
+
: [];
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const entry of raw) {
|
|
150
|
+
const ok = media(entry, hosts);
|
|
151
|
+
if (ok && !out.includes(ok))
|
|
152
|
+
out.push(ok);
|
|
153
|
+
if (out.length >= MOST)
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
/** Who serves this link, decided by host alone. */
|
|
159
|
+
export function provider(url) {
|
|
160
|
+
const name = named(url);
|
|
161
|
+
if (name === 'youtube.com' || name === 'm.youtube.com' || name === 'music.youtube.com') {
|
|
162
|
+
return 'youtube';
|
|
163
|
+
}
|
|
164
|
+
if (name === 'youtu.be')
|
|
165
|
+
return 'youtube';
|
|
166
|
+
if (name === 'twitch.tv' || name === 'm.twitch.tv' || name === 'player.twitch.tv') {
|
|
167
|
+
return 'twitch';
|
|
168
|
+
}
|
|
169
|
+
return 'other';
|
|
170
|
+
}
|
|
171
|
+
/** A YouTube video id is exactly 11 of these. */
|
|
172
|
+
const ID = /^[\w-]{11}$/;
|
|
173
|
+
/**
|
|
174
|
+
* The video id inside a YouTube URL, or '' if there is not one. Accepts the
|
|
175
|
+
* forms people actually paste — watch links, share links, shorts, embeds — and a
|
|
176
|
+
* bare id, which is what the address bar leaves you with often enough.
|
|
177
|
+
*/
|
|
178
|
+
export function videoId(value) {
|
|
179
|
+
const raw = value.trim();
|
|
180
|
+
if (ID.test(raw))
|
|
181
|
+
return raw;
|
|
182
|
+
const url = web(raw);
|
|
183
|
+
if (!url || provider(url) !== 'youtube')
|
|
184
|
+
return '';
|
|
185
|
+
const { pathname, searchParams } = new URL(url);
|
|
186
|
+
const query = searchParams.get('v') ?? '';
|
|
187
|
+
if (ID.test(query))
|
|
188
|
+
return query;
|
|
189
|
+
// youtu.be/ID, /embed/ID, /shorts/ID, /live/ID — the id is the last segment.
|
|
190
|
+
const last = pathname.split('/').filter(Boolean).pop() ?? '';
|
|
191
|
+
return ID.test(last) ? last : '';
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* What to ask the Twitch player for: a live `channel`, or a recorded `video`.
|
|
195
|
+
* Empty `name` means the URL names neither.
|
|
196
|
+
*
|
|
197
|
+
* A CLIP names neither. `twitch.tv/<channel>/clip/<slug>` is a few seconds
|
|
198
|
+
* somebody cut out, and the player this builds cannot ask for one — that embed
|
|
199
|
+
* is a different host with a different parameter. Falling back to the channel
|
|
200
|
+
* would put a LIVE STREAM on the canvas in place of the clip that was asked for:
|
|
201
|
+
* not the thing, and not obviously not the thing. So it is unplayable, and a
|
|
202
|
+
* menu says so with a link out. Refusing what we cannot serve beats serving
|
|
203
|
+
* something else.
|
|
204
|
+
*/
|
|
205
|
+
export function channel(value) {
|
|
206
|
+
const none = { key: 'channel', name: '' };
|
|
207
|
+
const url = web(value);
|
|
208
|
+
if (!url || provider(url) !== 'twitch')
|
|
209
|
+
return none;
|
|
210
|
+
const parsed = new URL(url);
|
|
211
|
+
const parts = parsed.pathname.split('/').filter(Boolean);
|
|
212
|
+
const direct = parsed.searchParams.get('channel') ?? parsed.searchParams.get('video') ?? '';
|
|
213
|
+
if (direct)
|
|
214
|
+
return { key: parsed.searchParams.get('video') ? 'video' : 'channel', name: direct };
|
|
215
|
+
if (parts[0] === 'videos' && parts[1])
|
|
216
|
+
return { key: 'video', name: parts[1] };
|
|
217
|
+
if (parts.includes('clip'))
|
|
218
|
+
return none;
|
|
219
|
+
return { key: 'channel', name: parts[0] ?? '' };
|
|
220
|
+
}
|
|
221
|
+
/** A playlist entry from a pasted URL, or null if it is not a URL at all. */
|
|
222
|
+
export function link(value) {
|
|
223
|
+
const url = web(value);
|
|
224
|
+
return url ? { url, provider: provider(url) } : null;
|
|
225
|
+
}
|
|
226
|
+
/** Whether this can actually go on the canvas. `other` never can. */
|
|
227
|
+
export function playable(entry) {
|
|
228
|
+
if (entry.provider === 'youtube')
|
|
229
|
+
return videoId(entry.url) !== '';
|
|
230
|
+
if (entry.provider === 'twitch')
|
|
231
|
+
return channel(entry.url).name !== '';
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
/** Every YouTube player param that keeps the chrome out of frame. */
|
|
235
|
+
const PLAYER = [
|
|
236
|
+
'autoplay=1',
|
|
237
|
+
'mute=1',
|
|
238
|
+
'controls=0',
|
|
239
|
+
'rel=0',
|
|
240
|
+
'playsinline=1',
|
|
241
|
+
'disablekb=1',
|
|
242
|
+
'fs=0',
|
|
243
|
+
'iv_load_policy=3',
|
|
244
|
+
'vq=hd1080',
|
|
245
|
+
// The listening handshake this enables is what lets the canvas wait for a real
|
|
246
|
+
// "playing" report before revealing anything.
|
|
247
|
+
'enablejsapi=1',
|
|
248
|
+
].join('&');
|
|
249
|
+
/**
|
|
250
|
+
* The embed URL for one or more YouTube videos.
|
|
251
|
+
*
|
|
252
|
+
* www.youtube.com, not youtube-nocookie: the nocookie host answers embeds with
|
|
253
|
+
* "video player configuration error" (153) in current Chrome.
|
|
254
|
+
*
|
|
255
|
+
* `loop` needs `playlist` even for a single video — that is YouTube's rule, not
|
|
256
|
+
* ours: a lone video with `loop=1` and no list plays once and stops.
|
|
257
|
+
*/
|
|
258
|
+
export function youtube(ids, loop, origin, start) {
|
|
259
|
+
const [first, ...rest] = ids;
|
|
260
|
+
const params = [PLAYER, `origin=${encodeURIComponent(origin)}`];
|
|
261
|
+
if (loop) {
|
|
262
|
+
params.push('loop=1', `playlist=${(rest.length ? rest : [first]).join(',')}`);
|
|
263
|
+
}
|
|
264
|
+
else if (rest.length) {
|
|
265
|
+
params.push(`playlist=${rest.join(',')}`);
|
|
266
|
+
}
|
|
267
|
+
// Where the video opens, in seconds. Native `start=` on the embed, so it is
|
|
268
|
+
// honoured before the first frame rather than seeked to after.
|
|
269
|
+
if (start && start > 0)
|
|
270
|
+
params.push(`start=${Math.floor(start)}`);
|
|
271
|
+
return `https://www.youtube.com/embed/${first}?${params.join('&')}`;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Optional knobs a brand carries in the SAME video URL, so one configuration
|
|
275
|
+
* value tunes all of them with no rebuild:
|
|
276
|
+
* `start` / `t` seconds to open on
|
|
277
|
+
* `rate` playback speed (YouTube quantises to its allowed steps —
|
|
278
|
+
* 0.25/0.5/0.75/1/…, so 0.42 plays at the nearest, 0.5)
|
|
279
|
+
* `zoom` cover-crop scale, to push a corner watermark off-frame
|
|
280
|
+
* Absent keys return undefined and the caller keeps its own default.
|
|
281
|
+
*/
|
|
282
|
+
export function knobs(value) {
|
|
283
|
+
const url = web(value);
|
|
284
|
+
if (!url)
|
|
285
|
+
return {};
|
|
286
|
+
const q = new URL(url).searchParams;
|
|
287
|
+
const num = (k) => {
|
|
288
|
+
const v = q.get(k);
|
|
289
|
+
if (v == null)
|
|
290
|
+
return undefined;
|
|
291
|
+
const n = Number(v);
|
|
292
|
+
return Number.isFinite(n) ? n : undefined;
|
|
293
|
+
};
|
|
294
|
+
return { start: num('start') ?? num('t'), rate: num('rate'), zoom: num('zoom') };
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The embed URL for a Twitch channel or VOD. `parent` is mandatory — Twitch
|
|
298
|
+
* refuses to frame for a host it was not told about — and must be the bare
|
|
299
|
+
* hostname, no scheme, no port.
|
|
300
|
+
*/
|
|
301
|
+
export function twitch(value, parent) {
|
|
302
|
+
const { key, name } = channel(value);
|
|
303
|
+
return (`https://player.twitch.tv/?${key}=${encodeURIComponent(name)}` +
|
|
304
|
+
`&parent=${encodeURIComponent(parent)}&muted=true&autoplay=true&controls=false`);
|
|
305
|
+
}
|
|
306
|
+
const MODES = ['off', 'photo', 'video', 'playlist', 'clips'];
|
|
307
|
+
/**
|
|
308
|
+
* Fold an untrusted description of a backdrop onto the current one.
|
|
309
|
+
*
|
|
310
|
+
* This is the ONE gate, and it is deliberately total: anything it cannot make
|
|
311
|
+
* sense of is dropped and the current value kept, so no caller can push a scene
|
|
312
|
+
* into a shape the canvas has to defend against later. Everything a surface
|
|
313
|
+
* takes in comes through here — a settings panel, a card a model can propose,
|
|
314
|
+
* and the read back out of storage on the next load, which matters because bytes
|
|
315
|
+
* in storage are not ours and a rule enforced only on the way in lasts exactly
|
|
316
|
+
* until the page is refreshed.
|
|
317
|
+
*/
|
|
318
|
+
export function merge(current, change, hosts = HOSTS) {
|
|
319
|
+
if (typeof change !== 'object' || change === null)
|
|
320
|
+
return current;
|
|
321
|
+
const next = { ...current };
|
|
322
|
+
const given = change;
|
|
323
|
+
if (MODES.includes(given.mode))
|
|
324
|
+
next.mode = given.mode;
|
|
325
|
+
if (typeof given.loop === 'boolean')
|
|
326
|
+
next.loop = given.loop;
|
|
327
|
+
if (typeof given.sound === 'boolean')
|
|
328
|
+
next.sound = given.sound;
|
|
329
|
+
const photo = media(given.photo, hosts);
|
|
330
|
+
if (photo)
|
|
331
|
+
next.photo = photo;
|
|
332
|
+
if (given.video !== undefined && videoId(String(given.video))) {
|
|
333
|
+
next.video = String(given.video).trim();
|
|
334
|
+
}
|
|
335
|
+
if (Array.isArray(given.playlist)) {
|
|
336
|
+
// The provider is recomputed from the URL every time rather than believed: a
|
|
337
|
+
// caller claiming `provider: 'youtube'` for a netflix.com link must not be
|
|
338
|
+
// able to talk the canvas into framing it.
|
|
339
|
+
next.playlist = given.playlist
|
|
340
|
+
.map((entry) => link(typeof entry === 'string' ? entry : String(entry?.url ?? '')))
|
|
341
|
+
.filter((entry) => entry !== null)
|
|
342
|
+
.slice(0, MOST);
|
|
343
|
+
}
|
|
344
|
+
// Re-validated on the way in AND on the way out, the same as the playlist, so
|
|
345
|
+
// a file the media policy would refuse cannot reach a `<video src>` by having
|
|
346
|
+
// been written to storage under an older rule.
|
|
347
|
+
if (given.clips !== undefined)
|
|
348
|
+
next.clips = files(given.clips, hosts);
|
|
349
|
+
return next;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* What a brand's single configuration string names.
|
|
353
|
+
*
|
|
354
|
+
* A list of direct video files — comma- or space-separated, or any value naming
|
|
355
|
+
* a media file the surface serves — opens on `clips`; anything else stays the
|
|
356
|
+
* YouTube video it has always been, so an existing single-URL value does not
|
|
357
|
+
* change meaning.
|
|
358
|
+
*
|
|
359
|
+
* Pure, and here rather than in a store, because "what does this value mean" is
|
|
360
|
+
* a rule about data, decidable in a test with no browser.
|
|
361
|
+
*/
|
|
362
|
+
export function scene(value, hosts = HOSTS) {
|
|
363
|
+
const list = files(value, hosts);
|
|
364
|
+
if (list.length > 0)
|
|
365
|
+
return { mode: 'clips', clips: list, video: '' };
|
|
366
|
+
return { mode: 'video', clips: [], video: value };
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* `/background` or `/bg`, and then a space or the end of the line — not merely a
|
|
370
|
+
* word boundary, which `/background-image …` also satisfies. A command is one of
|
|
371
|
+
* exactly two words; anything else is a message and is sent as one.
|
|
372
|
+
*/
|
|
373
|
+
const VERB = /^\/(?:background|bg)(?:\s|$)/i;
|
|
374
|
+
/**
|
|
375
|
+
* Read a `/background` (or `/bg`) line typed into a composer.
|
|
376
|
+
*
|
|
377
|
+
* Answers the scene it asks for, or null when the line is not one of these
|
|
378
|
+
* commands or names something unusable — null meaning "not handled", so the
|
|
379
|
+
* caller sends it on as an ordinary message and the viewer can see for
|
|
380
|
+
* themselves that nothing was applied.
|
|
381
|
+
*
|
|
382
|
+
* /bg off stop painting anything
|
|
383
|
+
* /bg photo <url> a still image
|
|
384
|
+
* /bg video <url> one YouTube video
|
|
385
|
+
* /bg add <url> append to the playlist and switch to it
|
|
386
|
+
* /bg loop on|off repeat at the end
|
|
387
|
+
*
|
|
388
|
+
* There is no `/bg <url>` that guesses: guessing is a second way to say what
|
|
389
|
+
* `photo` and `video` already say exactly, and the guess is wrong precisely when
|
|
390
|
+
* the URL is unusual — which is when being wrong costs the most.
|
|
391
|
+
*/
|
|
392
|
+
export function command(input, current, hosts = HOSTS) {
|
|
393
|
+
const line = input.trim();
|
|
394
|
+
if (!VERB.test(line))
|
|
395
|
+
return null;
|
|
396
|
+
const [, verb = '', ...rest] = line.split(/\s+/);
|
|
397
|
+
const argument = rest.join(' ').trim();
|
|
398
|
+
switch (verb.toLowerCase()) {
|
|
399
|
+
case 'off':
|
|
400
|
+
return { ...current, mode: 'off' };
|
|
401
|
+
case 'photo': {
|
|
402
|
+
const url = media(argument, hosts);
|
|
403
|
+
return url ? { ...current, mode: 'photo', photo: url } : null;
|
|
404
|
+
}
|
|
405
|
+
case 'video':
|
|
406
|
+
return videoId(argument) ? { ...current, mode: 'video', video: argument.trim() } : null;
|
|
407
|
+
case 'add': {
|
|
408
|
+
// Through `merge` so the list has ONE keeper of its length, rather than a
|
|
409
|
+
// second append here that forgets what the first one enforces.
|
|
410
|
+
const entry = link(argument);
|
|
411
|
+
return entry && playable(entry)
|
|
412
|
+
? merge(current, { mode: 'playlist', playlist: [...current.playlist, entry] }, hosts)
|
|
413
|
+
: null;
|
|
414
|
+
}
|
|
415
|
+
case 'loop': {
|
|
416
|
+
const on = argument.toLowerCase();
|
|
417
|
+
return on === 'on' || on === 'off' ? { ...current, loop: on === 'on' } : null;
|
|
418
|
+
}
|
|
419
|
+
default:
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
//# sourceMappingURL=scene.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scene.js","sourceRoot":"","sources":["../../src/backdrop/scene.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';"use strict";
|
|
2
|
-
|
|
2
|
+
"use client";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
exports.SelectScrollDownButton = exports.SelectScrollUpButton = exports.SelectSeparator = exports.SelectItem = exports.SelectLabel = exports.SelectContent = exports.SelectTrigger = exports.SelectValue = exports.SelectGroup = exports.Select = void 0;
|
|
5
5
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
@@ -12,38 +12,47 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
12
12
|
* `SelectGroup`. An item is written `<SelectItem value="x">Label</SelectItem>`
|
|
13
13
|
* and the numbering always matches render order.
|
|
14
14
|
*/
|
|
15
|
+
const react_1 = require("react");
|
|
15
16
|
const gui_1 = require("@hanzo/gui");
|
|
16
17
|
const lucide_icons_2_1 = require("@hanzogui/lucide-icons-2");
|
|
17
|
-
const
|
|
18
|
+
const control_1 = require("./control.cjs");
|
|
19
|
+
const gesture_1 = require("./gesture.cjs");
|
|
18
20
|
const ink_1 = require("./ink.cjs");
|
|
19
21
|
const slot_1 = require("./slot.cjs");
|
|
20
|
-
const gesture_1 = require("./gesture.cjs");
|
|
21
|
-
const control_1 = require("./control.cjs");
|
|
22
22
|
const ROW_H = 32;
|
|
23
23
|
const TRIGGER_H = control_1.CONTROL_H;
|
|
24
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Disabling the control is one fact with two spellings: gui puts `disabled` on
|
|
26
|
+
* the Trigger, the compound API puts it on the root. The root declares it and
|
|
27
|
+
* the trigger honours it, so a call site can say either and mean the same thing.
|
|
28
|
+
*/
|
|
29
|
+
const Disabled = /* @__PURE__ */ (0, react_1.createContext)(false);
|
|
30
|
+
const Select = /* @__PURE__ */ Object.assign(({ disabled = false, ...props }) => ((0, jsx_runtime_1.jsx)(Disabled.Provider, { value: disabled, children: (0, jsx_runtime_1.jsx)(gui_1.Select, { ...props }) })), gui_1.Select);
|
|
25
31
|
exports.Select = Select;
|
|
26
32
|
const SelectGroup = gui_1.Select.Group;
|
|
27
33
|
exports.SelectGroup = SelectGroup;
|
|
28
34
|
const SelectValue = gui_1.Select.Value;
|
|
29
35
|
exports.SelectValue = SelectValue;
|
|
30
|
-
const SelectTrigger = ({ children, ...props }) =>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
const SelectTrigger = ({ children, disabled, ...props }) => {
|
|
37
|
+
const inherited = (0, react_1.useContext)(Disabled);
|
|
38
|
+
return ((0, jsx_runtime_1.jsxs)(gui_1.Select.Trigger, { ...(0, slot_1.slot)("select-trigger"), unstyled: true, disabled: disabled ?? inherited, ...control_1.FIELD, items: "center", justify: "space-between", gap: "$2",
|
|
39
|
+
// A FLOOR, not a pin. `height` alone clips a child taller than the box, and
|
|
40
|
+
// the build stays green while it does — that is why 01ac46dce moved Button
|
|
41
|
+
// off a pinned height. Select and Input kept theirs, so the same trap was
|
|
42
|
+
// still armed in the two controls that sit in every form.
|
|
43
|
+
height: "auto", minHeight: TRIGGER_H,
|
|
44
|
+
// The trigger states its own vertical padding because otherwise something
|
|
45
|
+
// else states it. `unstyled` does not survive gui's ListItem (it is
|
|
46
|
+
// destructured into a local and never reaches the frame), so the frame
|
|
47
|
+
// styles itself at `size: '$true'` and writes `paddingVertical: $3.5` — and
|
|
48
|
+
// under `height: auto` that padding, not the floor, is what decides the box:
|
|
49
|
+
// 14 + a 20px line + 14 + 2px of border rendered a 50px Select beside a 36px
|
|
50
|
+
// Input. At 0 the floor decides again and `items="center"` does the
|
|
51
|
+
// centring, which is exactly how the Input already behaves.
|
|
52
|
+
py: 0, cursor: "pointer", ...(0, gesture_1.touch)(TRIGGER_H, 44, "y"), ...props, children: [(0, ink_1.ink)(children, undefined, { size: "$3" }), (0, jsx_runtime_1.jsx)(gui_1.Select.Icon, { children: (0, jsx_runtime_1.jsx)(lucide_icons_2_1.ChevronDown, { size: 16, opacity: 0.5 }) })] }));
|
|
53
|
+
};
|
|
45
54
|
exports.SelectTrigger = SelectTrigger;
|
|
46
|
-
const SelectItem = ({ children, ...props }) => ((0, jsx_runtime_1.jsxs)(gui_1.Select.Item, { ...(0, slot_1.slot)(
|
|
55
|
+
const SelectItem = ({ children, ...props }) => ((0, jsx_runtime_1.jsxs)(gui_1.Select.Item, { ...(0, slot_1.slot)("select-item"), unstyled: true, index: props.index ?? 0, items: "center", gap: "$2", minH: ROW_H, pl: "$2", pr: "$7", rounded: "$2", cursor: "pointer", ...(0, gesture_1.touch)(ROW_H, 44, "y"), hoverStyle: { bg: "$raised" }, focusStyle: { bg: "$raised" }, ...props, children: [(0, jsx_runtime_1.jsx)(gui_1.Select.ItemText, { children: children }), (0, jsx_runtime_1.jsx)(gui_1.XStack, { position: "absolute", r: "$2", items: "center", justify: "center", pointerEvents: "none", children: (0, jsx_runtime_1.jsx)(gui_1.Select.ItemIndicator, { children: (0, jsx_runtime_1.jsx)(lucide_icons_2_1.Check, { size: 16 }) }) })] }));
|
|
47
56
|
exports.SelectItem = SelectItem;
|
|
48
57
|
/** Depth-first stamp of `index` onto every `SelectItem` in render order. */
|
|
49
58
|
const stampIndices = (children, next) => react_1.Children.map(children, (child) => {
|
|
@@ -52,18 +61,20 @@ const stampIndices = (children, next) => react_1.Children.map(children, (child)
|
|
|
52
61
|
if (child.type === SelectItem)
|
|
53
62
|
return (0, react_1.cloneElement)(child, { index: next() });
|
|
54
63
|
const kids = child.props.children;
|
|
55
|
-
return kids === undefined
|
|
64
|
+
return kids === undefined
|
|
65
|
+
? child
|
|
66
|
+
: (0, react_1.cloneElement)(child, undefined, stampIndices(kids, next));
|
|
56
67
|
});
|
|
57
|
-
const SelectScrollUpButton = (props) => ((0, jsx_runtime_1.jsx)(gui_1.Select.ScrollUpButton, { ...(0, slot_1.slot)(
|
|
68
|
+
const SelectScrollUpButton = (props) => ((0, jsx_runtime_1.jsx)(gui_1.Select.ScrollUpButton, { ...(0, slot_1.slot)("select-scroll-up-button"), items: "center", justify: "center", py: "$1", ...props, children: (0, jsx_runtime_1.jsx)(lucide_icons_2_1.ChevronUp, { size: 16 }) }));
|
|
58
69
|
exports.SelectScrollUpButton = SelectScrollUpButton;
|
|
59
|
-
const SelectScrollDownButton = (props) => ((0, jsx_runtime_1.jsx)(gui_1.Select.ScrollDownButton, { ...(0, slot_1.slot)(
|
|
70
|
+
const SelectScrollDownButton = (props) => ((0, jsx_runtime_1.jsx)(gui_1.Select.ScrollDownButton, { ...(0, slot_1.slot)("select-scroll-down-button"), items: "center", justify: "center", py: "$1", ...props, children: (0, jsx_runtime_1.jsx)(lucide_icons_2_1.ChevronDown, { size: 16 }) }));
|
|
60
71
|
exports.SelectScrollDownButton = SelectScrollDownButton;
|
|
61
72
|
const SelectContent = ({ children, ...props }) => {
|
|
62
73
|
let n = 0;
|
|
63
|
-
return ((0, jsx_runtime_1.jsxs)(gui_1.Select.Content, { ...(0, slot_1.slot)(
|
|
74
|
+
return ((0, jsx_runtime_1.jsxs)(gui_1.Select.Content, { ...(0, slot_1.slot)("select-content"), ...props, children: [(0, jsx_runtime_1.jsx)(SelectScrollUpButton, {}), (0, jsx_runtime_1.jsx)(gui_1.Select.Viewport, { minW: 128, p: "$1", bg: "$panel", borderWidth: 1, borderColor: "$borderColor", rounded: "$4", children: stampIndices(children, () => n++) }), (0, jsx_runtime_1.jsx)(SelectScrollDownButton, {})] }));
|
|
64
75
|
};
|
|
65
76
|
exports.SelectContent = SelectContent;
|
|
66
|
-
const SelectLabel = (props) => ((0, jsx_runtime_1.jsx)(gui_1.Select.Label, { ...(0, slot_1.slot)(
|
|
77
|
+
const SelectLabel = (props) => ((0, jsx_runtime_1.jsx)(gui_1.Select.Label, { ...(0, slot_1.slot)("select-label"), px: "$2", py: "$1.5", fontSize: "$1", color: "$quiet", ...props }));
|
|
67
78
|
exports.SelectLabel = SelectLabel;
|
|
68
|
-
const SelectSeparator = (props) => ((0, jsx_runtime_1.jsx)(gui_1.SelectSeparator, { ...(0, slot_1.slot)(
|
|
79
|
+
const SelectSeparator = (props) => ((0, jsx_runtime_1.jsx)(gui_1.SelectSeparator, { ...(0, slot_1.slot)("select-separator"), my: "$1", borderColor: "$borderColor", ...props }));
|
|
69
80
|
exports.SelectSeparator = SelectSeparator;
|
|
@@ -7,20 +7,29 @@
|
|
|
7
7
|
* `SelectGroup`. An item is written `<SelectItem value="x">Label</SelectItem>`
|
|
8
8
|
* and the numbering always matches render order.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
10
|
+
import { type ComponentProps, type ReactNode } from "react";
|
|
11
|
+
import { Select as GuiSelect, SelectSeparator as GuiSelectSeparator } from "@hanzo/gui";
|
|
12
|
+
export type SelectProps = ComponentProps<typeof GuiSelect> & {
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The root's own signature, then the compound statics gui hangs off it. Written
|
|
17
|
+
* out because the inferred type of an `Object.assign` cannot be named in a
|
|
18
|
+
* declaration file, and this package emits one.
|
|
19
|
+
*/
|
|
20
|
+
type SelectRoot = ((props: SelectProps) => ReactNode) & typeof GuiSelect;
|
|
21
|
+
declare const Select: SelectRoot;
|
|
13
22
|
declare const SelectGroup: typeof GuiSelect.Group;
|
|
14
23
|
declare const SelectValue: typeof GuiSelect.Value;
|
|
15
24
|
export type SelectTriggerProps = ComponentProps<typeof GuiSelect.Trigger>;
|
|
16
25
|
export type SelectContentProps = ComponentProps<typeof GuiSelect.Content>;
|
|
17
|
-
export type SelectItemProps = Omit<ComponentProps<typeof GuiSelect.Item>,
|
|
26
|
+
export type SelectItemProps = Omit<ComponentProps<typeof GuiSelect.Item>, "index"> & {
|
|
18
27
|
index?: number;
|
|
19
28
|
};
|
|
20
29
|
export type SelectLabelProps = ComponentProps<typeof GuiSelect.Label>;
|
|
21
30
|
export type SelectSeparatorProps = ComponentProps<typeof GuiSelectSeparator>;
|
|
22
31
|
export type SelectScrollButtonProps = ComponentProps<typeof GuiSelect.ScrollUpButton>;
|
|
23
|
-
declare const SelectTrigger: ({ children, ...props }: SelectTriggerProps) => import("react").JSX.Element;
|
|
32
|
+
declare const SelectTrigger: ({ children, disabled, ...props }: SelectTriggerProps) => import("react").JSX.Element;
|
|
24
33
|
declare const SelectItem: ({ children, ...props }: SelectItemProps) => import("react").JSX.Element;
|
|
25
34
|
declare const SelectScrollUpButton: (props: SelectScrollButtonProps) => import("react").JSX.Element;
|
|
26
35
|
declare const SelectScrollDownButton: (props: SelectScrollButtonProps) => import("react").JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../../src/backends/gui/select.tsx"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../../src/backends/gui/select.tsx"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,OAAO,EAML,KAAK,cAAc,EACnB,KAAK,SAAS,EACf,MAAM,OAAO,CAAA;AACd,OAAO,EACL,MAAM,IAAI,SAAS,EACnB,eAAe,IAAI,kBAAkB,EAEtC,MAAM,YAAY,CAAA;AAkBnB,MAAM,MAAM,WAAW,GAAG,cAAc,CAAC,OAAO,SAAS,CAAC,GAAG;IAC3D,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAED;;;;GAIG;AACH,KAAK,UAAU,GAAG,CAAC,CAAC,KAAK,EAAE,WAAW,KAAK,SAAS,CAAC,GAAG,OAAO,SAAS,CAAA;AAExE,QAAA,MAAM,MAAM,EAAE,UAOC,CAAA;AACf,QAAA,MAAM,WAAW,EAAE,OAAO,SAAS,CAAC,KAAuB,CAAA;AAC3D,QAAA,MAAM,WAAW,EAAE,OAAO,SAAS,CAAC,KAAuB,CAAA;AAE3D,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,OAAO,SAAS,CAAC,OAAO,CAAC,CAAA;AACzE,MAAM,MAAM,kBAAkB,GAAG,cAAc,CAAC,OAAO,SAAS,CAAC,OAAO,CAAC,CAAA;AACzE,MAAM,MAAM,eAAe,GAAG,IAAI,CAChC,cAAc,CAAC,OAAO,SAAS,CAAC,IAAI,CAAC,EACrC,OAAO,CACR,GAAG;IACF,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AACD,MAAM,MAAM,gBAAgB,GAAG,cAAc,CAAC,OAAO,SAAS,CAAC,KAAK,CAAC,CAAA;AACrE,MAAM,MAAM,oBAAoB,GAAG,cAAc,CAAC,OAAO,kBAAkB,CAAC,CAAA;AAC5E,MAAM,MAAM,uBAAuB,GAAG,cAAc,CAClD,OAAO,SAAS,CAAC,cAAc,CAChC,CAAA;AAED,QAAA,MAAM,aAAa,qCAIhB,kBAAkB,gCAwCpB,CAAA;AAED,QAAA,MAAM,UAAU,2BAA4B,eAAe,gCA8B1D,CAAA;AAcD,QAAA,MAAM,oBAAoB,UAAW,uBAAuB,gCAU3D,CAAA;AAED,QAAA,MAAM,sBAAsB,UAAW,uBAAuB,gCAU7D,CAAA;AAED,QAAA,MAAM,aAAa,2BAA4B,kBAAkB,gCAkBhE,CAAA;AAED,QAAA,MAAM,WAAW,UAAW,gBAAgB,gCAS3C,CAAA;AAED,QAAA,MAAM,eAAe,UAAW,oBAAoB,gCAOnD,CAAA;AAED,OAAO,EACL,MAAM,EACN,WAAW,EACX,WAAW,EACX,aAAa,EACb,aAAa,EACb,WAAW,EACX,UAAU,EACV,eAAe,EACf,oBAAoB,EACpB,sBAAsB,GACvB,CAAA"}
|