auricui-skill 0.0.0-stage → 0.1.2

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 (362) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -2
  3. package/bin/install.mjs +66 -0
  4. package/package.json +29 -4
  5. package/skill/SKILL.md +64 -0
  6. package/skill/agents/openai.yaml +4 -0
  7. package/skill/assets/examples/ApprovalCard.tsx +8 -0
  8. package/skill/assets/examples/BasicApp.tsx +38 -0
  9. package/skill/assets/examples/BriefingCard.tsx +8 -0
  10. package/skill/assets/examples/ChatControls.tsx +49 -0
  11. package/skill/assets/examples/ChatPanel.tsx +73 -0
  12. package/skill/assets/examples/Checkbox.tsx +13 -0
  13. package/skill/assets/examples/Combobox.tsx +14 -0
  14. package/skill/assets/examples/Drawer.tsx +10 -0
  15. package/skill/assets/examples/MemoryCard.tsx +9 -0
  16. package/skill/assets/examples/MessageList.tsx +8 -0
  17. package/skill/assets/examples/Theme.tsx +14 -0
  18. package/skill/assets/examples/ToolCall.tsx +8 -0
  19. package/skill/assets/examples/Tree.tsx +15 -0
  20. package/skill/assets/examples/examples.css +11 -0
  21. package/skill/assets/library/auricui-0.1.0-preview.1.tgz +0 -0
  22. package/skill/assets/library/auricui-0.1.0-preview.3.tgz +0 -0
  23. package/skill/assets/library/auricui-0.1.0-preview.4.tgz +0 -0
  24. package/skill/assets/upstream/LICENSE +21 -0
  25. package/skill/assets/upstream/apps/desktop/chat/src/App.tsx +1099 -0
  26. package/skill/assets/upstream/apps/desktop/chat/src/app-bridge.ts +154 -0
  27. package/skill/assets/upstream/apps/desktop/chat/src/chat.css +1812 -0
  28. package/skill/assets/upstream/apps/desktop/chat/src/components/AddSkill.tsx +223 -0
  29. package/skill/assets/upstream/apps/desktop/chat/src/components/AgentActivityRow.tsx +716 -0
  30. package/skill/assets/upstream/apps/desktop/chat/src/components/AgentIcons.tsx +46 -0
  31. package/skill/assets/upstream/apps/desktop/chat/src/components/AttachmentCard.tsx +34 -0
  32. package/skill/assets/upstream/apps/desktop/chat/src/components/AttachmentPreview.tsx +331 -0
  33. package/skill/assets/upstream/apps/desktop/chat/src/components/BalanceNotice.tsx +22 -0
  34. package/skill/assets/upstream/apps/desktop/chat/src/components/BrandLogo.tsx +35 -0
  35. package/skill/assets/upstream/apps/desktop/chat/src/components/ComposerCard.tsx +182 -0
  36. package/skill/assets/upstream/apps/desktop/chat/src/components/ConversationSessionBar.tsx +292 -0
  37. package/skill/assets/upstream/apps/desktop/chat/src/components/CopyDiagnostics.tsx +24 -0
  38. package/skill/assets/upstream/apps/desktop/chat/src/components/Divider.tsx +47 -0
  39. package/skill/assets/upstream/apps/desktop/chat/src/components/FileChangesList.tsx +119 -0
  40. package/skill/assets/upstream/apps/desktop/chat/src/components/Markdown.tsx +198 -0
  41. package/skill/assets/upstream/apps/desktop/chat/src/components/ModelPopover.tsx +280 -0
  42. package/skill/assets/upstream/apps/desktop/chat/src/components/NoticeCard.tsx +87 -0
  43. package/skill/assets/upstream/apps/desktop/chat/src/components/Popover.tsx +195 -0
  44. package/skill/assets/upstream/apps/desktop/chat/src/components/PreviewFrame.tsx +129 -0
  45. package/skill/assets/upstream/apps/desktop/chat/src/components/ProjectConversation.tsx +852 -0
  46. package/skill/assets/upstream/apps/desktop/chat/src/components/ProjectStarters.tsx +118 -0
  47. package/skill/assets/upstream/apps/desktop/chat/src/components/Tooltip.tsx +191 -0
  48. package/skill/assets/upstream/apps/desktop/chat/src/components/TurnLog.tsx +583 -0
  49. package/skill/assets/upstream/apps/desktop/chat/src/components/TurnQuestionCard.tsx +133 -0
  50. package/skill/assets/upstream/apps/desktop/chat/src/components/dock-pane-bar.ts +14 -0
  51. package/skill/assets/upstream/apps/desktop/chat/src/components/project-version.ts +37 -0
  52. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/MentionMenuPlugin.tsx +372 -0
  53. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptEditor.tsx +476 -0
  54. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptPill.tsx +399 -0
  55. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/PromptReferenceNode.tsx +164 -0
  56. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/index.ts +31 -0
  57. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/mention-menu.ts +206 -0
  58. package/skill/assets/upstream/apps/desktop/chat/src/components/prompt-editor/prompt-text.ts +171 -0
  59. package/skill/assets/upstream/apps/desktop/chat/src/components/useStickToBottom.ts +320 -0
  60. package/skill/assets/upstream/apps/desktop/chat/src/components/useTicker.ts +55 -0
  61. package/skill/assets/upstream/apps/desktop/chat/src/i18n/index.ts +42 -0
  62. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/en-US.ts +408 -0
  63. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/es.ts +407 -0
  64. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/ja.ts +407 -0
  65. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/ko.ts +403 -0
  66. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/words.ts +6 -0
  67. package/skill/assets/upstream/apps/desktop/chat/src/i18n/locales/zh-CN.ts +407 -0
  68. package/skill/assets/upstream/apps/desktop/chat/src/i18n/lookup.ts +12 -0
  69. package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-activity-runs.ts +154 -0
  70. package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-menu-rows.ts +41 -0
  71. package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-refs.ts +105 -0
  72. package/skill/assets/upstream/apps/desktop/chat/src/lib/agent-transcript.ts +440 -0
  73. package/skill/assets/upstream/apps/desktop/chat/src/lib/asset-lists.ts +75 -0
  74. package/skill/assets/upstream/apps/desktop/chat/src/lib/attachment-place.ts +17 -0
  75. package/skill/assets/upstream/apps/desktop/chat/src/lib/balance.ts +16 -0
  76. package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-layer.ts +10 -0
  77. package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-menu.ts +18 -0
  78. package/skill/assets/upstream/apps/desktop/chat/src/lib/chat-store.ts +101 -0
  79. package/skill/assets/upstream/apps/desktop/chat/src/lib/composer-attachments.ts +189 -0
  80. package/skill/assets/upstream/apps/desktop/chat/src/lib/desktop-attachments.ts +178 -0
  81. package/skill/assets/upstream/apps/desktop/chat/src/lib/desktop-bridge.ts +291 -0
  82. package/skill/assets/upstream/apps/desktop/chat/src/lib/developer.ts +39 -0
  83. package/skill/assets/upstream/apps/desktop/chat/src/lib/esc-stop.ts +23 -0
  84. package/skill/assets/upstream/apps/desktop/chat/src/lib/file-changes.ts +32 -0
  85. package/skill/assets/upstream/apps/desktop/chat/src/lib/live-activity.ts +20 -0
  86. package/skill/assets/upstream/apps/desktop/chat/src/lib/local-agent-run.ts +1174 -0
  87. package/skill/assets/upstream/apps/desktop/chat/src/lib/local-sessions.ts +25 -0
  88. package/skill/assets/upstream/apps/desktop/chat/src/lib/model-calls.ts +87 -0
  89. package/skill/assets/upstream/apps/desktop/chat/src/lib/pretty-json.ts +12 -0
  90. package/skill/assets/upstream/apps/desktop/chat/src/lib/preview-top.ts +15 -0
  91. package/skill/assets/upstream/apps/desktop/chat/src/lib/project-resources.ts +66 -0
  92. package/skill/assets/upstream/apps/desktop/chat/src/lib/project-sessions.ts +66 -0
  93. package/skill/assets/upstream/apps/desktop/chat/src/lib/project-versions.ts +116 -0
  94. package/skill/assets/upstream/apps/desktop/chat/src/lib/ref-pictures.ts +119 -0
  95. package/skill/assets/upstream/apps/desktop/chat/src/lib/relative-time.ts +46 -0
  96. package/skill/assets/upstream/apps/desktop/chat/src/lib/skill-pages.ts +29 -0
  97. package/skill/assets/upstream/apps/desktop/chat/src/lib/step-file.ts +23 -0
  98. package/skill/assets/upstream/apps/desktop/chat/src/lib/structural-share.ts +52 -0
  99. package/skill/assets/upstream/apps/desktop/chat/src/lib/studio-film.ts +44 -0
  100. package/skill/assets/upstream/apps/desktop/chat/src/lib/studio-transfer.ts +40 -0
  101. package/skill/assets/upstream/apps/desktop/chat/src/lib/unprompted.ts +27 -0
  102. package/skill/assets/upstream/apps/desktop/chat/src/lib/upload-limits.ts +53 -0
  103. package/skill/assets/upstream/apps/desktop/chat/src/mount.tsx +19 -0
  104. package/skill/assets/upstream/apps/desktop/chat/src/state/local-agent-store.ts +296 -0
  105. package/skill/assets/upstream/apps/desktop/chat/src/studio-refs.ts +307 -0
  106. package/skill/assets/upstream/apps/desktop/package.json +51 -0
  107. package/skill/assets/upstream/apps/desktop/shell/bridge.ts +415 -0
  108. package/skill/assets/upstream/apps/desktop/shell/index.html +27 -0
  109. package/skill/assets/upstream/apps/desktop/shell/main.tsx +17 -0
  110. package/skill/assets/upstream/apps/desktop/shell/shell.css +17 -0
  111. package/skill/assets/upstream/apps/desktop/shell/vite.config.ts +65 -0
  112. package/skill/assets/upstream/apps/desktop/src/agent-definitions.mjs +31 -0
  113. package/skill/assets/upstream/packages/openfilm/LICENSE +21 -0
  114. package/skill/assets/upstream/packages/openfilm/package.json +88 -0
  115. package/skill/assets/upstream/packages/openfilm/src/film-doc.d.mts +125 -0
  116. package/skill/assets/upstream/packages/openfilm/src/film-doc.mjs +986 -0
  117. package/skill/assets/upstream/packages/openfilm/src/overrides.mjs +73 -0
  118. package/skill/assets/upstream/packages/openfilm/src/timeline.mjs +1280 -0
  119. package/skill/assets/upstream/packages/openfilm/studio/server/bridge.js +209 -0
  120. package/skill/assets/upstream/packages/openfilm/studio/server/film-subtitle.mjs +785 -0
  121. package/skill/assets/upstream/packages/openfilm/studio/server/ops.mjs +403 -0
  122. package/skill/assets/upstream/packages/openfilm/studio/server/stage.js +1649 -0
  123. package/skill/assets/upstream/packages/openfilm/studio/server/track-layout.mjs +43 -0
  124. package/skill/assets/upstream/packages/openfilm/studio/ui/src/App.tsx +156 -0
  125. package/skill/assets/upstream/packages/openfilm/studio/ui/src/api.ts +328 -0
  126. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/AudioMeters.tsx +188 -0
  127. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BlockThumbs.tsx +84 -0
  128. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BlockWaveform.tsx +201 -0
  129. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BrandLoadingScreen.tsx +56 -0
  130. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/BrandLogo.tsx +25 -0
  131. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Card.tsx +89 -0
  132. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ColorPicker.tsx +204 -0
  133. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ConfirmDialog.tsx +96 -0
  134. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ContextMenu.tsx +422 -0
  135. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/DesktopDialog.tsx +271 -0
  136. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ExamplesGallery.tsx +372 -0
  137. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmFullscreenBar.tsx +147 -0
  138. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmInspector.tsx +2310 -0
  139. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FilmStageSelect.tsx +3296 -0
  140. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FocusNav.tsx +28 -0
  141. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FontFamilyPicker.tsx +293 -0
  142. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/FramePicker.tsx +69 -0
  143. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/InspectorPanel.tsx +25 -0
  144. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Launcher.tsx +38 -0
  145. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/LayersPanel.tsx +277 -0
  146. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/LiveTime.tsx +101 -0
  147. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MediaPool.tsx +1413 -0
  148. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MediaPreview.tsx +188 -0
  149. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MenuSelect.tsx +161 -0
  150. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/MgOutline.tsx +24 -0
  151. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/OperationHistory.tsx +184 -0
  152. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PaneDivider.tsx +292 -0
  153. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PasteAttrsDialog.tsx +69 -0
  154. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PlaybackMenu.tsx +110 -0
  155. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Popover.tsx +159 -0
  156. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectCover.tsx +60 -0
  157. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectHistoryControls.tsx +748 -0
  158. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectLayoutMenu.tsx +82 -0
  159. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectMenu.tsx +111 -0
  160. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectSources.tsx +587 -0
  161. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectView.tsx +3222 -0
  162. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectsLibrary.tsx +567 -0
  163. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ProjectsView.tsx +1593 -0
  164. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/PropertiesSection.tsx +418 -0
  165. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ResourceInspector.tsx +100 -0
  166. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SelectionToolbar.tsx +157 -0
  167. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SkillsLibrary.tsx +504 -0
  168. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SourcePreview.tsx +464 -0
  169. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SpendAsks.tsx +88 -0
  170. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/StageMaskOverlay.tsx +299 -0
  171. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/StageTransformFrame.tsx +246 -0
  172. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleInspector.module.css +46 -0
  173. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleInspector.tsx +884 -0
  174. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleOverlay.tsx +148 -0
  175. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/SubtitleRow.tsx +419 -0
  176. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/TabStrip.tsx +188 -0
  177. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Timeline.tsx +4911 -0
  178. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/TimelineMarkers.tsx +329 -0
  179. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Toast.tsx +55 -0
  180. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/Tooltip.tsx +193 -0
  181. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/UpdateButton.tsx +104 -0
  182. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/ViewerBar.tsx +98 -0
  183. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/WhatsNew.tsx +147 -0
  184. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/dock-pane-bar.ts +16 -0
  185. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/export/ExportCenter.tsx +1502 -0
  186. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/export/ExportTray.tsx +284 -0
  187. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/film-stage-hit.ts +18 -0
  188. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/inspector-fields.tsx +863 -0
  189. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/inspector-look.tsx +1357 -0
  190. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/player-controls/controls-bar.tsx +397 -0
  191. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/player-controls/transport.tsx +294 -0
  192. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/replace-drop.ts +20 -0
  193. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/AgentsSection.tsx +613 -0
  194. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/CustomServices.tsx +104 -0
  195. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/DiagnosticsGroup.tsx +202 -0
  196. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ModelsSection.tsx +179 -0
  197. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ProviderMark.tsx +24 -0
  198. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ProvidersSection.tsx +177 -0
  199. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/SettingsModal.tsx +511 -0
  200. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/SettingsSelect.tsx +84 -0
  201. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ShortcutsSection.tsx +291 -0
  202. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/StorageGroup.tsx +119 -0
  203. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/theme.ts +47 -0
  204. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/ui.tsx +139 -0
  205. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/settings/use-settings.ts +10 -0
  206. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/stage-menu.tsx +283 -0
  207. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/timeline-head-extras.tsx +91 -0
  208. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/typing-target.ts +45 -0
  209. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-editor-tools.tsx +408 -0
  210. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-page-changes.ts +48 -0
  211. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/use-project-history.ts +113 -0
  212. package/skill/assets/upstream/packages/openfilm/studio/ui/src/components/useBlockDrag.ts +639 -0
  213. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/Preview.tsx +426 -0
  214. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/player.ts +558 -0
  215. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/preview-frames.ts +36 -0
  216. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/steps.ts +36 -0
  217. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/undo.ts +156 -0
  218. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/use-film.ts +212 -0
  219. package/skill/assets/upstream/packages/openfilm/studio/ui/src/editor/use-preview-renders.ts +89 -0
  220. package/skill/assets/upstream/packages/openfilm/studio/ui/src/globals.css +559 -0
  221. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/en.ts +1901 -0
  222. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/es.ts +1900 -0
  223. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/index.ts +130 -0
  224. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/ja.ts +1900 -0
  225. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/ko.ts +1896 -0
  226. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/words.ts +6 -0
  227. package/skill/assets/upstream/packages/openfilm/studio/ui/src/i18n/zh-CN.ts +1901 -0
  228. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/agent-row.ts +21 -0
  229. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/asset-timeline.ts +469 -0
  230. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/audio-levels.ts +60 -0
  231. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/batch-map.ts +28 -0
  232. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/browser.ts +10 -0
  233. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/canvas-fit.ts +116 -0
  234. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/chat-marks.ts +114 -0
  235. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-attrs.ts +102 -0
  236. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-fade.ts +70 -0
  237. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-look.ts +520 -0
  238. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-mask.ts +536 -0
  239. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/clip-replace.ts +60 -0
  240. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/component-props.ts +128 -0
  241. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/context-length.ts +12 -0
  242. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/desktop-build.ts +44 -0
  243. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/diagnostics.ts +68 -0
  244. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/double-press.ts +22 -0
  245. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/dropped-overrides.ts +139 -0
  246. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/export-jobs.ts +191 -0
  247. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/export-spec.ts +261 -0
  248. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/field-input.ts +155 -0
  249. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-clip-edits.ts +82 -0
  250. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-frame.ts +134 -0
  251. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-overrides.ts +107 -0
  252. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film-shape.ts +172 -0
  253. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/film.ts +80 -0
  254. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/font-menu.ts +236 -0
  255. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/font-styles.ts +129 -0
  256. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/freeze-frame.ts +49 -0
  257. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/host.ts +438 -0
  258. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/inspect-scrub.ts +26 -0
  259. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/keymap-store.ts +74 -0
  260. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/keymap.ts +529 -0
  261. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/layers-tree.ts +133 -0
  262. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/look.ts +83 -0
  263. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/loudness.ts +58 -0
  264. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/markers.ts +152 -0
  265. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-bar.ts +58 -0
  266. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-folders.ts +45 -0
  267. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-place.ts +102 -0
  268. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-queue.ts +107 -0
  269. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/media-selection.ts +64 -0
  270. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/menu-position.ts +77 -0
  271. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/mg-transform.ts +111 -0
  272. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/mixed.ts +3 -0
  273. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/page-changes.ts +45 -0
  274. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/picture-files.ts +19 -0
  275. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/preview-prefs.ts +46 -0
  276. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/preview-renders.ts +71 -0
  277. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-delete.ts +20 -0
  278. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-folders.ts +144 -0
  279. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-history.ts +188 -0
  280. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/project-sources.ts +283 -0
  281. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/projects-page.ts +40 -0
  282. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/provider-marks.ts +35 -0
  283. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/providers.ts +163 -0
  284. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/resource-drag.ts +131 -0
  285. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/retry-image.ts +17 -0
  286. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/saved-insert.ts +40 -0
  287. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/saved.ts +125 -0
  288. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/selection-toolbar.ts +158 -0
  289. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/shortcut-hint.ts +21 -0
  290. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/similar-layers.ts +46 -0
  291. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/solo.ts +19 -0
  292. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-clip.ts +60 -0
  293. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-crop.ts +232 -0
  294. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-gesture.ts +277 -0
  295. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-layers.ts +71 -0
  296. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-mask.ts +176 -0
  297. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-measure.ts +53 -0
  298. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/stage-types.ts +155 -0
  299. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/studio-refs.ts +248 -0
  300. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/studio-version.ts +41 -0
  301. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/subtitle-cues.ts +238 -0
  302. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/subtitles.ts +263 -0
  303. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/tab-menu.ts +6 -0
  304. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/template-controls.ts +131 -0
  305. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/theme-changes.ts +31 -0
  306. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-cache.ts +63 -0
  307. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-cell.ts +8 -0
  308. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-frame.ts +76 -0
  309. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/thumb-pick.ts +42 -0
  310. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timecode.ts +161 -0
  311. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-clipboard.ts +279 -0
  312. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-drag.ts +589 -0
  313. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-edit.ts +1138 -0
  314. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-keys.ts +63 -0
  315. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-layout.ts +1038 -0
  316. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-nav.ts +160 -0
  317. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-prefs.ts +73 -0
  318. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-split.ts +40 -0
  319. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/timeline-view.ts +105 -0
  320. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/title.ts +39 -0
  321. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/track-heights.ts +55 -0
  322. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/track-lock.ts +27 -0
  323. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/transitions.ts +459 -0
  324. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/type-metrics.ts +83 -0
  325. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-film-subtitles.ts +111 -0
  326. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-fonts.ts +181 -0
  327. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-loaded-thumbs.ts +91 -0
  328. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-edits.ts +134 -0
  329. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-font.ts +51 -0
  330. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/use-subtitle-style.ts +68 -0
  331. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/waveform-columns.ts +48 -0
  332. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/web-update.ts +41 -0
  333. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/whats-new.ts +401 -0
  334. package/skill/assets/upstream/packages/openfilm/studio/ui/src/lib/workspace-resources.ts +373 -0
  335. package/skill/assets/upstream/packages/openfilm/studio/ui/src/main.tsx +10 -0
  336. package/skill/assets/upstream/packages/openfilm/studio/ui/vite.config.ts +12 -0
  337. package/skill/assets/upstream/packages/shared/package.json +45 -0
  338. package/skill/assets/upstream/packages/shared/src/agent-activity.ts +263 -0
  339. package/skill/assets/upstream/packages/shared/src/contract.ts +285 -0
  340. package/skill/assets/upstream/packages/shared/src/index.ts +17 -0
  341. package/skill/assets/upstream/packages/shared/src/markdown-blocks.ts +252 -0
  342. package/skill/assets/upstream/packages/shared/src/modality.ts +4 -0
  343. package/skill/assets/upstream/packages/shared/src/prompt-reference.ts +113 -0
  344. package/skill/assets/upstream/packages/shared/src/title.ts +76 -0
  345. package/skill/assets/upstream/packages/shared/src/turn.ts +68 -0
  346. package/skill/assets/upstream/packages/shared/src/video.ts +26 -0
  347. package/skill/references/agent-components.md +33 -0
  348. package/skill/references/api-index.json +2693 -0
  349. package/skill/references/chat-components.md +65 -0
  350. package/skill/references/components.md +23 -0
  351. package/skill/references/core-components.md +44 -0
  352. package/skill/references/dependencies.md +22 -0
  353. package/skill/references/esm-package.md +48 -0
  354. package/skill/references/integration.md +43 -0
  355. package/skill/references/library-manifest.json +12 -0
  356. package/skill/references/source-manifest.json +325 -0
  357. package/skill/references/studio-components.md +24 -0
  358. package/skill/references/theme-motion.md +49 -0
  359. package/skill/references/troubleshooting.md +27 -0
  360. package/skill/references/verification.md +13 -0
  361. package/skill/scripts/check_dependencies.py +109 -0
  362. package/skill/scripts/inspect_api.py +60 -0
