@hyperframes/studio 0.8.104 → 0.8.106

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 (225) hide show
  1. package/dist/assets/{hyperframes-player-DAoaajuM.js → hyperframes-player-dl6K6ABp.js} +1 -1
  2. package/dist/assets/index-CMiYVGv5.js +399 -0
  3. package/dist/assets/{index-WV9LroAD.js → index-DKT5NF1Y.js} +1 -1
  4. package/dist/assets/index-lrT-gfHa.css +1 -0
  5. package/dist/assets/{index-BDK4rTMb.js → index-ooaZZhKf.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-HIGHVWYD.js → chunk-ICTZJPQQ.js} +3 -2
  9. package/dist/chunk-ICTZJPQQ.js.map +1 -0
  10. package/dist/{chunk-6GAO75IU.js → chunk-NCUBAVGK.js} +10 -8
  11. package/dist/chunk-NCUBAVGK.js.map +1 -0
  12. package/dist/index.css +6 -1
  13. package/dist/index.css.map +1 -1
  14. package/dist/index.d.ts +15 -4
  15. package/dist/index.html +11 -2
  16. package/dist/index.js +10391 -9836
  17. package/dist/index.js.map +1 -1
  18. package/dist/player.js +2 -2
  19. package/dist/styles/tailwind-preset.js.map +1 -1
  20. package/dist/styles.css +1 -1
  21. package/dist/ui.js +2 -2
  22. package/package.json +9 -9
  23. package/src/captions/components/CaptionAnimationPanel.tsx +2 -2
  24. package/src/captions/components/CaptionPropertyPanel.tsx +2 -2
  25. package/src/captions/components/CaptionTimeline.tsx +3 -3
  26. package/src/captions/keyboard.test.ts +6 -0
  27. package/src/captions/keyboard.ts +2 -1
  28. package/src/components/AskAgentModal.tsx +1 -1
  29. package/src/components/LintModal.tsx +35 -35
  30. package/src/components/StudioErrorBoundary.tsx +1 -1
  31. package/src/components/StudioHeader.dom.test.tsx +35 -13
  32. package/src/components/StudioHeader.tsx +6 -2
  33. package/src/components/StudioToast.tsx +7 -16
  34. package/src/components/ThemeToggle.tsx +99 -0
  35. package/src/components/TimelineToolPicker.tsx +1 -1
  36. package/src/components/TimelineToolbar.tsx +11 -11
  37. package/src/components/dock/Dock.tsx +1 -1
  38. package/src/components/dock/dock.css +9 -1
  39. package/src/components/editor/AnimationCard.tsx +5 -5
  40. package/src/components/editor/AnimationCardParts.tsx +1 -1
  41. package/src/components/editor/BlockParamsPanel.tsx +3 -3
  42. package/src/components/editor/BorderRadiusEditor.tsx +8 -15
  43. package/src/components/editor/CanvasContextMenu.tsx +1 -1
  44. package/src/components/editor/ComputedTweenNotice.tsx +1 -1
  45. package/src/components/editor/DomEditOverlay.tsx +37 -5
  46. package/src/components/editor/DomEditRotateHandle.tsx +1 -1
  47. package/src/components/editor/EaseCurveSection.tsx +14 -19
  48. package/src/components/editor/EaseModeControls.tsx +2 -2
  49. package/src/components/editor/EaseParamFields.tsx +8 -8
  50. package/src/components/editor/FileTree.tsx +1 -1
  51. package/src/components/editor/FileTreeIcons.tsx +47 -26
  52. package/src/components/editor/FileTreeNodes.tsx +11 -8
  53. package/src/components/editor/GestureRecordControl.tsx +3 -3
  54. package/src/components/editor/GsapAddAnimationControl.tsx +1 -1
  55. package/src/components/editor/GsapAnimationSection.tsx +2 -2
  56. package/src/components/editor/InlineTextToolbar.tsx +4 -4
  57. package/src/components/editor/InspectorHeaderActions.tsx +1 -1
  58. package/src/components/editor/KeyframeDiamond.tsx +6 -4
  59. package/src/components/editor/KeyframeNavigation.tsx +4 -4
  60. package/src/components/editor/LayersPanel.tsx +3 -3
  61. package/src/components/editor/PromotableControl.tsx +2 -2
  62. package/src/components/editor/PropertyPanelEmptyState.tsx +4 -4
  63. package/src/components/editor/PropertyPanelFlatFooter.tsx +2 -2
  64. package/src/components/editor/PropertyPanelFlatHeader.test.tsx +1 -1
  65. package/src/components/editor/PropertyPanelFlatHeader.tsx +2 -2
  66. package/src/components/editor/SnapToolbar.tsx +5 -5
  67. package/src/components/editor/SourceEditor.tsx +2 -6
  68. package/src/components/editor/TimelineFxPopover.tsx +4 -4
  69. package/src/components/editor/Transform3DCube.tsx +1 -1
  70. package/src/components/editor/anchoredResizeReleaseShift.test.ts +0 -84
  71. package/src/components/editor/domEditOverlayStartGesture.ts +127 -115
  72. package/src/components/editor/easeCurveSvg.tsx +1 -1
  73. package/src/components/editor/gestureHoldsPreviewReload.test.tsx +275 -0
  74. package/src/components/editor/manualEditsDom.ts +36 -3
  75. package/src/components/editor/manualEditsDomPatches.test.ts +2 -86
  76. package/src/components/editor/manualEditsDomPatches.ts +0 -30
  77. package/src/components/editor/manualEditsSnapshot.ts +1 -0
  78. package/src/components/editor/marqueeCommit.test.tsx +69 -1
  79. package/src/components/editor/marqueeCommit.ts +6 -1
  80. package/src/components/editor/overlayFrameLoop.test.ts +27 -0
  81. package/src/components/editor/overlayFrameLoop.ts +21 -0
  82. package/src/components/editor/persistSeam.integration.test.ts +0 -18
  83. package/src/components/editor/plainTranslate.ts +34 -3
  84. package/src/components/editor/propertyPanelColorCurveGraph.tsx +2 -2
  85. package/src/components/editor/propertyPanelColorGradingControls.tsx +1 -1
  86. package/src/components/editor/propertyPanelColorGradingSection.tsx +8 -8
  87. package/src/components/editor/propertyPanelColorGradingSlider.tsx +1 -1
  88. package/src/components/editor/propertyPanelColorSecondary.tsx +4 -4
  89. package/src/components/editor/propertyPanelFill.tsx +6 -6
  90. package/src/components/editor/propertyPanelFlatColorGradingAccessory.tsx +3 -3
  91. package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +1 -1
  92. package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +6 -9
  93. package/src/components/editor/propertyPanelFlatEffectsSection.tsx +4 -4
  94. package/src/components/editor/propertyPanelFlatMediaSection.tsx +2 -2
  95. package/src/components/editor/propertyPanelFlatMotionSection.tsx +2 -2
  96. package/src/components/editor/propertyPanelFlatOverlaysSection.tsx +2 -2
  97. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +4 -3
  98. package/src/components/editor/propertyPanelFlatPrimitives.tsx +1 -1
  99. package/src/components/editor/propertyPanelFlatSliderReadout.tsx +1 -1
  100. package/src/components/editor/propertyPanelFont.test.tsx +80 -3
  101. package/src/components/editor/propertyPanelFont.tsx +71 -48
  102. package/src/components/editor/propertyPanelFxCarveModule.tsx +1 -1
  103. package/src/components/editor/propertyPanelFxControls.tsx +2 -2
  104. package/src/components/editor/propertyPanelFxEqModule.tsx +2 -2
  105. package/src/components/editor/propertyPanelFxNodeRow.tsx +1 -1
  106. package/src/components/editor/propertyPanelFxPresetRun.tsx +1 -1
  107. package/src/components/editor/propertyPanelHelpers.ts +1 -2
  108. package/src/components/editor/propertyPanelMediaSection.tsx +1 -1
  109. package/src/components/editor/propertyPanelPrimitives.tsx +1 -1
  110. package/src/components/editor/propertyPanelSections.tsx +2 -2
  111. package/src/components/editor/propertyPanelValueTier.test.ts +1 -1
  112. package/src/components/editor/propertyPanelValueTier.ts +1 -1
  113. package/src/components/editor/studioCodeTheme.test.ts +20 -0
  114. package/src/components/editor/studioCodeTheme.ts +99 -0
  115. package/src/components/editor/useDomEditNudge.test.tsx +31 -0
  116. package/src/components/editor/useDomEditNudge.ts +2 -2
  117. package/src/components/editor/useInlineTextEditing.tsx +1 -1
  118. package/src/components/editor/useMotionPathData.test.tsx +64 -0
  119. package/src/components/editor/useMotionPathData.ts +5 -1
  120. package/src/components/feedback/StudioFeedbackCard.tsx +11 -23
  121. package/src/components/nle/AssetPreviewOverlay.tsx +1 -1
  122. package/src/components/nle/AudioMeterStrip.tsx +2 -2
  123. package/src/components/nle/NLEPreview.test.ts +8 -4
  124. package/src/components/nle/PreviewOverlays.tsx +1 -1
  125. package/src/components/nle/PreviewPane.tsx +3 -3
  126. package/src/components/nle/PreviewPoster.tsx +29 -1
  127. package/src/components/nle/PreviewZoomOverlay.tsx +1 -1
  128. package/src/components/panels/SlideshowPanel.tsx +4 -4
  129. package/src/components/panels/SlideshowSubPanels.tsx +19 -21
  130. package/src/components/panels/VariablesBindElement.tsx +1 -1
  131. package/src/components/panels/VariablesDeclarationForm.tsx +1 -1
  132. package/src/components/panels/VariablesPanel.tsx +4 -6
  133. package/src/components/panels/VariablesRowAction.tsx +1 -1
  134. package/src/components/renders/FfmpegRequiredNotice.tsx +4 -4
  135. package/src/components/renders/RenderQueue.tsx +2 -2
  136. package/src/components/renders/RenderQueueItem.tsx +3 -3
  137. package/src/components/sidebar/AssetCard.tsx +3 -3
  138. package/src/components/sidebar/AssetContextMenu.tsx +7 -7
  139. package/src/components/sidebar/AssetsTab.tsx +6 -6
  140. package/src/components/sidebar/AudioRow.tsx +5 -3
  141. package/src/components/sidebar/BlocksTab.tsx +6 -6
  142. package/src/components/sidebar/CompositionsTab.drag.test.tsx +3 -69
  143. package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +4 -50
  144. package/src/components/sidebar/CompositionsTab.thumbnails.test.tsx +256 -0
  145. package/src/components/sidebar/CompositionsTab.tsx +65 -12
  146. package/src/components/sidebar/PromptPreviewModal.tsx +3 -3
  147. package/src/components/sidebar/SidebarLintButton.tsx +3 -3
  148. package/src/components/sidebar/compositionsTabTestUtils.tsx +39 -0
  149. package/src/components/timelineToolbarStyles.ts +3 -3
  150. package/src/components/ui/Button.tsx +4 -4
  151. package/src/components/ui/HyperframesLogo.tsx +19 -18
  152. package/src/components/ui/Input.tsx +1 -1
  153. package/src/components/ui/Menu.tsx +4 -2
  154. package/src/hooks/appHotkeysDispatch.ts +2 -1
  155. package/src/hooks/elementOffsetStager.ts +8 -7
  156. package/src/hooks/previewPromotionSelection.test.tsx +180 -0
  157. package/src/hooks/useAppHotkeys.test.ts +32 -0
  158. package/src/hooks/useAppHotkeys.ts +1 -2
  159. package/src/hooks/useClipboard.source.test.ts +141 -23
  160. package/src/hooks/useClipboard.ts +154 -123
  161. package/src/hooks/useDomEditCommits.ts +0 -3
  162. package/src/hooks/useDomEditSession.test.tsx +76 -72
  163. package/src/hooks/useDomEditSession.ts +7 -43
  164. package/src/hooks/useDomGeometryCommit.test.tsx +4 -1
  165. package/src/hooks/useDomGeometryCommit.ts +0 -1
  166. package/src/hooks/useDomGeometryCommits.test.tsx +177 -32
  167. package/src/hooks/useDomGeometryCommits.ts +4 -49
  168. package/src/hooks/useGsapTweenCache.ts +1 -2
  169. package/src/hooks/useInlineTextEdit.ts +16 -2
  170. package/src/hooks/useLivePreviewIframe.ts +18 -0
  171. package/src/hooks/usePreviewPersistence.history.test.tsx +47 -0
  172. package/src/hooks/usePreviewPersistence.ts +2 -5
  173. package/src/hooks/useRenderClipContent.ts +3 -3
  174. package/src/index.ts +2 -0
  175. package/src/livePreviewIframeExport.test.tsx +33 -0
  176. package/src/main.tsx +4 -1
  177. package/src/player/components/AudioWaveform.test.tsx +36 -15
  178. package/src/player/components/AudioWaveform.tsx +24 -7
  179. package/src/player/components/ClipContextMenu.tsx +1 -1
  180. package/src/player/components/CompositionThumbnail.tsx +23 -11
  181. package/src/player/components/EditModal.tsx +6 -6
  182. package/src/player/components/KeyframeDiamondContextMenu.tsx +2 -2
  183. package/src/player/components/PlayerControls.tsx +3 -3
  184. package/src/player/components/ShortcutsPanel.tsx +8 -20
  185. package/src/player/components/TimelineCanvas.tsx +1 -1
  186. package/src/player/components/TimelineClip.test.tsx +38 -0
  187. package/src/player/components/TimelineClip.tsx +16 -8
  188. package/src/player/components/TimelineClipFades.test.tsx +376 -74
  189. package/src/player/components/TimelineClipFades.tsx +368 -137
  190. package/src/player/components/TimelineEmptyState.tsx +2 -2
  191. package/src/player/components/TimelineLanes.tsx +0 -1
  192. package/src/player/components/TrackClipCount.tsx +1 -1
  193. package/src/player/components/VolumeControl.tsx +1 -1
  194. package/src/player/components/timelineClipChildren.tsx +1 -1
  195. package/src/player/components/timelineMotionStyles.test.ts +6 -22
  196. package/src/player/components/trackHeaderLabelRows.tsx +16 -16
  197. package/src/player/components/useTimelineKeyboardActor.ts +1 -1
  198. package/src/player/hooks/timelinePlayerTestHarness.ts +27 -0
  199. package/src/player/hooks/useShadowPreviewReload.ts +72 -5
  200. package/src/player/lib/automationStoreSync.test.ts +18 -1
  201. package/src/player/lib/automationStoreSync.ts +29 -17
  202. package/src/player/lib/thumbnailScheduler.ts +4 -5
  203. package/src/player/lib/timelineViewportBudgets.test.ts +1 -1
  204. package/src/player/lib/timelineViewportBudgets.ts +1 -1
  205. package/src/player/sceneSwap.ts +18 -0
  206. package/src/styles/components.css +26 -0
  207. package/src/styles/hex-baseline.json +7 -19
  208. package/src/styles/studio.css +4 -7
  209. package/src/styles/tailwind-preset.shared.js +2 -1
  210. package/src/styles/theme.css +214 -147
  211. package/src/styles/theme.test.ts +13 -9
  212. package/src/utils/blockCategories.ts +41 -9
  213. package/src/utils/clipboardPayload.ts +12 -1
  214. package/src/utils/studioPendingEdits.ts +21 -0
  215. package/src/utils/studioTheme.test.tsx +141 -0
  216. package/src/utils/studioTheme.ts +34 -0
  217. package/src/utils/studioUiPreferences.ts +3 -0
  218. package/src/utils/typingTarget.test.ts +8 -1
  219. package/src/utils/typingTarget.ts +5 -0
  220. package/dist/assets/index-CQN3M56N.css +0 -1
  221. package/dist/assets/index-De2aO4XM.js +0 -399
  222. package/dist/chunk-62DDWOSH.js.map +0 -1
  223. package/dist/chunk-6GAO75IU.js.map +0 -1
  224. package/dist/chunk-HIGHVWYD.js.map +0 -1
  225. package/src/hooks/gsapTargetCache.ts +0 -62
