@hypit/hypit 0.1.5 → 0.1.6
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/examples/semantic-composition/README.md +2 -1
- package/examples/semantic-composition/chat.svml +1 -1
- package/examples/semantic-composition/hypit.runtime.json +1 -1
- package/package.json +4 -1
- package/packages/caption-fine/README.md +7 -0
- package/packages/cli/src/view.ts +4 -3
- package/packages/film/src/surface.ts +4 -1
- package/packages/runtime-local/README.md +17 -0
- package/packages/script/README.md +15 -3
- package/packages/script/src/manifest.ts +2 -1
- package/packages/script/src/parser.ts +45 -6
- package/packages/studio/LOCALIZATION.md +98 -0
- package/packages/studio/README.md +13 -0
- package/packages/studio/locales/en.json +197 -0
- package/packages/studio/locales/zh-CN.json +191 -0
- package/packages/studio/src/localization-node.ts +46 -0
- package/packages/studio/src/localization.ts +85 -0
- package/packages/studio/src/style.css +15 -3
- package/packages/studio/src/ui/artifact-name.ts +8 -6
- package/packages/studio/src/ui/artifact-preview.ts +5 -4
- package/packages/studio/src/ui/code.ts +17 -16
- package/packages/studio/src/ui/comments.ts +26 -25
- package/packages/studio/src/ui/dropdown.ts +68 -0
- package/packages/studio/src/ui/i18n.ts +109 -0
- package/packages/studio/src/ui/icons.ts +1 -0
- package/packages/studio/src/ui/library.ts +76 -65
- package/packages/studio/src/ui/main.ts +84 -131
- package/packages/studio/src/ui/scrub-preview.ts +8 -6
- package/packages/studio/src/ui/stage.ts +28 -27
- package/packages/studio/src/ui/timeline.ts +17 -14
- package/packages/studio/start.ts +39 -22
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { bindDropdown } from "./dropdown.js";
|
|
2
|
+
import { uiLabel, uiAttribute, uiText, uiAttr, userText, languageMenu, initializeI18n, type Message } from "./i18n.js";
|
|
1
3
|
import type { Clip, StudioFailure, StudioInspectorDomain, StudioSnapshot } from "../shared.js";
|
|
2
4
|
import type { CanonicalValue, ValueSchema } from "@hypit/protocol";
|
|
3
5
|
import { parameterAuthorValue, parameterControlForSchema, parameterNumber, parameterOption, parameterRecordSchema, parameterRecordVariants, validateParameterValue } from "../parameter-values.js";
|
|
@@ -16,6 +18,8 @@ import { createTimeline } from "./timeline.js";
|
|
|
16
18
|
import { createComments } from "./comments.js";
|
|
17
19
|
import "../style.css";
|
|
18
20
|
|
|
21
|
+
await initializeI18n();
|
|
22
|
+
|
|
19
23
|
const app = document.querySelector<HTMLElement>("#app")!;
|
|
20
24
|
app.innerHTML = `
|
|
21
25
|
<header class="topbar">
|
|
@@ -40,9 +44,10 @@ app.innerHTML = `
|
|
|
40
44
|
<div class="topbar-right">
|
|
41
45
|
<div class="meta" data-meta></div>
|
|
42
46
|
<div class="status" data-status></div>
|
|
43
|
-
<div
|
|
44
|
-
|
|
45
|
-
<button type="button" role="tab" data-view="
|
|
47
|
+
<div data-language-menu></div>
|
|
48
|
+
<div class="view-tabs" role="tablist" ${uiAttribute("aria-label", "app.studio-view")}>
|
|
49
|
+
<button type="button" role="tab" data-view="studio" aria-selected="true">${icon("studio")}${uiLabel("app.studio")}</button>
|
|
50
|
+
<button type="button" role="tab" data-view="comments" aria-selected="false">${icon("comments")}${uiLabel("app.comments")}</button>
|
|
46
51
|
</div>
|
|
47
52
|
</div>
|
|
48
53
|
</header>
|
|
@@ -52,7 +57,7 @@ app.innerHTML = `
|
|
|
52
57
|
<div class="preview-panel" data-stage></div>
|
|
53
58
|
<aside class="workspace-panel">
|
|
54
59
|
<div class="pane-heading workspace-heading" data-workspace-heading>
|
|
55
|
-
<div class="pane-title">${icon("tune")}<h2
|
|
60
|
+
<div class="pane-title">${icon("tune")}<h2>${uiLabel("inspector.properties")}</h2></div>
|
|
56
61
|
</div>
|
|
57
62
|
<div class="workspace-scroll">
|
|
58
63
|
<section class="workspace-section">
|
|
@@ -65,6 +70,8 @@ app.innerHTML = `
|
|
|
65
70
|
</main>
|
|
66
71
|
<pre class="failure" data-failure></pre>`;
|
|
67
72
|
|
|
73
|
+
app.querySelector("[data-language-menu]")!.replaceWith(languageMenu());
|
|
74
|
+
|
|
68
75
|
const store = createStore();
|
|
69
76
|
const code = createCodePane();
|
|
70
77
|
const stage = createStage(store, (id) => library.selectArtifact(id));
|
|
@@ -144,7 +151,7 @@ changeView();
|
|
|
144
151
|
timeline.element.addEventListener("studio:write", (event) => {
|
|
145
152
|
const state = (event as CustomEvent<{ readonly state?: string }>).detail.state;
|
|
146
153
|
status.className = state === "error" ? "status error" : state === "saved" ? "status saved" : "status saving";
|
|
147
|
-
status
|
|
154
|
+
uiText(status, state === "error" ? "common.save-failed" : state === "saved" ? "common.saved" : "common.saving");
|
|
148
155
|
});
|
|
149
156
|
|
|
150
157
|
// Program-level facts never change while a Source is being read, so they live in
|
|
@@ -154,14 +161,14 @@ function renderMeta(snapshot: StudioSnapshot): void {
|
|
|
154
161
|
project.title = snapshot.source.path;
|
|
155
162
|
// Sequence facts belong to the project/inspector, not the application chrome.
|
|
156
163
|
// The timeline transport is the single persistent time readout.
|
|
157
|
-
meta
|
|
164
|
+
userText(meta, "");
|
|
158
165
|
}
|
|
159
166
|
|
|
160
|
-
function property(label:
|
|
167
|
+
function property(label: Message, value: string, tone?: string): HTMLElement {
|
|
161
168
|
const node = document.createElement("div");
|
|
162
169
|
node.className = `property${tone === undefined ? "" : ` ${tone}`}`;
|
|
163
170
|
node.innerHTML = "<span></span><strong></strong>";
|
|
164
|
-
node.querySelector("span")
|
|
171
|
+
uiText(node.querySelector("span")!, label);
|
|
165
172
|
node.querySelector("strong")!.textContent = value;
|
|
166
173
|
node.querySelector("strong")!.title = value;
|
|
167
174
|
return node;
|
|
@@ -179,6 +186,12 @@ function group(label: string, items: readonly HTMLElement[], className = ""): HT
|
|
|
179
186
|
return node;
|
|
180
187
|
}
|
|
181
188
|
|
|
189
|
+
function uiGroup(label: Message, items: readonly HTMLElement[]): HTMLElement {
|
|
190
|
+
const node = group(label, items);
|
|
191
|
+
uiText(node.querySelector("h3")!, label);
|
|
192
|
+
return node;
|
|
193
|
+
}
|
|
194
|
+
|
|
182
195
|
function aspectRatio(width: number, height: number): string {
|
|
183
196
|
let a = width;
|
|
184
197
|
let b = height;
|
|
@@ -186,10 +199,10 @@ function aspectRatio(width: number, height: number): string {
|
|
|
186
199
|
return `${width / a}:${height / a}`;
|
|
187
200
|
}
|
|
188
201
|
|
|
189
|
-
const domainPresentation: Readonly<Record<StudioInspectorDomain, { readonly label:
|
|
190
|
-
where: { label: "
|
|
191
|
-
how: { label: "
|
|
192
|
-
when: { label: "
|
|
202
|
+
const domainPresentation: Readonly<Record<StudioInspectorDomain, { readonly label: Message; readonly icon: string }>> = {
|
|
203
|
+
where: { label: "inspector.where", icon: "where" },
|
|
204
|
+
how: { label: "inspector.how", icon: "how" },
|
|
205
|
+
when: { label: "inspector.when", icon: "when" },
|
|
193
206
|
};
|
|
194
207
|
const domainOrder: readonly StudioInspectorDomain[] = ["where", "when", "how"];
|
|
195
208
|
const inspectorDomainByEntity = new Map<string, StudioInspectorDomain>();
|
|
@@ -197,7 +210,7 @@ const inspectorPageByEntity = new Map<string, string>();
|
|
|
197
210
|
|
|
198
211
|
function defaultWorkspaceHeading(): void {
|
|
199
212
|
workspaceHeading.className = "pane-heading workspace-heading";
|
|
200
|
-
workspaceHeading.innerHTML = `<div class="pane-title">${icon("tune")}<h2
|
|
213
|
+
workspaceHeading.innerHTML = `<div class="pane-title">${icon("tune")}<h2>${uiLabel("inspector.properties")}</h2></div>`;
|
|
201
214
|
}
|
|
202
215
|
|
|
203
216
|
function inspectorHeading(
|
|
@@ -213,7 +226,7 @@ function inspectorHeading(
|
|
|
213
226
|
button.type = "button";
|
|
214
227
|
button.className = `inspector-domain-tab${domain === active ? " active" : ""}`;
|
|
215
228
|
button.dataset.domain = domain;
|
|
216
|
-
button.innerHTML = `${icon(presentation.icon)}<strong>${presentation.label}</strong>`;
|
|
229
|
+
button.innerHTML = `${icon(presentation.icon)}<strong>${uiLabel(presentation.label)}</strong>`;
|
|
217
230
|
button.setAttribute("aria-pressed", String(domain === active));
|
|
218
231
|
button.addEventListener("click", () => {
|
|
219
232
|
inspectorDomainByEntity.set(entityId, domain);
|
|
@@ -244,7 +257,7 @@ function commitControl(entityId: string, parameter: Clip["inspector"][number], r
|
|
|
244
257
|
void writeParameter(entityId, parameter, replacement);
|
|
245
258
|
} catch (error) {
|
|
246
259
|
restoreParameterControls(entityId);
|
|
247
|
-
status
|
|
260
|
+
uiText(status, "common.invalid-value"); status.className = "status error";
|
|
248
261
|
status.title = error instanceof Error ? error.message : String(error);
|
|
249
262
|
}
|
|
250
263
|
}
|
|
@@ -329,70 +342,7 @@ function selectControl(
|
|
|
329
342
|
});
|
|
330
343
|
menu.append(...options);
|
|
331
344
|
|
|
332
|
-
const close = (
|
|
333
|
-
control.classList.remove("open", "open-up");
|
|
334
|
-
trigger.setAttribute("aria-expanded", "false");
|
|
335
|
-
menu.hidden = true;
|
|
336
|
-
if (restoreFocus) trigger.focus();
|
|
337
|
-
};
|
|
338
|
-
const open = (focus: "selected" | "first" | "last" = "selected"): void => {
|
|
339
|
-
control.classList.add("open");
|
|
340
|
-
trigger.setAttribute("aria-expanded", "true");
|
|
341
|
-
menu.hidden = false;
|
|
342
|
-
control.classList.remove("open-up");
|
|
343
|
-
const scroll = control.closest<HTMLElement>(".workspace-scroll");
|
|
344
|
-
if (scroll !== null) {
|
|
345
|
-
const menuBox = menu.getBoundingClientRect();
|
|
346
|
-
const scrollBox = scroll.getBoundingClientRect();
|
|
347
|
-
if (menuBox.bottom > scrollBox.bottom && trigger.getBoundingClientRect().top - menuBox.height >= scrollBox.top) {
|
|
348
|
-
control.classList.add("open-up");
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
const target = focus === "first" ? options[0]
|
|
352
|
-
: focus === "last" ? options.at(-1)
|
|
353
|
-
: options.find((option) => option.classList.contains("active")) ?? options[0];
|
|
354
|
-
target?.focus();
|
|
355
|
-
};
|
|
356
|
-
const moveOptionFocus = (offset: number): void => {
|
|
357
|
-
const current = options.indexOf(document.activeElement as HTMLButtonElement);
|
|
358
|
-
const next = current < 0 ? 0 : (current + offset + options.length) % options.length;
|
|
359
|
-
options[next]?.focus();
|
|
360
|
-
};
|
|
361
|
-
|
|
362
|
-
trigger.addEventListener("click", () => {
|
|
363
|
-
if (control.classList.contains("open")) close(false);
|
|
364
|
-
else open();
|
|
365
|
-
});
|
|
366
|
-
trigger.addEventListener("keydown", (event) => {
|
|
367
|
-
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
368
|
-
event.preventDefault();
|
|
369
|
-
event.stopPropagation();
|
|
370
|
-
open(event.key === "ArrowDown" ? "first" : "last");
|
|
371
|
-
} else if (event.key === "Escape" && control.classList.contains("open")) {
|
|
372
|
-
event.preventDefault();
|
|
373
|
-
event.stopPropagation();
|
|
374
|
-
close(false);
|
|
375
|
-
}
|
|
376
|
-
});
|
|
377
|
-
menu.addEventListener("keydown", (event) => {
|
|
378
|
-
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
379
|
-
event.preventDefault();
|
|
380
|
-
event.stopPropagation();
|
|
381
|
-
moveOptionFocus(event.key === "ArrowDown" ? 1 : -1);
|
|
382
|
-
} else if (event.key === "Home" || event.key === "End") {
|
|
383
|
-
event.preventDefault();
|
|
384
|
-
event.stopPropagation();
|
|
385
|
-
options[event.key === "Home" ? 0 : options.length - 1]?.focus();
|
|
386
|
-
} else if (event.key === "Escape") {
|
|
387
|
-
event.preventDefault();
|
|
388
|
-
event.stopPropagation();
|
|
389
|
-
close(true);
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
control.addEventListener("focusout", (event) => {
|
|
393
|
-
if (event.relatedTarget instanceof Node && control.contains(event.relatedTarget)) return;
|
|
394
|
-
close(false);
|
|
395
|
-
});
|
|
345
|
+
const { close } = bindDropdown(control, trigger, menu, options);
|
|
396
346
|
control.append(trigger, menu);
|
|
397
347
|
return control;
|
|
398
348
|
}
|
|
@@ -413,7 +363,7 @@ function colorValueControl(
|
|
|
413
363
|
const picker = document.createElement("input");
|
|
414
364
|
picker.type = "color";
|
|
415
365
|
picker.className = "parameter-color-native";
|
|
416
|
-
picker
|
|
366
|
+
uiAttr(picker, "aria-label", "inspector.color-picker", { label });
|
|
417
367
|
picker.title = label;
|
|
418
368
|
const exact = /^#[0-9a-f]{6}(?:[0-9a-f]{2})?$/iu.test(initial);
|
|
419
369
|
picker.value = exact ? initial.slice(0, 7) : "#000000";
|
|
@@ -530,7 +480,7 @@ function structuredControl(entityId: string, parameter: Clip["inspector"][number
|
|
|
530
480
|
const save = (): void => {
|
|
531
481
|
const changed = !sameValue(draft, parameter.value);
|
|
532
482
|
notice.hidden = !changed || valid();
|
|
533
|
-
notice
|
|
483
|
+
uiText(notice, "inspector.invalid-fields");
|
|
534
484
|
if (changed && valid()) change(draft);
|
|
535
485
|
};
|
|
536
486
|
|
|
@@ -563,9 +513,8 @@ function structuredControl(entityId: string, parameter: Clip["inspector"][number
|
|
|
563
513
|
const summary = document.createElement("div");
|
|
564
514
|
summary.className = "parameter-structured-summary";
|
|
565
515
|
const count = document.createElement("span");
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
: "Structured value";
|
|
516
|
+
if (schema.kind === "array" && Array.isArray(draft)) uiText(count, "inspector.item-count", { count: draft.length });
|
|
517
|
+
else uiText(count, "inspector.structured-value");
|
|
569
518
|
summary.append(count);
|
|
570
519
|
if (schema.kind === "array") shell.append(summary);
|
|
571
520
|
|
|
@@ -600,19 +549,19 @@ function structuredControl(entityId: string, parameter: Clip["inspector"][number
|
|
|
600
549
|
const up = document.createElement("button");
|
|
601
550
|
up.type = "button";
|
|
602
551
|
up.textContent = "↑";
|
|
603
|
-
up
|
|
552
|
+
uiAttr(up, "title", "inspector.move-up");
|
|
604
553
|
up.disabled = index === 0;
|
|
605
554
|
up.addEventListener("click", () => move(-1));
|
|
606
555
|
const down = document.createElement("button");
|
|
607
556
|
down.type = "button";
|
|
608
557
|
down.textContent = "↓";
|
|
609
|
-
down
|
|
558
|
+
uiAttr(down, "title", "inspector.move-down");
|
|
610
559
|
down.disabled = index === list.length - 1;
|
|
611
560
|
down.addEventListener("click", () => move(1));
|
|
612
561
|
const remove = document.createElement("button");
|
|
613
562
|
remove.type = "button";
|
|
614
563
|
remove.innerHTML = icon("minus");
|
|
615
|
-
remove
|
|
564
|
+
uiAttr(remove, "title", "inspector.remove-item");
|
|
616
565
|
remove.disabled = schema.minItems !== undefined && list.length <= schema.minItems;
|
|
617
566
|
remove.addEventListener("click", () => {
|
|
618
567
|
const current = Array.isArray(draft) ? draft : [];
|
|
@@ -639,7 +588,7 @@ function structuredControl(entityId: string, parameter: Clip["inspector"][number
|
|
|
639
588
|
const add = document.createElement("button");
|
|
640
589
|
add.type = "button";
|
|
641
590
|
add.className = "parameter-structured-add";
|
|
642
|
-
add.innerHTML = `${icon("plus")}
|
|
591
|
+
add.innerHTML = `${icon("plus")}${uiLabel(schema.items.kind === "string" && schema.items.format === "color" ? "inspector.add-color" : "inspector.add-item")}`;
|
|
643
592
|
add.disabled = schema.maxItems !== undefined && list.length >= schema.maxItems;
|
|
644
593
|
add.addEventListener("click", () => {
|
|
645
594
|
const current = Array.isArray(draft) ? draft : [];
|
|
@@ -708,7 +657,7 @@ function parameterControl(
|
|
|
708
657
|
const palette = document.createElement("span"); palette.className = "parameter-swatches";
|
|
709
658
|
for (const color of parameter.swatches) {
|
|
710
659
|
const swatch = document.createElement("button"); swatch.type = "button";
|
|
711
|
-
swatch.style.backgroundColor = color; swatch.title = color; swatch
|
|
660
|
+
swatch.style.backgroundColor = color; swatch.title = color; uiAttr(swatch, "aria-label", "inspector.use-color", { color });
|
|
712
661
|
swatch.addEventListener("click", () => commit(color)); palette.append(swatch);
|
|
713
662
|
}
|
|
714
663
|
right.append(palette);
|
|
@@ -784,13 +733,13 @@ function parameterGroups(entityId: string, fields: readonly Clip["inspector"][nu
|
|
|
784
733
|
});
|
|
785
734
|
}
|
|
786
735
|
|
|
787
|
-
let parameterWriteState: "" | "
|
|
736
|
+
let parameterWriteState: "" | "common.saving" | "common.saved" | "common.failed" = "";
|
|
788
737
|
async function writeParameter(entityId: string, parameter: Clip["inspector"][number], replacement: CanonicalValue): Promise<void> {
|
|
789
738
|
const state = store.current();
|
|
790
739
|
if (state === undefined) return;
|
|
791
|
-
parameterWriteState = "
|
|
740
|
+
parameterWriteState = "common.saving";
|
|
792
741
|
status.title = "";
|
|
793
|
-
status
|
|
742
|
+
uiText(status, parameterWriteState);
|
|
794
743
|
status.className = "status saving";
|
|
795
744
|
try {
|
|
796
745
|
await applyStudioMutation({
|
|
@@ -800,15 +749,15 @@ async function writeParameter(entityId: string, parameter: Clip["inspector"][num
|
|
|
800
749
|
parameterId: parameter.id,
|
|
801
750
|
value: replacement,
|
|
802
751
|
});
|
|
803
|
-
parameterWriteState = "
|
|
804
|
-
status
|
|
752
|
+
parameterWriteState = "common.saved";
|
|
753
|
+
uiText(status, parameterWriteState);
|
|
805
754
|
status.className = "status saved";
|
|
806
755
|
} catch (error) {
|
|
807
756
|
// Validation and stale-revision rejections do not publish a new snapshot.
|
|
808
757
|
// Restore the accepted value just as a rejected compilation does.
|
|
809
758
|
restoreParameterControls(entityId);
|
|
810
|
-
parameterWriteState = "
|
|
811
|
-
status
|
|
759
|
+
parameterWriteState = "common.failed";
|
|
760
|
+
uiText(status, error instanceof Error ? "common.save-failed" : parameterWriteState);
|
|
812
761
|
status.className = "status error";
|
|
813
762
|
status.title = error instanceof Error ? error.message : String(error);
|
|
814
763
|
}
|
|
@@ -826,26 +775,26 @@ function renderInspector(snapshot: StudioSnapshot, clipId: string | undefined):
|
|
|
826
775
|
defaultWorkspaceHeading();
|
|
827
776
|
const fps = snapshot.space.frameRate.numerator / snapshot.space.frameRate.denominator;
|
|
828
777
|
inspector.replaceChildren(
|
|
829
|
-
|
|
830
|
-
property("
|
|
831
|
-
property("
|
|
832
|
-
property("
|
|
833
|
-
property("
|
|
778
|
+
uiGroup("inspector.project", [
|
|
779
|
+
property("inspector.author", snapshot.source.path, "property-code"),
|
|
780
|
+
property("inspector.run", snapshot.run.path, "property-code"),
|
|
781
|
+
property("inspector.sources", String(snapshot.source.files.length)),
|
|
782
|
+
property("inspector.tracks", String(snapshot.tracks.length), "property-number"),
|
|
834
783
|
]),
|
|
835
|
-
|
|
836
|
-
property("
|
|
837
|
-
property("
|
|
784
|
+
uiGroup("inspector.canvas", [
|
|
785
|
+
property("inspector.resolution", `${snapshot.space.canvasWidth} × ${snapshot.space.canvasHeight}`, "property-number"),
|
|
786
|
+
property("inspector.aspect-ratio", aspectRatio(snapshot.space.canvasWidth, snapshot.space.canvasHeight), "property-number"),
|
|
838
787
|
]),
|
|
839
|
-
|
|
840
|
-
property("
|
|
841
|
-
property("
|
|
842
|
-
property("
|
|
788
|
+
uiGroup("inspector.timeline", [
|
|
789
|
+
property("inspector.duration", `${snapshot.space.durationSec.toFixed(2)} s`, "property-number"),
|
|
790
|
+
property("inspector.frame-rate", `${fps.toFixed(Number.isInteger(fps) ? 0 : 2)} fps`, "property-number"),
|
|
791
|
+
property("inspector.frames", String(snapshot.space.frameCount), "property-number"),
|
|
843
792
|
]),
|
|
844
|
-
|
|
845
|
-
property("
|
|
793
|
+
uiGroup("inspector.build", [
|
|
794
|
+
property("inspector.targets", snapshot.run.targets
|
|
846
795
|
.map((target) => target.split("::output::").at(-1) ?? target)
|
|
847
796
|
.join(", ") || "—", "property-code"),
|
|
848
|
-
property("
|
|
797
|
+
property("inspector.candidates", String(snapshot.run.satisfactions.length), "property-number"),
|
|
849
798
|
]),
|
|
850
799
|
);
|
|
851
800
|
return;
|
|
@@ -855,7 +804,7 @@ function renderInspector(snapshot: StudioSnapshot, clipId: string | undefined):
|
|
|
855
804
|
defaultWorkspaceHeading();
|
|
856
805
|
const empty = document.createElement("div");
|
|
857
806
|
empty.className = "inspector-empty";
|
|
858
|
-
empty
|
|
807
|
+
uiText(empty, "inspector.empty");
|
|
859
808
|
inspector.replaceChildren(empty);
|
|
860
809
|
return;
|
|
861
810
|
}
|
|
@@ -908,7 +857,7 @@ function renderSemanticInspector(snapshot: StudioSnapshot, segmentId: string): v
|
|
|
908
857
|
defaultWorkspaceHeading();
|
|
909
858
|
const empty = document.createElement("div");
|
|
910
859
|
empty.className = "inspector-empty";
|
|
911
|
-
empty
|
|
860
|
+
uiText(empty, "inspector.empty");
|
|
912
861
|
inspector.replaceChildren(empty);
|
|
913
862
|
}
|
|
914
863
|
|
|
@@ -920,32 +869,36 @@ function semanticAnchorInspector(snapshot: StudioSnapshot, kind: "selection" | "
|
|
|
920
869
|
.map((handle) => ({ clip, handle })));
|
|
921
870
|
const current = kind === "selection" ? semantic?.selections.find((item) => item.id === id)
|
|
922
871
|
: semantic?.moments.find((item) => item.id === id);
|
|
923
|
-
if (!semantic || !current) return
|
|
924
|
-
const endpoints = "anchorId" in current ? [["
|
|
925
|
-
: [["
|
|
926
|
-
return
|
|
872
|
+
if (!semantic || !current) return uiGroup("inspector.timing", []);
|
|
873
|
+
const endpoints: readonly (readonly [Message, string])[] = "anchorId" in current ? [["inspector.moment", current.anchorId]]
|
|
874
|
+
: [["inspector.start", current.startAnchorId], ["inspector.end", current.endAnchorId]];
|
|
875
|
+
return uiGroup("inspector.semantic-anchors", endpoints.map(([label, anchorId]) => {
|
|
927
876
|
const anchor = semantic.anchors.find((item) => item.id === anchorId)!;
|
|
928
|
-
const describe = (item: typeof anchor) => {
|
|
877
|
+
const describe = (node: Element, item: typeof anchor) => {
|
|
929
878
|
const word = semantic.tokens.find((token) => token.id === item.tokenId)?.text;
|
|
930
|
-
|
|
879
|
+
uiText(node, `inspector.anchor.${item.kind}`, { detail: `${word ? ` · ${word}` : ""}${item.segmentId ? ` · ${item.segmentId}` : ""}` });
|
|
931
880
|
};
|
|
932
881
|
const candidates = semantic.anchors.filter((item) => item.frame === anchor.frame).flatMap((item) => {
|
|
933
882
|
const target: SemanticTarget = "anchorId" in current ? { kind: "moment", anchorId: item.id }
|
|
934
|
-
: { kind: "selection", startAnchorId: label === "
|
|
935
|
-
endAnchorId: label === "
|
|
883
|
+
: { kind: "selection", startAnchorId: label === "inspector.start" ? item.id : current.startAnchorId,
|
|
884
|
+
endAnchorId: label === "inspector.end" ? item.id : current.endAnchorId };
|
|
936
885
|
const owner = consumers.find(({ handle }) => semanticGestureSpan(semantic.anchors, handle, target) !== undefined);
|
|
937
886
|
return owner ? [{ item, target, ...owner }] : [];
|
|
938
887
|
});
|
|
939
|
-
if (candidates.length < 2)
|
|
888
|
+
if (candidates.length < 2) {
|
|
889
|
+
const row = property(label!, "");
|
|
890
|
+
describe(row.querySelector("strong")!, anchor);
|
|
891
|
+
return row;
|
|
892
|
+
}
|
|
940
893
|
const row = document.createElement("label");
|
|
941
894
|
row.className = "property";
|
|
942
895
|
const name = document.createElement("span");
|
|
943
|
-
name
|
|
896
|
+
uiText(name, label!);
|
|
944
897
|
const control = document.createElement("select");
|
|
945
898
|
control.className = "parameter-value";
|
|
946
|
-
control
|
|
899
|
+
uiAttr(control, "aria-label", label === "inspector.start" ? "inspector.start-anchor" : label === "inspector.end" ? "inspector.end-anchor" : "inspector.moment-anchor");
|
|
947
900
|
for (const { item } of candidates) {
|
|
948
|
-
const option = document.createElement("option"); option.value = item.id; option
|
|
901
|
+
const option = document.createElement("option"); option.value = item.id; describe(option, item);
|
|
949
902
|
control.append(option);
|
|
950
903
|
}
|
|
951
904
|
control.value = anchorId!;
|
|
@@ -954,14 +907,14 @@ function semanticAnchorInspector(snapshot: StudioSnapshot, kind: "selection" | "
|
|
|
954
907
|
const span = semanticGestureSpan(semantic.anchors, choice.handle, choice.target)!;
|
|
955
908
|
const temporal = choice.handle.temporal!;
|
|
956
909
|
control.disabled = true;
|
|
957
|
-
status
|
|
910
|
+
uiText(status, "common.saving"); status.className = "status saving";
|
|
958
911
|
void applyStudioMutation({ type: "timeline.adjust", revision: snapshot.revision,
|
|
959
912
|
entityId: choice.clip.id, gesture: choice.handle.gesture,
|
|
960
913
|
target: temporal.kind === "instant" ? { kind: "instant", frame: span.startFrame, semantic: choice.target }
|
|
961
914
|
: { kind: "window", ...span, semantic: choice.target },
|
|
962
|
-
}).then(() => { status
|
|
915
|
+
}).then(() => { uiText(status, "common.saved"); status.className = "status saved"; })
|
|
963
916
|
.catch((error: unknown) => {
|
|
964
|
-
control.value = anchorId!; status
|
|
917
|
+
control.value = anchorId!; uiText(status, "common.save-failed"); status.className = "status error";
|
|
965
918
|
status.title = error instanceof Error ? error.message : String(error);
|
|
966
919
|
}).finally(() => { control.disabled = false; });
|
|
967
920
|
});
|
|
@@ -1145,10 +1098,10 @@ window.addEventListener("keydown", (event) => {
|
|
|
1145
1098
|
});
|
|
1146
1099
|
|
|
1147
1100
|
function applySnapshot(snapshot: StudioSnapshot): void {
|
|
1148
|
-
failureView
|
|
1101
|
+
userText(failureView, "");
|
|
1149
1102
|
status.className = "status";
|
|
1150
1103
|
status.title = "";
|
|
1151
|
-
status
|
|
1104
|
+
userText(status, "");
|
|
1152
1105
|
renderMeta(snapshot);
|
|
1153
1106
|
library.show(snapshot);
|
|
1154
1107
|
store.load(snapshot);
|
|
@@ -1156,7 +1109,7 @@ function applySnapshot(snapshot: StudioSnapshot): void {
|
|
|
1156
1109
|
|
|
1157
1110
|
function applyFailure(failure: StudioFailure): void {
|
|
1158
1111
|
status.className = "status error";
|
|
1159
|
-
status
|
|
1112
|
+
uiText(status, "common.compile-failed");
|
|
1160
1113
|
failureView.textContent = failure.error;
|
|
1161
1114
|
if (failure.range !== undefined) code.highlight([{ range: failure.range, tone: "element" }], true);
|
|
1162
1115
|
// A parameter control changes immediately in the browser, but the source
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { uiAttr, uiText, t } from "./i18n.js";
|
|
1
2
|
import type { StudioSnapshot } from "../shared.js";
|
|
2
3
|
import { feedbackClock } from "../feedback.js";
|
|
3
4
|
|
|
@@ -11,10 +12,11 @@ export function createScrubPreview(container: HTMLElement) {
|
|
|
11
12
|
const element = document.createElement("div");
|
|
12
13
|
element.className = "scrub-preview";
|
|
13
14
|
element.hidden = true;
|
|
14
|
-
element.innerHTML = '<div class="scrub-preview-picture"><span
|
|
15
|
+
element.innerHTML = '<div class="scrub-preview-picture"><span></span></div><div class="scrub-preview-time"></div>';
|
|
15
16
|
element.setAttribute("aria-hidden", "true");
|
|
16
17
|
const picture = element.querySelector<HTMLElement>(".scrub-preview-picture")!;
|
|
17
18
|
const status = picture.querySelector<HTMLElement>("span")!;
|
|
19
|
+
uiText(status, "player.frame-loading");
|
|
18
20
|
const time = element.querySelector<HTMLElement>(".scrub-preview-time")!;
|
|
19
21
|
container.append(element);
|
|
20
22
|
let iframe: HTMLIFrameElement | undefined;
|
|
@@ -34,7 +36,7 @@ export function createScrubPreview(container: HTMLElement) {
|
|
|
34
36
|
const target = current.contentWindow as PreviewWindow | null;
|
|
35
37
|
const placed = await target?.__hypitSeekFrame?.(frame);
|
|
36
38
|
if (iframe !== current) return;
|
|
37
|
-
if (!placed) throw new Error("
|
|
39
|
+
if (!placed) throw new Error(t("player.frame-unavailable"));
|
|
38
40
|
displayed = frame;
|
|
39
41
|
if (requested === frame) {
|
|
40
42
|
current.style.visibility = "visible";
|
|
@@ -43,7 +45,7 @@ export function createScrubPreview(container: HTMLElement) {
|
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
} catch {
|
|
46
|
-
if (iframe === current) status
|
|
48
|
+
if (iframe === current) uiText(status, "player.frame-unavailable");
|
|
47
49
|
} finally {
|
|
48
50
|
if (iframe === current) seeking = false;
|
|
49
51
|
}
|
|
@@ -65,7 +67,7 @@ export function createScrubPreview(container: HTMLElement) {
|
|
|
65
67
|
picture.style.width = `${canvasWidth * scale}px`;
|
|
66
68
|
picture.style.height = `${canvasHeight * scale}px`;
|
|
67
69
|
const created = document.createElement("iframe");
|
|
68
|
-
created
|
|
70
|
+
uiAttr(created, "title", "player.hovered-frame");
|
|
69
71
|
created.tabIndex = -1;
|
|
70
72
|
created.setAttribute("sandbox", "allow-scripts allow-same-origin");
|
|
71
73
|
created.setAttribute("allow", "autoplay 'none'");
|
|
@@ -80,7 +82,7 @@ export function createScrubPreview(container: HTMLElement) {
|
|
|
80
82
|
if (iframe !== created) return;
|
|
81
83
|
ready = true;
|
|
82
84
|
await seek();
|
|
83
|
-
})().catch(() => { if (iframe === created) status
|
|
85
|
+
})().catch(() => { if (iframe === created) uiText(status, "player.frame-unavailable"); });
|
|
84
86
|
});
|
|
85
87
|
created.srcdoc = snapshot.preview.srcdoc;
|
|
86
88
|
iframe = created;
|
|
@@ -94,7 +96,7 @@ export function createScrubPreview(container: HTMLElement) {
|
|
|
94
96
|
time.textContent = feedbackClock(frame * snapshot.space.frameRate.denominator / snapshot.space.frameRate.numerator);
|
|
95
97
|
if (frame !== displayed) {
|
|
96
98
|
iframe.style.visibility = "hidden";
|
|
97
|
-
status
|
|
99
|
+
uiText(status, "player.frame-loading");
|
|
98
100
|
status.hidden = false;
|
|
99
101
|
delete element.dataset.frame;
|
|
100
102
|
} else {
|