@hypit/hypit 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +1 -1
  2. package/dist/public/studio-adapter.d.ts +31 -2
  3. package/package.json +4 -1
  4. package/packages/audio-track-studio/src/index.ts +7 -4
  5. package/packages/build-result/README.md +58 -0
  6. package/packages/build-result/package.json +2 -1
  7. package/packages/build-result/src/decode.ts +10 -8
  8. package/packages/build-result/src/file-reference.ts +42 -0
  9. package/packages/build-result/src/index.ts +3 -0
  10. package/packages/build-result/src/replace-file-windows.ts +58 -0
  11. package/packages/build-result/src/replace-file.ts +11 -0
  12. package/packages/build-result/src/store.ts +44 -13
  13. package/packages/build-result/src/types.ts +38 -8
  14. package/packages/build-result/src/writer.ts +12 -1
  15. package/packages/build-result-fs/README.md +23 -1
  16. package/packages/build-result-s3/README.md +5 -0
  17. package/packages/build-result-s3/src/repository.ts +19 -5
  18. package/packages/caption-fine/README.md +17 -2
  19. package/packages/caption-fine/src/manifest.ts +1 -1
  20. package/packages/caption-fine/src/render.ts +48 -36
  21. package/packages/caption-fine-studio/package.json +8 -3
  22. package/packages/caption-fine-studio/src/index.ts +10 -1
  23. package/packages/cli/README.md +25 -0
  24. package/packages/cli/src/arguments.ts +16 -11
  25. package/packages/cli/src/build-planning.ts +6 -1
  26. package/packages/cli/src/command.ts +1 -1
  27. package/packages/cli/src/commands/environment.ts +42 -13
  28. package/packages/cli/src/machine-view.ts +12 -3
  29. package/packages/cli/src/main.ts +4 -0
  30. package/packages/cli/src/output.ts +28 -12
  31. package/packages/cli/src/result-export.ts +22 -13
  32. package/packages/cli/src/run-file.ts +19 -8
  33. package/packages/comment-sticker-studio/src/index.ts +2 -1
  34. package/packages/compiler-node/src/compiler.ts +1 -0
  35. package/packages/estimate/README.md +56 -3
  36. package/packages/estimate/src/program.ts +7 -8
  37. package/packages/fonts-open/README.md +5 -0
  38. package/packages/fonts-open/package.json +5 -1
  39. package/packages/fonts-open/src/studio.ts +21 -0
  40. package/packages/media-track-studio/src/index.ts +7 -2
  41. package/packages/provider-whisperx-local/README.md +57 -0
  42. package/packages/ranking/src/render.ts +11 -9
  43. package/packages/ranking-studio/README.md +11 -0
  44. package/packages/ranking-studio/package.json +8 -3
  45. package/packages/ranking-studio/src/index.ts +11 -4
  46. package/packages/runtime/src/catalog.ts +2 -0
  47. package/packages/runtime-host-node/src/index.ts +1 -0
  48. package/packages/runtime-local/README.md +19 -0
  49. package/packages/runtime-local/src/programs.ts +39 -24
  50. package/packages/runtime-local/src/runtime.ts +2 -0
  51. package/packages/script/README.md +22 -0
  52. package/packages/script/src/edit.ts +123 -73
  53. package/packages/script/src/lexical.ts +40 -6
  54. package/packages/script/src/parser.ts +36 -8
  55. package/packages/script/src/types.ts +4 -0
  56. package/packages/seedance-kits/README.md +3 -1
  57. package/packages/seedance-kits/kits/speaker-v1.svs +5 -5
  58. package/packages/speech-alignment/src/align.ts +45 -21
  59. package/packages/studio/INSPECTOR.md +104 -0
  60. package/packages/studio/README.md +65 -4
  61. package/packages/studio/src/build-library.ts +99 -84
  62. package/packages/studio/src/library-media.ts +19 -0
  63. package/packages/studio/src/parameter-values.ts +41 -1
  64. package/packages/studio/src/parameters.ts +20 -21
  65. package/packages/studio/src/server.ts +58 -30
  66. package/packages/studio/src/shared.ts +20 -6
  67. package/packages/studio/src/studio-registry.ts +22 -0
  68. package/packages/studio/src/style.css +135 -144
  69. package/packages/studio/src/temporal-edit.ts +131 -0
  70. package/packages/studio/src/ui/artifact-name.ts +80 -0
  71. package/packages/studio/src/ui/artifact-preview.ts +99 -0
  72. package/packages/studio/src/ui/icons.ts +5 -1
  73. package/packages/studio/src/ui/library.ts +373 -192
  74. package/packages/studio/src/ui/main.ts +153 -31
  75. package/packages/studio/src/ui/material-preview.ts +1 -1
  76. package/packages/studio/src/ui/overlay.ts +21 -20
  77. package/packages/studio/src/ui/sidebar-panel.ts +31 -0
  78. package/packages/studio/src/ui/stage.ts +124 -8
  79. package/packages/studio/src/ui/timeline.ts +16 -179
  80. package/packages/studio/start.ts +15 -3
  81. package/packages/studio-adapter/README.md +21 -0
  82. package/packages/studio-adapter/src/index.ts +26 -1
  83. package/packages/temporal-markup/EDITING.md +183 -0
  84. package/packages/temporal-markup/README.md +4 -0
  85. package/packages/typography-track-studio/package.json +8 -3
  86. package/packages/typography-track-studio/src/index.ts +5 -1
  87. package/packages/video-cli/README.md +12 -3
  88. package/packages/video-cli/src/creation.ts +11 -5
  89. package/packages/whisperx/README.md +7 -0
  90. package/packages/workspace/src/index.ts +2 -0
  91. package/packages/workspace-fs-node/README.md +4 -1
  92. package/packages/workspace-fs-node/src/workspace.ts +3 -1
@@ -51,8 +51,8 @@
51
51
  --tone-neutral: #6a737c;
52
52
  --tone-neutral-rail: #aeb9c4;
53
53
  --timeline-label-width: 188px;
54
- --source-width: 350px;
55
- --workspace-width: 300px;
54
+ --source-width: 416px;
55
+ --workspace-width: 320px;
56
56
  --timeline-height: 390px;
57
57
  --timeline-item-radius: 2px;
58
58
  --timeline-selection-stroke: 1px;