@@ -1,9 +1,17 @@
1
1
  @import "dockview-react/dist/styles/dockview.css";
2
2
 
3
+ .dv-shell:has(.hf-dock) {
4
+ /* dockview's runtime stylesheet pins `color-scheme: dark` on its theme class, which sits on
5
+ * the shell around the dock. */
6
+ color-scheme: inherit;
7
+ }
8
+
3
9
  .hf-dock {
4
10
  --dv-group-view-background-color: var(--panel-card-bg);
5
11
  --dv-tabs-and-actions-container-background-color: var(--panel-strip-bg);
6
12
  --dv-tabs-and-actions-container-height: 36px;
13
+ --dv-icon-hover-background-color: var(--color-hover);
14
+ --dv-tabs-container-scrollbar-color: var(--color-text-muted);
7
15
  /* The fill paints the shown tab, so every tab state is transparent. */
8
16
  --dv-activegroup-visiblepanel-tab-background-color: transparent;
9
17
  --dv-activegroup-hiddenpanel-tab-background-color: transparent;
@@ -178,7 +186,7 @@
178
186
  }
179
187
 
180
188
  .dv-groupview:hover .hf-dock-strip-actions {
181
- opacity: 0.5;
189
+ opacity: 0.8;
182
190
  }
183
191
 
