@hyperframes/studio 0.8.104 → 0.8.105

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 (169) hide show
  1. package/dist/assets/{hyperframes-player-DAoaajuM.js → hyperframes-player-tnfCiVdR.js} +1 -1
  2. package/dist/assets/{index-BDK4rTMb.js → index-Ca6LZlVb.js} +1 -1
  3. package/dist/assets/index-Cre9X4RS.css +1 -0
  4. package/dist/assets/{index-De2aO4XM.js → index-DEDhPmPD.js} +205 -205
  5. package/dist/assets/{index-WV9LroAD.js → index-zpe332hb.js} +1 -1
  6. package/dist/{chunk-62DDWOSH.js → chunk-EEXSYY5F.js} +19 -18
  7. package/dist/chunk-EEXSYY5F.js.map +1 -0
  8. package/dist/{chunk-6GAO75IU.js → chunk-NCUBAVGK.js} +10 -8
  9. package/dist/chunk-NCUBAVGK.js.map +1 -0
  10. package/dist/{chunk-HIGHVWYD.js → chunk-QADHJFDF.js} +2 -2
  11. package/dist/index.css +6 -1
  12. package/dist/index.css.map +1 -1
  13. package/dist/index.html +2 -2
  14. package/dist/index.js +614 -638
  15. package/dist/index.js.map +1 -1
  16. package/dist/player.js +2 -2
  17. package/dist/styles/tailwind-preset.js.map +1 -1
  18. package/dist/styles.css +1 -1
  19. package/dist/ui.js +2 -2
  20. package/package.json +8 -8
  21. package/src/captions/components/CaptionAnimationPanel.tsx +2 -2
  22. package/src/captions/components/CaptionPropertyPanel.tsx +2 -2
  23. package/src/captions/components/CaptionTimeline.tsx +3 -3
  24. package/src/components/AskAgentModal.tsx +1 -1
  25. package/src/components/LintModal.tsx +35 -35
  26. package/src/components/StudioErrorBoundary.tsx +1 -1
  27. package/src/components/StudioHeader.dom.test.tsx +4 -4
  28. package/src/components/StudioHeader.tsx +2 -1
  29. package/src/components/StudioToast.tsx +7 -16
  30. package/src/components/TimelineToolPicker.tsx +1 -1
  31. package/src/components/TimelineToolbar.tsx +11 -11
  32. package/src/components/dock/Dock.tsx +1 -1
  33. package/src/components/dock/dock.css +9 -1
  34. package/src/components/editor/AnimationCard.tsx +5 -5
  35. package/src/components/editor/AnimationCardParts.tsx +1 -1
  36. package/src/components/editor/BlockParamsPanel.tsx +3 -3
  37. package/src/components/editor/BorderRadiusEditor.tsx +8 -15
  38. package/src/components/editor/CanvasContextMenu.tsx +1 -1
  39. package/src/components/editor/ComputedTweenNotice.tsx +1 -1
  40. package/src/components/editor/DomEditRotateHandle.tsx +1 -1
  41. package/src/components/editor/EaseCurveSection.tsx +14 -19
  42. package/src/components/editor/EaseModeControls.tsx +2 -2
  43. package/src/components/editor/EaseParamFields.tsx +8 -8
  44. package/src/components/editor/FileTree.tsx +1 -1
  45. package/src/components/editor/FileTreeIcons.tsx +47 -26
  46. package/src/components/editor/FileTreeNodes.tsx +11 -8
  47. package/src/components/editor/GestureRecordControl.tsx +3 -3
  48. package/src/components/editor/GsapAddAnimationControl.tsx +1 -1
  49. package/src/components/editor/GsapAnimationSection.tsx +2 -2
  50. package/src/components/editor/InlineTextToolbar.tsx +4 -4
  51. package/src/components/editor/InspectorHeaderActions.tsx +1 -1
  52. package/src/components/editor/KeyframeDiamond.tsx +6 -4
  53. package/src/components/editor/KeyframeNavigation.tsx +4 -4
  54. package/src/components/editor/LayersPanel.tsx +3 -3
  55. package/src/components/editor/PromotableControl.tsx +2 -2
  56. package/src/components/editor/PropertyPanelEmptyState.tsx +4 -4
  57. package/src/components/editor/PropertyPanelFlatFooter.tsx +2 -2
  58. package/src/components/editor/PropertyPanelFlatHeader.test.tsx +1 -1
  59. package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
  60. package/src/components/editor/SnapToolbar.tsx +5 -5
  61. package/src/components/editor/TimelineFxPopover.tsx +4 -4
  62. package/src/components/editor/Transform3DCube.tsx +1 -1
  63. package/src/components/editor/anchoredResizeReleaseShift.test.ts +0 -84
  64. package/src/components/editor/easeCurveSvg.tsx +1 -1
  65. package/src/components/editor/manualEditsDom.ts +3 -3
  66. package/src/components/editor/manualEditsDomPatches.test.ts +2 -86
  67. package/src/components/editor/manualEditsDomPatches.ts +0 -30
  68. package/src/components/editor/manualEditsSnapshot.ts +1 -0
  69. package/src/components/editor/persistSeam.integration.test.ts +0 -18
  70. package/src/components/editor/plainTranslate.ts +34 -3
  71. package/src/components/editor/propertyPanelColorCurveGraph.tsx +2 -2
  72. package/src/components/editor/propertyPanelColorGradingControls.tsx +1 -1
  73. package/src/components/editor/propertyPanelColorGradingSection.tsx +8 -8
  74. package/src/components/editor/propertyPanelColorGradingSlider.tsx +1 -1
  75. package/src/components/editor/propertyPanelColorSecondary.tsx +4 -4
  76. package/src/components/editor/propertyPanelFill.tsx +6 -6
  77. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
  78. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +1 -1
  79. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +6 -9
  80. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
  81. package/src/components/editor/propertyPanelFlatMediaSection.tsx +2 -2
  82. package/src/components/editor/propertyPanelFlatMotionSection.tsx +2 -2
  83. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +2 -2
  84. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +4 -3
  85. package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -1
  86. package/src/components/editor/propertyPanelFlatSliderReadout.tsx +1 -1
  87. package/src/components/editor/propertyPanelFont.tsx +2 -2
  88. package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
  89. package/src/components/editor/propertyPanelFxControls.tsx +2 -2
  90. package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
  91. package/src/components/editor/propertyPanelFxNodeRow.tsx +1 -1
  92. package/src/components/editor/propertyPanelFxPresetRun.tsx +1 -1
  93. package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
  94. package/src/components/editor/propertyPanelPrimitives.tsx +1 -1
  95. package/src/components/editor/propertyPanelSections.tsx +2 -2
  96. package/src/components/editor/propertyPanelValueTier.test.ts +1 -1
  97. package/src/components/editor/propertyPanelValueTier.ts +1 -1
  98. package/src/components/feedback/StudioFeedbackCard.tsx +11 -23
  99. package/src/components/nle/AssetPreviewOverlay.tsx +1 -1
  100. package/src/components/nle/AudioMeterStrip.tsx +2 -2
  101. package/src/components/nle/NLEPreview.test.ts +8 -4
  102. package/src/components/nle/PreviewOverlays.tsx +1 -1
  103. package/src/components/nle/PreviewPane.tsx +3 -3
  104. package/src/components/nle/PreviewPoster.tsx +29 -1
  105. package/src/components/nle/PreviewZoomOverlay.tsx +1 -1
  106. package/src/components/panels/SlideshowPanel.tsx +4 -4
  107. package/src/components/panels/SlideshowSubPanels.tsx +19 -21
  108. package/src/components/panels/VariablesBindElement.tsx +1 -1
  109. package/src/components/panels/VariablesDeclarationForm.tsx +1 -1
  110. package/src/components/panels/VariablesPanel.tsx +4 -6
  111. package/src/components/panels/VariablesRowAction.tsx +1 -1
  112. package/src/components/renders/FfmpegRequiredNotice.tsx +4 -4
  113. package/src/components/renders/RenderQueue.tsx +2 -2
  114. package/src/components/renders/RenderQueueItem.tsx +3 -3
  115. package/src/components/sidebar/AssetCard.tsx +3 -3
  116. package/src/components/sidebar/AssetContextMenu.tsx +7 -7
  117. package/src/components/sidebar/AssetsTab.tsx +6 -6
  118. package/src/components/sidebar/AudioRow.tsx +5 -3
  119. package/src/components/sidebar/BlocksTab.tsx +6 -6
  120. package/src/components/sidebar/CompositionsTab.drag.test.tsx +3 -69
  121. package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +4 -50
  122. package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +256 -0
  123. package/src/components/sidebar/CompositionsTab.tsx +65 -12
  124. package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
  125. package/src/components/sidebar/SidebarLintButton.tsx +3 -3
  126. package/src/components/sidebar/compositionsTabTestUtils.tsx +39 -0
  127. package/src/components/timelineToolbarStyles.ts +3 -3
  128. package/src/components/ui/Button.tsx +4 -4
  129. package/src/components/ui/HyperframesLogo.tsx +19 -18
  130. package/src/components/ui/Input.tsx +1 -1
  131. package/src/components/ui/Menu.tsx +4 -2
  132. package/src/hooks/elementOffsetStager.ts +8 -7
  133. package/src/hooks/useClipboard.source.test.ts +42 -4
  134. package/src/hooks/useClipboard.ts +13 -3
  135. package/src/hooks/useDomEditCommits.ts +0 -3
  136. package/src/hooks/useDomGeometryCommit.test.tsx +4 -1
  137. package/src/hooks/useDomGeometryCommit.ts +0 -1
  138. package/src/hooks/useDomGeometryCommits.test.tsx +177 -32
  139. package/src/hooks/useDomGeometryCommits.ts +4 -49
  140. package/src/player/components/ClipContextMenu.tsx +1 -1
  141. package/src/player/components/CompositionThumbnail.tsx +23 -11
  142. package/src/player/components/EditModal.tsx +6 -6
  143. package/src/player/components/KeyframeDiamondContextMenu.tsx +2 -2
  144. package/src/player/components/PlayerControls.tsx +3 -3
  145. package/src/player/components/ShortcutsPanel.tsx +8 -20
  146. package/src/player/components/TimelineCanvas.tsx +1 -1
  147. package/src/player/components/TimelineClip.tsx +2 -2
  148. package/src/player/components/TimelineEmptyState.tsx +2 -2
  149. package/src/player/components/TimelineLanes.tsx +0 -1
  150. package/src/player/components/TrackClipCount.tsx +1 -1
  151. package/src/player/components/VolumeControl.tsx +1 -1
  152. package/src/player/components/timelineClipChildren.tsx +1 -1
  153. package/src/player/components/timelineMotionStyles.test.ts +6 -22
  154. package/src/player/components/trackHeaderLabelRows.tsx +16 -16
  155. package/src/player/lib/thumbnailScheduler.ts +4 -5
  156. package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
  157. package/src/player/lib/timelineViewportBudgets.ts +1 -1
  158. package/src/styles/hex-baseline.json +7 -19
  159. package/src/styles/studio.css +0 -9
  160. package/src/styles/tailwind-preset.shared.js +2 -1
  161. package/src/styles/theme.css +204 -147
  162. package/src/styles/theme.test.ts +13 -9
  163. package/src/utils/blockCategories.ts +41 -9
  164. package/src/utils/clipboardPayload.ts +2 -0
  165. package/dist/assets/index-CQN3M56N.css +0 -1
  166. package/dist/chunk-62DDWOSH.js.map +0 -1
  167. package/dist/chunk-6GAO75IU.js.map +0 -1
  168. package/src/hooks/gsapTargetCache.ts +0 -62
  169. /package/dist/{chunk-HIGHVWYD.js.map → chunk-QADHJFDF.js.map} +0 -0