@@ -261,33 +261,10 @@ button:focus-visible, textarea:focus-visible {
261
261
  box-shadow: 0 -3px 12px color-mix(in srgb, var(--interaction-accent) 28%, transparent);
262
262
  }
263
263
  .library-tab > span { position: relative; display: grid; place-items: center; width: 16px; height: 16px; flex: none; }
264
+ .library .icon { width: 14px; height: 14px; }
264
265
  .library-tab > strong { overflow: hidden; font-size: 12px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
265
266
  .library-view { flex: 1; min-width: 0; min-height: 0; display: none; flex-direction: column; }
266
267
  .library-view.active { display: flex; }
267
- .library-toolbar {
268
- min-height: 46px;
269
- flex: none;
270
- display: flex;
271
- align-items: center;
272
- justify-content: space-between;
273
- gap: 12px;
274
- padding: 0 12px;
275
- border-bottom: 1px solid rgba(255, 255, 255, .045);
276
- background: #16171a;
277
- }
278
- .library-toolbar > div { min-width: 0; }
279
- .library-toolbar strong, .library-toolbar small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
280
- .library-toolbar strong { color: #d9dbe0; font-size: 11px; font-weight: 600; }
281
- .library-toolbar small { margin-top: 1px; color: var(--text-3); font-size: 9px; font-weight: 500; }
282
- .library-context {
283
- flex: none;
284
- padding: 3px 6px;
285
- border: 1px solid #303238;
286
- border-radius: 4px;
287
- background: #1d1f23;
288
- color: #858a93;
289
- font: 500 9px/1.2 var(--font-mono);
290
- }
291
268
  .library-refresh {
292
269
  width: 27px;
293
270
  height: 27px;
@@ -306,7 +283,7 @@ button:focus-visible, textarea:focus-visible {
306
283
  .library.is-refreshing .library-refresh .icon { animation: library-spin 700ms linear infinite; }
307
284
  @keyframes library-spin { to { transform: rotate(360deg); } }
308
285
 
309
- .source-workspace {
286
+ .sidebar-panel {
310
287
  flex: 1;
311
288
  min-width: 0;
312
289
  min-height: 0;
@@ -314,20 +291,20 @@ button:focus-visible, textarea:focus-visible {
314
291
  grid-template-columns: 44px minmax(0, 1fr);
315
292
  grid-template-rows: 36px minmax(0, 1fr);
316
293
  }
317
- .source-sidebar-heading,
318
- .source-code-heading {
294
+ .sidebar-panel-heading,
295
+ .sidebar-panel-toolbar {
319
296
  min-width: 0;
320
297
  display: flex;
321
298
  align-items: center;
322
299
  border-bottom: 1px solid var(--line);
323
300
  background: #151619;
324
301
  }
325
- .source-sidebar-heading { justify-content: center; border-right: 1px solid var(--line); }
326
- .source-sidebar-mark { width: 28px; height: 28px; display: grid; place-items: center; color: var(--text-3); }
327
- .source-sidebar-mark .icon { width: 14px; height: 14px; }
328
- .source-code-heading { padding: 0 7px 0 10px; }
329
- .source-code-heading .code-toolbar { width: 100%; }
330
- .source-files {
302
+ .sidebar-panel-heading { justify-content: center; border-right: 1px solid var(--line); }
303
+ .sidebar-panel-mark { width: 28px; height: 28px; display: grid; place-items: center; color: var(--text-3); }
304
+ .sidebar-panel-mark .icon { width: 14px; height: 14px; }
305
+ .sidebar-panel-toolbar { padding: 0 7px 0 10px; }
306
+ .sidebar-panel-toolbar .code-toolbar { width: 100%; }
307
+ .sidebar-panel-nav {
331
308
  min-width: 0;
332
309
  min-height: 0;
333
310
  overflow-y: auto;
@@ -337,14 +314,12 @@ button:focus-visible, textarea:focus-visible {
337
314
  background: #121316;
338
315
  scrollbar-width: none;
339
316
  }
340
- .source-files::-webkit-scrollbar { width: 0; }
341
- .source-file {
317
+ .sidebar-panel-nav::-webkit-scrollbar { width: 0; }
318
+ .sidebar-panel-item {
342
319
  width: 32px;
343
320
  height: 32px;
344
321
  display: grid;
345
- grid-template-columns: 16px;
346
- justify-content: center;
347
- align-items: center;
322
+ place-items: center;
348
323
  margin: 0 auto 2px;
349
324
  padding: 0;
350
325
  border: 0;
@@ -354,105 +329,89 @@ button:focus-visible, textarea:focus-visible {
354
329
  text-align: left;
355
330
  transition: color 100ms ease;
356
331
  }
357
- .source-file:hover { color: #bfc2c9; }
358
- .source-file.active {
332
+ .sidebar-panel-item:hover { color: #bfc2c9; }
333
+ .sidebar-panel-item.active {
359
334
  background: #181a1f;
360
335
  color: #cfd2d8;
361
336
  }
362
- .source-file-icon { width: 16px; height: 16px; display: grid; place-items: center; color: #858a93; }
363
- .source-file.active .source-file-icon { color: #858a93; }
364
- .source-file-icon .icon { width: 14px; height: 14px; }
337
+ .sidebar-panel-item .icon { width: 14px; height: 14px; }
338
+ .sidebar-panel-content { min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
339
+ .sidebar-panel-toolbar { justify-content: space-between; gap: 8px; }
340
+ .media-status { padding: 8px 10px; color: #e58b92; font-size: 10px; }
365
341
  .source-code { min-width: 0; min-height: 0; overflow: hidden; }
366
342
  .source-code > .code { height: 100%; }
367
343
  .source-code .code-scroll { background: #141518; }
368
344
 
369
- .library-context-row { flex: none; min-height: 36px; padding: 7px 10px; border-bottom: 1px solid rgba(255, 255, 255, .04); background: #141518; }
370
- .runtime-chip {
371
- min-width: 0;
372
- display: flex;
373
- align-items: center;
374
- gap: 7px;
375
- padding: 4px 7px;
376
- border: 1px solid #292c31;
377
- border-radius: 5px;
378
- background: #191a1e;
379
- }
380
- .runtime-chip::before { content: ""; width: 6px; height: 6px; flex: none; border-radius: 50%; background: var(--interaction-accent); box-shadow: 0 0 8px var(--brand-soft); }
381
- .runtime-chip > span { min-width: 0; overflow: hidden; color: #a9adb5; font: 9px/1.2 var(--font-mono); text-overflow: ellipsis; white-space: nowrap; }
382
- .runtime-chip > strong { margin-left: auto; color: #666b74; font-size: 9px; font-weight: 500; }
383
- .task-list { flex: 1; min-height: 0; overflow: auto; padding: 8px 9px 28px; background: #121316; }
384
- .task-card {
385
- min-width: 0;
386
- display: grid;
387
- grid-template-columns: 18px minmax(0, 1fr);
388
- gap: 8px;
389
- margin-bottom: 6px;
390
- padding: 10px 10px 9px 8px;
391
- border: 1px solid #292b31;
392
- border-radius: 7px;
393
- background: linear-gradient(135deg, #1c1e22, #191a1e);
394
- box-shadow: 0 1px 0 rgba(255, 255, 255, .018), 0 6px 18px rgba(0, 0, 0, .08);
395
- transition: border-color 120ms ease, transform 120ms ease, background 120ms ease;
396
- }
397
- .task-card:hover { border-color: #3a3d44; background: linear-gradient(135deg, #202227, #1b1d21); transform: translateY(-1px); }
398
- .task-state { display: grid; place-items: start center; padding-top: 4px; }
399
- .task-state span { width: 8px; height: 8px; border-radius: 50%; background: #686d76; box-shadow: 0 0 0 3px rgba(104, 109, 118, .1); }
400
- .task-running .task-state span, .task-queued .task-state span, .task-waiting .task-state span, .task-active .task-state span { background: #49b9d0; box-shadow: 0 0 0 3px rgba(73, 185, 208, .11), 0 0 10px rgba(73, 185, 208, .18); }
401
- .task-complete .task-state span { background: var(--ok); box-shadow: 0 0 0 3px rgba(100, 201, 154, .1); }
402
- .task-failed .task-state span, .task-cancelled .task-state span, .task-blocked .task-state span { background: #d8646d; box-shadow: 0 0 0 3px rgba(216, 100, 109, .1); }
345
+ .task-list { flex: 1; min-height: 0; overflow: auto; padding: 12px 10px 28px; background: #121316; }
346
+ .task-card { min-width: 0; display: grid; grid-template-columns: 24px minmax(0, 1fr) 20px; align-items: start; gap: 9px; margin-bottom: 8px; padding: 11px 9px; border: 1px solid #25272c; border-radius: 4px; background: #191b20; }
347
+ .task-state { display: grid; place-items: center; width: 24px; height: 24px; color: #858c98; }
348
+ .task-state .icon { width: 17px; height: 17px; }
403
349
  .task-copy { min-width: 0; }
404
- .task-title { min-width: 0; display: flex; align-items: center; gap: 7px; }
405
- .task-title strong { min-width: 0; flex: 1; overflow: hidden; color: #dfe1e5; font-size: 11px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
406
- .task-status { flex: none; padding: 2px 5px; border-radius: 8px; background: #292c31; color: #9297a1; font: 600 8px/1.2 var(--font-ui); text-transform: uppercase; }
407
- .task-running .task-status, .task-queued .task-status, .task-waiting .task-status, .task-active .task-status { background: rgba(73, 185, 208, .12); color: #79d1e2; }
408
- .task-complete .task-status { background: rgba(100, 201, 154, .11); color: #86d7b1; }
409
- .task-failed .task-status, .task-cancelled .task-status, .task-blocked .task-status { background: rgba(216, 100, 109, .1); color: #e58b92; }
410
- .task-source { margin: 3px 0 6px; overflow: hidden; color: #6f747d; font: 9px/1.25 var(--font-mono); text-overflow: ellipsis; white-space: nowrap; }
411
- .task-facts { min-width: 0; display: flex; align-items: center; gap: 5px; overflow: hidden; }
412
- .task-facts span { min-width: 0; overflow: hidden; color: #686d76; font-size: 9px; text-overflow: ellipsis; white-space: nowrap; }
413
- .task-facts span + span::before { content: "·"; margin-right: 5px; color: #3e4148; }
414
- .task-progress { margin-top: 7px; padding-top: 6px; border-top: 1px solid rgba(255, 255, 255, .045); color: #8d929b; font-size: 9px; }
350
+ .task-title { min-width: 0; }
351
+ .task-name { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; max-width: 100%; overflow: hidden; padding: 0; border: 0; background: none; color: #dfe1e5; font: 500 11px/17px var(--font-ui); text-align: left; overflow-wrap: anywhere; cursor: pointer; }
352
+ .task-name:hover { color: #fff; }
353
+ .task-name:focus-visible { outline: 1px solid #626875; outline-offset: 2px; border-radius: 2px; }
354
+ .task-facts { min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 3px 10px; margin-top: 5px; line-height: 14px; }
355
+ .task-status { color: #949aa5; font-size: 10px; }
356
+ .task-facts time { color: #727985; font-size: 9px; }
357
+ .task-running .task-state, .task-running .task-status, .task-queued .task-state, .task-queued .task-status, .task-waiting .task-state, .task-waiting .task-status, .task-saving-result .task-state, .task-saving-result .task-status { color: #8daabb; }
358
+ .task-complete .task-state, .task-complete .task-status { color: #87a896; }
359
+ .task-failed .task-state, .task-failed .task-status, .task-attention .task-state, .task-attention .task-status { color: #c99490; }
360
+ .task-progress { margin-top: 7px; overflow: hidden; color: #9ba1ac; font-size: 10px; line-height: 15px; white-space: nowrap; text-overflow: ellipsis; }
361
+ .task-detail { margin-top: 7px; color: #bc9693; font-size: 10px; line-height: 15px; }
362
+ .task-error-message { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; overflow-wrap: anywhere; white-space: pre-wrap; user-select: text; }
363
+ .task-card.selected { background: #24272d; }
364
+ .task-card.selected .task-error-message { display: block; }
365
+ .task-copy-error { display: none; margin-top: 6px; padding: 0; border: 0; background: transparent; color: #aeb4bf; font: inherit; cursor: pointer; }
366
+ .task-card.selected .task-copy-error { display: inline-block; }
367
+ .task-card:focus-visible { outline: 1px solid #626875; outline-offset: -1px; }
368
+ .task-context { flex: 1; min-width: 0; display: flex; align-items: center; gap: 3px; color: #b9bfca; }
369
+ .task-context::before { content: "/"; flex: none; color: #5d6572; margin-right: 3px; }
370
+ .sidebar-panel-toolbar > .library-refresh { margin-left: auto; }
371
+ .sidebar-panel-toolbar:has(.task-context:not([hidden])) > .code-location { flex: none; max-width: 90px; }
372
+ .task-context[hidden] { display: none; }
373
+ .task-context-name { flex: 1; min-width: 0; display: flex; align-items: center; gap: 6px; padding: 3px 0; border: 0; background: transparent; color: inherit; font: 500 10px/16px var(--font-ui); text-align: left; }
374
+ .task-context-name .icon { flex: none; }
375
+ .task-context-name span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
376
+ .task-context-clear { flex: none; display: grid; place-items: center; width: 22px; height: 22px; padding: 0; border: 0; border-radius: 3px; background: transparent; color: #929aa7; }
377
+ .task-context-clear:hover, .task-context-name:hover { color: #f1f2f5; }
378
+ .task-context-clear:hover { background: #30343c; }
379
+ .task-context button:focus-visible { outline: 1px solid #626875; }
380
+ .task-open { display: grid; place-items: center; width: 20px; height: 24px; padding: 0; border: 0; border-radius: 3px; background: transparent; color: #707885; }
381
+ .task-open .icon { width: 14px; height: 14px; }
382
+ .task-open:hover { color: #dfe1e5; background: #25282f; }
383
+ .task-open:focus-visible { outline: 1px solid #626875; }
415
384
 
416
- .artifact-filters { min-height: 34px; flex: none; display: flex; align-items: center; gap: 4px; padding: 6px 10px; border-bottom: 1px solid rgba(255, 255, 255, .04); background: #141518; }
417
- .artifact-filters button { height: 22px; padding: 0 9px; border: 0; border-radius: 4px; background: transparent; color: #777c85; font-size: 9px; }
418
- .artifact-filters button:hover { background: #202227; color: #b9bdc5; }
419
- .artifact-filters button.active { background: #2a2c31; color: #eef0f3; }
420
- .artifact-grid { flex: 1; min-height: 0; overflow: auto; display: grid; grid-template-columns: repeat(auto-fill, minmax(142px, 1fr)); align-content: start; gap: 9px; padding: 10px 10px 30px; background: #121316; }
421
- .artifact-card { height: 190px; min-width: 0; overflow: hidden; display: flex; flex-direction: column; border: 1px solid #292b31; border-radius: 7px; background: #1a1c20; color: inherit; text-decoration: none; box-shadow: 0 7px 18px rgba(0, 0, 0, .09); transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease; }
422
- .artifact-card.artifact-highlighted { border-color: rgba(237, 152, 185, .55); box-shadow: 0 0 0 1px rgba(237, 152, 185, .08), 0 9px 22px rgba(0, 0, 0, .14); }
423
- .artifact-card:hover { border-color: #454952; transform: translateY(-2px); box-shadow: 0 10px 22px rgba(0, 0, 0, .17); }
424
- .artifact-preview { position: relative; flex: 1 1 auto; min-height: 0; overflow: hidden; display: grid; place-items: center; border-bottom: 1px solid rgba(255, 255, 255, .055); background: radial-gradient(circle at 35% 24%, #30343c, #202228 58%, #1a1b1f); }
425
- .artifact-preview::after { content: ""; position: absolute; inset: 0; background: linear-gradient(180deg, transparent 54%, rgba(8, 9, 11, .32)); pointer-events: none; }
426
- .artifact-image .artifact-preview { background: #202228; }
427
- .artifact-image .artifact-preview img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
428
- .artifact-glyph { position: relative; z-index: 1; width: 36px; height: 36px; display: grid; place-items: center; border: 1px solid rgba(255, 255, 255, .075); border-radius: 9px; background: rgba(12, 13, 16, .28); color: #aab0ba; backdrop-filter: blur(7px); }
429
- .artifact-image .artifact-glyph { display: none; }
430
- .artifact-glyph .icon { width: 20px; height: 20px; }
431
- .artifact-video .artifact-glyph { color: #75badf; }
432
- .artifact-audio .artifact-glyph { color: #83cf9d; }
433
- .artifact-type { position: absolute; z-index: 2; right: 6px; bottom: 5px; padding: 2px 5px; border-radius: 4px; background: rgba(10, 11, 13, .62); color: #d2d5da; font: 600 8px/1.2 var(--font-ui); backdrop-filter: blur(6px); }
434
- .artifact-highlight { position: absolute; z-index: 2; left: 6px; top: 6px; padding: 3px 6px; border-radius: 4px; background: rgba(77, 38, 55, .82); color: #f1abc5; font: 600 8px/1 var(--font-ui); letter-spacing: .02em; }
435
- .artifact-copy { min-width: 0; flex: none; min-height: 45px; padding: 8px 9px 9px; }
436
- .artifact-copy strong, .artifact-copy span { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
437
- .artifact-copy strong { color: #d8dae0; font-size: 10px; font-weight: 600; }
438
- .artifact-meta { margin-top: 3px; color: #666b74; font-size: 8px; }
385
+ .artifact-grid { flex: 1; min-height: 0; overflow: auto; display: grid; grid-template-columns: repeat(auto-fill, 104px); align-content: start; justify-content: space-between; gap: 12px; padding: 14px 12px 30px; background: #121316; }
386
+ .artifact-card { --artifact-selection: #24272d; min-width: 0; padding: 0; border: 0; border-radius: 3px; display: flex; flex-direction: column; color: inherit; text-decoration: none; }
387
+ .artifact-card:focus-visible { outline: none; }
388
+ .artifact-card:focus-visible .artifact-preview { outline: 1px solid #656b79; outline-offset: 2px; border-radius: 3px; }
389
+ .artifact-preview { position: relative; flex: none; min-height: 0; width: 100%; aspect-ratio: 1; display: flex; align-items: center; justify-content: center; }
390
+ .artifact-preview img, .artifact-preview video { position: absolute; display: block; max-width: 100%; max-height: 100%; width: auto; height: auto; border-radius: 2px; object-fit: contain; }
391
+ .artifact-video .artifact-preview video { opacity: 0; pointer-events: none; }
392
+ .artifact-video .artifact-preview video.is-ready { opacity: 1; }
393
+ .artifact-glyph { display: none; }
394
+ .artifact-audio .artifact-glyph { display: grid; position: absolute; place-items: center; width: 100%; height: 42px; color: #87a896; background: #202824; border-radius: 2px; }
395
+ .artifact-audio .artifact-waveform { display: block; visibility: hidden; width: 100%; height: 42px; padding: 8px 4px; background: #202824; }
396
+ .artifact-audio .artifact-waveform.is-ready { visibility: visible; }
397
+ .artifact-waveform.is-ready ~ .artifact-glyph { display: none; }
398
+ .artifact-type { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
399
+ .artifact-copy { min-width: 0; height: 44px; padding-top: 6px; text-align: center; }
400
+ .artifact-name, .artifact-name-editor { box-sizing: border-box; width: 100%; margin: 0; padding: 0 4px; border: 1px solid transparent; border-radius: 3px; color: #d8dae0; background: transparent; font: 500 11px/18px var(--font-ui); text-align: center; word-break: break-all; overflow-wrap: anywhere; }
401
+ .artifact-name { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; max-height: 38px; overflow: hidden; white-space: normal; cursor: text; }
402
+ .artifact-name-text { padding: 1px 2px; border-radius: 3px; -webkit-box-decoration-break: clone; box-decoration-break: clone; }
403
+ .artifact-name:focus-visible { outline: none; box-shadow: inset 0 0 0 1px #626875; }
404
+ .artifact-name[hidden] { display: none; }
405
+ .artifact-copy.editing { height: auto; min-height: 44px; }
406
+ .artifact-name-editor { display: block; min-height: 20px; resize: none; overflow: hidden; outline: none; color: #f1f2f5; box-shadow: inset 0 0 0 1px #626875; }
407
+ .artifact-name-editor:focus-visible { outline: none; box-shadow: inset 0 0 0 1px #626875; }
408
+ .artifact-name-editor::selection { color: #fff; background: #4b5263; }
409
+ .artifact-name-error { display: block; color: #bd9c92; font-size: 9px; line-height: 13px; overflow-wrap: anywhere; }
439
410
  .library-empty { grid-column: 1 / -1; min-height: 210px; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 28px; text-align: center; }
440
411
  .library-empty-icon { width: 44px; height: 44px; display: grid; place-items: center; margin-bottom: 12px; border: 1px solid #292c31; border-radius: 12px; background: linear-gradient(145deg, #202227, #18191d); color: #666b74; box-shadow: 0 9px 24px rgba(0, 0, 0, .12); }
441
412
  .library-empty-icon .icon { width: 21px; height: 21px; }
442
413
  .library-empty strong { color: #aeb2ba; font-size: 11px; font-weight: 600; }
443
414
  .library-empty p { max-width: 260px; margin: 6px 0 0; color: #666b74; font-size: 9px; line-height: 1.55; }
444
- .library-older {
445
- grid-column: 1 / -1;
446
- width: 100%;
447
- min-height: 32px;
448
- margin-top: 3px;
449
- border: 1px solid #292c31;
450
- border-radius: 6px;
451
- background: #191a1e;
452
- color: #858a93;
453
- font-size: 9px;
454
- }
455
- .library-older:hover { border-color: #3a3d44; background: #202227; color: #d1d4da; }
456
415
 
457
416
  .code { --code-inline-end: 12px; flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
458
417
  .code-toolbar { min-width: 0; display: flex; align-items: center; justify-content: space-between; gap: 8px; }
@@ -904,6 +863,7 @@ button:focus-visible, textarea:focus-visible {
904
863
  box-shadow: 0 10px 26px rgba(0, 0, 0, .42);
905
864
  }
906
865
  .parameter-select.open-up .parameter-select-menu { top: auto; bottom: calc(100% + 4px); }
866
+ .parameter-select.rich-options .parameter-select-menu { width: 240px; max-height: 240px; }
907
867
  .parameter-select-menu[hidden] { display: none; }
908
868
  .parameter-select-option {
909
869
  width: 100%;
@@ -921,6 +881,19 @@ button:focus-visible, textarea:focus-visible {
921
881
  .parameter-select-option.active { background: #202226; color: #d5d7dc; }
922
882
  .parameter-select-option:focus-visible { outline: 0; background: #292b30; color: var(--text); }
923
883
  .parameter-right > small { flex: none; color: var(--text-3); font-size: 10px; }
884
+ .parameter-value[type="number"] { appearance: textfield; }
885
+ .parameter-value[type="number"]::-webkit-inner-spin-button { opacity: .55; }
886
+ .parameter-value:invalid { border-color: #a45c60; }
887
+ .parameter-value[readonly] { color: var(--text-3); }
888
+ .parameter-multiline { min-height: 64px; resize: vertical; text-align: left; line-height: 1.5; }
889
+ .parameter-select-option { padding: 6px 8px; white-space: normal; text-align: left; }
890
+ .parameter-select-option > small { display: block; margin-top: 3px; color: var(--text-3); font: 10px/1.4 var(--font-ui); }
891
+ .parameter-option-swatch { display: inline-block; width: 12px; height: 12px; margin-right: 7px; border-radius: 2px; vertical-align: middle; box-shadow: inset 0 0 0 1px #ffffff22; }
892
+ .parameter-option-font { display: block; margin-top: 4px; font-size: 18px; }
893
+ .control-color .parameter-right { flex-wrap: wrap; }
894
+ .parameter-swatches { display: flex; flex-wrap: wrap; gap: 5px; padding-top: 3px; }
895
+ .parameter-swatches button { width: 18px; height: 18px; border: 1px solid #ffffff22; border-radius: 3px; }
896
+ .parameter-swatches button:focus-visible { outline: 1px solid var(--text); outline-offset: 2px; }
924
897
  .status.saving { color: var(--text-2); }
925
898
  .status.saved { color: var(--ok); }
926
899
 
@@ -945,6 +918,7 @@ button:focus-visible, textarea:focus-visible {
945
918
  font-variant-numeric: tabular-nums;
946
919
  letter-spacing: .01em;
947
920
  }
921
+ .timeline { --timeline-ruler-height: 22px; }
948
922
  .timeline-body {
949
923
  flex: 1;
950
924
  min-height: 0;
@@ -969,7 +943,7 @@ button:focus-visible, textarea:focus-visible {
969
943
  position: sticky;
970
944
  top: 0;
971
945
  z-index: 10;
972
- height: 22px;
946
+ height: var(--timeline-ruler-height);
973
947
  display: flex;
974
948
  align-items: center;
975
949
  justify-content: flex-end;
@@ -1014,8 +988,6 @@ button:focus-visible, textarea:focus-visible {
1014
988
  text-align: left;
1015
989
  }
1016
990
  .track-copy strong { color: #cfd2d8; font-size: 11px; font-weight: 500; line-height: 18px; }
1017
- .track-label-has-state { grid-template-columns: 18px minmax(0, 1fr) 18px; }
1018
- .track-state { display: grid; place-items: center; }
1019
991
  .track-attachment-mark {
1020
992
  position: relative;
1021
993
  width: 18px;
@@ -1056,7 +1028,7 @@ button:focus-visible, textarea:focus-visible {
1056
1028
  position: sticky;
1057
1029
  top: 0;
1058
1030
  z-index: 9;
1059
- height: 22px;
1031
+ height: var(--timeline-ruler-height);
1060
1032
  overflow: hidden;
1061
1033
  border-bottom: 1px solid var(--line);
1062
1034
  background: #111216;
@@ -1102,8 +1074,8 @@ button:focus-visible, textarea:focus-visible {
1102
1074
  }
1103
1075
  .tick-last { padding-right: 4px; padding-left: 0; border-right: 1px solid #45484e; border-left: 0; transform: translateX(-100%); }
1104
1076
  .lanes { min-height: 0; cursor: default; }
1105
- .lane { position: relative; min-height: 0; overflow: hidden; border-bottom: 1px solid #191b1f; background: var(--lane); }
1106
- .semantic-lane { min-height: 0; background: var(--lane); }
1077
+ .lane { position: relative; isolation: isolate; min-height: 0; overflow: hidden; border-bottom: 1px solid #191b1f; background: var(--lane); }
1078
+ .semantic-lane { position: sticky; top: var(--timeline-ruler-height); z-index: 8; min-height: 0; border-bottom: 1px solid var(--line); background: var(--lane); }
1107
1079
  .semantic-band {
1108
1080
  position: absolute;
1109
1081
  right: 0;
@@ -1208,21 +1180,11 @@ button:focus-visible, textarea:focus-visible {
1208
1180
  background: var(--semantic-intent);
1209
1181
  }
1210
1182
  .semantic-moment:focus-visible { outline: 0; }
1183
+ .track-label-semantic { position: sticky; top: var(--timeline-ruler-height); z-index: 8; border-bottom: 1px solid var(--line); background: #111216; }
1211
1184
  .track-label-semantic::before {
1212
1185
  border-radius: 0;
1213
1186
  background: #181a1f;
1214
1187
  }
1215
- .track-fold {
1216
- min-width: 17px;
1217
- height: 17px;
1218
- padding: 0 3px;
1219
- border: 0;
1220
- border-radius: 3px;
1221
- background: #24262a;
1222
- color: #92969e;
1223
- font-size: 11px;
1224
- line-height: 17px;
1225
- }
1226
1188
  .track-label-attached {
1227
1189
  /* The branch marker occupies the same 18px prefix as a root icon;
1228
1190
  attached and root labels therefore share one exact text column. */
@@ -1231,6 +1193,7 @@ button:focus-visible, textarea:focus-visible {
1231
1193
  }
1232
1194
  .track-label-attached::before { inset: var(--timeline-item-inset-y) 6px; background: #181a1f; }
1233
1195
  .clip {
1196
+ z-index: 0;
1234
1197
  --track: var(--visual);
1235
1198
  --item-head: color-mix(in srgb, var(--track) 58%, var(--lane));
1236
1199
  --item-body: color-mix(in srgb, var(--track) 20%, var(--lane));
@@ -1274,7 +1237,7 @@ button:focus-visible, textarea:focus-visible {
1274
1237
  cursor: ew-resize !important;
1275
1238
  }
1276
1239
  .clip.selected {
1277
- z-index: 4;
1240
+ z-index: 1;
1278
1241
  overflow: visible;
1279
1242
  box-shadow: none;
1280
1243
  color: #fff;
@@ -1520,6 +1483,34 @@ button:focus-visible, textarea:focus-visible {
1520
1483
  @media (max-width: 920px) {
1521
1484
  .meta, .topbar-right { display: none; }
1522
1485
  .topbar { grid-template-columns: 1fr minmax(120px, 1fr); }
1523
- .upper-shell { grid-template-columns: minmax(280px, var(--source-width)) 8px minmax(300px, 1fr); }
1486
+ .upper-shell { grid-template-columns: minmax(320px, var(--source-width)) 8px minmax(300px, 1fr); }
1524
1487
  .workspace-handle, .workspace-panel { display: none; }
1525
1488
  }
1489
+
1490
+
1491
+ /* The same transport serves the composition and selected Result media. */
1492
+ .stage-transport { display: flex; align-items: center; gap: 2px; }
1493
+ .stage-bar button[hidden], .stage-transport[hidden], .stage-artifact[hidden], .stage-scaler[hidden] { display: none; }
1494
+ .stage-bar .stage-mute { position: absolute; right: 12px; margin: 0; }
1495
+ .stage-time { position: absolute; left: 12px; color: #969ba5; font: 10px var(--font-mono); }
1496
+ .stage-title { min-width: 0; }
1497
+ .stage-title strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1498
+ .stage-return { flex: none; margin-left: auto; border: 0; background: transparent; color: #aeb3bd; font: 10px var(--font-ui); cursor: pointer; }
1499
+ .stage-return[hidden] { display: none; }
1500
+ .stage-artifact { position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; min-height: 0; }
1501
+ .stage-artifact > img, .stage-artifact > video { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; }
1502
+ .stage-artifact audio { display: none; }
1503
+ .stage-audio-wave { width: 85%; padding: 24px 12px; border-radius: 2px; color: #b5c2bb; background: #202824; text-align: center; }
1504
+ .stage-audio-wave img { display: block; width: 100%; height: 100px; }
1505
+ .stage-media-error { position: absolute; bottom: 12px; padding: 8px; background: #24262b; color: #cacdd4; font-size: 11px; }
1506
+ .stage-scrubber { flex: none; width: 100%; height: 10px; padding: 0; margin: -5px 0; z-index: 2; appearance: none; background: transparent; cursor: pointer; }
1507
+ .stage-scrubber::-webkit-slider-runnable-track { height: 2px; background: #676c75; }
1508
+ .stage-scrubber::-webkit-slider-thumb { appearance: none; width: 8px; height: 8px; border-radius: 50%; margin-top: -3px; background: #e1e4e9; }
1509
+ .stage-scrubber::-moz-range-track { height: 2px; background: #676c75; }
1510
+ .stage-scrubber::-moz-range-thumb { width: 8px; height: 8px; border: 0; background: #e1e4e9; }
1511
+
1512
+ .preview-unavailable .artifact-type { position: static; width: auto; height: auto; clip-path: none; color: #9298a2; font-size: 10px; }
1513
+
1514
+ .artifact-card.selected .artifact-preview { background: var(--artifact-selection); border-radius: 3px; }
1515
+ .artifact-card.selected .artifact-name-text { color: #f1f2f5; background: var(--artifact-selection); }
1516
+ .library-continuation { grid-column: 1 / -1; height: 28px; display: grid; place-items: center; color: #777e89; font-size: 10px; }
@@ -0,0 +1,131 @@
1
+ import type { StudioEditHandle, StudioSemanticAnchor, StudioTemporalInstantProjection } from "@hypit/studio-adapter";
2
+
3
+ export type SemanticTarget =
4
+ | { readonly kind: "selection"; readonly startAnchorId: string; readonly endAnchorId: string }
5
+ | { readonly kind: "moment"; readonly anchorId: string };
6
+
7
+ type Span = { readonly startFrame: number; readonly endFrameExclusive: number };
8
+
9
+ /** Project only the semantic write exposed by the handle. This does not invert expressions. */
10
+ export function semanticGestureSpan(
11
+ anchors: readonly StudioSemanticAnchor[], handle: StudioEditHandle, target: SemanticTarget,
12
+ ): Span | undefined {
13
+ const semantic = handle.semantic;
14
+ const temporal = handle.temporal;
15
+ if (!semantic || !temporal || target.kind !== semantic.kind) return undefined;
16
+ const byId = new Map(anchors.map((anchor, order) => [anchor.id, { ...anchor, order }]));
17
+ let span: Span;
18
+ if (semantic.kind === "moment" && target.kind === "moment") {
19
+ const previous = byId.get(semantic.anchorId);
20
+ const next = byId.get(target.anchorId);
21
+ if (!previous || !next) return undefined;
22
+ const delta = next.frame - previous.frame;
23
+ span = temporal.kind === "instant"
24
+ ? { startFrame: temporal.frame + delta, endFrameExclusive: temporal.frame + delta + 1 }
25
+ : { startFrame: temporal.startFrame + delta, endFrameExclusive: temporal.endFrameExclusive + delta };
26
+ } else if (semantic.kind === "selection" && target.kind === "selection") {
27
+ const start = byId.get(target.startAnchorId);
28
+ const end = byId.get(target.endAnchorId);
29
+ if (!start || !end || start.order > end.order) return undefined;
30
+ if (temporal.kind === "instant") {
31
+ if (temporal.authority.kind !== "semantic") return undefined;
32
+ if (temporal.authority.boundary === "start" && target.endAnchorId !== semantic.endAnchorId
33
+ || temporal.authority.boundary === "end" && target.startAnchorId !== semantic.startAnchorId) return undefined;
34
+ const frame = temporal.authority.boundary === "start" ? start.frame : end.frame;
35
+ span = { startFrame: frame, endFrameExclusive: frame + 1 };
36
+ } else {
37
+ if (handle.gesture === "trim-start" && target.endAnchorId !== semantic.endAnchorId
38
+ || handle.gesture === "trim-end" && target.startAnchorId !== semantic.startAnchorId) return undefined;
39
+ if (handle.gesture === "move") {
40
+ const stops = [...new Set(anchors.map((anchor) => anchor.frame))].sort((a, b) => a - b);
41
+ const previousStart = byId.get(semantic.startAnchorId);
42
+ const previousEnd = byId.get(semantic.endAnchorId);
43
+ if (!previousStart || !previousEnd
44
+ || stops.indexOf(start.frame) - stops.indexOf(previousStart.frame)
45
+ !== stops.indexOf(end.frame) - stops.indexOf(previousEnd.frame)) return undefined;
46
+ }
47
+ span = { startFrame: start.frame, endFrameExclusive: end.frame };
48
+ }
49
+ } else return undefined;
50
+ if (!Number.isSafeInteger(span.startFrame) || span.startFrame < 0
51
+ || !Number.isSafeInteger(span.endFrameExclusive) || span.endFrameExclusive <= span.startFrame) return undefined;
52
+ if (temporal.kind === "window"
53
+ && ((handle.gesture === "trim-start" && span.endFrameExclusive !== temporal.endFrameExclusive)
54
+ || (handle.gesture === "trim-end" && span.startFrame !== temporal.startFrame))) return undefined;
55
+ return span;
56
+ }
57
+
58
+ /** A stop is a frame position; coincident anchors remain distinct choices at that stop. */
59
+ export function chooseSemanticGesture(input: {
60
+ readonly anchors: readonly StudioSemanticAnchor[];
61
+ readonly handle: StudioEditHandle;
62
+ readonly pointerStart: number;
63
+ readonly pointerNow: number;
64
+ readonly frameCount: number;
65
+ }): SemanticTarget | undefined {
66
+ const { anchors, handle } = input;
67
+ const semantic = handle.semantic;
68
+ const temporal = handle.temporal;
69
+ if (!semantic || !temporal) return undefined;
70
+ const order = new Map(anchors.map((anchor, index) => [anchor.id, index]));
71
+ const byId = new Map(anchors.map((anchor) => [anchor.id, anchor]));
72
+ const at = new Map<number, StudioSemanticAnchor[]>();
73
+ for (const anchor of anchors) at.set(anchor.frame, [...at.get(anchor.frame) ?? [], anchor]);
74
+ const stops = [...at.keys()].sort((a, b) => a - b);
75
+ const ranked = (choices: readonly StudioSemanticAnchor[], preferred: StudioSemanticAnchor) => [...choices].sort((a, b) =>
76
+ Number(a.id !== preferred.id) - Number(b.id !== preferred.id)
77
+ || Number(a.kind !== preferred.kind) - Number(b.kind !== preferred.kind)
78
+ || order.get(a.id)! - order.get(b.id)!);
79
+ const valid = (target: SemanticTarget) => {
80
+ const span = semanticGestureSpan(anchors, handle, target);
81
+ return span !== undefined && (temporal.kind === "instant" ? span.startFrame : span.endFrameExclusive) <= input.frameCount;
82
+ };
83
+ const delta = input.pointerNow - input.pointerStart;
84
+ if (semantic.kind === "moment") {
85
+ const current = byId.get(semantic.anchorId);
86
+ if (!current) return undefined;
87
+ const candidates = ranked(anchors, current).sort((a, b) =>
88
+ Math.abs(a.frame - current.frame - delta) - Math.abs(b.frame - current.frame - delta));
89
+ return candidates.map((anchor): SemanticTarget => ({ kind: "moment", anchorId: anchor.id })).find(valid);
90
+ }
91
+ const start = byId.get(semantic.startAnchorId);
92
+ const end = byId.get(semantic.endAnchorId);
93
+ if (!start || !end) return undefined;
94
+ const target = (a: StudioSemanticAnchor, b: StudioSemanticAnchor): SemanticTarget => ({
95
+ kind: "selection", startAnchorId: a.id, endAnchorId: b.id,
96
+ });
97
+ if (handle.gesture === "move" && temporal.kind === "window") {
98
+ const first = stops.indexOf(start.frame);
99
+ const last = stops.indexOf(end.frame);
100
+ const shifts = stops.map((_, index) => index - first)
101
+ .filter((shift) => last + shift >= 0 && last + shift < stops.length)
102
+ .sort((a, b) => Math.abs(stops[first + a]! - start.frame - delta)
103
+ - Math.abs(stops[first + b]! - start.frame - delta) || Math.abs(a) - Math.abs(b));
104
+ for (const shift of shifts) {
105
+ for (const a of ranked(at.get(stops[first + shift]!)!, start)) {
106
+ for (const b of ranked(at.get(stops[last + shift]!)!, end)) {
107
+ const candidate = target(a, b);
108
+ if (valid(candidate)) return candidate;
109
+ }
110
+ }
111
+ }
112
+ return undefined;
113
+ }
114
+ const boundary = temporal.kind === "instant" && temporal.authority.kind === "semantic"
115
+ ? temporal.authority.boundary : handle.gesture === "trim-start" ? "start" : "end";
116
+ const current = boundary === "start" ? start : end;
117
+ const candidates = ranked(anchors, current).sort((a, b) =>
118
+ Math.abs(a.frame - current.frame - delta) - Math.abs(b.frame - current.frame - delta));
119
+ return candidates.map((anchor) => boundary === "start" ? target(anchor, end) : target(start, anchor)).find(valid);
120
+ }
121
+
122
+
123
+ /** Keep the reference and write its local frame offset; a bare reference starts at zero. */
124
+ export function formatTemporalPointEdit(
125
+ reference: StudioTemporalInstantProjection["reference"], desired: number, base?: number,
126
+ ): string {
127
+ if (reference === "absolute") return `${desired}f`;
128
+ if (base === undefined) throw new Error(`The ${reference} projection base is unavailable.`);
129
+ const offset = desired - base;
130
+ return `${reference}${offset >= 0 ? "+" : ""}${offset}f`;
131
+ }
@@ -0,0 +1,80 @@
1
+ /** Two-line label with an expanding editor; the caller owns persistence. */
2
+ export function createArtifactName(options: {
3
+ readonly name: string;
4
+ readonly editable: boolean;
5
+ readonly select: () => void;
6
+ readonly save: (name: string) => Promise<string>;
7
+ }) {
8
+ const element = document.createElement("div");
9
+ element.className = "artifact-copy";
10
+ const label = document.createElement("button");
11
+ label.type = "button";
12
+ label.className = "artifact-name";
13
+ let name = options.name;
14
+ const text = document.createElement("span");
15
+ text.className = "artifact-name-text";
16
+ text.textContent = name;
17
+ label.append(text);
18
+ label.title = options.editable ? `${name}\nDouble-click or press F2 to rename` : `${name}\nNaming is available when this Build finishes`;
19
+ element.append(label);
20
+ label.addEventListener("click", (event) => { event.stopPropagation(); options.select(); });
21
+ let editing = false;
22
+
23
+ function edit(): void {
24
+ if (editing || !options.editable) return;
25
+ options.select();
26
+ editing = true;
27
+ element.classList.add("editing");
28
+ label.hidden = true;
29
+ const field = document.createElement("textarea");
30
+ field.className = "artifact-name-editor";
31
+ field.value = name;
32
+ field.rows = 1;
33
+ field.setAttribute("aria-label", "Display name");
34
+ const error = document.createElement("span");
35
+ error.className = "artifact-name-error";
36
+ error.setAttribute("role", "status");
37
+ element.append(field, error);
38
+ const resize = () => { field.style.height = "0px"; field.style.height = `${field.scrollHeight + field.offsetHeight - field.clientHeight}px`; };
39
+ resize();
40
+ field.focus();
41
+ field.setSelectionRange(name.length, name.length);
42
+ field.addEventListener("input", resize);
43
+ field.addEventListener("click", (event) => event.stopPropagation());
44
+ let saving = false;
45
+ const close = () => {
46
+ editing = false;
47
+ field.remove();
48
+ error.remove();
49
+ label.hidden = false;
50
+ element.classList.remove("editing");
51
+ };
52
+ const save = async () => {
53
+ if (!editing || saving) return;
54
+ const value = field.value.trim();
55
+ if (value === name) { close(); return; }
56
+ if (value.length === 0) { error.textContent = "Enter a name."; return; }
57
+ saving = true;
58
+ field.readOnly = true;
59
+ error.textContent = "Saving…";
60
+ try {
61
+ name = await options.save(value);
62
+ text.textContent = name;
63
+ label.title = `${name}\nDouble-click or press F2 to rename`;
64
+ close();
65
+ } catch (cause) {
66
+ error.textContent = cause instanceof Error ? cause.message : String(cause);
67
+ } finally {
68
+ saving = false;
69
+ field.readOnly = false;
70
+ }
71
+ };
72
+ field.addEventListener("blur", () => { void save(); });
73
+ field.addEventListener("keydown", (event) => {
74
+ if (event.key === "Escape" && !saving) { event.preventDefault(); event.stopPropagation(); close(); label.focus(); }
75
+ else if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); void save(); }
76
+ });
77
+ }
78
+ label.addEventListener("dblclick", (event) => { event.preventDefault(); event.stopPropagation(); edit(); });
79
+ return { element, edit };
80
+ }