184
192
  .hf-dock-strip-actions:hover,
@@ -201,7 +201,7 @@ export const AnimationCard = memo(function AnimationCard({
201
201
  className="flex w-full items-center gap-2 py-1.5 active:scale-[0.99]"
202
202
  >
203
203
  <span
204
- className="rounded-sm bg-panel-accent/10 px-1.5 py-0.5 text-[10px] font-semibold text-panel-accent"
204
+ className="rounded-sm bg-panel-accent/10 px-1.5 py-0.5 text-[10px] font-semibold text-accent-ink"
205
205
  title={METHOD_TOOLTIPS[animation.method]}
206
206
  >
207
207
  {methodLabel}
@@ -354,7 +354,7 @@ export const AnimationCard = memo(function AnimationCard({
354
354
 
355
355
  {animation.method === "fromTo" && (
356
356
  <div className="space-y-1">
357
- <p className="text-[9px] font-semibold uppercase tracking-wider text-orange-400/70">
357
+ <p className="text-[9px] font-semibold uppercase tracking-wider text-warning-ink">
358
358
  From
359
359
  </p>
360
360
  <div className="space-y-1.5">
@@ -378,14 +378,14 @@ export const AnimationCard = memo(function AnimationCard({
378
378
  onAdd={(prop) => onAddFromProperty?.(animation.id, prop)}
379
379
  onOpen={() => setAddingFromProp(true)}
380
380
  onClose={() => setAddingFromProp(false)}
381
- buttonClassName="text-[11px] font-medium text-orange-400/70 transition-colors hover:text-orange-300"
381
+ buttonClassName="text-[11px] font-medium text-warning-ink transition-colors hover:text-text-0"
382
382
  />
383
383
  </div>
384
384
  </div>
385
385
  )}
386
386
 
387
387
  {animation.method === "fromTo" && Object.keys(animation.properties).length > 0 && (
388
- <p className="text-[9px] font-semibold uppercase tracking-wider text-panel-accent/70">
388
+ <p className="text-[9px] font-semibold uppercase tracking-wider text-accent-ink">
389
389
  To
390
390
  </p>
391
391
  )}
@@ -455,7 +455,7 @@ export const AnimationCard = memo(function AnimationCard({
455
455
  <button
456
456
  type="button"
457
457
  onClick={() => onDeleteAnimation(animation.id)}
458
- className="ml-auto text-[11px] font-medium text-red-400 transition-colors hover:text-red-300"
458
+ className="ml-auto text-[11px] font-medium rounded-sm text-danger-ink transition-colors hover:bg-danger/15"
459
459
  title="Remove this animation"
460
460
  >
461
461
  Remove
@@ -46,7 +46,7 @@ function RemoveButton({ onClick, title }: { onClick: () => void; title: string }
46
46
  <button
47
47
  type="button"
48
48
  onClick={onClick}
49
- className="relative shrink-0 rounded-sm p-1.5 text-neutral-600 transition-colors hover:bg-neutral-800 hover:text-red-400 active:scale-[0.95]"
49
+ className="relative shrink-0 rounded-sm p-1.5 text-neutral-600 transition-colors hover:bg-neutral-800 hover:text-danger-ink active:scale-[0.95]"
50
50
  title={title}
51
51
  aria-label={title}
52
52
  >
@@ -170,7 +170,7 @@ export const BlockParamsPanel = memo(function BlockParamsPanel({
170
170
  <div className="text-[10px] text-neutral-500">This block has no editable parameters.</div>
171
171
  )}
172
172
  {!fileManager && params.length > 0 && (
173
- <div className="text-[10px] text-amber-400/90">
173
+ <div className="text-[10px] text-warning-ink">
174
174
  Block params can't be edited here — no project file access.
175
175
  </div>
176
176
  )}
@@ -189,12 +189,12 @@ export const BlockParamsPanel = memo(function BlockParamsPanel({
189
189
  </div>
190
190
  )}
191
191
  {commitState.tone === "saved" && (
192
- <div className="text-[10px] text-emerald-500/90" role="status">
192
+ <div className="text-[10px] text-accent-ink" role="status">
193
193
  Saved to {compositionPath}
194
194
  </div>
195
195
  )}
196
196
  {commitState.tone === "error" && (
197
- <div className="text-[10px] text-red-400" role="alert">
197
+ <div className="text-[10px] text-danger-ink" role="alert">
198
198
  {commitState.message}
199
199
  </div>
200
200
  )}
@@ -63,6 +63,9 @@ export function BorderRadiusEditor({
63
63
  }, [linked, uniform, tl, onCommit]);
64
64
 
65
65
  const path = buildRoundedRectPath(PREVIEW_W, PREVIEW_H, sTL, sTR, sBR, sBL);
66
+ const dotFill = linked
67
+ ? "fill-[light-dark(oklch(0.5_0.2_260),#3b82f6)]"
68
+ : "fill-[light-dark(oklch(0.55_0.2_294),#a78bfa)]";
66
69
 
67
70
  return (
68
71
  <div className="space-y-3">
@@ -73,21 +76,11 @@ export function BorderRadiusEditor({
73
76
  viewBox={`0 0 ${PREVIEW_W} ${PREVIEW_H}`}
74
77
  className="shrink-0"
75
78
  >
76
- <path
77
- d={path}
78
- fill="rgba(255,255,255,0.06)"
79
- stroke="rgba(255,255,255,0.24)"
80
- strokeWidth={1.5}
81
- />
82
- <circle cx={sTL} cy={sTL} r={3} fill={linked ? "#3b82f6" : "#a78bfa"} />
83
- <circle cx={PREVIEW_W - sTR} cy={sTR} r={3} fill={linked ? "#3b82f6" : "#a78bfa"} />
84
- <circle
85
- cx={PREVIEW_W - sBR}
86
- cy={PREVIEW_H - sBR}
87
- r={3}
88
- fill={linked ? "#3b82f6" : "#a78bfa"}
89
- />
90
- <circle cx={sBL} cy={PREVIEW_H - sBL} r={3} fill={linked ? "#3b82f6" : "#a78bfa"} />
79
+ <path d={path} className="fill-text-0/[0.06] stroke-text-0/25" strokeWidth={1.5} />
80
+ <circle cx={sTL} cy={sTL} r={3} className={dotFill} />
81
+ <circle cx={PREVIEW_W - sTR} cy={sTR} r={3} className={dotFill} />
82
+ <circle cx={PREVIEW_W - sBR} cy={PREVIEW_H - sBR} r={3} className={dotFill} />
83
+ <circle cx={sBL} cy={PREVIEW_H - sBL} r={3} className={dotFill} />
91
84
  </svg>
92
85
 
93
86
  <button
@@ -257,7 +257,7 @@ export const CanvasContextMenu = memo(function CanvasContextMenu({
257
257
  {hasDelete && (
258
258
  <button
259
259
  type="button"
260
- className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-red-400 hover:bg-neutral-800 cursor-pointer text-left"
260
+ className="w-full flex items-center justify-between px-3 py-1.5 text-xs text-danger-ink hover:bg-neutral-800 cursor-pointer text-left"
261
261
  onPointerDown={(e) => {
262
262
  if (e.button !== 0) return;
263
263
  e.preventDefault();
@@ -29,7 +29,7 @@ export function ComputedTweenNotice({
29
29
  <button
30
30
  type="button"
31
31
  onClick={onUnroll}
32
- className="shrink-0 rounded-sm px-1.5 py-0.5 text-[9px] font-medium text-panel-accent hover:bg-neutral-800"
32
+ className="shrink-0 rounded-sm px-1.5 py-0.5 text-[9px] font-medium text-accent-ink hover:bg-neutral-800"
33
33
  title="Rewrite the helper/loop into explicit tweens so this keyframe edits directly"
34
34
  >
35
35
  Unroll to edit
@@ -31,6 +31,8 @@ import { useDomEditCompositionRect } from "./useDomEditCompositionRect";
31
31
  import { CanvasContextMenu } from "./CanvasContextMenu";
32
32
  import { useInlineTextEditing } from "./useInlineTextEditing";
33
33
  import { usePreviewReadOnly } from "./previewReadOnlyContext";
34
+ import { useMountEffect } from "../../hooks/useMountEffect";
35
+ import { noteStudioManualEditSave as noteSave } from "./manualEditsDom";
34
36
  import type { ZOrderAction, ZOrderPatch } from "./canvasContextMenuZOrder";
35
37
  import { getPreviewTargetFromPointer } from "../../utils/studioPreviewHelpers";
36
38
  import { logSelect } from "../../utils/selectDebug";
@@ -193,16 +195,34 @@ export const DomEditOverlay = memo(function DomEditOverlay({
193
195
  onTextEditingChangeRef.current?.(true);
194
196
  return () => onTextEditingChangeRef.current?.(false);
195
197
  }, [inlineText.editing]);
198
+ // Each canvas save is counted before it starts, so a reload requested earlier loads again.
196
199
  const onPathOffsetCommitRef = useRef(onPathOffsetCommit);
197
- onPathOffsetCommitRef.current = onPathOffsetCommit;
200
+ onPathOffsetCommitRef.current = (sel, ...rest) => {
201
+ noteSave(sel.element);
202
+ return onPathOffsetCommit(sel, ...rest);
203
+ };
198
204
  const onGroupPathOffsetCommitRef = useRef(onGroupPathOffsetCommit);
199
- onGroupPathOffsetCommitRef.current = onGroupPathOffsetCommit;
205
+ onGroupPathOffsetCommitRef.current = (updates) => {
206
+ if (updates[0]) noteSave(updates[0].selection.element);
207
+ return onGroupPathOffsetCommit(updates);
208
+ };
200
209
  const onBoxSizeCommitRef = useRef(onBoxSizeCommit);
201
- onBoxSizeCommitRef.current = onBoxSizeCommit;
210
+ onBoxSizeCommitRef.current = (sel, ...rest) => {
211
+ noteSave(sel.element);
212
+ return onBoxSizeCommit(sel, ...rest);
213
+ };
202
214
  const onRotationCommitRef = useRef(onRotationCommit);
203
- onRotationCommitRef.current = onRotationCommit;
215
+ onRotationCommitRef.current = (sel, next) => {
216
+ noteSave(sel.element);
217
+ return onRotationCommit(sel, next);
218
+ };
204
219
  const onStyleCommitRef = useRef(onStyleCommit);
205
- onStyleCommitRef.current = onStyleCommit;
220
+ onStyleCommitRef.current =
221
+ onStyleCommit &&
222
+ ((property, value) => {
223
+ if (selectionRef.current) noteSave(selectionRef.current.element);
224
+ return onStyleCommit(property, value);
225
+ });
206
226
  const onBlockedMoveRef = useRef(onBlockedMove);
207
227
  onBlockedMoveRef.current = onBlockedMove;
208
228
  const onManualDragStartRef = useRef(onManualDragStart);
@@ -282,6 +302,17 @@ export const DomEditOverlay = memo(function DomEditOverlay({
282
302
  useEffect(() => {
283
303
  if (readOnly) gestures.clearPointerState(selectionRef);
284
304
  }, [gestures, readOnly, selectionRef]);
305
+ // A gesture that loses its pointer, the window or the overlay is cancelled, so its mark goes too.
306
+ const cancelGestureRef = useRef(() => {});
307
+ cancelGestureRef.current = () => gestures.clearPointerState(selectionRef);
308
+ useMountEffect(() => {
309
+ const cancel = () => cancelGestureRef.current();
310
+ window.addEventListener("blur", cancel);
311
+ return () => {
312
+ window.removeEventListener("blur", cancel);
313
+ cancel();
314
+ };
315
+ });
285
316
 
286
317
  // Arrow-key nudge (1px, Shift = 10px) — commits through the same
287
318
  // path-offset callbacks as a drag, one undo entry per key burst.
@@ -463,6 +494,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
463
494
  onPointerLeave={() => onCanvasPointerLeaveRef.current()}
464
495
  onPointerUp={marquee.onPointerUp}
465
496
  onPointerCancel={marquee.onPointerCancel}
497
+ onLostPointerCapture={() => cancelGestureRef.current()}
466
498
  onContextMenu={hostInput ? undefined : handleContextMenu}
467
499
  >
468
500
  {!hostInput && hoverSelection && hoverRect && compRect.width > 0 && (
@@ -38,7 +38,7 @@ export function DomEditRotateHandle({
38
38
  aria-label="Rotate selection"
39
39
  onPointerDown={onStartRotate}
40
40
  >
41
- <span className="pointer-events-none flex h-[18px] w-[18px] items-center justify-center rounded-full border border-studio-accent/70 bg-studio-surface text-studio-accent shadow-[0_0_3px_rgba(0,0,0,0.45)]">
41
+ <span className="pointer-events-none flex h-[18px] w-[18px] items-center justify-center rounded-full border border-studio-accent/70 bg-studio-surface text-accent-ink shadow-[0_0_3px_rgba(0,0,0,0.45)]">
42
42
  <svg
43
43
  width="11"
44
44
  height="11"
@@ -42,7 +42,6 @@ const VMIN = -HR / S; // bottom of visible view (undershoot headroom)
42
42
  // of snapping to the view edge; the handle DOT is still clampView'd into view.
43
43
  const DRAG_VMAX = 2;
44
44
  const DRAG_VMIN = -1;
45
- const ACCENT = "#3CE6AC";
46
45
 
47
46
  // Figma-style ease-type dropdown: the current ease (glyph + name) as a button
48
47
  // that opens the preset grid in a popover. This is where a preset is selected —
@@ -130,7 +129,7 @@ function EaseTypeDropdown({
130
129
  aria-expanded={open}
131
130
  aria-controls={open ? menuId : undefined}
132
131
  onClick={() => setOpen((prev) => !prev)}
133
- className="flex w-full items-center gap-2 rounded-md border border-white/10 bg-black/20 px-2 py-1.5 text-left transition-colors hover:border-white/20"
132
+ className="flex w-full items-center gap-2 rounded-md border border-border bg-input px-2 py-1.5 text-left transition-colors hover:border-border-strong"
134
133
  >
135
134
  <MiniCurveSvg ease={ease} active size={16} />
136
135
  <span className="text-[11px] text-neutral-200">{label}</span>
@@ -151,7 +150,7 @@ function EaseTypeDropdown({
151
150
  role="menu"
152
151
  aria-label={`${MODE_LABELS[kind]} ease presets`}
153
152
  onKeyDown={handleMenuKeyDown}
154
- className="absolute inset-x-0 top-full z-20 mt-1 rounded-md border border-white/10 bg-neutral-900 p-2 shadow-xl"
153
+ className="absolute inset-x-0 top-full z-20 mt-1 rounded-md border border-border bg-raised p-2 shadow-xl"
155
154
  >
156
155
  <EasePresetGrid
157
156
  kind={kind}
@@ -407,7 +406,7 @@ export function EaseCurveSection({
407
406
  {showGraph ? (
408
407
  <>
409
408
  <div
410
- className="mx-auto overflow-hidden rounded-md border border-white/5 bg-black/20"
409
+ className="mx-auto overflow-hidden rounded-md border border-border bg-input"
411
410
  style={{ aspectRatio: `${SVGW} / ${SVGH}`, width: "100%", maxWidth: 230 }}
412
411
  >
413
412
  <svg
@@ -429,7 +428,7 @@ export function EaseCurveSection({
429
428
  y1={top}
430
429
  x2={xToSvg(q)}
431
430
  y2={bottom}
432
- stroke="white"
431
+ className="stroke-text-0"
433
432
  strokeOpacity="0.05"
434
433
  strokeWidth="1"
435
434
  />
@@ -441,7 +440,7 @@ export function EaseCurveSection({
441
440
  y1={yToSvg(q)}
442
441
  x2={right}
443
442
  y2={yToSvg(q)}
444
- stroke="white"
443
+ className="stroke-text-0"
445
444
  strokeOpacity="0.05"
446
445
  strokeWidth="1"
447
446
  />
@@ -453,7 +452,7 @@ export function EaseCurveSection({
453
452
  width={S}
454
453
  height={bottom - top}
455
454
  fill="none"
456
- stroke="white"
455
+ className="stroke-text-0"
457
456
  strokeOpacity="0.1"
458
457
  strokeWidth="1"
459
458
  />
@@ -463,7 +462,7 @@ export function EaseCurveSection({
463
462
  y1={a0.y}
464
463
  x2={a1.x}
465
464
  y2={a1.y}
466
- stroke="white"
465
+ className="stroke-text-0"
467
466
  strokeOpacity="0.08"
468
467
  strokeWidth="1"
469
468
  strokeDasharray="3 4"
@@ -476,8 +475,7 @@ export function EaseCurveSection({
476
475
  y1={a0.y}
477
476
  x2={p1.x}
478
477
  y2={p1.y}
479
- stroke={ACCENT}
480
- strokeOpacity="0.5"
478
+ className="stroke-accent-ink"
481
479
  strokeWidth="1.5"
482
480
  />
483
481
  <line
@@ -485,8 +483,7 @@ export function EaseCurveSection({
485
483
  y1={a1.y}
486
484
  x2={p2.x}
487
485
  y2={p2.y}
488
- stroke={ACCENT}
489
- strokeOpacity="0.5"
486
+ className="stroke-accent-ink"
490
487
  strokeWidth="1.5"
491
488
  />
492
489
  </>
@@ -495,13 +492,13 @@ export function EaseCurveSection({
495
492
  <path
496
493
  d={curvePath}
497
494
  fill="none"
498
- stroke={ACCENT}
495
+ className="stroke-accent-ink"
499
496
  strokeWidth="2.5"
500
497
  strokeLinecap="round"
501
498
  />
502
499
  {/* Anchors at (0,0) and (1,1) */}
503
- <circle cx={a0.x} cy={a0.y} r="3" fill={ACCENT} />
504
- <circle cx={a1.x} cy={a1.y} r="3" fill={ACCENT} />
500
+ <circle cx={a0.x} cy={a0.y} r="3" className="fill-accent-ink" />
501
+ <circle cx={a1.x} cy={a1.y} r="3" className="fill-accent-ink" />
505
502
  {/* Draggable control handles (large transparent hit area + visible dot) */}
506
503
  {showHandles &&
507
504
  [["p1", p1] as const, ["p2", p2] as const].map(([key, pt]) => (
@@ -518,7 +515,7 @@ export function EaseCurveSection({
518
515
  aria-valuemax={1}
519
516
  aria-valuenow={key === "p1" ? x1 : x2}
520
517
  aria-valuetext={`x ${key === "p1" ? x1 : x2}, y ${key === "p1" ? y1 : y2}`}
521
- className="cursor-grab stroke-transparent outline-hidden active:cursor-grabbing focus-visible:stroke-white focus-visible:stroke-[2px]"
518
+ className="cursor-grab stroke-transparent outline-hidden active:cursor-grabbing focus-visible:stroke-text-0 focus-visible:stroke-[2px]"
522
519
  onPointerDown={(e) => handlePointerDown(key, e)}
523
520
  onKeyDown={(event) => handleKeyDown(key, event)}
524
521
  onPointerEnter={() => setHover(key)}
@@ -528,10 +525,8 @@ export function EaseCurveSection({
528
525
  cx={pt.x}
529
526
  cy={pt.y}
530
527
  r={hover === key || draggingRef.current === key ? 7 : 5.5}
531
- fill="#0a0a1a"
532
- stroke={ACCENT}
533
528
  strokeWidth="2.5"
534
- className="pointer-events-none transition-[r]"
529
+ className="pointer-events-none fill-input stroke-accent-ink transition-[r]"
535
530
  />
536
531
  </g>
537
532
  ))}
@@ -59,7 +59,7 @@ export const EasePresetGrid = function EasePresetGrid({
59
59
  <MiniCurveSvg ease={preset.ease} active={isActive} />
60
60
  <span
61
61
  className={`text-center text-[8px] leading-none ${
62
- isActive ? "text-panel-accent" : "text-neutral-500"
62
+ isActive ? "text-accent-ink" : "text-neutral-500"
63
63
  }`}
64
64
  >
65
65
  {preset.label}
@@ -80,7 +80,7 @@ export function EaseModeToggle({
80
80
  }) {
81
81
  return (
82
82
  <div
83
- className="mb-2 grid grid-cols-3 rounded-md bg-black/20 p-0.5"
83
+ className="mb-2 grid grid-cols-3 rounded-md bg-press p-0.5"
84
84
  role="radiogroup"
85
85
  aria-label="Ease editor mode"
86
86
  >
@@ -93,17 +93,17 @@ export function EaseBezierField({
93
93
  }
94
94
  }}
95
95
  onBlur={(event) => commit(event.currentTarget.value)}
96
- className={`w-full rounded border bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden ${
96
+ className={`w-full rounded border bg-input px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden ${
97
97
  error
98
- ? "border-red-500/70 focus:border-red-400"
99
- : "border-white/10 focus:border-panel-accent/50"
98
+ ? "border-danger-ink/70 focus:border-danger-ink"
99
+ : "border-border focus:border-panel-accent/50"
100
100
  }`}
101
101
  />
102
102
  </div>
103
103
  <p
104
104
  id={errorId}
105
105
  aria-live="polite"
106
- className={`mt-1 text-[9px] text-red-400 ${error ? "" : "sr-only"}`}
106
+ className={`mt-1 text-[9px] text-danger-ink ${error ? "" : "sr-only"}`}
107
107
  >
108
108
  {error ?? "Valid bezier values"}
109
109
  </p>
@@ -183,7 +183,7 @@ export function SpringBounceField({
183
183
  const bounce = parseSpringBounce(`spring(${value})`);
184
184
  if (bounce !== null) onCommit(`spring(${round2(bounce)})`);
185
185
  }}
186
- className="w-16 rounded-sm border border-white/10 bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
186
+ className="w-16 rounded-sm border border-border bg-input px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
187
187
  />
188
188
  </div>
189
189
  );
@@ -207,7 +207,7 @@ export function WiggleField({
207
207
  min={1}
208
208
  step={1}
209
209
  onCommit={(value) => commitWiggle(onCommit, value, config.type, amplitude)}
210
- className="w-14 rounded-sm border border-white/10 bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
210
+ className="w-14 rounded-sm border border-border bg-input px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
211
211
  />
212
212
  </div>
213
213
  <div className="flex items-center gap-1">
@@ -220,7 +220,7 @@ export function WiggleField({
220
220
  commitWiggle(onCommit, config.wiggles, event.currentTarget.value, amplitude);
221
221
  }
222
222
  }}
223
- className="rounded-sm border border-white/10 bg-black/20 px-1.5 py-1 text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
223
+ className="rounded-sm border border-border bg-input px-1.5 py-1 text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
224
224
  >
225
225
  {WIGGLE_TYPES.map((type) => (
226
226
  <option key={type} value={type}>
@@ -238,7 +238,7 @@ export function WiggleField({
238
238
  max={1}
239
239
  step={0.01}
240
240
  onCommit={(value) => commitWiggle(onCommit, config.wiggles, config.type, value)}
241
- className="w-16 rounded-sm border border-white/10 bg-black/20 px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
241
+ className="w-16 rounded-sm border border-border bg-input px-1.5 py-1 font-mono text-[10px] text-neutral-300 outline-hidden focus:border-panel-accent/50"
242
242
  />
243
243
  </div>
244
244
  </div>
@@ -263,7 +263,7 @@ export const FileTree = memo(function FileTree({
263
263
  <div
264
264
  className={`flex-1 overflow-y-auto py-1 transition-colors ${
265
265
  dragOverFolder === ""
266
- ? "bg-[#3CE6AC]/5 outline-solid outline-1 outline-[#3CE6AC]/30 -outline-offset-1"
266
+ ? "bg-accent/5 outline-solid outline-1 outline-accent-ink -outline-offset-1"
267
267
  : ""
268
268
  }`}
269
269
  onContextMenu={handleRootContextMenu}
@@ -21,34 +21,55 @@ import {
21
21
  const SZ = 14;
22
22
  const W = "duotone" as const;
23
23
 
24
+ type Icon = typeof File;
25
+ // Brand hues; each light half is darkened to hold 3:1 on a hovered or selected row.
26
+ const RED = "text-[light-dark(oklch(0.59_0.194_35),oklch(0.71_0.194_35))]";
27
+ const BLUE = "text-[light-dark(oklch(0.56_0.14_253),oklch(0.69_0.14_253))]";
28
+ const GREEN = "text-[light-dark(oklch(0.53_0.192_150),#22C55E)]";
29
+ const GRAY = "text-[light-dark(oklch(0.56_0.019_261),#9CA3AF)]";
30
+ export const DIM = "text-[light-dark(oklch(0.55_0.023_264),oklch(0.69_0.023_264))]";
31
+ const ICONS = new Map<string, [Icon, string]>([
32
+ ["html", [FileHtml, RED]],
33
+ ["css", [FileCss, "text-[light-dark(oklch(0.5_0.2_266),oklch(0.71_0.2_266))]"]],
34
+ ["js", [FileJs, "text-[light-dark(oklch(0.56_0.156_101),#F0DB4F)]"]],
35
+ ["jsx", [FileJsx, "text-[light-dark(oklch(0.55_0.117_219),#61DAFB)]"]],
36
+ ["ts", [FileTs, BLUE]],
37
+ ["tsx", [FileTsx, BLUE]],
38
+ ["json", [FileCode, "text-[light-dark(oklch(0.54_0.182_152),#4ADE80)]"]],
39
+ ["svg", [FileSvg, "text-[light-dark(oklch(0.58_0.187_48),#F97316)]"]],
40
+ ["md", [FileMd, GRAY]],
41
+ ["txt", [FileTxt, GRAY]],
42
+ ["png", [FilePng, GREEN]],
43
+ ["jpg", [FileJpg, GREEN]],
44
+ ["webp", [PhImage, GREEN]],
45
+ ["mp4", [FileVideo, "text-[light-dark(oklch(0.59_0.2_304),oklch(0.71_0.2_304))]"]],
46
+ ["mp3", [Waveform, "text-[light-dark(oklch(0.53_0.159_165),#3CE6AC)]"]],
47
+ ["woff", [TextAa, DIM]],
48
+ ]);
49
+ const ALIAS = new Map(
50
+ Object.entries({
51
+ mjs: "js",
52
+ cjs: "js",
53
+ mts: "ts",
54
+ mdx: "md",
55
+ jpeg: "jpg",
56
+ gif: "webp",
57
+ ico: "webp",
58
+ webm: "mp4",
59
+ mov: "mp4",
60
+ wav: "mp3",
61
+ ogg: "mp3",
62
+ m4a: "mp3",
63
+ woff2: "woff",
64
+ ttf: "woff",
65
+ otf: "woff",
66
+ }),
67
+ );
68
+
24
69
  export function FileIcon({ path }: { path: string }) {
25
70
  const ext = path.split(".").pop()?.toLowerCase() ?? "";
26
- const c = "shrink-0";
27
- if (ext === "html") return <FileHtml size={SZ} weight={W} color="#E44D26" className={c} />;
28
- if (ext === "css") return <FileCss size={SZ} weight={W} color="#264DE4" className={c} />;
29
- if (ext === "js" || ext === "mjs" || ext === "cjs")
30
- return <FileJs size={SZ} weight={W} color="#F0DB4F" className={c} />;
31
- if (ext === "jsx") return <FileJsx size={SZ} weight={W} color="#61DAFB" className={c} />;
32
- if (ext === "ts" || ext === "mts")
33
- return <FileTs size={SZ} weight={W} color="#3178C6" className={c} />;
34
- if (ext === "tsx") return <FileTsx size={SZ} weight={W} color="#3178C6" className={c} />;
35
- if (ext === "json") return <FileCode size={SZ} weight={W} color="#4ADE80" className={c} />;
36
- if (ext === "svg") return <FileSvg size={SZ} weight={W} color="#F97316" className={c} />;
37
- if (ext === "md" || ext === "mdx")
38
- return <FileMd size={SZ} weight={W} color="#9CA3AF" className={c} />;
39
- if (ext === "txt") return <FileTxt size={SZ} weight={W} color="#9CA3AF" className={c} />;
40
- if (ext === "png") return <FilePng size={SZ} weight={W} color="#22C55E" className={c} />;
41
- if (ext === "jpg" || ext === "jpeg")
42
- return <FileJpg size={SZ} weight={W} color="#22C55E" className={c} />;
43
- if (ext === "webp" || ext === "gif" || ext === "ico")
44
- return <PhImage size={SZ} weight={W} color="#22C55E" className={c} />;
45
- if (ext === "mp4" || ext === "webm" || ext === "mov")
46
- return <FileVideo size={SZ} weight={W} color="#A855F7" className={c} />;
47
- if (ext === "mp3" || ext === "wav" || ext === "ogg" || ext === "m4a")
48
- return <Waveform size={SZ} weight={W} color="#3CE6AC" className={c} />;
49
- if (ext === "woff" || ext === "woff2" || ext === "ttf" || ext === "otf")
50
- return <TextAa size={SZ} weight={W} color="#6B7280" className={c} />;
51
- return <File size={SZ} weight={W} color="#6B7280" className={c} />;
71
+ const [Glyph, color] = ICONS.get(ALIAS.get(ext) ?? ext) ?? [File, DIM];
72
+ return <Glyph size={SZ} weight={W} className={`shrink-0 ${color}`} />;
52
73
  }
53
74
 
54
75
  // ── Tree Types ──
@@ -16,6 +16,7 @@ import {
16
16
  type TreeNode,
17
17
  type ContextMenuState,
18
18
  type InlineInputState,
19
+ DIM,
19
20
  } from "./FileTreeIcons";
20
21
 
21
22
  export type { ContextMenuState, InlineInputState };
@@ -173,7 +174,7 @@ export function ContextMenu({
173
174
  <div className="border-t border-neutral-700 my-1" />
174
175
  <button
175
176
  role="menuitem"
176
- className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-red-400 hover:bg-red-900/30 focus-visible:bg-red-900/30 active:bg-red-900/50 outline-hidden cursor-pointer text-left"
177
+ className="w-full flex items-center gap-2 px-3 py-1.5 text-xs text-danger-ink hover:bg-danger/25 focus-visible:bg-danger/25 active:bg-danger/25 outline-hidden cursor-pointer text-left"
177
178
  onClick={() => {
178
179
  onDelete(state.targetPath);
179
180
  onClose();
@@ -263,7 +264,7 @@ export function InlineInput({
263
264
  style={{ paddingLeft: `${8 + depth * 12 + (isFolder ? 0 : 14)}px` }}
264
265
  >
265
266
  {isFolder ? (
266
- <FolderSimple size={SZ_ICON} weight="duotone" color="#6B7280" className="shrink-0" />
267
+ <FolderSimple size={SZ_ICON} weight="duotone" className={`shrink-0 ${DIM}`} />
267
268
  ) : (
268
269
  <FileIcon path={value} />
269
270
  )}
@@ -279,12 +280,12 @@ export function InlineInput({
279
280
  onBlur={handleBlur}
280
281
  aria-invalid={error ? true : undefined}
281
282
  className={`w-full min-w-0 bg-neutral-800 text-neutral-200 text-xs px-1.5 py-0.5 rounded border outline-hidden ${
282
- error ? "border-red-500/70" : "border-neutral-600 focus:border-[#3CE6AC]"
283
+ error ? "border-danger-ink" : "border-neutral-600 focus:border-accent-ink"
283
284
  }`}
284
285
  spellCheck={false}
285
286
  />
286
287
  {error && (
287
- <div className="mt-0.5 text-[10px] text-red-400" role="alert">
288
+ <div className="mt-0.5 text-[10px] text-danger-ink" role="alert">
288
289
  {error}
289
290
  </div>
290
291
  )}
@@ -344,13 +345,13 @@ export function DeleteConfirm({
344
345
  <div className="flex gap-1.5">
345
346
  <button
346
347
  onClick={onCancel}
347
- className="flex-1 px-2 py-1 rounded-sm bg-neutral-700 text-neutral-300 hover:bg-neutral-600 transition-colors"
348
+ className="flex-1 px-2 py-1 rounded-sm bg-hover text-neutral-300 hover:bg-press transition-colors"
348
349
  >
349
350
  Cancel
350
351
  </button>
351
352
  <button
352
353
  onClick={onConfirm}
353
- className="flex-1 px-2 py-1 rounded-sm bg-red-900/60 text-red-300 hover:bg-red-800/60 transition-colors"
354
+ className="flex-1 px-2 py-1 rounded-sm bg-danger/15 text-danger-ink hover:bg-danger/25 transition-colors"
354
355
  >
355
356
  Delete
356
357
  </button>
@@ -419,7 +420,7 @@ export const TreeFile = memo(function TreeFile({
419
420
  <span className="truncate flex-1">{node.name}</span>
420
421
  {lintInfo && lintInfo.count > 0 && (
421
422
  <span
422
- className="shrink-0 min-w-[16px] rounded-full bg-amber-500/20 px-1 text-[8px] font-bold text-amber-400 text-center mr-1"
423
+ className="shrink-0 min-w-[16px] rounded-full bg-amber-500/20 px-1 text-[8px] font-bold text-warning-ink text-center mr-1"
423
424
  title={lintInfo.messages.join("\n")}
424
425
  >
425
426
  {lintInfo.count}
@@ -505,7 +506,9 @@ export const TreeFolder = memo(function TreeFolder({
505
506
  }}
506
507
  onDragLeave={onDragLeave}
507
508
  className={`w-full flex items-center gap-1.5 px-2.5 py-1 min-h-7 text-left text-xs text-neutral-400 hover:bg-neutral-800/30 hover:text-neutral-300 transition-colors ${
508
- isDragOver ? "bg-[#3CE6AC]/10 outline-solid outline-1 outline-[#3CE6AC]/40" : ""
509
+ isDragOver
510
+ ? "bg-accent/10 outline-solid outline-1 outline-accent-ink -outline-offset-1"
511
+ : ""
509
512
  }`}
510
513
  style={{ paddingLeft: `${8 + depth * 12}px` }}
511
514
  >
@@ -43,7 +43,7 @@ export function GestureRecordPanelButton({
43
43
  }}
44
44
  className={`w-full flex items-center justify-center gap-2 rounded-lg py-2 text-[11px] font-medium transition-colors ${
45
45
  recording
46
- ? "bg-red-500/15 text-red-400 border border-red-500/30 animate-pulse"
46
+ ? "bg-red-500/15 text-danger-ink border border-red-500/30 animate-pulse"
47
47
  : "bg-panel-input text-panel-text-2 hover:bg-panel-hover border border-panel-border"
48
48
  }`}
49
49
  >
@@ -77,8 +77,8 @@ export function GestureRecordBadge({
77
77
  title={label}
78
78
  className={`pointer-events-auto absolute z-20 flex h-7 w-7 items-center justify-center rounded-full border shadow-lg transition-colors ${
79
79
  recording
80
- ? "border-red-400/60 bg-red-500 text-white animate-pulse"
81
- : "border-studio-accent/60 bg-neutral-950 text-studio-accent hover:bg-neutral-900"
80
+ ? "border-danger/60 bg-danger text-on-danger animate-pulse"
81
+ : "border-studio-accent/60 bg-neutral-950 text-accent-ink hover:bg-neutral-900"
82
82
  }`}
83
83
  style={{
84
84
  left: Math.max(0, rect.left + rect.width + 8),