@vanillaskyai/video 0.10.15 → 0.10.17
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/CHANGELOG.md +12 -0
- package/dist/react.js +19 -34
- package/dist/server.js +6 -3
- package/package.json +1 -1
- package/starters/video-chat/package.json +1 -1
- package/starters/video-chat/stock.ts +9 -4
- package/styles/video-chat.css +42 -20
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ VanillaSky follows semantic versioning. This changelog begins with the 0.1 beta.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.10.17
|
|
8
|
+
|
|
9
|
+
- Let video playback prepare without waiting for an optional poster, while retaining actual video-frame and narration readiness checks.
|
|
10
|
+
- Guide shorter, useful spoken openings and compact streamed briefs, with clearer direct answers, comparisons, and narration-aligned actions.
|
|
11
|
+
|
|
12
|
+
## 0.10.16
|
|
13
|
+
|
|
14
|
+
- Start the first fully prepared scene without waiting for an eight-second startup buffer, while preserving narration and visual readiness.
|
|
15
|
+
- Keep generated narration alive when a superseded playback attempt rejects after pause and resume.
|
|
16
|
+
- Give ending suggestions larger responsive cards with complete labels, including narrow embedded players.
|
|
17
|
+
- Match simple singular and plural stock subjects in the starter without discarding required subjects or exclusions.
|
|
18
|
+
|
|
7
19
|
## 0.10.15
|
|
8
20
|
|
|
9
21
|
- Recover a mislabeled first chat brief only when its complete authored content validates, preserving its shots and ending. Keep malformed JSON and incomplete or later records rejected.
|
package/dist/react.js
CHANGED
|
@@ -1385,18 +1385,6 @@ function createSceneTimeline(options) {
|
|
|
1385
1385
|
}
|
|
1386
1386
|
|
|
1387
1387
|
// src/player/scene-readiness.ts
|
|
1388
|
-
var READY_AHEAD_SECONDS = 8;
|
|
1389
|
-
function canStartPreparedSequence(scenes, complete) {
|
|
1390
|
-
let seconds = 0;
|
|
1391
|
-
let count = 0;
|
|
1392
|
-
for (const scene of scenes) {
|
|
1393
|
-
if (!scene) break;
|
|
1394
|
-
count += 1;
|
|
1395
|
-
seconds += scene.timing.fixedDuration ?? 0;
|
|
1396
|
-
if (seconds >= READY_AHEAD_SECONDS) return true;
|
|
1397
|
-
}
|
|
1398
|
-
return complete && count > 0 && count === scenes.length;
|
|
1399
|
-
}
|
|
1400
1388
|
function preparedSceneDuration(scene, spokenSeconds, metadata) {
|
|
1401
1389
|
const timing = metadata?.timing;
|
|
1402
1390
|
const authored = (timing?.revealSeconds ?? 0) + (timing?.holdSeconds ?? 0) + (timing?.exitSeconds ?? 0);
|
|
@@ -1513,9 +1501,7 @@ async function prepareSceneMedia(variables, signal) {
|
|
|
1513
1501
|
const url = typeof variables.mediaUrl === "string" ? variables.mediaUrl.trim() : "";
|
|
1514
1502
|
if (!url || variables.mediaType === "gradient") return "graphic";
|
|
1515
1503
|
const video = variables.mediaType === "video" || /\.(mp4|webm|mov)(?:[?#]|$)/i.test(url);
|
|
1516
|
-
|
|
1517
|
-
if (video && !poster) return "awaiting-video-frame";
|
|
1518
|
-
let posterFailed = false;
|
|
1504
|
+
if (video) return "awaiting-video-frame";
|
|
1519
1505
|
await new Promise((resolve, reject) => {
|
|
1520
1506
|
const image = new Image();
|
|
1521
1507
|
let settled = false;
|
|
@@ -1538,13 +1524,10 @@ async function prepareSceneMedia(variables, signal) {
|
|
|
1538
1524
|
void (image.decode ? image.decode() : Promise.resolve()).then(() => finish(), () => finish(new Error("Could not prepare scene image")));
|
|
1539
1525
|
};
|
|
1540
1526
|
image.onerror = () => finish(new Error("Could not prepare scene image"));
|
|
1541
|
-
image.src =
|
|
1527
|
+
image.src = url;
|
|
1542
1528
|
if (image.complete && image.naturalWidth > 0) image.onload(new Event("load"));
|
|
1543
|
-
}).catch((error) => {
|
|
1544
|
-
if (!video || signal.aborted) throw error;
|
|
1545
|
-
posterFailed = true;
|
|
1546
1529
|
});
|
|
1547
|
-
return
|
|
1530
|
+
return "image";
|
|
1548
1531
|
}
|
|
1549
1532
|
|
|
1550
1533
|
// src/video-chat/voice.ts
|
|
@@ -1586,6 +1569,18 @@ function createVideoChatVoice(options = {}) {
|
|
|
1586
1569
|
let disposed = false;
|
|
1587
1570
|
let generatedSpeechUnavailable = false;
|
|
1588
1571
|
let playbackFailure;
|
|
1572
|
+
let playbackAttempt = 0;
|
|
1573
|
+
const playGenerated = (element, fail) => {
|
|
1574
|
+
const attempt = ++playbackAttempt;
|
|
1575
|
+
const reject = () => {
|
|
1576
|
+
if (attempt === playbackAttempt && !held && sounding === element) fail?.();
|
|
1577
|
+
};
|
|
1578
|
+
try {
|
|
1579
|
+
void element.play().catch(reject);
|
|
1580
|
+
} catch {
|
|
1581
|
+
reject();
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1589
1584
|
const notifyFallback = () => {
|
|
1590
1585
|
try {
|
|
1591
1586
|
void Promise.resolve(options.onFallback?.()).catch(() => void 0);
|
|
@@ -1698,6 +1693,7 @@ function createVideoChatVoice(options = {}) {
|
|
|
1698
1693
|
return { seconds: line.seconds, ...line.source === "generated" && line.measured === true ? { supportsOffsets: true } : {} };
|
|
1699
1694
|
},
|
|
1700
1695
|
pause() {
|
|
1696
|
+
playbackAttempt++;
|
|
1701
1697
|
held = true;
|
|
1702
1698
|
sounding?.pause();
|
|
1703
1699
|
globalThis.speechSynthesis?.pause();
|
|
@@ -1712,12 +1708,7 @@ function createVideoChatVoice(options = {}) {
|
|
|
1712
1708
|
} catch {
|
|
1713
1709
|
}
|
|
1714
1710
|
}
|
|
1715
|
-
if (sounding)
|
|
1716
|
-
const fail = playbackFailure;
|
|
1717
|
-
void sounding.play().catch(() => {
|
|
1718
|
-
if (!held) fail?.();
|
|
1719
|
-
});
|
|
1720
|
-
}
|
|
1711
|
+
if (sounding) playGenerated(sounding, playbackFailure);
|
|
1721
1712
|
if (!silent) globalThis.speechSynthesis?.resume();
|
|
1722
1713
|
},
|
|
1723
1714
|
setMuted(muted) {
|
|
@@ -1848,13 +1839,7 @@ function createVideoChatVoice(options = {}) {
|
|
|
1848
1839
|
element.onended = finish;
|
|
1849
1840
|
element.onerror = fail;
|
|
1850
1841
|
signal.addEventListener("abort", stop, { once: true });
|
|
1851
|
-
|
|
1852
|
-
if (!held) void element.play().catch(() => {
|
|
1853
|
-
if (!held) fail();
|
|
1854
|
-
});
|
|
1855
|
-
} catch {
|
|
1856
|
-
fail();
|
|
1857
|
-
}
|
|
1842
|
+
if (!held) playGenerated(element, fail);
|
|
1858
1843
|
});
|
|
1859
1844
|
} catch {
|
|
1860
1845
|
playbackFailed = true;
|
|
@@ -2372,7 +2357,7 @@ function useVideoChatSession(options = {}) {
|
|
|
2372
2357
|
return;
|
|
2373
2358
|
}
|
|
2374
2359
|
if (!timeline) {
|
|
2375
|
-
if (!style || openingActive || heldRef.current || available === appended
|
|
2360
|
+
if (!style || openingActive || heldRef.current || available === appended) return;
|
|
2376
2361
|
timeline = createSceneTimeline({ style, orientation });
|
|
2377
2362
|
timelineRef.current = timeline;
|
|
2378
2363
|
openingController.abort(new DOMException("Opening replaced by response", "AbortError"));
|
package/dist/server.js
CHANGED
|
@@ -1217,18 +1217,21 @@ function createVideoChatResponseInstructions(generatedVideoAvailable, openingAlr
|
|
|
1217
1217
|
return [
|
|
1218
1218
|
'Use the exact record type "answer" for the first brief and "shot" for developing beats. Output JSON records only, with no prose outside them, including when explaining a limitation.',
|
|
1219
1219
|
"Write a complete, intentful video answer as newline-delimited JSON. Match the user's form and tone; mixed intents can combine directions.",
|
|
1220
|
-
`First write one brief: {"type":"answer","intent":"explanation|story|comedy|imagination|practical","opening":"
|
|
1220
|
+
`First write one brief: {"type":"answer","intent":"explanation|story|comedy|imagination|practical","opening":"one useful spoken line of 4\u20137 ordinary words","subject":"literal visual subject","development":"the essential development of this answer","visualDirection":"consistent subjects, appearance and visual approach","ending":{"title":"short meaningful chapter title, at most 65 characters","narration":"the authored payoff","subject":"literal subject","action":"visible action or change","durationSec":${clipDurationSec},"continuity":"cut|continue"}}.`,
|
|
1221
|
+
"The opening should take roughly 2\u20133 seconds at a natural pace: give the core answer, a useful starting cue, or the story's immediate situation. No greeting, topic announcement, promise to explain, or description of loading. Never compress away an essential qualifier just to hit the word target.",
|
|
1221
1222
|
`Then stream each developing shot on its own line: {"type":"shot","title":"short meaningful chapter title, at most 65 characters","narration":"the exact spoken beat","subject":"2\u20138 literal filmable words, at most 80 characters","action":"concrete subject, action or visible change and useful framing","durationSec":${clipDurationSec},"continuity":"cut|continue"}.`,
|
|
1222
1223
|
`The selected footage mode is ${mode === "pexels" ? "Pexels stock search: use literal filmable subjects; never imply stock proves a mechanism or depicts fictional events exactly" : `AI video, with at most ${generatedVideoAvailable ? maxGeneratedVideos : 0} generation attempts`}. Missing footage becomes the authored chapter title, with complete narration. Never truncate already-authored narration when footage fails. The host selects providers; do not make source choices.`,
|
|
1223
1224
|
...mode === "cinematic" ? [generatedVideoAvailable && maxGeneratedVideos > 0 ? `Plan at most ${maxGeneratedVideos} generated-video beats in total, including the saved ending. Use at most ${maxGeneratedVideos - 1} developing shot records; the ending uses the remaining beat. The opening chapter does not consume a generated clip. Before writing, choose a concise, complete treatment that fits this budget: combine related ideas, preserve essential facts and qualifiers, and finish the requested answer. Do not plan an extra chapter tail simply because generation attempts will run out.${maxGeneratedVideos === 1 ? " Put the complete answer in the saved ending, set development to an empty string, and emit no developing shot records." : ""}` : "No generated-video attempts are available; plan a complete chapter-led answer with a useful ending. Do not omit the answer to satisfy a zero clip budget."] : [],
|
|
1224
1225
|
...mode === "pexels" ? ["Stock queries must retain the essential subject, activity and distinguishing equipment in the shot's subject field, within its word limit. That field alone is the search query; action and visualDirection do not refine it. Prefer common observable actions with usable framing. Do not replace the required actor or activity with scenery, a different sport or a loosely related setting. Preserve fictional or comic narration, but do not depend on stock showing an exact invented expression or sequence; choose an illustrative action that supports the beat."] : [],
|
|
1225
1226
|
...mode === "pexels" ? ['Include stockSelection on every shot and the saved ending when the essential subject is known: "stockSelection":{"subject":"essential actor or object category","activity":"optional literal activity","equipment":"optional distinguishing equipment","exclude":["optional contradictory subject or activity"]}. Each phrase must be 1\u20134 words and at most 48 characters; exclude has at most 3 phrases. The essential subject is separate from the setting: do not use scenery, mood, camera framing or incidental appearance as the actor. Keep the search query broad enough to find footage; the optional hint helps select results without substituting a different actor or task. Use exclusions only for actual contradictions, not every detail absent from the story. Omit unknown fields or the whole hint rather than inventing an anchor. This is selection guidance, not verification that footage depicts the exact narration.'] : [],
|
|
1227
|
+
"Keep development to one concise sentence and visualDirection to the few details needed for consistency. Emit the complete brief, then the first developing shot immediately when developing shots are needed and allowed by the budget; otherwise end after the brief. Continue the same stream without an outline, recap or second planning pass. The saved ending must still contain the complete payoff before the brief is emitted.",
|
|
1226
1228
|
"For a very short answer whose ending alone fulfills the request, development may be empty and no developing shots are needed. Otherwise, develop the essential content before the ending.",
|
|
1227
1229
|
"The brief's ending is saved and played after your developing shots. Do not repeat it as a shot. Stop writing after the last developing shot. No technical events, identifiers, template choices, media providers, URLs or unlisted fields.",
|
|
1228
1230
|
"Every shot uses moving footage with separate narration and subtitles. Generated footage is silent: do not ask its subjects to speak or render words. No headline cards or on-screen explanatory text.",
|
|
1229
1231
|
`Each clip has at most ${clipDurationSec} seconds. Write spoken beats that fit naturally, usually ${Math.floor(clipDurationSec * 1.6)}\u2013${Math.floor(clipDurationSec * 2)} words per shot. Split longer ideas across purposeful shots, preserving facts and qualifiers. Never truncate a claim to meet a word target. Use only the shots needed within the total duration, including the ending; do not pad to a fixed count.`,
|
|
1230
|
-
"Identify the full answer and its ending before developing shots. Each action must support what is said: camera movement alone is not progression. Vary scale, viewpoint and meaningful details while keeping subjects consistent.",
|
|
1231
|
-
"Explanations:
|
|
1232
|
+
"Identify the full answer and its ending before developing shots. Each shot should carry one clear action or change, timed to the narration of that beat; do not describe an outcome before its shot. Use framing that lets the viewer see the relevant action, not just its setting. Each action must support what is said: camera movement alone is not progression. Vary scale, viewpoint and meaningful details while keeping subjects consistent.",
|
|
1233
|
+
"Explanations: answer the actual question first, then show the essential causal link rather than a tour of the topic. Clarify the actual causal mechanism, separating physical cause from a metaphor. Generated cutaways and animation illustrate ideas; they are not factual evidence. Preserve uncertainty, quantities and conditions; never invent evidence or quotations.",
|
|
1234
|
+
"Comparisons and choices: Compare the same criteria for both alternatives, using only supported or supplied differences. Finish with the requested choice and the condition that makes it appropriate; if evidence is insufficient, say what is missing. Do not invent scores, advantages or a winner. Use explanation or practical intent as appropriate, not a new record type.",
|
|
1232
1235
|
"Stories: portray characters making choices and experiencing consequences; use consistent character descriptions and an earned resolution, not a promised next scene.",
|
|
1233
1236
|
"Comedy: establish the premise, time the visual or spoken reveal, allow a reaction beat, and stop on the payoff without explaining the joke.",
|
|
1234
1237
|
"Imagination: make the impossible action concrete, establish the world's internal rules and keep its imagery consistent. Do not replace imagination with an explanation of it.",
|
package/package.json
CHANGED
|
@@ -17,6 +17,11 @@ const cache = new Map<string, { expires: number; media: StockVideo | null }>();
|
|
|
17
17
|
const words = (value: string): string[] => value.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
18
18
|
const ignored = new Set(['a','an','the','in','on','at','of','with','and','to']);
|
|
19
19
|
const terms = (value: string) => words(value).filter(word => !ignored.has(word));
|
|
20
|
+
// Only simple English -s forms; avoid aggressive stemming and ambiguous endings.
|
|
21
|
+
function wordForm(word: string) {
|
|
22
|
+
return /^[a-z]{3,}s$/.test(word) && !/(?:ss|us|is|ies)$/.test(word) && word !== 'news'
|
|
23
|
+
? word.slice(0, -1) : word;
|
|
24
|
+
}
|
|
20
25
|
function selectionHint(value: unknown) {
|
|
21
26
|
const phrase = (input: unknown) => {
|
|
22
27
|
if (typeof input !== 'string') return undefined;
|
|
@@ -48,7 +53,7 @@ export async function findStockFootage(query: string, orientation: VideoOrientat
|
|
|
48
53
|
const normalized = query.trim().toLowerCase().replace(/\s+/g, " ");
|
|
49
54
|
const tokens = words(normalized);
|
|
50
55
|
const selection = selectionHint(rawSelection);
|
|
51
|
-
const key = JSON.stringify({version:
|
|
56
|
+
const key = JSON.stringify({version: 3, orientation, query: normalized, selection});
|
|
52
57
|
const apiKey = process.env.PEXELS_API_KEY;
|
|
53
58
|
if (!apiKey || !tokens.length || normalized.length > 80 || tokens.length > 8) return null;
|
|
54
59
|
const existing = cache.get(key);
|
|
@@ -66,10 +71,10 @@ export async function findStockFootage(query: string, orientation: VideoOrientat
|
|
|
66
71
|
const slug = new URL(video.url).pathname.replace(/^\/video\//, "");
|
|
67
72
|
const title = typeof video.title === "string" ? video.title : "";
|
|
68
73
|
const tags = Array.isArray(video.tags) ? video.tags.filter((tag): tag is string => typeof tag === "string").join(" ") : "";
|
|
69
|
-
const subject = words(`${slug} ${title} ${typeof video.description === "string" ? video.description : ""} ${tags}`).filter(token => !/^\d+$/.test(token));
|
|
70
|
-
let matches = tokens.filter(token => subject.includes(token)).length;
|
|
74
|
+
const subject = words(`${slug} ${title} ${typeof video.description === "string" ? video.description : ""} ${tags}`).filter(token => !/^\d+$/.test(token)).map(wordForm);
|
|
75
|
+
let matches = tokens.filter(token => subject.includes(wordForm(token))).length;
|
|
71
76
|
if (selection && subject.length) {
|
|
72
|
-
const covers = (phrase: string) => terms(phrase).every(word => subject.includes(word));
|
|
77
|
+
const covers = (phrase: string) => terms(phrase).every(word => subject.includes(wordForm(word)));
|
|
73
78
|
if (!covers(selection.subject) || selection.exclude?.some(covers)) continue;
|
|
74
79
|
// Query context breaks equal hint matches without outweighing a hint.
|
|
75
80
|
const contextScore = matches / (tokens.length + 1);
|
package/styles/video-chat.css
CHANGED
|
@@ -348,12 +348,15 @@
|
|
|
348
348
|
|
|
349
349
|
.vanillasky-video-chat .ending-body {
|
|
350
350
|
position: absolute;
|
|
351
|
-
inset:
|
|
351
|
+
inset: 96px 0 180px;
|
|
352
352
|
display: flex;
|
|
353
353
|
flex-direction: column;
|
|
354
354
|
align-items: flex-start;
|
|
355
355
|
gap: clamp(0.5rem, 1.4cqh, 0.9rem);
|
|
356
|
-
padding:
|
|
356
|
+
padding: 12px clamp(20px, 3.5cqw, 56px);
|
|
357
|
+
overflow-y: auto;
|
|
358
|
+
overscroll-behavior: contain;
|
|
359
|
+
justify-content: safe flex-end;
|
|
357
360
|
color: oklch(1 0 0);
|
|
358
361
|
}
|
|
359
362
|
|
|
@@ -367,9 +370,13 @@
|
|
|
367
370
|
text-shadow: 0 1px 3px oklch(0.14 0.05 265 / 70%);
|
|
368
371
|
}
|
|
369
372
|
|
|
370
|
-
.vanillasky-video-chat .ending .cards {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
+
.vanillasky-video-chat .ending .cards {
|
|
374
|
+
margin-top: 0;
|
|
375
|
+
display: grid;
|
|
376
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
377
|
+
flex-shrink: 0;
|
|
378
|
+
overflow: visible;
|
|
379
|
+
}
|
|
373
380
|
|
|
374
381
|
.vanillasky-video-chat .ending .card-dots { align-self: flex-start; margin-left: -0.35rem; }
|
|
375
382
|
|
|
@@ -410,7 +417,6 @@
|
|
|
410
417
|
animation: vanillasky-video-chat-fade-in 320ms ease both;
|
|
411
418
|
}
|
|
412
419
|
|
|
413
|
-
|
|
414
420
|
@keyframes vanillasky-video-chat-fade-in { from { opacity: 0; } to { opacity: 1; } }
|
|
415
421
|
|
|
416
422
|
@keyframes vanillasky-video-chat-rise { from { opacity: 0; transform: translate(-50%, 6px); } to { opacity: 1; transform: translate(-50%, 0); } }
|
|
@@ -600,7 +606,6 @@
|
|
|
600
606
|
|
|
601
607
|
.vanillasky-video-chat .panel .error { pointer-events: auto; width: min(560px, 100%); background: var(--vs-bg); }
|
|
602
608
|
|
|
603
|
-
|
|
604
609
|
.vanillasky-video-chat .welcome { background: var(--vs-media-ground); }
|
|
605
610
|
|
|
606
611
|
.vanillasky-video-chat .welcome .frame-media { max-width: none; max-height: none; object-fit: cover; }
|
|
@@ -625,10 +630,6 @@
|
|
|
625
630
|
|
|
626
631
|
.vanillasky-video-chat .card-dots { display: none; }
|
|
627
632
|
|
|
628
|
-
.vanillasky-video-chat .ending-body { padding: 0 var(--vs-media-gutter) 190px; }
|
|
629
|
-
|
|
630
|
-
.vanillasky-video-chat .ending .cards { margin-top: 0; }
|
|
631
|
-
|
|
632
633
|
.vanillasky-video-chat .ending-label { font-size: 15px; margin-bottom: 6px; }
|
|
633
634
|
|
|
634
635
|
@media (max-width: 700px) {
|
|
@@ -643,15 +644,13 @@
|
|
|
643
644
|
.vanillasky-video-chat .cards { gap: 12px; width: calc(100% + 20px); max-width: calc(100% + 20px); padding: 12px 20px 12px 12px; }
|
|
644
645
|
.vanillasky-video-chat .cards button { width: 172px; aspect-ratio: .95; }
|
|
645
646
|
.vanillasky-video-chat .card-prompt { font-size: 14px; padding: 14px; }
|
|
646
|
-
|
|
647
|
-
.vanillasky-video-chat .ending .cards button { width: 165px; aspect-ratio: 1.15; }
|
|
647
|
+
|
|
648
648
|
}
|
|
649
649
|
|
|
650
650
|
@media (max-height: 650px) and (min-width: 701px) {
|
|
651
651
|
.vanillasky-video-chat .welcome-body { padding-top: 110px; padding-bottom: 118px; }
|
|
652
652
|
.vanillasky-video-chat .welcome-title { font-size: clamp(32px, 4vw, 54px); }
|
|
653
653
|
.vanillasky-video-chat .cards button { width: 160px; }
|
|
654
|
-
.vanillasky-video-chat .ending-body { padding-bottom: 160px; }
|
|
655
654
|
}
|
|
656
655
|
|
|
657
656
|
@media (max-height: 650px) and (max-width: 700px) {
|
|
@@ -709,8 +708,6 @@
|
|
|
709
708
|
|
|
710
709
|
.vanillasky-video-chat .expanded-captions .eyebrow { color: var(--vs-media-muted); }
|
|
711
710
|
|
|
712
|
-
|
|
713
|
-
|
|
714
711
|
.vanillasky-video-chat .dock-hint { position: absolute; inset: auto 0 7px; text-align: center; margin: 0; color: var(--vs-media-muted); font-size: 11px; line-height: 16px; }
|
|
715
712
|
|
|
716
713
|
.vanillasky-video-chat .panel-inner:has(.dock-hint) { padding-bottom: max(38px, calc(env(safe-area-inset-bottom) + 20px)); }
|
|
@@ -784,7 +781,6 @@
|
|
|
784
781
|
|
|
785
782
|
.vanillasky-video-chat .composer-meta:empty { display: none; }
|
|
786
783
|
|
|
787
|
-
|
|
788
784
|
.vanillasky-video-chat .composer-meta .error { width: auto; }
|
|
789
785
|
|
|
790
786
|
@media (max-width: 700px) {
|
|
@@ -922,7 +918,6 @@
|
|
|
922
918
|
.vanillasky-video-chat .developer-links button { width: 100%; border: 0; background: transparent; text-align: left; cursor: pointer; font-family: inherit; }
|
|
923
919
|
.vanillasky-video-chat .developer-about p { margin: 0; padding: 2px 12px 12px; color: var(--vs-fg-muted); font-size: 12px; line-height: 1.6; }
|
|
924
920
|
|
|
925
|
-
|
|
926
921
|
.vanillasky-video-chat .history { display: grid; gap: 4px; }
|
|
927
922
|
|
|
928
923
|
.vanillasky-video-chat .history .section-label { padding-top: 8px; padding-bottom: 5px; }
|
|
@@ -943,7 +938,6 @@
|
|
|
943
938
|
|
|
944
939
|
.vanillasky-video-chat .history-row small { display: block; margin-top: 4px; font-size: 11px; font-weight: 400; color: #aeb2c7; }
|
|
945
940
|
|
|
946
|
-
|
|
947
941
|
@media (max-width: 700px) {
|
|
948
942
|
.vanillasky-video-chat .sheet-popover { right: 12px; width: min(360px, calc(100% - 24px)); max-height: calc(100dvh - 102px); padding: 16px; }
|
|
949
943
|
.vanillasky-video-chat .popover-heading .round { width: 44px; height: 44px; }
|
|
@@ -1028,7 +1022,6 @@
|
|
|
1028
1022
|
}
|
|
1029
1023
|
}
|
|
1030
1024
|
|
|
1031
|
-
|
|
1032
1025
|
/* Caption controls float outside the reading measure, so the complete cue
|
|
1033
1026
|
keeps the same width when controls appear or disappear. */
|
|
1034
1027
|
.vanillasky-video-chat .caption-actions {
|
|
@@ -1085,3 +1078,32 @@
|
|
|
1085
1078
|
.vanillasky-video-chat .media-credit { color: var(--vs-fg); font-size: 11px; margin-left: 12px; text-underline-offset: 3px; }
|
|
1086
1079
|
@keyframes vanillasky-chapter-enter { from { opacity: 0; } to { opacity: 1; } }
|
|
1087
1080
|
@media (prefers-reduced-motion: reduce) { .vanillasky-video-chat .opening-chapter { animation: none; } }
|
|
1081
|
+
|
|
1082
|
+
.vanillasky-video-chat .ending .cards li { min-width: 0; }
|
|
1083
|
+
|
|
1084
|
+
.vanillasky-video-chat .ending .cards button {
|
|
1085
|
+
display: flex;
|
|
1086
|
+
flex-direction: column;
|
|
1087
|
+
justify-content: flex-end;
|
|
1088
|
+
width: 100%;
|
|
1089
|
+
height: 100%;
|
|
1090
|
+
min-height: 180px;
|
|
1091
|
+
aspect-ratio: auto;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
.vanillasky-video-chat .ending .card-prompt {
|
|
1095
|
+
position: relative;
|
|
1096
|
+
inset: auto;
|
|
1097
|
+
overflow-wrap: anywhere;
|
|
1098
|
+
font-size: clamp(14px, 1.2vw, 18px);
|
|
1099
|
+
line-height: 1.4;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
@container (min-width: 1200px) {
|
|
1103
|
+
.vanillasky-video-chat .ending .cards { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
|
1104
|
+
.vanillasky-video-chat .ending .cards button { min-height: 210px; }
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
@media (max-height: 650px) {
|
|
1108
|
+
.vanillasky-video-chat .ending-body { inset-block: 80px 180px; }
|
|
1109
|
+
}
|