@@ -0,0 +1,884 @@
1
+ import React from 'react';
2
+ import { Loader2, MessageSquarePlus } from 'lucide-react';
3
+ import { useT } from '@/i18n';
4
+ import { STUDIO_REF_TYPE, type StudioRef } from '@/lib/host';
5
+ import {
6
+ FILM_SUBTITLE_KARAOKE_DEFAULT, FILM_SUBTITLE_LANGUAGES, joinColor, matchSubtitleLook, sameFilmSubtitleLanguage, splitColor,
7
+ SUBTITLE_DEFAULT, SUBTITLE_LOOKS, SUBTITLE_LOOK_IDS, SUBTITLE_SIZE_PCT_MAX, SUBTITLE_SIZE_PCT_MIN, subtitleCss,
8
+ type SubtitleCue, type SubtitleLookId, type SubtitleStyle, type SubtitleTranslateResult,
9
+ } from '@/lib/subtitles';
10
+ import { SUBTITLE_FONT_SYSTEM, useSubtitleFonts, type SubtitleFontOption } from '@/lib/use-subtitle-font';
11
+ import { Tooltip } from './Tooltip';
12
+ import { MenuSelect } from './MenuSelect';
13
+ import { InspectorPanel } from './InspectorPanel';
14
+ import { SubtitleRowToggle } from './SubtitleRow';
15
+ import styles from './SubtitleInspector.module.css';
16
+
17
+ /**
18
+ * A line of the panel as the person points at it: the line, its number in the list (from 1), the language it is shown
19
+ * in (null: the voices' own) and its words as shown, and the words selected in them (character offsets; null: none).
20
+ */
21
+ export interface SubtitleLineAt {
22
+ cue: SubtitleCue;
23
+ index: number;
24
+ language: string | null;
25
+ text: string;
26
+ pick: { from: number; to: number } | null;
27
+ }
28
+
29
+ /** A line's key in the list, the same across a refresh of the lines (a correction keeps it). */
30
+ export const subtitleLineKey = (cue: SubtitleCue) => `${cue.src ?? ''}:${cue.line ?? cue.startMs}:${cue.startMs}`;
31
+
32
+ /**
33
+ * With an app that has a chat (lib/host.ts `refer`): each line's "Reference in chat" (a button shown on hover), ⌘L in
34
+ * a line, and a line dragged out by its time or that button, or words selected in it dragged out, all reference it
35
+ * (`of` makes the reference, `send` gives it to the app); `pick` tells of the line being worked on (focused, or words
36
+ * selected in it), which comes along with the selection.
37
+ */
38
+ export interface SubtitleLineRefer {
39
+ of: (line: SubtitleLineAt) => StudioRef;
40
+ send: (ref: StudioRef) => void;
41
+ pick: (line: SubtitleLineAt) => void;
42
+ shortcut: string;
43
+ }
44
+
45
+ /** The Inspector's subtitle style panel: changes show at once on the picture and are kept with the project. */
46
+ export function SubtitleInspector({ subtitles, width, onClose }: {
47
+ subtitles: {
48
+ style: SubtitleStyle;
49
+ onStyle: (next: Partial<SubtitleStyle>) => void;
50
+ /** Translate the film's voices into this language (useFilmSubtitles().translate). Without it, no translating. */
51
+ translate?: (language: string) => Promise<SubtitleTranslateResult>;
52
+ /** The voices' language, as far as the script tells; null = can't tell. */
53
+ sourceLanguage?: string | null;
54
+ /** The film's lines as transcribed (with their translations), to correct; `onEditLine` writes one back (with a
55
+ `language`, its translation into it). */
56
+ lines?: readonly SubtitleCue[];
57
+ onEditLine?: (cue: SubtitleCue, text: string, language?: string) => void;
58
+ /** The playhead (film ms), which the lines follow, and moving it to a line. */
59
+ timeMs?: number;
60
+ onSeek?: (ms: number) => void;
61
+ /** Speech with no transcript yet, and making it (the timeline's CC button does the same with no lines at all). */
62
+ untranscribed?: number;
63
+ onMake?: () => void;
64
+ making?: boolean;
65
+ refer?: SubtitleLineRefer;
66
+ };
67
+ width: number;
68
+ onClose: () => void;
69
+ }) {
70
+ const t = useT();
71
+ const fonts = useSubtitleFonts();
72
+ const s = subtitles.style;
73
+ const set = subtitles.onStyle;
74
+ /** One field of the font: it is a nested object, and `onStyle({ font: {...} })` alone would drop the rest. */
75
+ const setFont = (next: Partial<SubtitleStyle['font']>) => set({ font: { ...s.font, ...next } });
76
+ const aligns = [
77
+ { id: 'left' as const, label: t('timeline.subtitleAlignLeft') },
78
+ { id: 'center' as const, label: t('timeline.subtitleAlignCenter') },
79
+ { id: 'right' as const, label: t('timeline.subtitleAlignRight') },
80
+ ];
81
+
82
+ return (
83
+ <InspectorPanel title={t('timeline.subtitleStyle')} width={width} onClose={onClose}>
84
+ <div data-subtitle-inspector className="py-2">
85
+ {/* the looks are shown as samples, not names: "heavy outline" and "bold" sound alike and look nothing alike.
86
+ Picked = the style is exactly that look (worked out, not stored: change a color and it is no longer it) */}
87
+ <div className="flex gap-1.5 pb-1.5 pt-0.5">
88
+ {SUBTITLE_LOOK_IDS.map((id) => (
89
+ <LookSwatch
90
+ key={id}
91
+ look={id}
92
+ active={matchSubtitleLook(s) === id}
93
+ label={t(`timeline.subtitleLook.${id}`)}
94
+ onPick={() => set(SUBTITLE_LOOKS[id])}
95
+ />
96
+ ))}
97
+ </div>
98
+
99
+ <SubtitleLanguage style={s} set={set} translate={subtitles.translate} sourceLanguage={subtitles.sourceLanguage} />
100
+
101
+ {subtitles.lines ? (
102
+ <SubtitleLines lines={subtitles.lines} onEdit={subtitles.onEditLine} untranscribed={subtitles.untranscribed ?? 0}
103
+ onMake={subtitles.onMake} making={!!subtitles.making} timeMs={subtitles.timeMs ?? 0} onSeek={subtitles.onSeek} refer={subtitles.refer}
104
+ language={s.language && !sameFilmSubtitleLanguage(s.language, subtitles.sourceLanguage ?? null) ? s.language : null} />
105
+ ) : null}
106
+
107
+ {/* word highlight first: the most visible effect here, and the one short videos ask for most */}
108
+ <StyleGroup label={t('timeline.subtitleEffectGroup')}>
109
+ <StyleLayer
110
+ label={t('timeline.subtitleKaraoke')}
111
+ on={!!s.karaoke}
112
+ onToggle={() => set({ karaoke: s.karaoke ? null : FILM_SUBTITLE_KARAOKE_DEFAULT })}
113
+ >
114
+ {s.karaoke ? (
115
+ <>
116
+ <Segmented
117
+ label={t('timeline.subtitleKaraokeMode')}
118
+ items={[
119
+ { id: 'box' as const, label: t('timeline.subtitleKaraokeBox') },
120
+ { id: 'color' as const, label: t('timeline.subtitleKaraokeColor') },
121
+ { id: 'pop' as const, label: t('timeline.subtitleKaraokePop') },
122
+ ]}
123
+ value={s.karaoke.mode}
124
+ onPick={(mode) => set({ karaoke: { ...s.karaoke!, mode } })}
125
+ />
126
+ <StyleColor
127
+ label={t('timeline.subtitleKaraokeTint')}
128
+ value={s.karaoke.color}
129
+ onChange={(color) => set({ karaoke: { ...s.karaoke!, color } })}
130
+ />
131
+ </>
132
+ ) : null}
133
+ </StyleLayer>
134
+ <StyleSlider
135
+ label={t('timeline.subtitleFade')}
136
+ value={s.fadeMs}
137
+ min={0}
138
+ max={1000}
139
+ step={50}
140
+ format={(v) => `${Math.round(v)} ms`}
141
+ onChange={(fadeMs) => set({ fadeMs })}
142
+ />
143
+ </StyleGroup>
144
+
145
+ <StyleGroup label={t('timeline.subtitleFontGroup')}>
146
+ <StyleFamily
147
+ label={t('timeline.subtitleFamily')}
148
+ value={s.font.family}
149
+ fonts={fonts}
150
+ onPick={(family) => setFont({ family })}
151
+ />
152
+ <StyleSlider
153
+ label={t('timeline.subtitleSize')}
154
+ value={s.font.sizePct}
155
+ min={SUBTITLE_SIZE_PCT_MIN}
156
+ max={SUBTITLE_SIZE_PCT_MAX}
157
+ step={0.1}
158
+ format={(v) => `${v.toFixed(1)}%`}
159
+ onChange={(sizePct) => setFont({ sizePct })}
160
+ />
161
+ <StyleSlider
162
+ label={t('timeline.subtitleWeight')}
163
+ value={s.font.weight}
164
+ min={100}
165
+ max={900}
166
+ step={100}
167
+ format={(v) => String(v)}
168
+ onChange={(weight) => setFont({ weight })}
169
+ />
170
+ <StyleSlider
171
+ label={t('timeline.subtitleTracking')}
172
+ value={s.font.letterSpacingEm}
173
+ min={-0.05}
174
+ max={0.3}
175
+ step={0.005}
176
+ format={(v) => v.toFixed(3)}
177
+ onChange={(letterSpacingEm) => setFont({ letterSpacingEm })}
178
+ />
179
+ <StyleSlider
180
+ label={t('timeline.subtitleLeading')}
181
+ value={s.font.lineHeight}
182
+ min={0.9}
183
+ max={2.2}
184
+ step={0.05}
185
+ format={(v) => v.toFixed(2)}
186
+ onChange={(lineHeight) => setFont({ lineHeight })}
187
+ />
188
+ <div className="flex gap-1 px-1 pb-1 pt-0.5">
189
+ <StyleToggle
190
+ label={t('timeline.subtitleItalic')}
191
+ on={s.font.italic}
192
+ onPick={() => setFont({ italic: !s.font.italic })}
193
+ />
194
+ <StyleToggle
195
+ label={t('timeline.subtitleUpper')}
196
+ on={s.font.upper}
197
+ onPick={() => setFont({ upper: !s.font.upper })}
198
+ />
199
+ </div>
200
+ </StyleGroup>
201
+
202
+ <StyleGroup label={t('timeline.subtitleInkGroup')}>
203
+ <StyleColor
204
+ label={t('timeline.subtitleFill')}
205
+ value={s.fill}
206
+ onChange={(fill) => set({ fill })}
207
+ />
208
+ {/* stroke, shadow and box switch off as a whole: off is "this film has no stroke", not a width of 0 */}
209
+ <StyleLayer
210
+ label={t('timeline.subtitleStroke')}
211
+ on={!!s.stroke}
212
+ onToggle={() => set({ stroke: s.stroke ? null : SUBTITLE_DEFAULT.stroke })}
213
+ >
214
+ {s.stroke ? (
215
+ <>
216
+ <StyleSlider
217
+ label={t('timeline.subtitleStroke')}
218
+ value={s.stroke.widthEm}
219
+ min={0.005}
220
+ max={0.2}
221
+ step={0.005}
222
+ format={(v) => v.toFixed(3)}
223
+ onChange={(widthEm) => set({ stroke: { ...s.stroke!, widthEm } })}
224
+ />
225
+ <StyleColor
226
+ label={t('timeline.subtitleFill')}
227
+ value={s.stroke.color}
228
+ onChange={(color) => set({ stroke: { ...s.stroke!, color } })}
229
+ />
230
+ </>
231
+ ) : null}
232
+ </StyleLayer>
233
+ <StyleLayer
234
+ label={t('timeline.subtitleShadow')}
235
+ on={!!s.shadow}
236
+ onToggle={() => set({ shadow: s.shadow ? null : SUBTITLE_DEFAULT.shadow })}
237
+ >
238
+ {s.shadow ? (
239
+ <>
240
+ <StyleSlider
241
+ label={t('timeline.subtitleOffsetY')}
242
+ value={s.shadow.dyEm}
243
+ min={-0.2}
244
+ max={0.3}
245
+ step={0.005}
246
+ format={(v) => v.toFixed(3)}
247
+ onChange={(dyEm) => set({ shadow: { ...s.shadow!, dyEm } })}
248
+ />
249
+ <StyleSlider
250
+ label={t('timeline.subtitleBlur')}
251
+ value={s.shadow.blurEm}
252
+ min={0}
253
+ max={0.4}
254
+ step={0.005}
255
+ format={(v) => v.toFixed(3)}
256
+ onChange={(blurEm) => set({ shadow: { ...s.shadow!, blurEm } })}
257
+ />
258
+ <StyleColor
259
+ label={t('timeline.subtitleFill')}
260
+ value={s.shadow.color}
261
+ onChange={(color) => set({ shadow: { ...s.shadow!, color } })}
262
+ />
263
+ </>
264
+ ) : null}
265
+ </StyleLayer>
266
+ <StyleLayer
267
+ label={t('timeline.subtitleBox')}
268
+ on={!!s.box}
269
+ onToggle={() => set({ box: s.box ? null : SUBTITLE_LOOKS.bar.box })}
270
+ >
271
+ {s.box ? (
272
+ <>
273
+ <StyleColor
274
+ label={t('timeline.subtitleFill')}
275
+ value={s.box.color}
276
+ withAlpha={false}
277
+ onChange={(color) => set({ box: { ...s.box!, color } })}
278
+ />
279
+ <StyleSlider
280
+ label={t('timeline.subtitleOpacity')}
281
+ value={s.box.opacity}
282
+ min={0}
283
+ max={1}
284
+ step={0.02}
285
+ format={(v) => v.toFixed(2)}
286
+ onChange={(opacity) => set({ box: { ...s.box!, opacity } })}
287
+ />
288
+ <StyleSlider
289
+ label={t('timeline.subtitleRadius')}
290
+ value={s.box.radiusEm}
291
+ min={0}
292
+ max={0.8}
293
+ step={0.02}
294
+ format={(v) => v.toFixed(2)}
295
+ onChange={(radiusEm) => set({ box: { ...s.box!, radiusEm } })}
296
+ />
297
+ </>
298
+ ) : null}
299
+ </StyleLayer>
300
+ </StyleGroup>
301
+
302
+ <StyleGroup label={t('timeline.subtitleLayoutGroup')}>
303
+ <Segmented
304
+ label={t('timeline.subtitleAlign')}
305
+ items={aligns}
306
+ value={s.align}
307
+ onPick={(align) => set({ align })}
308
+ />
309
+ <StyleSlider
310
+ label={t('timeline.subtitleWidth')}
311
+ value={s.maxWidthPct}
312
+ min={30}
313
+ max={100}
314
+ step={1}
315
+ format={(v) => `${Math.round(v)}%`}
316
+ onChange={(maxWidthPct) => set({ maxWidthPct })}
317
+ />
318
+ </StyleGroup>
319
+
320
+ <button
321
+ type="button"
322
+ onClick={() => set(SUBTITLE_DEFAULT)}
323
+ onMouseDown={(e) => e.preventDefault()}
324
+ className="mt-1 w-full rounded-[6px] px-1 py-1 text-left text-[11px] transition hover:bg-black/5 dark:hover:bg-white/10"
325
+ style={{ color: 'var(--text-muted)' }}
326
+ >
327
+ {t('timeline.subtitleReset')}
328
+ </button>
329
+
330
+ {/* the position is not here: it is dragged on the picture, which nobody would guess without being told */}
331
+ <p className="px-1 pt-1 text-[10px] leading-snug" style={{ color: 'var(--text-muted)' }}>
332
+ {t('timeline.subtitleDrag')}
333
+ <br />
334
+ {t('timeline.subtitleSaved')}
335
+ </p>
336
+ </div>
337
+ </InspectorPanel>
338
+ );
339
+ }
340
+
341
+ /* ── the panel's parts (only this panel uses them) ───────────────────────── */
342
+
343
+ /**
344
+ * The subtitle language: one menu, "Original" first (named when the script tells which), then the languages it can
345
+ * be translated into (never the voices' own). Picking one has the voices translated (seconds to a minute or two); until
346
+ * then the original shows. Once picked, "Show: translation / original + translation".
347
+ *
348
+ * One status line: translating (a spinner), or could not (a plain sentence and Retry; details on hover). Done says
349
+ * nothing: the subtitles changing on the picture is the answer.
350
+ */
351
+ /** How long after the person scrolls the lines themselves they stop following the playhead. */
352
+ const FOLLOW_PAUSE_MS = 2500;
353
+
354
+ const lineTime = (ms: number) => `${Math.floor(ms / 60000)}:${String(Math.floor(ms / 1000) % 60).padStart(2, '0')}`;
355
+
356
+ /**
357
+ * The film's lines, each corrected in place (Enter or leaving the field keeps it; emptied, the line goes). The line
358
+ * the playhead is in is lit, and the list keeps it in view as the film plays, except while the person types in it or
359
+ * has just scrolled it themselves; a line's time moves the playhead there.
360
+ *
361
+ * With a translation shown (`language`), each line is that language's, to correct, with the original faint above it
362
+ * for reference; a line not translated yet is empty and can be written.
363
+ */
364
+ function SubtitleLines({ lines, onEdit, untranscribed, onMake, making, timeMs, onSeek, language, refer }: {
365
+ lines: readonly SubtitleCue[];
366
+ onEdit?: (cue: SubtitleCue, text: string, language?: string) => void;
367
+ untranscribed: number;
368
+ onMake?: () => void;
369
+ making: boolean;
370
+ timeMs: number;
371
+ onSeek?: (ms: number) => void;
372
+ language: string | null;
373
+ refer?: SubtitleLineRefer;
374
+ }) {
375
+ const t = useT();
376
+ const list = React.useRef<HTMLUListElement>(null);
377
+ const touched = React.useRef(0);
378
+ /* the rows are drawn once per change of the lines, not per frame: what changes as the film plays is which is lit */
379
+ const calls = React.useRef({ onEdit, onSeek, refer });
380
+ calls.current = { onEdit, onSeek, refer };
381
+ const edit = React.useCallback((cue: SubtitleCue, text: string, lang: string | null) => calls.current.onEdit?.(cue, text, lang ?? undefined), []);
382
+ const go = React.useCallback((ms: number) => calls.current.onSeek?.(ms), []);
383
+ const lineRefer = React.useMemo<LineRefer | null>(() => (refer ? {
384
+ of: (line) => calls.current.refer!.of(line),
385
+ send: (line) => { const r = calls.current.refer; if (r) r.send(r.of(line)); },
386
+ pick: (line) => calls.current.refer?.pick(line),
387
+ } : null), [!!refer]); // eslint-disable-line react-hooks/exhaustive-deps
388
+ const active = lines.findIndex((cue) => timeMs >= cue.startMs && timeMs < cue.startMs + cue.durMs);
389
+ /* between lines, the list follows to the next one */
390
+ const follow = active >= 0 ? active : lines.findIndex((cue) => cue.startMs > timeMs);
391
+
392
+ React.useEffect(() => {
393
+ const ul = list.current;
394
+ const row = follow >= 0 ? ul?.children[follow] as HTMLElement | undefined : undefined;
395
+ if (!ul || !row) return;
396
+ if (ul.contains(document.activeElement) || performance.now() - touched.current < FOLLOW_PAUSE_MS) return;
397
+ /* the list alone scrolls (scrollIntoView would move the whole panel too); it is the rows' offset parent */
398
+ const top = row.offsetTop;
399
+ if (top < ul.scrollTop || top + row.offsetHeight > ul.scrollTop + ul.clientHeight) {
400
+ ul.scrollTo({ top: Math.max(0, top - ul.clientHeight / 3), behavior: 'smooth' });
401
+ }
402
+ }, [follow]);
403
+
404
+ const nameOf = (code: string) => FILM_SUBTITLE_LANGUAGES.find((item) => item.code === code)?.native ?? code;
405
+ const ask = {
406
+ go: t('timeline.subtitleLineGo'),
407
+ at: language ? t('timeline.subtitleLineInLanguage').replace('{lang}', nameOf(language)) : t('timeline.subtitleLineAt'),
408
+ untranslated: t('timeline.subtitleLineUntranslated'),
409
+ refer: t('host.refer'),
410
+ shortcut: refer?.shortcut ?? '',
411
+ };
412
+ const mark = () => { touched.current = performance.now(); };
413
+ return (
414
+ <StyleGroup label={t('timeline.subtitleLines')} aside={<SubtitleRowToggle />}>
415
+ {untranscribed > 0 && onMake ? (
416
+ <button type="button" onClick={onMake} disabled={making}
417
+ className="mb-1.5 flex h-7 w-full items-center justify-center gap-1.5 rounded-md border text-[11px] disabled:opacity-60"
418
+ style={{ background: 'var(--fill-tsp)', borderColor: 'var(--border)', color: 'var(--text)' }}>
419
+ {making ? <><Loader2 size={12} className="animate-spin" aria-hidden />{t('timeline.subtitlesMaking')}</>
420
+ : t(untranscribed === 1 ? 'timeline.subtitleLinesMakeOne' : 'timeline.subtitleLinesMake').replace('{n}', String(untranscribed))}
421
+ </button>
422
+ ) : null}
423
+ {lines.length ? (
424
+ <ul ref={list} onWheel={mark} onPointerDown={mark} onTouchMove={mark}
425
+ className="relative flex max-h-[260px] flex-col gap-0.5 overflow-y-auto pb-1">
426
+ {lines.map((cue, i) => (
427
+ <SubtitleLine key={subtitleLineKey(cue)} cue={cue} index={i + 1} lit={i === active}
428
+ language={language} editable={!!onEdit && cue.src !== undefined} seekable={!!onSeek} onEdit={edit} onGo={go} ask={ask}
429
+ refer={lineRefer} />
430
+ ))}
431
+ </ul>
432
+ ) : null}
433
+ </StyleGroup>
434
+ );
435
+ }
436
+
437
+ /** A line's references, made by the panel's owner (SubtitleLineRefer) from what the row knows. */
438
+ type LineRefer = { of: (line: SubtitleLineAt) => StudioRef; send: (line: SubtitleLineAt) => void; pick: (line: SubtitleLineAt) => void };
439
+
440
+ const SubtitleLine = React.memo(function SubtitleLine({ cue, index, lit, language, editable, seekable, onEdit, onGo, ask, refer }: {
441
+ cue: SubtitleCue;
442
+ index: number;
443
+ lit: boolean;
444
+ language: string | null;
445
+ editable: boolean;
446
+ seekable: boolean;
447
+ onEdit: (cue: SubtitleCue, text: string, language: string | null) => void;
448
+ onGo: (ms: number) => void;
449
+ ask: { go: string; at: string; untranslated: string; refer: string; shortcut: string };
450
+ refer: LineRefer | null;
451
+ }) {
452
+ const time = lineTime(cue.startMs);
453
+ const text = language ? cue.alt?.[language] ?? '' : cue.text;
454
+ const field = React.useRef<HTMLInputElement>(null);
455
+ /* the line as the field has it now: its words as typed (the reference says what the person sees), and the words
456
+ selected in it while it has the focus (a field keeps its last selection after it loses it, unseen) */
457
+ const lineAt = (withPick = true): SubtitleLineAt => {
458
+ const input = field.current;
459
+ const focused = !!input && input === document.activeElement;
460
+ const from = input?.selectionStart ?? 0;
461
+ const to = input?.selectionEnd ?? 0;
462
+ return { cue, index, language, text: input?.value ?? text, pick: withPick && focused && to > from ? { from, to } : null };
463
+ };
464
+ const drag = (e: React.DragEvent, withPick: boolean) => {
465
+ if (!refer) return;
466
+ const ref = refer.of(lineAt(withPick));
467
+ e.dataTransfer.setData(STUDIO_REF_TYPE, JSON.stringify([ref]));
468
+ if (!withPick) e.dataTransfer.setData('text/plain', ref.kind === 'subtitle' ? ref.text : text);
469
+ e.dataTransfer.effectAllowed = 'copy';
470
+ };
471
+ return (
472
+ <li className="group flex items-start gap-1.5 rounded-md py-0.5 pr-0.5"
473
+ style={lit ? { background: 'var(--bg-active)', boxShadow: 'inset 2px 0 0 var(--accent)' } : undefined}
474
+ aria-current={lit ? 'true' : undefined}>
475
+ <button type="button" disabled={!seekable} onClick={() => onGo(cue.startMs)}
476
+ draggable={!!refer} onDragStart={(e) => drag(e, false)}
477
+ aria-label={ask.go.replace('{time}', time)}
478
+ className="w-9 shrink-0 rounded pt-1.5 text-right text-[10.5px] tabular-nums hover:underline disabled:hover:no-underline"
479
+ style={{ color: lit ? 'var(--accent)' : 'var(--text-faint)', fontWeight: lit ? 600 : undefined }}>
480
+ {time}
481
+ </button>
482
+ <div className="flex min-w-0 flex-1 flex-col gap-0.5">
483
+ {language ? (
484
+ <span className="truncate px-2 pt-1 text-[10.5px]" title={cue.text} style={{ color: 'var(--text-faint)' }}>{cue.text}</span>
485
+ ) : null}
486
+ <input
487
+ ref={field}
488
+ defaultValue={text}
489
+ key={`${language ?? ''}:${text}`}
490
+ placeholder={language ? ask.untranslated : undefined}
491
+ aria-label={ask.at.replace('{time}', time)}
492
+ disabled={!editable}
493
+ onFocus={() => { if (seekable && !lit) onGo(cue.startMs); refer?.pick(lineAt(false)); }}
494
+ /* words selected in the line: they are what ⌘L, a drag and the selection carry (with their own times) */
495
+ onSelect={() => refer?.pick(lineAt())}
496
+ onChange={() => refer?.pick(lineAt())}
497
+ onMouseUp={() => refer?.pick(lineAt())}
498
+ onKeyUp={(e) => { if (e.shiftKey || e.key.startsWith('Arrow')) refer?.pick(lineAt()); }}
499
+ onDragStart={(e) => drag(e, true)}
500
+ onKeyDown={(e) => {
501
+ if (refer && (e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'l') {
502
+ e.preventDefault();
503
+ refer.send(lineAt());
504
+ return;
505
+ }
506
+ if (e.key === 'Enter') e.currentTarget.blur();
507
+ if (e.key === 'Escape') { e.currentTarget.value = text; e.currentTarget.blur(); }
508
+ }}
509
+ onBlur={(e) => { const next = e.currentTarget.value; if (next.trim() !== text) onEdit(cue, next, language); }}
510
+ className="h-7 min-w-0 rounded-md border px-2 text-[11.5px]"
511
+ style={{ background: 'var(--fill-tsp)', borderColor: lit ? 'var(--accent)' : 'var(--border)', color: 'var(--text)' }}
512
+ />
513
+ </div>
514
+ {refer ? (
515
+ <Tooltip label={ask.refer} shortcut={ask.shortcut || undefined}>
516
+ <button type="button" aria-label={ask.refer} draggable onDragStart={(e) => drag(e, false)}
517
+ onMouseDown={(e) => e.preventDefault()}
518
+ onClick={() => refer.send(lineAt())}
519
+ className="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded opacity-0 transition hover:bg-[var(--bg-hover)] focus-visible:opacity-100 group-hover:opacity-100"
520
+ style={{ color: 'var(--text-muted)' }}>
521
+ <MessageSquarePlus size={13} aria-hidden />
522
+ </button>
523
+ </Tooltip>
524
+ ) : null}
525
+ </li>
526
+ );
527
+ });
528
+
529
+ function SubtitleLanguage({ style, set, translate, sourceLanguage }: {
530
+ style: SubtitleStyle;
531
+ set: (next: Partial<SubtitleStyle>) => void;
532
+ translate?: (language: string) => Promise<SubtitleTranslateResult>;
533
+ sourceLanguage?: string | null;
534
+ }) {
535
+ const t = useT();
536
+ const [state, setState] = React.useState<{ busy: boolean; error: string | null; needsTranslator: boolean }>({ busy: false, error: null, needsTranslator: false });
537
+ const nameOf = (code: string) => FILM_SUBTITLE_LANGUAGES.find((item) => item.code === code)?.native ?? code;
538
+ const run = async (code: string) => {
539
+ if (!translate) return;
540
+ setState({ busy: true, error: null, needsTranslator: false });
541
+ const result = await translate(code);
542
+ setState({ busy: false, error: result.ok ? null : result.error ?? '', needsTranslator: !!result.needsTranslator });
543
+ };
544
+ const pick = (code: string) => {
545
+ if (!code || sameFilmSubtitleLanguage(code, sourceLanguage ?? null)) {
546
+ set({ language: null });
547
+ setState({ busy: false, error: null, needsTranslator: false });
548
+ return;
549
+ }
550
+ set({ language: code });
551
+ void run(code);
552
+ };
553
+ const targets = FILM_SUBTITLE_LANGUAGES.filter((item) => !sameFilmSubtitleLanguage(item.code, sourceLanguage ?? null));
554
+ /* a saved language that is the voices' own is just the original */
555
+ const chosen = style.language && !sameFilmSubtitleLanguage(style.language, sourceLanguage ?? null) ? style.language : null;
556
+ const network = state.error !== null && /fetch failed|network|socket|TLS|ECONN|timed? ?out|HTTP 5\d\d/i.test(state.error);
557
+ const failure = state.needsTranslator ? 'timeline.subtitleTranslateConnect' : network ? 'timeline.subtitleTranslateNetwork' : 'timeline.subtitleTranslateFailed';
558
+ return (
559
+ <StyleGroup label={t('timeline.subtitleLanguageGroup')}>
560
+ <div className="flex min-h-8 items-center gap-2 py-1">
561
+ <MenuSelect
562
+ label={t('timeline.subtitleLanguageGroup')}
563
+ value={chosen ?? ''}
564
+ disabled={state.busy}
565
+ onPick={pick}
566
+ groups={[
567
+ { choices: [{ value: '', label: sourceLanguage ? t('timeline.subtitleLanguageOriginalNamed').replace('{lang}', nameOf(sourceLanguage)) : t('timeline.subtitleLanguageOriginal') }] },
568
+ { label: t('timeline.subtitleLanguageTranslate'), choices: targets.map((item) => ({ value: item.code, label: item.native })) },
569
+ ]}
570
+ />
571
+ </div>
572
+ {state.busy ? (
573
+ <p className="flex items-center gap-1.5 pb-1 text-[11px]" role="status" style={{ color: 'var(--text-muted)' }}>
574
+ <Loader2 size={12} className="animate-spin" aria-hidden />
575
+ {t('timeline.subtitleTranslating').replace('{lang}', nameOf(chosen ?? ''))}
576
+ </p>
577
+ ) : state.error !== null && chosen ? (
578
+ <p className="flex items-center gap-2 pb-1 text-[11px]" role="alert" title={state.error || undefined} style={{ color: 'var(--danger, #c2410c)' }}>
579
+ <span className="min-w-0 flex-1">{t(failure)}</span>
580
+ <button type="button" className="shrink-0 underline underline-offset-2" onClick={() => void run(chosen)}>
581
+ {t('timeline.subtitleTranslateRetry')}
582
+ </button>
583
+ </p>
584
+ ) : null}
585
+ {chosen ? (
586
+ <Segmented
587
+ label={t('timeline.subtitleLanguageShow')}
588
+ items={[
589
+ { id: 'translation' as const, label: t('timeline.subtitleShowTranslation') },
590
+ { id: 'both' as const, label: t('timeline.subtitleShowBoth') },
591
+ ]}
592
+ value={style.bilingual ? 'both' : 'translation'}
593
+ onPick={(id) => set({ bilingual: id === 'both' })}
594
+ />
595
+ ) : null}
596
+ </StyleGroup>
597
+ );
598
+ }
599
+
600
+ /** A group: a faint rule and a small heading. A dozen controls in one run would be impossible to find. */
601
+ function StyleGroup({ label, aside, children }: { label: string; aside?: React.ReactNode; children: React.ReactNode }) {
602
+ return (
603
+ <div className="mt-3 border-t pt-3" style={{ borderColor: 'var(--border)' }}>
604
+ <div className="flex items-center justify-between gap-2 pb-2">
605
+ <p className="text-[11px] font-medium" style={{ color: 'var(--text-muted)' }}>
606
+ {label}
607
+ </p>
608
+ {aside}
609
+ </div>
610
+ {children}
611
+ </div>
612
+ );
613
+ }
614
+
615
+ /** A slider with its name on the left and its value on the right (a number is easier to match in the next film). */
616
+ function StyleSlider({
617
+ label, value, min, max, step, format, onChange,
618
+ }: {
619
+ label: string;
620
+ value: number;
621
+ min: number;
622
+ max: number;
623
+ step: number;
624
+ format: (v: number) => string;
625
+ onChange: (v: number) => void;
626
+ }) {
627
+ return (
628
+ <div className="flex min-h-8 items-center gap-2 py-1">
629
+ <span className="w-[58px] shrink-0 text-[11px]" style={{ color: 'var(--text-muted)' }}>
630
+ {label}
631
+ </span>
632
+ <input
633
+ type="range"
634
+ min={min}
635
+ max={max}
636
+ step={step}
637
+ value={value}
638
+ aria-label={label}
639
+ onChange={(e) => onChange(Number(e.target.value))}
640
+ className={styles.range}
641
+ />
642
+ <span
643
+ className="w-[40px] shrink-0 rounded bg-[var(--fill-tsp)] py-1 text-center text-[10px] tabular-nums"
644
+ style={{ color: 'var(--text-muted)' }}
645
+ >
646
+ {format(value)}
647
+ </span>
648
+ </div>
649
+ );
650
+ }
651
+
652
+ /**
653
+ * A color swatch and an alpha slider. A checkerboard under the swatch: strokes and shadows are often translucent,
654
+ * which a plain background would hide.
655
+ */
656
+ function StyleColor({
657
+ label, value, onChange, withAlpha = true,
658
+ }: {
659
+ label: string;
660
+ value: string;
661
+ onChange: (v: string) => void;
662
+ /** The box has its own opacity slider (box.opacity): no second one here. */
663
+ withAlpha?: boolean;
664
+ }) {
665
+ const { hex, alpha } = splitColor(value);
666
+ return (
667
+ <div className="flex min-h-8 items-center gap-2 py-1">
668
+ <span className="w-[58px] shrink-0 text-[11px]" style={{ color: 'var(--text-muted)' }}>
669
+ {label}
670
+ </span>
671
+ <label
672
+ className={`relative h-6 overflow-hidden rounded border ${
673
+ withAlpha ? 'w-6 shrink-0' : 'w-10 shrink-0'
674
+ }`}
675
+ style={{
676
+ borderColor: 'var(--border)',
677
+ backgroundImage:
678
+ 'linear-gradient(45deg,#888 25%,transparent 25%,transparent 75%,#888 75%),'
679
+ + 'linear-gradient(45deg,#888 25%,transparent 25%,transparent 75%,#888 75%)',
680
+ backgroundSize: '8px 8px',
681
+ backgroundPosition: '0 0, 4px 4px',
682
+ }}
683
+ >
684
+ <span className="absolute inset-0" style={{ background: value }} />
685
+ <input
686
+ type="color"
687
+ value={hex}
688
+ aria-label={label}
689
+ onChange={(e) => onChange(joinColor(e.target.value, alpha))}
690
+ className="absolute inset-0 h-full w-full cursor-pointer opacity-0"
691
+ />
692
+ </label>
693
+ {withAlpha ? (
694
+ <>
695
+ <input
696
+ type="range"
697
+ min={0}
698
+ max={1}
699
+ step={0.02}
700
+ value={alpha}
701
+ aria-label={`${label} alpha`}
702
+ onChange={(e) => onChange(joinColor(hex, Number(e.target.value)))}
703
+ className={styles.range}
704
+ />
705
+ <span className="w-[40px] shrink-0 rounded bg-[var(--fill-tsp)] py-1 text-center text-[10px] tabular-nums text-[var(--text-muted)]">{Math.round(alpha * 100)}%</span>
706
+ </>
707
+ ) : <span className="text-[10px] uppercase tabular-nums text-[var(--text-muted)]">{hex}</span>}
708
+ </div>
709
+ );
710
+ }
711
+
712
+ /** A switch, and what shows only while it is on. Off clears the whole layer. */
713
+ function StyleLayer({
714
+ label, on, onToggle, children,
715
+ }: {
716
+ label: string;
717
+ on: boolean;
718
+ onToggle: () => void;
719
+ children: React.ReactNode;
720
+ }) {
721
+ return (
722
+ <div className="pt-0.5">
723
+ <button
724
+ type="button"
725
+ onClick={onToggle}
726
+ onMouseDown={(e) => e.preventDefault()}
727
+ aria-pressed={on}
728
+ className="flex min-h-8 w-full flex-row-reverse items-center justify-between gap-2 rounded py-1.5 text-left transition hover:bg-[var(--bg-hover)]"
729
+ >
730
+ <span
731
+ className="flex h-[14px] w-[24px] shrink-0 items-center rounded-full px-[2px] transition"
732
+ style={{ background: on ? 'var(--text-dim)' : 'var(--border)' }}
733
+ >
734
+ <span
735
+ className="h-[10px] w-[10px] rounded-full transition-transform"
736
+ style={{ transform: on ? 'translateX(10px)' : 'none', background: on ? 'var(--panel)' : 'var(--text-muted)' }}
737
+ />
738
+ </span>
739
+ <span className="text-[11px]" style={{ color: on ? 'var(--text)' : 'var(--text-muted)' }}>
740
+ {label}
741
+ </span>
742
+ </button>
743
+ {on ? children : null}
744
+ </div>
745
+ );
746
+ }
747
+
748
+ /**
749
+ * The typeface, grouped by script (someone subtitling in Chinese need not search eighty families for the seven
750
+ * Chinese ones). First, "Default" (the system stack): not a font but "follow the system", so not in the alphabet.
751
+ */
752
+ function StyleFamily({
753
+ label, value, fonts, onPick,
754
+ }: {
755
+ label: string;
756
+ value: string;
757
+ fonts: SubtitleFontOption[];
758
+ onPick: (family: string) => void;
759
+ }) {
760
+ const t = useT();
761
+ const groups = React.useMemo(() => {
762
+ const by = new Map<string, SubtitleFontOption[]>();
763
+ for (const f of fonts) {
764
+ const list = by.get(f.locale) ?? [];
765
+ list.push(f);
766
+ by.set(f.locale, list);
767
+ }
768
+ return [...by.entries()];
769
+ }, [fonts]);
770
+
771
+ return (
772
+ <div className="flex min-h-8 items-center gap-2 py-1">
773
+ <span className="w-[58px] shrink-0 text-[11px]" style={{ color: 'var(--text-muted)' }}>
774
+ {label}
775
+ </span>
776
+ <MenuSelect
777
+ label={label}
778
+ value={value}
779
+ onPick={onPick}
780
+ groups={[
781
+ { choices: [{ value: SUBTITLE_FONT_SYSTEM, label: t('timeline.subtitleFamilyDefault') }] },
782
+ ...groups.map(([locale, list]) => ({ label: locale, choices: list.map((f) => ({ value: f.family, label: f.family })) })),
783
+ ]}
784
+ />
785
+ </div>
786
+ );
787
+ }
788
+
789
+ /** A small button that stays lit while on (italic, uppercase). */
790
+ function StyleToggle({ label, on, onPick }: { label: string; on: boolean; onPick: () => void }) {
791
+ return (
792
+ <button
793
+ type="button"
794
+ onClick={onPick}
795
+ onMouseDown={(e) => e.preventDefault()}
796
+ aria-pressed={on}
797
+ className="flex-1 rounded-[6px] px-1 py-1 text-[11px] transition"
798
+ style={{
799
+ background: on ? 'var(--surface-3)' : 'var(--fill-tsp)',
800
+ color: on ? 'var(--text)' : 'var(--text-muted)',
801
+ outline: on ? '1px solid var(--text-muted)' : 'none',
802
+ outlineOffset: -1,
803
+ }}
804
+ >
805
+ {label}
806
+ </button>
807
+ );
808
+ }
809
+
810
+ /**
811
+ * A look's sample: "Aa" drawn in that look, through the same function as the subtitles on the picture, so a sample
812
+ * and the real thing never part. Fixed at 16px here (a frame-relative size would overflow the tile), on near black:
813
+ * these looks are made to sit on pictures.
814
+ */
815
+ function LookSwatch({
816
+ look, active, label, onPick,
817
+ }: {
818
+ look: SubtitleLookId;
819
+ active: boolean;
820
+ label: string;
821
+ onPick: () => void;
822
+ }) {
823
+ const s = subtitleCss({ ...SUBTITLE_DEFAULT, ...SUBTITLE_LOOKS[look] }).text;
824
+ return (
825
+ <Tooltip label={label}>
826
+ <button
827
+ type="button"
828
+ onClick={onPick}
829
+ onMouseDown={(e) => e.preventDefault()}
830
+ aria-label={label}
831
+ aria-pressed={active}
832
+ className="flex h-10 min-w-0 flex-1 items-center justify-center rounded-md transition hover:brightness-125"
833
+ style={{
834
+ background: '#2a2a2a',
835
+ outline: active ? '1px solid var(--text)' : '1px solid var(--border)',
836
+ outlineOffset: -1,
837
+ }}
838
+ >
839
+ <span
840
+ style={{
841
+ ...(s as React.CSSProperties),
842
+ fontSize: 16,
843
+ lineHeight: 1,
844
+ }}
845
+ >
846
+ Aa
847
+ </span>
848
+ </button>
849
+ </Tooltip>
850
+ );
851
+ }
852
+
853
+ /** A row: a name on the left, two or three small buttons on the right. */
854
+ function Segmented<T extends string>({
855
+ label, items, value, onPick,
856
+ }: {
857
+ label: string;
858
+ items: readonly { id: T; label: string }[];
859
+ value: T;
860
+ onPick: (id: T) => void;
861
+ }) {
862
+ return (
863
+ <div className="flex flex-wrap items-center gap-x-2 gap-y-1 py-1">
864
+ <span className="shrink-0 text-[11px]" style={{ color: 'var(--text-muted)' }}>{label}</span>
865
+ <div className="ml-auto flex min-w-0 gap-0.5 rounded-md bg-[var(--fill-tsp)] p-0.5">
866
+ {items.map((item) => (
867
+ <button
868
+ key={item.id}
869
+ type="button"
870
+ onClick={() => onPick(item.id)}
871
+ onMouseDown={(e) => e.preventDefault()}
872
+ className="rounded-[6px] px-1.5 py-0.5 text-[11px] transition"
873
+ style={item.id === value
874
+ ? { color: 'var(--text)', background: 'var(--surface-3)', boxShadow: '0 1px 3px #0002' }
875
+ : { color: 'var(--text-muted)', background: 'transparent' }}
876
+ aria-pressed={item.id === value}
877
+ >
878
+ {item.label}
879
+ </button>
880
+ ))}
881
+ </div>
882
+ </div>
883
+ );
884
+ }