@@ -1,5 +1,10 @@
1
1
  import { useCallback, useRef, useState } from "react";
2
2
  import { buildProjectApiPath } from "../../utils/projectRouting";
3
+ import {
4
+ createThumbnailKey,
5
+ thumbnailScheduler,
6
+ type ThumbnailRequest,
7
+ } from "../../player/lib/thumbnailScheduler";
3
8
  import type { PreviewCompositionSize } from "../../utils/previewCompositionSize";
4
9
 
5
10
  /** Frame 0 as the thumbnail route last rendered it; `cachedOnly` never starts a render. */
@@ -65,7 +70,7 @@ export function usePreviewPoster(
65
70
  const renderMissingPoster = useCallback(() => {
66
71
  if (missingForRef.current !== visit || liveReadyForRef.current !== visit) return;
67
72
  missingForRef.current = null;
68
- void fetch(previewPosterUrl(projectId, false)).catch(() => {});
73
+ renderPosterForNextOpen(projectId);
69
74
  }, [visit, projectId]);
70
75
  const onLiveReadyToShowChange = useCallback(
71
76
  (ready: boolean) => {
@@ -91,6 +96,29 @@ export function usePreviewPoster(
91
96
  };
92
97
  }
93
98
 
99
+ /** Renders frame 0 under the thumbnail cap; the lease ends once the render settles. */
100
+ export function renderPosterForNextOpen(projectId: string): void {
101
+ const url = previewPosterUrl(projectId, false);
102
+ const request: ThumbnailRequest = {
103
+ key: createThumbnailKey({ kind: "poster-render", url }),
104
+ projectId,
105
+ sessionEpoch: 0,
106
+ kind: "composition",
107
+ priority: "overscan",
108
+ discardWhenReleased: true,
109
+ load: async (signal) => {
110
+ await (await fetch(url, { signal })).arrayBuffer();
111
+ return { value: { kind: "image", url, aspect: 16 / 9 }, weight: 0 };
112
+ },
113
+ };
114
+ const settled = () => ["ready", "error"].includes(thumbnailScheduler.getSnapshot(request).status);
115
+ let lease: { release(): void } | null = null;
116
+ lease = thumbnailScheduler.acquire(request, () => {
117
+ if (settled()) lease?.release();
118
+ });
119
+ if (settled()) lease.release();
120
+ }
121
+
94
122
  function useVisitNumber(key: string): number {
95
123
  const [seen, setSeen] = useState({ key, visit: 0 });
96
124
  if (seen.key === key) return seen.visit;
@@ -85,7 +85,7 @@ export function PreviewZoomOverlay({
85
85
  </span>
86
86
  <button
87
87
  type="button"
88
- className="rounded px-1.5 py-0.5 font-medium text-studio-accent hover:bg-white/10 transition-colors"
88
+ className="rounded px-1.5 py-0.5 font-medium text-accent-ink hover:bg-white/10 transition-colors"
89
89
  onClick={onFit}
90
90
  aria-label="Fit the whole frame in view"
91
91
  data-testid="preview-zoom-fit"
@@ -413,7 +413,7 @@ export function SlideshowPanel({ scenes, onPersist, onPersistNotes }: SlideshowP
413
413
 
414
414
  return (
415
415
  <div
416
- className="flex flex-col h-full overflow-y-auto text-white"
416
+ className="flex flex-col h-full overflow-y-auto text-text-0"
417
417
  onKeyDown={(e) => {
418
418
  // In-panel undo — scoped so it never fights the app-level file undo.
419
419
  if ((e.metaKey || e.ctrlKey) && !e.shiftKey && e.key.toLowerCase() === "z") {
@@ -428,14 +428,14 @@ export function SlideshowPanel({ scenes, onPersist, onPersistNotes }: SlideshowP
428
428
  {persistError && (
429
429
  <div
430
430
  role="alert"
431
- className="flex items-center justify-between gap-2 px-3 py-2 bg-red-950/40 border-b border-red-500/40"
431
+ className="flex items-center justify-between gap-2 px-3 py-2 bg-danger/15 border-b border-red-500/40"
432
432
  >
433
- <span className="text-[11px] text-red-300">Changes not saved</span>
433
+ <span className="text-[11px] text-danger-ink">Changes not saved</span>
434
434
  <button
435
435
  type="button"
436
436
  disabled={retrying}
437
437
  onClick={handleRetryPersist}
438
- className="px-2 py-0.5 text-[10px] rounded-sm bg-red-600 text-white enabled:hover:bg-red-500 enabled:active:scale-[0.97] disabled:opacity-50 transition-colors"
438
+ className="px-2 py-0.5 text-[10px] rounded-sm bg-danger text-on-danger enabled:hover:brightness-95 enabled:active:scale-[0.97] disabled:opacity-50 transition-colors"
439
439
  >
440
440
  {retrying ? "Retrying…" : "Retry"}
441
441
  </button>
@@ -72,9 +72,7 @@ export function SlideList({
72
72
  tabIndex={0}
73
73
  aria-pressed={isSelected}
74
74
  className={`flex items-center gap-2 px-3 py-1.5 rounded cursor-pointer text-[11px] transition-colors ${
75
- isSelected
76
- ? "bg-studio-accent/20 text-white"
77
- : "hover:bg-neutral-800/60 text-neutral-300"
75
+ isSelected ? "bg-on text-text-0" : "hover:bg-neutral-800/60 text-neutral-300"
78
76
  }`}
79
77
  onClick={() => onSelect(scene.id)}
80
78
  onKeyDown={(e) => {
@@ -100,7 +98,7 @@ export function SlideList({
100
98
  aria-label="Move slide up"
101
99
  title="Move up"
102
100
  disabled={slideIndex <= 0}
103
- className="px-1 py-0.5 text-[10px] text-neutral-400 enabled:hover:text-white enabled:active:scale-[0.95] disabled:opacity-30 disabled:cursor-not-allowed"
101
+ className="px-1 py-0.5 text-[10px] text-neutral-400 enabled:hover:text-text-0 enabled:active:scale-[0.95] disabled:opacity-30 disabled:cursor-not-allowed"
104
102
  onClick={(e) => {
105
103
  e.stopPropagation();
106
104
  onReorder(scene.id, "up");
@@ -113,7 +111,7 @@ export function SlideList({
113
111
  aria-label="Move slide down"
114
112
  title="Move down"
115
113
  disabled={slideIndex === slides.length - 1}
116
- className="px-1 py-0.5 text-[10px] text-neutral-400 enabled:hover:text-white enabled:active:scale-[0.95] disabled:opacity-30 disabled:cursor-not-allowed"
114
+ className="px-1 py-0.5 text-[10px] text-neutral-400 enabled:hover:text-text-0 enabled:active:scale-[0.95] disabled:opacity-30 disabled:cursor-not-allowed"
117
115
  onClick={(e) => {
118
116
  e.stopPropagation();
119
117
  onReorder(scene.id, "down");
@@ -162,7 +160,7 @@ export function SlideInspector({
162
160
  <div className="flex flex-col gap-1">
163
161
  <label className="text-[11px] text-neutral-400">Notes</label>
164
162
  <textarea
165
- className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1.5 text-[11px] text-white resize-none placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
163
+ className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1.5 text-[11px] text-text-0 resize-none placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
166
164
  rows={3}
167
165
  placeholder="Speaker notes or script..."
168
166
  value={slide?.notes ?? ""}
@@ -174,7 +172,7 @@ export function SlideInspector({
174
172
  <span className="text-[11px] text-neutral-400">Fragment hold-points</span>
175
173
  <button
176
174
  type="button"
177
- className="text-[10px] px-2 py-0.5 rounded-sm bg-neutral-700 hover:bg-neutral-600 text-neutral-200 transition-colors"
175
+ className="text-[10px] px-2 py-0.5 rounded-sm bg-hover hover:bg-press text-neutral-200 transition-colors"
178
176
  onClick={onMarkFragment}
179
177
  title={`Mark ${currentTime.toFixed(2)}s as hold-point`}
180
178
  >
@@ -192,7 +190,7 @@ export function SlideInspector({
192
190
  <button
193
191
  type="button"
194
192
  aria-label={`Remove fragment at ${t.toFixed(2)}s`}
195
- className="text-neutral-400 hover:text-red-400 transition-colors"
193
+ className="text-neutral-400 hover:text-danger-ink transition-colors"
196
194
  onClick={() => onRemoveFragment(t)}
197
195
  >
198
196
  ×
@@ -249,7 +247,7 @@ export function BranchTree({
249
247
  <input
250
248
  id={inputId}
251
249
  type="text"
252
- className="flex-1 bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-white placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
250
+ className="flex-1 bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-text-0 placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
253
251
  placeholder="New branch name..."
254
252
  value={newLabel}
255
253
  onChange={(e) => setNewLabel(e.target.value)}
@@ -260,7 +258,7 @@ export function BranchTree({
260
258
  />
261
259
  <button
262
260
  type="button"
263
- className="px-2 py-1 rounded-sm bg-neutral-700 hover:bg-neutral-600 text-[11px] text-neutral-200 transition-colors shrink-0"
261
+ className="px-2 py-1 rounded-sm bg-hover hover:bg-press text-[11px] text-neutral-200 transition-colors shrink-0"
264
262
  onClick={handleCreate}
265
263
  >
266
264
  Add
@@ -326,7 +324,7 @@ function BranchItem({
326
324
  <div className="flex items-center gap-1">
327
325
  {editing ? (
328
326
  <input
329
- className="flex-1 bg-neutral-800 border border-neutral-600 rounded-sm px-1.5 py-0.5 text-[11px] text-white focus:border-studio-accent/60 focus:outline-hidden"
327
+ className="flex-1 bg-neutral-800 border border-neutral-600 rounded-sm px-1.5 py-0.5 text-[11px] text-text-0 focus:border-studio-accent/60 focus:outline-hidden"
330
328
  value={draft}
331
329
  autoFocus
332
330
  onChange={(e) => setDraft(e.target.value)}
@@ -341,7 +339,7 @@ function BranchItem({
341
339
  <span
342
340
  role="button"
343
341
  tabIndex={0}
344
- className="flex-1 text-[11px] text-white font-medium truncate cursor-pointer hover:text-neutral-300"
342
+ className="flex-1 text-[11px] text-text-0 font-medium truncate cursor-pointer hover:text-neutral-300"
345
343
  title="Click to rename"
346
344
  onClick={() => setEditing(true)}
347
345
  onKeyDown={(e) => {
@@ -357,15 +355,15 @@ function BranchItem({
357
355
  <button
358
356
  type="button"
359
357
  aria-label={`Delete branch ${seq.label}`}
360
- className="text-[10px] text-neutral-500 hover:text-red-400 active:scale-[0.95] transition-colors px-1"
358
+ className="text-[10px] text-neutral-500 hover:text-danger-ink active:scale-[0.95] transition-colors px-1"
361
359
  onClick={() => setConfirmingDelete(true)}
362
360
  >
363
361
  ✕
364
362
  </button>
365
363
  </div>
366
364
  {confirmingDelete && (
367
- <div className="px-2 py-1.5 bg-red-950/30 border-l-2 border-red-500 flex flex-col gap-1 rounded-xs">
368
- <span className="text-[10px] text-red-400">
365
+ <div className="px-2 py-1.5 bg-danger/15 border-l-2 border-red-500 flex flex-col gap-1 rounded-xs">
366
+ <span className="text-[10px] text-danger-ink">
369
367
  Delete branch &ldquo;{seq.label}&rdquo;
370
368
  {seq.slides.length > 0
371
369
  ? ` and its ${seq.slides.length} slide${seq.slides.length === 1 ? "" : "s"}`
@@ -379,7 +377,7 @@ function BranchItem({
379
377
  setConfirmingDelete(false);
380
378
  onDelete(seq.id);
381
379
  }}
382
- className="px-2 py-0.5 text-[10px] rounded-sm bg-red-600 text-white hover:bg-red-500 active:bg-red-700 transition-colors"
380
+ className="px-2 py-0.5 text-[10px] rounded-sm bg-danger text-on-danger hover:brightness-95 active:brightness-90 transition-colors"
383
381
  >
384
382
  Delete
385
383
  </button>
@@ -414,7 +412,7 @@ function BranchItem({
414
412
  type="button"
415
413
  aria-pressed={isSelected}
416
414
  className={`flex-1 text-left truncate transition-colors hover:text-neutral-200 ${
417
- isSelected ? "text-white" : "text-neutral-400"
415
+ isSelected ? "text-text-0" : "text-neutral-400"
418
416
  }`}
419
417
  onClick={() => onSelectBranchSlide(seq.id, scene.id)}
420
418
  >
@@ -497,7 +495,7 @@ export function HotspotTool({
497
495
  <label className="text-[11px] text-neutral-400">Hotspot label</label>
498
496
  <input
499
497
  type="text"
500
- className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-white placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
498
+ className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-text-0 placeholder-neutral-600 focus:border-studio-accent/60 focus:outline-hidden"
501
499
  placeholder="Button label..."
502
500
  value={hotspotLabel}
503
501
  onChange={(e) => setHotspotLabel(e.target.value)}
@@ -505,7 +503,7 @@ export function HotspotTool({
505
503
  />
506
504
  <label className="text-[11px] text-neutral-400">Target branch</label>
507
505
  <select
508
- className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-white focus:border-studio-accent/60 focus:outline-hidden"
506
+ className="bg-neutral-800 border border-neutral-700 rounded-sm px-2 py-1 text-[11px] text-text-0 focus:border-studio-accent/60 focus:outline-hidden"
509
507
  value={targetSequenceId}
510
508
  onChange={(e) => setTargetSequenceId(e.target.value)}
511
509
  aria-label="Target branch sequence"
@@ -527,7 +525,7 @@ export function HotspotTool({
527
525
  ? "Choose a target branch first"
528
526
  : undefined
529
527
  }
530
- className="px-3 py-1.5 rounded-sm bg-studio-accent/80 enabled:hover:bg-studio-accent enabled:active:scale-[0.98] text-white text-[11px] font-medium transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
528
+ className="px-3 py-1.5 rounded-sm bg-accent enabled:hover:bg-accent-hover enabled:active:scale-[0.98] text-on-accent text-[11px] font-medium transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
531
529
  onClick={handleMakeHotspot}
532
530
  >
533
531
  Make hotspot
@@ -550,7 +548,7 @@ export function HotspotTool({
550
548
  <button
551
549
  type="button"
552
550
  aria-label={`Remove hotspot ${h.label}`}
553
- className="text-[10px] text-neutral-500 hover:text-red-400 transition-colors"
551
+ className="text-[10px] text-neutral-500 hover:text-danger-ink transition-colors"
554
552
  onClick={() => onRemoveHotspot(selectedSceneId, h.id)}
555
553
  >
556
554
  ✕
@@ -215,7 +215,7 @@ export function VariablesBindElement({
215
215
  className={`${VARIABLES_INPUT_CLASS} font-mono`}
216
216
  />
217
217
  {existingDecl && (
218
- <p className="text-[9px] leading-snug text-amber-400/90">
218
+ <p className="text-[9px] leading-snug text-warning-ink">
219
219
  "{trimmedId}" already exists. This element will use its current value
220
220
  {existingDecl.default !== undefined && (
221
221
  <span className="font-mono"> ({String(existingDecl.default)})</span>
@@ -299,7 +299,7 @@ export function DeclarationForm({
299
299
  className={VARIABLES_INPUT_CLASS}
300
300
  />
301
301
  </Field>
302
- {error && <p className="text-[10px] text-red-400">{error}</p>}
302
+ {error && <p className="text-[10px] text-danger-ink">{error}</p>}
303
303
  <div className="flex items-center justify-end gap-2 pt-1">
304
304
  <button
305
305
  type="button"
@@ -56,9 +56,9 @@ function formatIssue(issue: VariableValidationIssue): string {
56
56
  function ValidationStrip({ issues }: { issues: VariableValidationIssue[] }) {
57
57
  if (issues.length === 0) return null;
58
58
  return (
59
- <div className="space-y-1 rounded-lg border border-red-900/60 bg-red-950/30 p-2">
59
+ <div className="space-y-1 rounded-lg border border-danger/40 bg-danger/15 p-2">
60
60
  {issues.map((issue) => (
61
- <p key={`${issue.kind}:${issue.variableId}`} className="text-[10px] text-red-300">
61
+ <p key={`${issue.kind}:${issue.variableId}`} className="text-[10px] text-danger-ink">
62
62
  {formatIssue(issue)}
63
63
  </p>
64
64
  ))}
@@ -99,7 +99,7 @@ function VariableRow({
99
99
  </span>
100
100
  {unused && (
101
101
  <span
102
- className="rounded-sm bg-amber-900/40 px-1 py-px text-[8px] text-amber-400"
102
+ className="rounded-sm bg-amber-500/15 px-1 py-px text-[8px] text-warning-ink"
103
103
  title="No script reads this variable"
104
104
  >
105
105
  unused
@@ -175,9 +175,7 @@ function PreviewModeHeader({
175
175
  <span className="text-[11px] font-semibold text-neutral-200">Variables</span>
176
176
  <span
177
177
  className={`rounded-full px-2 py-0.5 text-[9px] font-medium ${
178
- hasOverrides
179
- ? "bg-studio-accent/20 text-studio-accent"
180
- : "bg-neutral-800 text-neutral-500"
178
+ hasOverrides ? "bg-studio-accent/20 text-accent-ink" : "bg-neutral-800 text-neutral-500"
181
179
  }`}
182
180
  >
183
181
  {hasOverrides ? `Previewing ${overrideCount} custom` : "Previewing defaults"}
@@ -16,7 +16,7 @@ export function RowAction({
16
16
  onClick={onClick}
17
17
  title={title}
18
18
  className={`h-5 rounded px-1.5 text-[9px] text-neutral-500 hover:bg-neutral-800 ${
19
- danger ? "hover:text-red-400" : "hover:text-neutral-200"
19
+ danger ? "hover:text-danger-ink" : "hover:text-neutral-200"
20
20
  }`}
21
21
  >
22
22
  {label}
@@ -67,7 +67,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
67
67
  className="flex flex-col gap-2 rounded-xl border border-amber-500/30 bg-amber-500/10 p-2.5"
68
68
  >
69
69
  <div className="flex flex-col gap-0.5">
70
- <span className="text-[11px] font-semibold text-amber-300">
70
+ <span className="text-[11px] font-semibold text-warning-ink">
71
71
  {status.title ?? "FFmpeg not found"}
72
72
  </span>
73
73
  {/* text-2, not the panel's usual text-4 for secondary copy: the amber
@@ -81,7 +81,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
81
81
 
82
82
  {status.command ? (
83
83
  <div className="flex items-center gap-1.5">
84
- <code className="flex-1 overflow-x-auto whitespace-nowrap rounded-xs bg-black/40 px-2 py-1 text-[10px] text-panel-text-2">
84
+ <code className="flex-1 overflow-x-auto whitespace-nowrap rounded-xs bg-input px-2 py-1 text-[10px] text-panel-text-2">
85
85
  {status.command}
86
86
  </code>
87
87
  {/* Fixed width so swapping the label to "Copied" cannot shift the
@@ -89,7 +89,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
89
89
  <button
90
90
  type="button"
91
91
  onClick={() => void copy(status.command ?? "")}
92
- className={`h-6 w-14 shrink-0 rounded-xs border border-amber-500/30 text-[10px] font-medium text-amber-200 transition-colors hover:bg-amber-500/20 active:scale-[0.98] ${FOCUS_RING}`}
92
+ className={`h-6 w-14 shrink-0 rounded-xs border border-amber-500/30 text-[10px] font-medium text-warning-ink transition-colors hover:bg-amber-500/20 active:scale-[0.98] ${FOCUS_RING}`}
93
93
  >
94
94
  {copied ? "Copied" : "Copy"}
95
95
  </button>
@@ -107,7 +107,7 @@ export const FfmpegRequiredNotice = memo(function FfmpegRequiredNotice({
107
107
  type="button"
108
108
  onClick={onRecheck}
109
109
  disabled={checking}
110
- className={`rounded-xs py-0.5 text-[10px] font-medium text-amber-200 underline-offset-2 transition-colors hover:underline disabled:opacity-50 ${FOCUS_RING}`}
110
+ className={`rounded-xs py-0.5 text-[10px] font-medium text-warning-ink underline-offset-2 transition-colors hover:underline disabled:opacity-50 ${FOCUS_RING}`}
111
111
  >
112
112
  {checking ? "Checking…" : "Recheck"}
113
113
  </button>
@@ -467,7 +467,7 @@ export const RenderQueue = memo(function RenderQueue({
467
467
  role="alert"
468
468
  className="flex items-start justify-between gap-2 px-3 py-2 border-b border-border bg-danger/10"
469
469
  >
470
- <span className="text-step-10 text-danger">{actionError}</span>
470
+ <span className="text-step-10 text-danger-ink">{actionError}</span>
471
471
  {onDismissActionError && (
472
472
  <IconButton
473
473
  size="sm"
@@ -497,7 +497,7 @@ export const RenderQueue = memo(function RenderQueue({
497
497
  <div ref={listRef} className="flex-1 overflow-y-auto">
498
498
  {loadError && jobs.length === 0 ? (
499
499
  <div className="flex flex-col items-center justify-center h-full px-4 gap-2" role="alert">
500
- <p className="text-step-10 text-danger text-center">{loadError}</p>
500
+ <p className="text-step-10 text-danger-ink text-center">{loadError}</p>
501
501
  {onRetryLoad && (
502
502
  <Button size="sm" variant="secondary" onClick={onRetryLoad}>
503
503
  Retry
@@ -115,7 +115,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
115
115
  )}
116
116
  {job.status === "failed" && (
117
117
  <div className="w-full h-full flex items-center justify-center">
118
- <div className="w-2 h-2 rounded-full bg-danger" />
118
+ <div className="w-2 h-2 rounded-full bg-danger-ink" />
119
119
  </div>
120
120
  )}
121
121
  {job.status === "cancelled" && (
@@ -140,7 +140,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
140
140
  <div className="mt-1">
141
141
  <div className="flex items-center justify-between mb-0.5">
142
142
  <span className="text-step-9 text-text-4">{job.stage || "Rendering"}</span>
143
- <span className="text-step-9 font-mono text-accent">{job.progress}%</span>
143
+ <span className="text-step-9 font-mono text-accent-ink">{job.progress}%</span>
144
144
  </div>
145
145
  <div
146
146
  className="w-full h-1 bg-border rounded-full overflow-hidden"
@@ -159,7 +159,7 @@ export const RenderQueueItem = memo(function RenderQueueItem({
159
159
  )}
160
160
 
161
161
  {job.status === "failed" && job.error && (
162
- <span className="text-step-9 text-danger mt-0.5 block">{job.error}</span>
162
+ <span className="text-step-9 text-danger-ink mt-0.5 block">{job.error}</span>
163
163
  )}
164
164
  {job.status === "cancelled" && (
165
165
  <span className="text-step-9 text-text-4 mt-0.5 block">Cancelled</span>
@@ -31,7 +31,7 @@ function CopyChip({ feedback, asset }: { feedback: CopyFeedback; asset: string }
31
31
  <span
32
32
  role="status"
33
33
  className={`shrink-0 text-[9px] font-medium px-1.5 py-px rounded ${
34
- feedback.ok ? "text-panel-accent bg-panel-accent/10" : "text-red-400 bg-red-500/10"
34
+ feedback.ok ? "text-accent-ink bg-panel-accent/10" : "text-danger-ink bg-red-500/10"
35
35
  }`}
36
36
  >
37
37
  {feedback.ok ? "Copied" : "Copy failed"}
@@ -355,7 +355,7 @@ export function FontRow({
355
355
  }`}
356
356
  >
357
357
  <div className="w-[50px] h-[32px] rounded-sm overflow-hidden bg-neutral-900 shrink-0 flex items-center justify-center">
358
- <span className="text-[9px] font-medium text-neutral-700">{extension}</span>
358
+ <span className="text-[9px] font-medium text-text-muted">{extension}</span>
359
359
  </div>
360
360
  <div className="min-w-0 flex-1">
361
361
  <span
@@ -366,7 +366,7 @@ export function FontRow({
366
366
  <div className="flex items-center gap-1.5">
367
367
  <span className="text-[10px] text-neutral-600 truncate">{extension}</span>
368
368
  {used && (
369
- <span className="text-[9px] font-medium text-panel-accent bg-panel-accent/10 px-1.5 py-px rounded-sm">
369
+ <span className="text-[9px] font-medium text-accent-ink bg-panel-accent/10 px-1.5 py-px rounded-sm">
370
370
  in use
371
371
  </span>
372
372
  )}
@@ -147,7 +147,7 @@ export function ContextMenu({
147
147
  <button
148
148
  role="menuitem"
149
149
  onClick={() => setMode("confirm-delete")}
150
- className={`${itemCls} text-red-400`}
150
+ className={`${itemCls} text-danger-ink`}
151
151
  >
152
152
  Delete
153
153
  </button>
@@ -181,9 +181,9 @@ export function ContextMenu({
181
181
  }
182
182
  }}
183
183
  aria-label={`Rename ${filename(asset)}`}
184
- className="w-full bg-neutral-800 border border-neutral-600 rounded-sm px-1.5 py-1 text-[11px] text-white focus:border-studio-accent/60 focus:outline-hidden"
184
+ className="w-full bg-neutral-800 border border-neutral-600 rounded-sm px-1.5 py-1 text-[11px] text-text-0 focus:border-studio-accent/60 focus:outline-hidden"
185
185
  />
186
- {renameError && <span className="text-[10px] text-red-400">{renameError}</span>}
186
+ {renameError && <span className="text-[10px] text-danger-ink">{renameError}</span>}
187
187
  <div className="flex items-center justify-end gap-1">
188
188
  <button
189
189
  onClick={() => setMode("menu")}
@@ -193,7 +193,7 @@ export function ContextMenu({
193
193
  </button>
194
194
  <button
195
195
  onClick={commitRename}
196
- className="px-2 py-0.5 text-[10px] rounded-sm bg-studio-accent/80 hover:bg-studio-accent text-white transition-colors"
196
+ className="px-2 py-0.5 text-[10px] rounded-sm bg-accent hover:bg-accent-hover text-on-accent transition-colors"
197
197
  >
198
198
  Rename
199
199
  </button>
@@ -215,12 +215,12 @@ function DeleteConfirm({
215
215
  onCancel: () => void;
216
216
  }) {
217
217
  return (
218
- <div className="px-2 py-1.5 bg-red-950/30 border-l-2 border-red-500 flex items-center justify-between gap-2">
219
- <span className="text-[10px] text-red-400 truncate">Delete {name}?</span>
218
+ <div className="px-2 py-1.5 bg-danger/15 border-l-2 border-red-500 flex items-center justify-between gap-2">
219
+ <span className="text-[10px] text-danger-ink truncate">Delete {name}?</span>
220
220
  <div className="flex items-center gap-1 shrink-0">
221
221
  <button
222
222
  onClick={onConfirm}
223
- className="px-2 py-0.5 text-[10px] rounded-sm bg-red-600 text-white hover:bg-red-500 active:bg-red-700 transition-colors"
223
+ className="px-2 py-0.5 text-[10px] rounded-sm bg-danger text-on-danger hover:brightness-95 active:brightness-90 transition-colors"
224
224
  >
225
225
  Delete
226
226
  </button>
@@ -177,7 +177,7 @@ function EmptyState({
177
177
  fill="none"
178
178
  stroke="currentColor"
179
179
  strokeWidth="1.5"
180
- className="text-neutral-700"
180
+ className="text-text-off"
181
181
  >
182
182
  <path
183
183
  d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"
@@ -349,7 +349,7 @@ export const AssetsTab = memo(function AssetsTab({
349
349
  onClick={() => setViewMode(m)}
350
350
  className={`flex-1 px-2 py-1 text-[11px] font-medium rounded transition-colors ${
351
351
  viewMode === m
352
- ? "bg-panel-accent/15 text-panel-accent"
352
+ ? "bg-panel-accent/15 text-accent-ink"
353
353
  : "text-panel-text-3 hover:text-panel-text-1"
354
354
  }`}
355
355
  >
@@ -396,7 +396,7 @@ export const AssetsTab = memo(function AssetsTab({
396
396
  aria-pressed={activeFilter === "all"}
397
397
  className={`px-2.5 py-1 text-[11px] font-medium rounded-md transition-colors active:scale-[0.98] ${
398
398
  activeFilter === "all"
399
- ? "bg-panel-accent/15 text-panel-accent"
399
+ ? "bg-panel-accent/15 text-accent-ink"
400
400
  : "bg-panel-input text-panel-text-3 hover:text-panel-text-1"
401
401
  }`}
402
402
  >
@@ -410,7 +410,7 @@ export const AssetsTab = memo(function AssetsTab({
410
410
  aria-pressed={activeFilter === cat}
411
411
  className={`px-2.5 py-1 text-[11px] font-medium rounded-md transition-colors active:scale-[0.98] ${
412
412
  activeFilter === cat
413
- ? "bg-panel-accent/15 text-panel-accent"
413
+ ? "bg-panel-accent/15 text-accent-ink"
414
414
  : "bg-panel-input text-panel-text-3 hover:text-panel-text-1"
415
415
  }`}
416
416
  >
@@ -425,7 +425,7 @@ export const AssetsTab = memo(function AssetsTab({
425
425
  onClick={() => setUsageFilter(usageFilter === "used" ? "all" : "used")}
426
426
  className={`px-2.5 py-1 text-[11px] font-medium rounded-md transition-colors ${
427
427
  usageFilter === "used"
428
- ? "bg-panel-accent/15 text-panel-accent"
428
+ ? "bg-panel-accent/15 text-accent-ink"
429
429
  : "bg-panel-input text-panel-text-3 hover:text-panel-text-1"
430
430
  }`}
431
431
  >
@@ -435,7 +435,7 @@ export const AssetsTab = memo(function AssetsTab({
435
435
  onClick={() => setUsageFilter(usageFilter === "unused" ? "all" : "unused")}
436
436
  className={`px-2.5 py-1 text-[11px] font-medium rounded-md transition-colors ${
437
437
  usageFilter === "unused"
438
- ? "bg-panel-accent/15 text-panel-accent"
438
+ ? "bg-panel-accent/15 text-accent-ink"
439
439
  : "bg-panel-input text-panel-text-3 hover:text-panel-text-1"
440
440
  }`}
441
441
  >
@@ -240,7 +240,7 @@ export function AudioRow({
240
240
  aria-pressed={playing}
241
241
  className={`w-7 h-7 rounded-md shrink-0 flex items-center justify-center transition-colors active:scale-[0.95] ${
242
242
  playing
243
- ? "bg-panel-accent/15 text-panel-accent"
243
+ ? "bg-panel-accent/15 text-accent-ink"
244
244
  : "text-panel-text-5 group-hover:text-panel-text-3"
245
245
  }`}
246
246
  onClick={(e) => {
@@ -273,7 +273,7 @@ export function AudioRow({
273
273
  </span>
274
274
  )}
275
275
  {used && (
276
- <span className="text-[9px] font-medium text-panel-accent bg-panel-accent/10 px-1.5 py-px rounded-sm shrink-0">
276
+ <span className="text-[9px] font-medium text-accent-ink bg-panel-accent/10 px-1.5 py-px rounded-sm shrink-0">
277
277
  in use
278
278
  </span>
279
279
  )}
@@ -281,7 +281,9 @@ export function AudioRow({
281
281
  <span
282
282
  role="status"
283
283
  className={`shrink-0 text-[9px] font-medium px-1.5 py-px rounded ${
284
- copyFailed ? "text-red-400 bg-red-500/10" : "text-panel-accent bg-panel-accent/10"
284
+ copyFailed
285
+ ? "text-danger-ink bg-red-500/10"
286
+ : "text-accent-ink bg-panel-accent/10"
285
287
  }`}
286
288
  >
287
289
  {copyFailed ? "Copy failed" : "Copied"}
@@ -40,7 +40,7 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }:
40
40
 
41
41
  if (error) {
42
42
  return (
43
- <div className="flex flex-1 items-center justify-center text-red-400 text-xs px-4 text-center">
43
+ <div className="flex flex-1 items-center justify-center text-danger-ink text-xs px-4 text-center">
44
44
  {error}
45
45
  </div>
46
46
  );
@@ -77,9 +77,9 @@ export const BlocksTab = memo(function BlocksTab({ onAddBlock, onPreviewBlock }:
77
77
  {/* Block grid */}
78
78
  <div className="flex-1 overflow-y-auto min-h-0 px-2 pb-2">
79
79
  {category === "vfx" && (
80
- <div className="mb-2 px-2 py-1.5 rounded-md bg-purple-500/10 border border-purple-500/20 text-[9px] text-purple-300 leading-relaxed">
80
+ <div className="mb-2 px-2 py-1.5 rounded-md bg-purple-500/10 border border-purple-500/20 text-[9px] text-text-2 leading-relaxed">
81
81
  VFX blocks use WebGL via HTML-in-Canvas. Enable{" "}
82
- <span className="font-mono text-purple-200">chrome://flags/#html-in-canvas</span> for
82
+ <span className="font-mono text-text-0">chrome://flags/#html-in-canvas</span> for
83
83
  preview.
84
84
  </div>
85
85
  )}
@@ -423,8 +423,8 @@ function BlockCard({
423
423
  title="Add to composition at current time"
424
424
  className={`flex items-center gap-1 px-3 py-1.5 rounded-md text-[10px] font-semibold transition-colors active:scale-[0.97] ${
425
425
  addState === "failed"
426
- ? "bg-red-500 text-white"
427
- : "bg-white text-black hover:bg-neutral-200"
426
+ ? "bg-danger text-on-danger"
427
+ : "bg-white text-black hover:bg-white/85"
428
428
  }`}
429
429
  >
430
430
  <svg
@@ -453,7 +453,7 @@ function BlockCard({
453
453
  className={`flex items-center gap-1.5 px-3 ${onAdd ? "py-1" : "py-1.5"} rounded-md transition-colors active:scale-[0.97] ${
454
454
  onAdd
455
455
  ? "bg-white/15 text-white/90 hover:bg-white/25 text-[9px]"
456
- : "bg-white text-black hover:bg-neutral-200 text-[10px] font-semibold"
456
+ : "bg-text-0 text-bg-0 hover:bg-text-2 text-[10px] font-semibold"
457
457
  }`}
458
458
  >
459
459
  <svg