@smoothstream/vue 0.1.0
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/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/base.css +349 -0
- package/dist/index.d.ts +112 -0
- package/dist/index.js +706 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +617 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import "@smoothstream/styles/base.css";
|
|
3
|
+
import "@smoothstream/styles/styles.css";
|
|
4
|
+
|
|
5
|
+
// src/Smoothstream.ts
|
|
6
|
+
import {
|
|
7
|
+
codeBlockRequestKey,
|
|
8
|
+
resolveCodeHighlight,
|
|
9
|
+
StreamingSession,
|
|
10
|
+
unitsReadyForCodeHighlights
|
|
11
|
+
} from "@smoothstream/core";
|
|
12
|
+
import {
|
|
13
|
+
createWebPresentation,
|
|
14
|
+
createWebPresentationCache
|
|
15
|
+
} from "@smoothstream/core/web";
|
|
16
|
+
import {
|
|
17
|
+
computed,
|
|
18
|
+
defineComponent as defineComponent2,
|
|
19
|
+
Fragment,
|
|
20
|
+
h as h2,
|
|
21
|
+
mergeProps,
|
|
22
|
+
nextTick,
|
|
23
|
+
onBeforeUnmount as onBeforeUnmount2,
|
|
24
|
+
onMounted,
|
|
25
|
+
onUpdated,
|
|
26
|
+
shallowRef,
|
|
27
|
+
Teleport,
|
|
28
|
+
ref as ref2,
|
|
29
|
+
watch as watch2
|
|
30
|
+
} from "vue";
|
|
31
|
+
|
|
32
|
+
// src/render-web.ts
|
|
33
|
+
import { find, html, svg } from "property-information";
|
|
34
|
+
import {
|
|
35
|
+
defineComponent,
|
|
36
|
+
createTextVNode,
|
|
37
|
+
h,
|
|
38
|
+
onBeforeUnmount,
|
|
39
|
+
ref,
|
|
40
|
+
watch
|
|
41
|
+
} from "vue";
|
|
42
|
+
var htmlPropertyInfoByName = /* @__PURE__ */ new Map();
|
|
43
|
+
var svgPropertyInfoByName = /* @__PURE__ */ new Map();
|
|
44
|
+
var tableContainers = /* @__PURE__ */ new Set(["table", "tbody", "tfoot", "thead", "tr"]);
|
|
45
|
+
var vuePropertyInfo = (property, namespace) => {
|
|
46
|
+
const cache = namespace === "svg" ? svgPropertyInfoByName : htmlPropertyInfoByName;
|
|
47
|
+
const cached = cache.get(property);
|
|
48
|
+
if (cached) return cached;
|
|
49
|
+
const information = find(namespace === "svg" ? svg : html, property);
|
|
50
|
+
const result = {
|
|
51
|
+
commaSeparated: information.commaSeparated,
|
|
52
|
+
name: information.attribute
|
|
53
|
+
};
|
|
54
|
+
cache.set(property, result);
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
var vueElementProperties = (node) => {
|
|
58
|
+
const result = {};
|
|
59
|
+
for (const [property, originalValue] of Object.entries(node.properties)) {
|
|
60
|
+
if (property === "children" || originalValue === null || originalValue === void 0 || typeof originalValue === "number" && Number.isNaN(originalValue)) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const information = vuePropertyInfo(property, node.namespace);
|
|
64
|
+
result[information.name] = Array.isArray(originalValue) ? originalValue.map(String).join(information.commaSeparated ? ", " : " ").trim() : originalValue;
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
};
|
|
68
|
+
var textContent = (node) => node.type === "text" ? node.value : node.children.map(textContent).join("");
|
|
69
|
+
var codeBlockValue = (node) => {
|
|
70
|
+
const code = node.children.find(
|
|
71
|
+
(child) => child.type === "element" && child.tagName === "code"
|
|
72
|
+
);
|
|
73
|
+
const value = code ? textContent(code) : "";
|
|
74
|
+
return value.endsWith("\n") ? value.slice(0, -1) : value;
|
|
75
|
+
};
|
|
76
|
+
var writeClipboardText = async (button, value) => {
|
|
77
|
+
const document = button.ownerDocument;
|
|
78
|
+
const view = document.defaultView;
|
|
79
|
+
if (!view) throw new Error("Clipboard copy requires a browser window.");
|
|
80
|
+
if (view.navigator.clipboard?.writeText) {
|
|
81
|
+
await view.navigator.clipboard.writeText(value);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const textarea = document.createElement("textarea");
|
|
85
|
+
textarea.value = value;
|
|
86
|
+
textarea.setAttribute("readonly", "");
|
|
87
|
+
textarea.style.position = "fixed";
|
|
88
|
+
textarea.style.opacity = "0";
|
|
89
|
+
document.body.append(textarea);
|
|
90
|
+
textarea.select();
|
|
91
|
+
const copied = document.execCommand?.("copy") === true;
|
|
92
|
+
textarea.remove();
|
|
93
|
+
if (!copied) throw new Error("Clipboard copy is unavailable.");
|
|
94
|
+
};
|
|
95
|
+
var webChildrenToVue = (children, filterTableWhitespace, copyContext) => children.flatMap((child) => {
|
|
96
|
+
if (filterTableWhitespace && child.type === "text" && /^\s*$/u.test(child.value)) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
return [webNodeToVue(child, copyContext)];
|
|
100
|
+
});
|
|
101
|
+
var webElementToVue = (node, copyContext) => {
|
|
102
|
+
const properties = vueElementProperties(node);
|
|
103
|
+
if (copyContext) {
|
|
104
|
+
if (node.properties["data-smoothstream-code-copy"] === true) {
|
|
105
|
+
properties.onClick = copyContext.copy;
|
|
106
|
+
if (copyContext.copied) properties["aria-label"] = "Code copied";
|
|
107
|
+
}
|
|
108
|
+
if (node.properties["data-smoothstream-code-icon-swap"] === true) {
|
|
109
|
+
properties["data-state"] = copyContext.copied ? "check" : "copy";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return h(
|
|
113
|
+
node.tagName,
|
|
114
|
+
{ ...properties, key: node.key },
|
|
115
|
+
webChildrenToVue(
|
|
116
|
+
node.children,
|
|
117
|
+
tableContainers.has(node.tagName),
|
|
118
|
+
copyContext
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
var WebCodeBlock = defineComponent({
|
|
123
|
+
name: "SmoothstreamCodeBlock",
|
|
124
|
+
props: {
|
|
125
|
+
node: {
|
|
126
|
+
required: true,
|
|
127
|
+
type: Object
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
setup(props) {
|
|
131
|
+
const copied = ref(false);
|
|
132
|
+
let resetTimer;
|
|
133
|
+
const clearResetTimer = () => {
|
|
134
|
+
if (resetTimer === void 0) return;
|
|
135
|
+
const view = typeof window === "undefined" ? void 0 : window;
|
|
136
|
+
view?.clearTimeout(resetTimer);
|
|
137
|
+
resetTimer = void 0;
|
|
138
|
+
};
|
|
139
|
+
watch(
|
|
140
|
+
() => codeBlockValue(props.node),
|
|
141
|
+
() => {
|
|
142
|
+
copied.value = false;
|
|
143
|
+
clearResetTimer();
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
onBeforeUnmount(clearResetTimer);
|
|
147
|
+
const copy = (event) => {
|
|
148
|
+
if (props.node.properties["data-smoothstream-code-copy-ready"] !== true) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const button = event.currentTarget;
|
|
152
|
+
if (!button || button.localName !== "button") return;
|
|
153
|
+
const code = codeBlockValue(props.node);
|
|
154
|
+
void writeClipboardText(button, code).then(
|
|
155
|
+
() => {
|
|
156
|
+
copied.value = true;
|
|
157
|
+
clearResetTimer();
|
|
158
|
+
const view = button.ownerDocument.defaultView;
|
|
159
|
+
if (!view) return;
|
|
160
|
+
resetTimer = view.setTimeout(() => {
|
|
161
|
+
copied.value = false;
|
|
162
|
+
resetTimer = void 0;
|
|
163
|
+
}, 2e3);
|
|
164
|
+
},
|
|
165
|
+
() => {
|
|
166
|
+
copied.value = false;
|
|
167
|
+
}
|
|
168
|
+
);
|
|
169
|
+
};
|
|
170
|
+
return () => webElementToVue(props.node, {
|
|
171
|
+
copied: copied.value,
|
|
172
|
+
copy
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
var webNodeToVue = (node, copyContext) => {
|
|
177
|
+
if (node.type === "text") {
|
|
178
|
+
const text = createTextVNode(node.value);
|
|
179
|
+
text.key = node.key;
|
|
180
|
+
return text;
|
|
181
|
+
}
|
|
182
|
+
if (!copyContext && node.tagName === "pre" && node.properties["data-smoothstream-code-block"] === true) {
|
|
183
|
+
return h(WebCodeBlock, { key: node.key, node });
|
|
184
|
+
}
|
|
185
|
+
return webElementToVue(node, copyContext);
|
|
186
|
+
};
|
|
187
|
+
var webNodesToVue = (nodes) => nodes.map((node) => webNodeToVue(node));
|
|
188
|
+
|
|
189
|
+
// src/Smoothstream.ts
|
|
190
|
+
var COMPLETION_ANNOUNCEMENT = "Content ready.";
|
|
191
|
+
var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
192
|
+
var PHASE_SELECTOR = "[data-smoothstream-animation-start]";
|
|
193
|
+
var PHASE_PROPERTY = "--smoothstream-animation-delay";
|
|
194
|
+
var screenReaderOnlyStyle = {
|
|
195
|
+
border: 0,
|
|
196
|
+
clip: "rect(0 0 0 0)",
|
|
197
|
+
clipPath: "inset(50%)",
|
|
198
|
+
height: "1px",
|
|
199
|
+
margin: "-1px",
|
|
200
|
+
overflow: "hidden",
|
|
201
|
+
padding: 0,
|
|
202
|
+
position: "absolute",
|
|
203
|
+
whiteSpace: "nowrap",
|
|
204
|
+
width: "1px"
|
|
205
|
+
};
|
|
206
|
+
var browserClock = {
|
|
207
|
+
now: () => typeof window === "undefined" ? 0 : window.performance?.now() ?? Date.now()
|
|
208
|
+
};
|
|
209
|
+
var emptyPlayback = () => ({
|
|
210
|
+
animationComplete: true,
|
|
211
|
+
immediate: false,
|
|
212
|
+
now: 0,
|
|
213
|
+
schedules: /* @__PURE__ */ new Map(),
|
|
214
|
+
visibleUnitCount: 0
|
|
215
|
+
});
|
|
216
|
+
var Smoothstream = defineComponent2({
|
|
217
|
+
name: "Smoothstream",
|
|
218
|
+
inheritAttrs: false,
|
|
219
|
+
props: {
|
|
220
|
+
codeHighlighter: Object,
|
|
221
|
+
duration: {
|
|
222
|
+
default: 1e3,
|
|
223
|
+
type: Number
|
|
224
|
+
},
|
|
225
|
+
interval: {
|
|
226
|
+
default: 3,
|
|
227
|
+
type: Number
|
|
228
|
+
},
|
|
229
|
+
markdown: {
|
|
230
|
+
default: "",
|
|
231
|
+
type: String
|
|
232
|
+
},
|
|
233
|
+
mode: {
|
|
234
|
+
default: "streaming",
|
|
235
|
+
type: String
|
|
236
|
+
},
|
|
237
|
+
receiving: {
|
|
238
|
+
default: false,
|
|
239
|
+
type: Boolean
|
|
240
|
+
},
|
|
241
|
+
reducedMotion: {
|
|
242
|
+
default: "system",
|
|
243
|
+
type: String
|
|
244
|
+
},
|
|
245
|
+
reveal: {
|
|
246
|
+
default: "character",
|
|
247
|
+
type: String
|
|
248
|
+
},
|
|
249
|
+
unstyled: {
|
|
250
|
+
default: false,
|
|
251
|
+
type: Boolean
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
setup(props, { attrs }) {
|
|
255
|
+
const root = ref2(null);
|
|
256
|
+
const mounted = ref2(false);
|
|
257
|
+
const motionDisabled = ref2(props.reducedMotion === "always");
|
|
258
|
+
const currentInput = shallowRef();
|
|
259
|
+
const currentPlayback = shallowRef(
|
|
260
|
+
emptyPlayback()
|
|
261
|
+
);
|
|
262
|
+
const codeHighlights = shallowRef(
|
|
263
|
+
/* @__PURE__ */ new Map()
|
|
264
|
+
);
|
|
265
|
+
const imageReadiness = shallowRef(
|
|
266
|
+
/* @__PURE__ */ new Map()
|
|
267
|
+
);
|
|
268
|
+
const resourceNow = ref2(0);
|
|
269
|
+
const completionAnnouncement = ref2("");
|
|
270
|
+
let session = new StreamingSession(browserClock, {
|
|
271
|
+
duration: props.duration,
|
|
272
|
+
interval: props.interval
|
|
273
|
+
});
|
|
274
|
+
let presentationCache = createWebPresentationCache();
|
|
275
|
+
let lastPresentation;
|
|
276
|
+
let requestedSource = props.markdown;
|
|
277
|
+
let pendingInput = {
|
|
278
|
+
receiving: props.receiving,
|
|
279
|
+
source: props.markdown
|
|
280
|
+
};
|
|
281
|
+
let inputFrame;
|
|
282
|
+
let playbackFrame;
|
|
283
|
+
let mediaQuery;
|
|
284
|
+
let destroyed = false;
|
|
285
|
+
let announcedSource;
|
|
286
|
+
let completionRendered = false;
|
|
287
|
+
const codeRequestsByBlock = /* @__PURE__ */ new Map();
|
|
288
|
+
const codeSessionsByBlock = /* @__PURE__ */ new Map();
|
|
289
|
+
const imageLoaders = /* @__PURE__ */ new Map();
|
|
290
|
+
const resourceTimers = /* @__PURE__ */ new Set();
|
|
291
|
+
const synchronizedPhases = /* @__PURE__ */ new WeakMap();
|
|
292
|
+
let phasedElements = /* @__PURE__ */ new Set();
|
|
293
|
+
const resolvedMotion = computed(() => {
|
|
294
|
+
if (props.reducedMotion === "always") return "none";
|
|
295
|
+
if (props.reducedMotion === "never") return "animate";
|
|
296
|
+
if (!mounted.value) return "pending";
|
|
297
|
+
return motionDisabled.value ? "none" : "animate";
|
|
298
|
+
});
|
|
299
|
+
const presentationImmediate = () => props.mode === "static" || resolvedMotion.value === "none";
|
|
300
|
+
const requestFrame = (callback) => typeof window.requestAnimationFrame === "function" ? window.requestAnimationFrame(callback) : window.setTimeout(() => callback(browserClock.now()), 16);
|
|
301
|
+
const cancelFrame = (frame) => {
|
|
302
|
+
if (typeof window.cancelAnimationFrame === "function") {
|
|
303
|
+
window.cancelAnimationFrame(frame);
|
|
304
|
+
} else {
|
|
305
|
+
window.clearTimeout(frame);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const playbackUnits = (input) => presentationImmediate() ? input.plan.units : unitsReadyForCodeHighlights(
|
|
309
|
+
input.plan.units,
|
|
310
|
+
props.codeHighlighter,
|
|
311
|
+
codeHighlights.value
|
|
312
|
+
);
|
|
313
|
+
const cancelPlaybackFrame = () => {
|
|
314
|
+
if (playbackFrame === void 0 || typeof window === "undefined") return;
|
|
315
|
+
cancelFrame(playbackFrame);
|
|
316
|
+
playbackFrame = void 0;
|
|
317
|
+
};
|
|
318
|
+
const ensurePlaybackFrame = () => {
|
|
319
|
+
if (destroyed || typeof window === "undefined" || presentationImmediate() || currentPlayback.value.animationComplete || playbackFrame !== void 0) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
playbackFrame = requestFrame(() => {
|
|
323
|
+
playbackFrame = void 0;
|
|
324
|
+
const previous = currentPlayback.value;
|
|
325
|
+
const next = session.advance();
|
|
326
|
+
if (next.visibleUnitCount !== previous.visibleUnitCount || next.animationComplete && !completionRendered) {
|
|
327
|
+
completionRendered = next.animationComplete;
|
|
328
|
+
currentPlayback.value = next;
|
|
329
|
+
}
|
|
330
|
+
ensurePlaybackFrame();
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
const refreshPlayback = (input = currentInput.value) => {
|
|
334
|
+
if (!input) return;
|
|
335
|
+
cancelPlaybackFrame();
|
|
336
|
+
const units = playbackUnits(input);
|
|
337
|
+
if (presentationImmediate()) {
|
|
338
|
+
currentPlayback.value = session.immediate(input, units);
|
|
339
|
+
} else if (mounted.value && resolvedMotion.value === "animate") {
|
|
340
|
+
currentPlayback.value = session.schedule(input, units);
|
|
341
|
+
} else {
|
|
342
|
+
currentPlayback.value = emptyPlayback();
|
|
343
|
+
}
|
|
344
|
+
completionRendered = currentPlayback.value.animationComplete;
|
|
345
|
+
ensurePlaybackFrame();
|
|
346
|
+
};
|
|
347
|
+
const publishResourceChange = () => {
|
|
348
|
+
resourceNow.value = browserClock.now();
|
|
349
|
+
};
|
|
350
|
+
const ensureImages = (descriptors) => {
|
|
351
|
+
if (typeof window === "undefined" || presentationImmediate()) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
for (const descriptor of descriptors) {
|
|
355
|
+
if (imageLoaders.has(descriptor.id) || imageReadiness.value.has(descriptor.id)) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
let finished = false;
|
|
359
|
+
const finish = (status, image2) => {
|
|
360
|
+
if (finished || destroyed) return;
|
|
361
|
+
finished = true;
|
|
362
|
+
imageLoaders.delete(descriptor.id);
|
|
363
|
+
const readyAt = browserClock.now();
|
|
364
|
+
const width = image2?.naturalWidth ?? 0;
|
|
365
|
+
const height = image2?.naturalHeight ?? 0;
|
|
366
|
+
const next = new Map(imageReadiness.value);
|
|
367
|
+
next.set(
|
|
368
|
+
descriptor.id,
|
|
369
|
+
status === "ready" && width > 0 && height > 0 ? { height, readyAt, status, width } : { readyAt, status }
|
|
370
|
+
);
|
|
371
|
+
imageReadiness.value = next;
|
|
372
|
+
publishResourceChange();
|
|
373
|
+
const timer = window.setTimeout(() => {
|
|
374
|
+
resourceTimers.delete(timer);
|
|
375
|
+
publishResourceChange();
|
|
376
|
+
}, props.duration + 20);
|
|
377
|
+
resourceTimers.add(timer);
|
|
378
|
+
};
|
|
379
|
+
if (descriptor.src.length === 0) {
|
|
380
|
+
finish("error");
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const image = new window.Image();
|
|
384
|
+
imageLoaders.set(descriptor.id, image);
|
|
385
|
+
image.decoding = "async";
|
|
386
|
+
image.onerror = () => finish("error");
|
|
387
|
+
let decodeStarted = false;
|
|
388
|
+
const handleLoad = () => {
|
|
389
|
+
if (decodeStarted || finished) return;
|
|
390
|
+
decodeStarted = true;
|
|
391
|
+
const decoded = typeof image.decode === "function" ? image.decode().catch(() => void 0) : Promise.resolve();
|
|
392
|
+
void decoded.then(() => finish("ready", image));
|
|
393
|
+
};
|
|
394
|
+
image.onload = handleLoad;
|
|
395
|
+
image.src = descriptor.src;
|
|
396
|
+
if (image.complete && image.naturalWidth > 0) handleLoad();
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
const commitCodeHighlight = (highlighter, request, key, result) => {
|
|
400
|
+
if (destroyed || props.codeHighlighter !== highlighter || codeRequestsByBlock.get(request.blockStart) !== key) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const next = new Map(codeHighlights.value);
|
|
404
|
+
next.set(
|
|
405
|
+
request.blockStart,
|
|
406
|
+
resolveCodeHighlight(
|
|
407
|
+
request,
|
|
408
|
+
result,
|
|
409
|
+
codeHighlights.value.get(request.blockStart)
|
|
410
|
+
)
|
|
411
|
+
);
|
|
412
|
+
codeHighlights.value = next;
|
|
413
|
+
refreshPlayback();
|
|
414
|
+
};
|
|
415
|
+
const ensureCodeHighlights = (requests) => {
|
|
416
|
+
const highlighter = props.codeHighlighter;
|
|
417
|
+
if (!mounted.value || !highlighter) return;
|
|
418
|
+
for (const request of requests) {
|
|
419
|
+
const key = codeBlockRequestKey(request);
|
|
420
|
+
const current = codeHighlights.value.get(request.blockStart);
|
|
421
|
+
if (current?.language === request.language && current.code === request.code || codeRequestsByBlock.get(request.blockStart) === key) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
codeRequestsByBlock.set(request.blockStart, key);
|
|
425
|
+
let privateSession = codeSessionsByBlock.get(request.blockStart);
|
|
426
|
+
if (!privateSession) {
|
|
427
|
+
privateSession = {};
|
|
428
|
+
codeSessionsByBlock.set(request.blockStart, privateSession);
|
|
429
|
+
}
|
|
430
|
+
void Promise.resolve(highlighter.highlight({
|
|
431
|
+
code: request.code,
|
|
432
|
+
language: request.language,
|
|
433
|
+
session: privateSession
|
|
434
|
+
})).then(
|
|
435
|
+
(result) => commitCodeHighlight(highlighter, request, key, result),
|
|
436
|
+
() => commitCodeHighlight(highlighter, request, key, void 0)
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
const flushInput = () => {
|
|
441
|
+
const pending = pendingInput;
|
|
442
|
+
pendingInput = void 0;
|
|
443
|
+
if (!pending || destroyed) return;
|
|
444
|
+
const input = session.prepareInput(
|
|
445
|
+
pending.source,
|
|
446
|
+
pending.receiving,
|
|
447
|
+
props.reveal
|
|
448
|
+
);
|
|
449
|
+
session.commitInput(input);
|
|
450
|
+
currentInput.value = input;
|
|
451
|
+
if (mounted.value) {
|
|
452
|
+
ensureCodeHighlights(input.codeBlocks);
|
|
453
|
+
ensureImages(input.images);
|
|
454
|
+
}
|
|
455
|
+
refreshPlayback(input);
|
|
456
|
+
};
|
|
457
|
+
const scheduleInputFlush = () => {
|
|
458
|
+
if (!mounted.value || typeof window === "undefined") {
|
|
459
|
+
flushInput();
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (inputFrame !== void 0) return;
|
|
463
|
+
inputFrame = requestFrame(() => {
|
|
464
|
+
inputFrame = void 0;
|
|
465
|
+
flushInput();
|
|
466
|
+
});
|
|
467
|
+
};
|
|
468
|
+
const clearResourceWork = () => {
|
|
469
|
+
for (const image of imageLoaders.values()) {
|
|
470
|
+
image.onload = null;
|
|
471
|
+
image.onerror = null;
|
|
472
|
+
}
|
|
473
|
+
imageLoaders.clear();
|
|
474
|
+
if (typeof window !== "undefined") {
|
|
475
|
+
for (const timer of resourceTimers) window.clearTimeout(timer);
|
|
476
|
+
}
|
|
477
|
+
resourceTimers.clear();
|
|
478
|
+
};
|
|
479
|
+
const resetSession = () => {
|
|
480
|
+
if (typeof window !== "undefined") {
|
|
481
|
+
if (inputFrame !== void 0) cancelFrame(inputFrame);
|
|
482
|
+
inputFrame = void 0;
|
|
483
|
+
cancelPlaybackFrame();
|
|
484
|
+
}
|
|
485
|
+
clearResourceWork();
|
|
486
|
+
codeRequestsByBlock.clear();
|
|
487
|
+
codeSessionsByBlock.clear();
|
|
488
|
+
codeHighlights.value = /* @__PURE__ */ new Map();
|
|
489
|
+
imageReadiness.value = /* @__PURE__ */ new Map();
|
|
490
|
+
resourceNow.value = 0;
|
|
491
|
+
presentationCache = createWebPresentationCache();
|
|
492
|
+
session = new StreamingSession(browserClock, {
|
|
493
|
+
duration: props.duration,
|
|
494
|
+
interval: props.interval
|
|
495
|
+
});
|
|
496
|
+
requestedSource = props.markdown;
|
|
497
|
+
pendingInput = {
|
|
498
|
+
receiving: props.receiving,
|
|
499
|
+
source: props.markdown
|
|
500
|
+
};
|
|
501
|
+
announcedSource = void 0;
|
|
502
|
+
completionAnnouncement.value = "";
|
|
503
|
+
flushInput();
|
|
504
|
+
};
|
|
505
|
+
function handleMotionChange(event) {
|
|
506
|
+
if (!event.matches || motionDisabled.value) return;
|
|
507
|
+
motionDisabled.value = true;
|
|
508
|
+
refreshPlayback();
|
|
509
|
+
}
|
|
510
|
+
const removeMediaListener = () => {
|
|
511
|
+
if (!mediaQuery) return;
|
|
512
|
+
if (typeof mediaQuery.removeEventListener === "function") {
|
|
513
|
+
mediaQuery.removeEventListener("change", handleMotionChange);
|
|
514
|
+
} else {
|
|
515
|
+
mediaQuery.removeListener?.(handleMotionChange);
|
|
516
|
+
}
|
|
517
|
+
mediaQuery = void 0;
|
|
518
|
+
};
|
|
519
|
+
const configureMotion = () => {
|
|
520
|
+
removeMediaListener();
|
|
521
|
+
if (!mounted.value || typeof window === "undefined" || props.reducedMotion !== "system" || typeof window.matchMedia !== "function") {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
mediaQuery = window.matchMedia(REDUCED_MOTION_QUERY);
|
|
525
|
+
if (mediaQuery.matches) motionDisabled.value = true;
|
|
526
|
+
if (typeof mediaQuery.addEventListener === "function") {
|
|
527
|
+
mediaQuery.addEventListener("change", handleMotionChange);
|
|
528
|
+
} else {
|
|
529
|
+
mediaQuery.addListener?.(handleMotionChange);
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
const synchronizeAnimationPhases = () => {
|
|
533
|
+
const element = root.value;
|
|
534
|
+
if (!element) return;
|
|
535
|
+
const now = browserClock.now();
|
|
536
|
+
const nextElements = /* @__PURE__ */ new Set();
|
|
537
|
+
for (const animated of element.querySelectorAll(PHASE_SELECTOR)) {
|
|
538
|
+
nextElements.add(animated);
|
|
539
|
+
const startAt = Number(animated.dataset.smoothstreamAnimationStart);
|
|
540
|
+
const duration = Number(animated.dataset.smoothstreamAnimationDuration);
|
|
541
|
+
if (!Number.isFinite(startAt) || !Number.isFinite(duration)) continue;
|
|
542
|
+
const synchronized = synchronizedPhases.get(animated);
|
|
543
|
+
if (synchronized?.startAt === startAt && synchronized.duration === duration) {
|
|
544
|
+
animated.style.setProperty(PHASE_PROPERTY, synchronized.delay);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
const currentTime = Math.max(0, Math.min(duration, now - startAt));
|
|
548
|
+
const delay = String(-currentTime) + "ms";
|
|
549
|
+
animated.style.setProperty(PHASE_PROPERTY, delay);
|
|
550
|
+
synchronizedPhases.set(animated, { delay, duration, startAt });
|
|
551
|
+
}
|
|
552
|
+
for (const previous of phasedElements) {
|
|
553
|
+
if (!nextElements.has(previous)) {
|
|
554
|
+
previous.style.removeProperty(PHASE_PROPERTY);
|
|
555
|
+
if (previous.style.length === 0) previous.removeAttribute("style");
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
phasedElements = nextElements;
|
|
559
|
+
};
|
|
560
|
+
const publishCompletionIfReady = () => {
|
|
561
|
+
const input = currentInput.value;
|
|
562
|
+
const presentation = lastPresentation;
|
|
563
|
+
const element = root.value;
|
|
564
|
+
if (props.mode !== "streaming" || !mounted.value || !input || !presentation || !element) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (input.inputOpen) completionAnnouncement.value = "";
|
|
568
|
+
if (input.inputOpen || input.plan.tree.children.length === 0 || !presentation.allPlannedUnitsCompacted || element.querySelector("[data-smoothstream-unit]") !== null || announcedSource === input.source) {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
announcedSource = input.source;
|
|
572
|
+
completionAnnouncement.value = COMPLETION_ANNOUNCEMENT;
|
|
573
|
+
};
|
|
574
|
+
const afterRender = () => {
|
|
575
|
+
synchronizeAnimationPhases();
|
|
576
|
+
publishCompletionIfReady();
|
|
577
|
+
};
|
|
578
|
+
watch2(
|
|
579
|
+
() => [props.markdown, props.receiving],
|
|
580
|
+
([source, receiving]) => {
|
|
581
|
+
if (!source.startsWith(requestedSource)) {
|
|
582
|
+
throw new Error(
|
|
583
|
+
"Smoothstream Markdown must be append-only. Give a different response a new Vue key."
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
requestedSource = source;
|
|
587
|
+
pendingInput = { receiving, source };
|
|
588
|
+
if (receiving || announcedSource !== source) {
|
|
589
|
+
announcedSource = void 0;
|
|
590
|
+
completionAnnouncement.value = "";
|
|
591
|
+
}
|
|
592
|
+
scheduleInputFlush();
|
|
593
|
+
},
|
|
594
|
+
{ flush: "sync" }
|
|
595
|
+
);
|
|
596
|
+
watch2(
|
|
597
|
+
() => [props.duration, props.interval, props.mode, props.reveal],
|
|
598
|
+
resetSession,
|
|
599
|
+
{ flush: "sync" }
|
|
600
|
+
);
|
|
601
|
+
watch2(
|
|
602
|
+
() => props.codeHighlighter,
|
|
603
|
+
() => {
|
|
604
|
+
codeRequestsByBlock.clear();
|
|
605
|
+
codeSessionsByBlock.clear();
|
|
606
|
+
codeHighlights.value = /* @__PURE__ */ new Map();
|
|
607
|
+
const input = currentInput.value;
|
|
608
|
+
if (input) ensureCodeHighlights(input.codeBlocks);
|
|
609
|
+
refreshPlayback(input);
|
|
610
|
+
},
|
|
611
|
+
{ flush: "sync" }
|
|
612
|
+
);
|
|
613
|
+
watch2(
|
|
614
|
+
() => props.reducedMotion,
|
|
615
|
+
() => {
|
|
616
|
+
configureMotion();
|
|
617
|
+
const input = currentInput.value;
|
|
618
|
+
if (input && !presentationImmediate()) ensureImages(input.images);
|
|
619
|
+
refreshPlayback(input);
|
|
620
|
+
},
|
|
621
|
+
{ flush: "sync" }
|
|
622
|
+
);
|
|
623
|
+
flushInput();
|
|
624
|
+
onMounted(() => {
|
|
625
|
+
mounted.value = true;
|
|
626
|
+
configureMotion();
|
|
627
|
+
const input = currentInput.value;
|
|
628
|
+
if (input) {
|
|
629
|
+
ensureCodeHighlights(input.codeBlocks);
|
|
630
|
+
ensureImages(input.images);
|
|
631
|
+
refreshPlayback(input);
|
|
632
|
+
}
|
|
633
|
+
void nextTick(afterRender);
|
|
634
|
+
});
|
|
635
|
+
onUpdated(afterRender);
|
|
636
|
+
onBeforeUnmount2(() => {
|
|
637
|
+
destroyed = true;
|
|
638
|
+
removeMediaListener();
|
|
639
|
+
if (typeof window !== "undefined") {
|
|
640
|
+
if (inputFrame !== void 0) cancelFrame(inputFrame);
|
|
641
|
+
cancelPlaybackFrame();
|
|
642
|
+
}
|
|
643
|
+
clearResourceWork();
|
|
644
|
+
codeRequestsByBlock.clear();
|
|
645
|
+
codeSessionsByBlock.clear();
|
|
646
|
+
});
|
|
647
|
+
const renderContent = () => {
|
|
648
|
+
const input = currentInput.value;
|
|
649
|
+
if (!input || props.mode === "streaming" && resolvedMotion.value === "pending") {
|
|
650
|
+
lastPresentation = void 0;
|
|
651
|
+
return [];
|
|
652
|
+
}
|
|
653
|
+
const playback = currentPlayback.value;
|
|
654
|
+
const now = Math.max(playback.now, resourceNow.value);
|
|
655
|
+
const presentation = session.present(input, playback, { now });
|
|
656
|
+
lastPresentation = presentation;
|
|
657
|
+
return webNodesToVue(createWebPresentation({
|
|
658
|
+
cache: presentationCache,
|
|
659
|
+
codeHighlighterEnabled: props.codeHighlighter !== void 0,
|
|
660
|
+
codeHighlights: codeHighlights.value,
|
|
661
|
+
compactedBlockIds: presentation.compactedBlockIds,
|
|
662
|
+
compactedUnitIds: presentation.compactedUnitIds,
|
|
663
|
+
images: imageReadiness.value,
|
|
664
|
+
immediate: presentationImmediate(),
|
|
665
|
+
now,
|
|
666
|
+
reveal: input.reveal,
|
|
667
|
+
schedules: presentation.schedules,
|
|
668
|
+
showLanguageLabels: props.codeHighlighter?.showLanguageLabels !== false,
|
|
669
|
+
tree: input.plan.tree,
|
|
670
|
+
units: input.plan.units
|
|
671
|
+
}));
|
|
672
|
+
};
|
|
673
|
+
return () => {
|
|
674
|
+
const motion = props.mode === "streaming" && resolvedMotion.value === "pending" ? "pending" : resolvedMotion.value === "none" ? "none" : "animate";
|
|
675
|
+
const rootNode = h2(
|
|
676
|
+
"div",
|
|
677
|
+
mergeProps(attrs, {
|
|
678
|
+
"data-smoothstream": true,
|
|
679
|
+
"data-smoothstream-mode": props.mode,
|
|
680
|
+
"data-smoothstream-motion": motion,
|
|
681
|
+
"data-smoothstream-reduced-motion": props.reducedMotion,
|
|
682
|
+
"data-smoothstream-reveal": props.reveal,
|
|
683
|
+
"data-smoothstream-theme": props.unstyled ? void 0 : "default",
|
|
684
|
+
ref: root,
|
|
685
|
+
style: {
|
|
686
|
+
"--smoothstream-duration": String(resolvedMotion.value === "none" ? 0 : props.duration) + "ms",
|
|
687
|
+
"--smoothstream-interval": String(resolvedMotion.value === "none" ? 0 : props.interval) + "ms"
|
|
688
|
+
}
|
|
689
|
+
}),
|
|
690
|
+
renderContent()
|
|
691
|
+
);
|
|
692
|
+
const announcer = props.mode === "streaming" && mounted.value ? h2(Teleport, { to: "body" }, h2("div", {
|
|
693
|
+
"aria-atomic": true,
|
|
694
|
+
"aria-live": "polite",
|
|
695
|
+
"data-smoothstream-announcer": true,
|
|
696
|
+
role: "status",
|
|
697
|
+
style: screenReaderOnlyStyle
|
|
698
|
+
}, completionAnnouncement.value)) : null;
|
|
699
|
+
return h2(Fragment, null, [rootNode, announcer]);
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
export {
|
|
704
|
+
Smoothstream
|
|
705
|
+
};
|
|
706
|
+
//# sourceMappingURL=index.js.map
|