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,418 @@
1
+ /**
2
+ * A page clip's Properties: what its page offers to change, each a property the person made of one of its layers (its
3
+ * words, its picture, whether it shows) or of one of its CSS variables (lib/component-props), as a design tool's
4
+ * component offers its properties. Only those, none worked out on their own. Each field paints first and writes when
5
+ * let go, as every inspector field does; what a clip fills in is an override, and a property can go back to its
6
+ * default. Its name is the person's: renamed with a double-click, removed from its menu. A pointer on a row outlines
7
+ * its layer on the picture.
8
+ */
9
+ import * as React from 'react';
10
+ import { HardDriveUpload, ImageIcon, Plus, RotateCcw } from 'lucide-react';
11
+
12
+ import { useT } from '@/i18n';
13
+ import type { FilmOverride } from '@/lib/film';
14
+ import {
15
+ ROOT, fieldOf, isFontVar, labelOf, relativeTo, valueOf, type TemplateControls, type VarField,
16
+ } from '@/lib/template-controls';
17
+ import {
18
+ propOf, propValue, renamedProp, withProp, withPropValue, withPropsReset, withoutProp, type ComponentProp, type PropValue,
19
+ } from '@/lib/component-props';
20
+ import { joinFontFamily, splitFontFamily } from '@/lib/font-styles';
21
+ import { readResourceDragData, RESOURCE_DRAG_TYPE } from '@/lib/resource-drag';
22
+ import { ColorField, FIELD, INPUT, NumField, Section, SectionAction } from './inspector-fields';
23
+ import { FontFamilyPicker } from './FontFamilyPicker';
24
+ import { ContextMenu, type ContextMenuEntry } from './ContextMenu';
25
+ import { pictureThumb } from './stage-menu';
26
+ import { Tooltip } from './Tooltip';
27
+
28
+ type T = ReturnType<typeof useT>;
29
+
30
+ /** What the section needs of the clip: its page as it is on the picture, its overrides and properties, the picture. */
31
+ export interface PropsHost {
32
+ /** What the page shows now (its variables, words and pictures); null when the clip is not on the picture now. */
33
+ read: () => Promise<TemplateControls | null>;
34
+ overrides: () => readonly FilmOverride[] | undefined;
35
+ /** The clip's properties (its data-props). */
36
+ props: () => readonly ComponentProp[];
37
+ /** Write the clip's properties. */
38
+ setProps: (next: readonly ComponentProp[]) => void;
39
+ /** Paints a change without writing it; null takes it back. */
40
+ preview: (target: { at: string; n?: number }, patch: { style?: Record<string, string>; text?: string } | null) => void;
41
+ /** The page's file, by its place in the project: a picture put in is named from its folder. */
42
+ page: string;
43
+ /** The project's pictures, by their place in the project, each with where its thumbnail is read from. */
44
+ pictures: () => Array<{ path: string; name: string; url?: string }>;
45
+ /** One from this computer, brought into the project: its place there (null: none picked). */
46
+ pickPicture?: () => Promise<string | null>;
47
+ /** Outline a layer of the page on the picture (a row pointed at); null: none. */
48
+ point?: (target: { at: string; n?: number } | null) => void;
49
+ }
50
+
51
+ const PICTURE = /\.(png|jpe?g|gif|webp|avif|svg|bmp)$/i;
52
+
53
+ /* a page clip off the picture is read again now and then: the playhead may come to it */
54
+ const OFF_PICTURE_EVERY_MS = 1500;
55
+
56
+ const isNamedColor = (v: string) => typeof CSS !== 'undefined' && typeof CSS.supports === 'function' && CSS.supports('color', v);
57
+
58
+ const sameAt = (a: { at: string; n?: number }, b: { at: string; n?: number }) => a.at === b.at && (a.n ?? null) === (b.n ?? null);
59
+
60
+ export function PropertiesSection({ host, commit }: {
61
+ host: PropsHost;
62
+ /** Writes the clip's next overrides (null: none left). */
63
+ commit: (next: FilmOverride[] | null) => void;
64
+ }) {
65
+ const t = useT();
66
+ const [controls, setControls] = React.useState<TemplateControls | null | undefined>(undefined);
67
+ const [menu, setMenu] = React.useState<{ x: number; y: number; items: ContextMenuEntry[] } | null>(null);
68
+ const [renaming, setRenaming] = React.useState<string | null>(null);
69
+ const overrides = host.overrides();
70
+ const props = host.props();
71
+ const kept = JSON.stringify(overrides ?? []);
72
+ React.useEffect(() => {
73
+ let live = true;
74
+ let timer = 0;
75
+ const read = () => {
76
+ void host.read().catch(() => null).then((next) => {
77
+ if (!live) return;
78
+ setControls((prev) => (JSON.stringify(prev) === JSON.stringify(next) ? prev : next));
79
+ if (!next) timer = window.setTimeout(read, OFF_PICTURE_EVERY_MS);
80
+ });
81
+ };
82
+ /* the picture shows a written change a moment after it is kept */
83
+ timer = window.setTimeout(read, 120);
84
+ return () => { live = false; window.clearTimeout(timer); };
85
+ }, [host, kept]);
86
+ /* nothing stays outlined once the section goes (another clip in hand) */
87
+ React.useEffect(() => () => { host.point?.(null); }, [host]);
88
+
89
+ const set = (prop: ComponentProp, value: PropValue | null) => commit(withPropValue(overrides, prop, value));
90
+ const changed = props.filter((p) => propValue(overrides, p) != null);
91
+ /* the page's variables not offered yet: what the + offers */
92
+ const vars = (controls?.vars ?? []).filter((v) => !propOf(props, 'var', v.name));
93
+ const add = (e: React.MouseEvent<HTMLButtonElement>) => {
94
+ const r = e.currentTarget.getBoundingClientRect();
95
+ setMenu({
96
+ x: r.left, y: r.bottom + 4,
97
+ items: vars.length
98
+ ? vars.map((v) => ({ id: `var:${v.name}`, label: labelOf(v.name), hint: v.name, onSelect: () => host.setProps(withProp(props, { name: labelOf(v.name), type: 'var', var: v.name })) }))
99
+ : [{ id: 'no-vars', label: t('inspector.propsNoVars'), disabled: true, onSelect: () => {} }],
100
+ });
101
+ };
102
+ const rowMenu = (e: React.MouseEvent, prop: ComponentProp) => {
103
+ e.preventDefault();
104
+ setMenu({
105
+ x: e.clientX, y: e.clientY,
106
+ items: [
107
+ { id: 'rename', label: t('inspector.propRename'), onSelect: () => setRenaming(prop.name) },
108
+ { id: 'remove', label: t('inspector.propRemove'), onSelect: () => host.setProps(withoutProp(props, prop.name)) },
109
+ ],
110
+ });
111
+ };
112
+ const pointing = (prop: ComponentProp) => {
113
+ const target = prop.type === 'var' ? null : { at: prop.at, ...(prop.n ? { n: prop.n } : {}) };
114
+ return target ? {
115
+ onPointerEnter: () => host.point?.(target),
116
+ onPointerLeave: () => host.point?.(null),
117
+ onFocus: () => host.point?.(target),
118
+ onBlur: () => host.point?.(null),
119
+ } : {};
120
+ };
121
+
122
+ const field = (prop: ComponentProp): React.ReactNode => {
123
+ const value = propValue(overrides, prop);
124
+ switch (prop.type) {
125
+ case 'text': {
126
+ const own = controls?.texts.find((x) => sameAt(x, prop))?.value ?? '';
127
+ const target = { at: prop.at, ...(prop.n ? { n: prop.n } : {}) };
128
+ return (
129
+ <WordsInput
130
+ label={prop.name}
131
+ value={typeof value === 'string' ? value : own}
132
+ onCommit={(next) => set(prop, next === own ? null : next)}
133
+ onPreview={(next) => host.preview(target, next == null ? null : { text: next })}
134
+ />
135
+ );
136
+ }
137
+ case 'image': {
138
+ const own = controls?.images?.find((x) => sameAt(x, prop))?.src ?? '';
139
+ /* what it draws now: the picture put in, read from the page's folder, else the page's own */
140
+ const src = typeof value === 'string' && controls?.page ? new URL(value, controls.page).href : own;
141
+ return <PictureInput src={src} host={host} label={prop.name} onPick={(path) => set(prop, relativeTo(host.page, path))} t={t} />;
142
+ }
143
+ case 'boolean':
144
+ return <Switch label={prop.name} on={value !== false} onChange={(on) => set(prop, on ? null : false)} />;
145
+ case 'var': {
146
+ const own = controls?.vars.find((v) => v.name === prop.var)?.value ?? '';
147
+ const now = typeof value === 'string' ? value : own;
148
+ const paint = (next: string | null) => host.preview({ at: ROOT }, next == null ? null : { style: { [prop.var]: next } });
149
+ return isFontVar(prop.var, now)
150
+ ? <FontInput label={prop.name} value={now} onCommit={(next) => set(prop, next)} onPreview={paint} />
151
+ : <VarInput name={prop.name} field={fieldOf(now, isNamedColor)} onCommit={(next) => set(prop, next)} onPreview={paint} />;
152
+ }
153
+ }
154
+ };
155
+
156
+ return (
157
+ <Section
158
+ id="properties"
159
+ title={t('inspector.props')}
160
+ aside={(
161
+ <span className="flex items-center">
162
+ {changed.length ? (
163
+ <SectionAction label={t('inspector.propsResetAll')} onClick={() => commit(withPropsReset(overrides, props))}><RotateCcw size={11} /></SectionAction>
164
+ ) : null}
165
+ <Tooltip label={t('inspector.propsAdd')}>
166
+ <button type="button" aria-label={t('inspector.propsAdd')} onClick={add} className="flex h-5 w-5 items-center justify-center rounded-[4px] text-[var(--text-muted)] transition hover:bg-[var(--bg-hover)] hover:text-[var(--text)]">
167
+ <Plus size={12} />
168
+ </button>
169
+ </Tooltip>
170
+ </span>
171
+ )}
172
+ >
173
+ {!props.length ? (
174
+ <p className="text-[10.5px] leading-[15px] text-[var(--text-faint)]">{t('inspector.propsEmpty')}</p>
175
+ ) : props.map((prop) => (
176
+ <div key={prop.name} {...pointing(prop)} onContextMenu={(e) => rowMenu(e, prop)}>
177
+ <Line
178
+ label={renaming === prop.name ? (
179
+ <NameInput
180
+ value={prop.name}
181
+ onDone={(next) => { setRenaming(null); host.setProps(renamedProp(props, prop.name, next)); }}
182
+ onCancel={() => setRenaming(null)}
183
+ />
184
+ ) : prop.name}
185
+ title={t('inspector.propRenameHint')}
186
+ onRename={() => setRenaming(prop.name)}
187
+ changed={propValue(overrides, prop) != null}
188
+ onReset={() => set(prop, null)}
189
+ t={t}
190
+ >
191
+ {field(prop)}
192
+ </Line>
193
+ </div>
194
+ ))}
195
+ {!controls && props.length ? (
196
+ <p className="text-[10.5px] leading-[15px] text-[var(--text-faint)]">{t('inspector.templateOffPicture')}</p>
197
+ ) : null}
198
+ {menu ? <ContextMenu x={menu.x} y={menu.y} items={menu.items} onClose={() => setMenu(null)} /> : null}
199
+ </Section>
200
+ );
201
+ }
202
+
203
+ /** A property's name typed anew: kept on Enter or when left, Esc gives up. */
204
+ function NameInput({ value, onDone, onCancel }: { value: string; onDone: (next: string) => void; onCancel: () => void }) {
205
+ const [raw, setRaw] = React.useState(value);
206
+ const done = React.useRef(false);
207
+ return (
208
+ <input
209
+ autoFocus
210
+ value={raw}
211
+ spellCheck={false}
212
+ onFocus={(e) => e.currentTarget.select()}
213
+ onChange={(e) => setRaw(e.target.value)}
214
+ onBlur={() => { if (!done.current) { done.current = true; onDone(raw); } }}
215
+ onKeyDown={(e) => {
216
+ if (e.key === 'Enter' && !e.nativeEvent.isComposing) { done.current = true; onDone(raw); }
217
+ if (e.key === 'Escape') { done.current = true; onCancel(); }
218
+ }}
219
+ className="h-5 w-full min-w-0 rounded-[4px] bg-[var(--bg)] px-1 text-[11px] text-[var(--text)] outline-none ring-1 ring-[var(--accent)]"
220
+ />
221
+ );
222
+ }
223
+
224
+ /** Shown or not: a switch. */
225
+ function Switch({ label, on, onChange }: { label: string; on: boolean; onChange: (on: boolean) => void }) {
226
+ return (
227
+ <button type="button" role="switch" aria-checked={on} aria-label={label} onClick={() => onChange(!on)} className="flex h-6 items-center">
228
+ <span aria-hidden className={`relative h-[16px] w-[28px] shrink-0 rounded-full transition-colors ${on ? 'bg-[var(--text)]' : 'bg-[var(--surface-2)] ring-1 ring-inset ring-[var(--border)]'}`}>
229
+ <span className={`absolute top-[3px] h-2.5 w-2.5 rounded-full transition-all ${on ? 'left-[15px] bg-[var(--bg)]' : 'left-[3px] bg-[var(--text-faint)]'}`} />
230
+ </span>
231
+ </button>
232
+ );
233
+ }
234
+
235
+ /** A picture: its thumbnail and name; clicked, the project's pictures and one from this computer; a picture dropped. */
236
+ export function PictureInput({ src, host, label, onPick, t }: {
237
+ src: string; host: Pick<PropsHost, 'pictures' | 'pickPicture'>; label: string; onPick: (path: string) => void; t: T;
238
+ }) {
239
+ const [menu, setMenu] = React.useState<{ x: number; y: number } | null>(null);
240
+ const [over, setOver] = React.useState(false);
241
+ const name = decodeURIComponent(src.split(/[?#]/)[0]!.split('/').pop() ?? '');
242
+ const entries = (): ContextMenuEntry[] => {
243
+ const list = host.pictures().slice(0, 40);
244
+ const pick = host.pickPicture;
245
+ return [
246
+ ...list.map((f) => ({ id: `pic:${f.path}`, label: f.name, icon: pictureThumb(f.url), onSelect: () => onPick(f.path) })),
247
+ ...(pick ? [
248
+ ...(list.length ? [{ id: 'pic-sep', separator: true as const }] : []),
249
+ { id: 'pic-computer', label: t('inspector.templatePictureComputer'), icon: <HardDriveUpload size={14} />, onSelect: () => { void pick().then((path) => { if (path && PICTURE.test(path)) onPick(path); }); } },
250
+ ] : []),
251
+ ];
252
+ };
253
+ return (
254
+ <>
255
+ <button
256
+ type="button"
257
+ aria-label={`${label}: ${t('inspector.templatePictureReplace')}`}
258
+ title={t('inspector.templatePictureReplace')}
259
+ onClick={(e) => { const r = e.currentTarget.getBoundingClientRect(); setMenu({ x: r.left, y: r.bottom + 4 }); }}
260
+ onDragOver={(e) => { if (e.dataTransfer.types.includes(RESOURCE_DRAG_TYPE)) { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; setOver(true); } }}
261
+ onDragLeave={() => setOver(false)}
262
+ onDrop={(e) => {
263
+ setOver(false);
264
+ const file = readResourceDragData(e.dataTransfer);
265
+ if (!file || file.saved || !PICTURE.test(file.path)) return;
266
+ e.preventDefault();
267
+ onPick(file.path);
268
+ }}
269
+ className={`${FIELD} gap-1.5 px-1 text-left ${over ? 'ring-[1.5px] ring-[var(--accent)]' : ''}`}
270
+ >
271
+ <span className="flex h-[18px] w-[28px] shrink-0 items-center justify-center overflow-hidden rounded-[3px] bg-[var(--bg-hover)]">
272
+ {src ? <img src={src} alt="" className="h-full w-full object-cover" draggable={false} /> : <ImageIcon size={12} />}
273
+ </span>
274
+ <span className="min-w-0 flex-1 truncate text-[11px] text-[var(--text)]">{name}</span>
275
+ </button>
276
+ {menu ? <ContextMenu x={menu.x} y={menu.y} items={entries()} onClose={() => setMenu(null)} /> : null}
277
+ </>
278
+ );
279
+ }
280
+
281
+ /** A custom property that names a font: the font picker over its first family, the fallbacks after it kept. */
282
+ function FontInput({ label, value, onCommit, onPreview }: {
283
+ label: string; value: string; onCommit: (value: string) => void; onPreview: (value: string | null) => void;
284
+ }) {
285
+ const [family, rest] = splitFontFamily(value);
286
+ return (
287
+ <FontFamilyPicker
288
+ label={label}
289
+ value={family}
290
+ sample=""
291
+ onPreview={(next) => onPreview(joinFontFamily(next, rest))}
292
+ onRevert={() => onPreview(null)}
293
+ onCommit={(next) => onCommit(joinFontFamily(next, rest))}
294
+ className={FIELD}
295
+ />
296
+ );
297
+ }
298
+
299
+ /** A property: its name on the left (a double-click renames it), its field, and going back to its default. */
300
+ function Line({ label, title, changed, onReset, onRename, t, children }: {
301
+ label: React.ReactNode; title: string; changed: boolean; onReset: () => void; onRename: () => void; t: T; children: React.ReactNode;
302
+ }) {
303
+ const reset = changed ? (
304
+ <SectionAction label={t('inspector.propReset')} onClick={onReset}><RotateCcw size={11} /></SectionAction>
305
+ ) : <span className="w-5" />;
306
+ const name = (
307
+ <span className="min-w-0 truncate text-[11px] text-[var(--text-muted)]" title={title} onDoubleClick={onRename}>
308
+ {changed ? <span aria-hidden className="mr-1 inline-block h-1.5 w-1.5 rounded-full bg-[var(--accent)] align-middle" /> : null}
309
+ {label}
310
+ </span>
311
+ );
312
+ return (
313
+ <div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1.5fr)_20px] items-center gap-1.5">
314
+ {name}
315
+ <div className="min-w-0">{children}</div>
316
+ {reset}
317
+ </div>
318
+ );
319
+ }
320
+
321
+ /** A custom property's field by its kind: a color, a number with its unit, or words. */
322
+ function VarInput({ name, field, onCommit, onPreview }: {
323
+ name: string;
324
+ field: VarField;
325
+ onCommit: (value: string) => void;
326
+ onPreview: (value: string | null) => void;
327
+ }) {
328
+ const label = labelOf(name);
329
+ if (field.kind === 'color') {
330
+ return <ColorField label={label} value={field.value} onCommit={onCommit} onPreview={onPreview} />;
331
+ }
332
+ if (field.kind === 'number') {
333
+ const step = Number.isInteger(field.value) && Math.abs(field.value) >= 2 ? 1 : 0.1;
334
+ return (
335
+ <NumField
336
+ label={label}
337
+ bare
338
+ value={field.value}
339
+ step={step}
340
+ {...(field.unit ? { unit: field.unit } : {})}
341
+ onCommit={(n) => onCommit(valueOf({ ...field, value: n }))}
342
+ onPreview={(n) => onPreview(valueOf({ ...field, value: n }))}
343
+ onRevert={() => onPreview(null)}
344
+ />
345
+ );
346
+ }
347
+ return (
348
+ <TypedInput
349
+ label={label}
350
+ value={field.value}
351
+ onCommit={(v) => onCommit(valueOf({ ...field, value: v }))}
352
+ />
353
+ );
354
+ }
355
+
356
+ /** One line typed and kept when left or on Enter; Esc gives up. */
357
+ function TypedInput({ label, value, onCommit }: { label: string; value: string; onCommit: (value: string) => void }) {
358
+ const [raw, setRaw] = React.useState(value);
359
+ React.useEffect(() => setRaw(value), [value]);
360
+ const cancelled = React.useRef(false);
361
+ return (
362
+ <div className={FIELD}>
363
+ <input
364
+ aria-label={label}
365
+ value={raw}
366
+ spellCheck={false}
367
+ onChange={(e) => setRaw(e.target.value)}
368
+ onBlur={() => {
369
+ if (cancelled.current) { cancelled.current = false; return; }
370
+ if (raw.trim() && raw !== value) onCommit(raw);
371
+ else setRaw(value);
372
+ }}
373
+ onKeyDown={(e) => {
374
+ if (e.key === 'Enter' && !e.nativeEvent.isComposing) (e.target as HTMLInputElement).blur();
375
+ if (e.key === 'Escape') { cancelled.current = true; setRaw(value); (e.target as HTMLInputElement).blur(); }
376
+ }}
377
+ className={`${INPUT} pl-2`}
378
+ />
379
+ </div>
380
+ );
381
+ }
382
+
383
+ /** A layer's words: the picture follows as they are typed, kept when left or on Enter (Shift+Enter: a new line). */
384
+ function WordsInput({ label, value, onCommit, onPreview }: {
385
+ label: string;
386
+ value: string;
387
+ onCommit: (value: string) => void;
388
+ onPreview: (value: string | null) => void;
389
+ }) {
390
+ const [raw, setRaw] = React.useState(value);
391
+ const typing = React.useRef(false);
392
+ React.useEffect(() => { if (!typing.current) setRaw(value); }, [value]);
393
+ const cancelled = React.useRef(false);
394
+ return (
395
+ <textarea
396
+ aria-label={label}
397
+ value={raw}
398
+ rows={Math.min(4, Math.max(1, raw.split('\n').length))}
399
+ onChange={(e) => { typing.current = true; setRaw(e.target.value); onPreview(e.target.value); }}
400
+ onBlur={() => {
401
+ typing.current = false;
402
+ if (cancelled.current) { cancelled.current = false; return; }
403
+ if (raw !== value) onCommit(raw);
404
+ }}
405
+ onKeyDown={(e) => {
406
+ if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) { e.preventDefault(); (e.target as HTMLTextAreaElement).blur(); }
407
+ if (e.key === 'Escape') {
408
+ cancelled.current = true;
409
+ typing.current = false;
410
+ setRaw(value);
411
+ onPreview(null);
412
+ (e.target as HTMLTextAreaElement).blur();
413
+ }
414
+ }}
415
+ className="w-full resize-none rounded-[5px] bg-[var(--fill-tsp)] px-2 py-1 text-[11.5px] leading-[1.5] text-[var(--text)] outline-none ring-1 ring-inset ring-transparent transition hover:ring-[var(--border)] focus:bg-[var(--bg)] focus:ring-[1.5px] focus:ring-[var(--accent)]"
416
+ />
417
+ );
418
+ }
@@ -0,0 +1,100 @@
1
+ import { Download, File as FileIcon } from 'lucide-react';
2
+ import { useT } from '@/i18n';
3
+ import { formatClipDuration, formatFileSize, type WorkspaceResource, type WorkspaceResourceKind } from '@/lib/workspace-resources';
4
+ import { InspectorPanel } from './InspectorPanel';
5
+ import { KIND_ICON } from './MediaPreview';
6
+
7
+ /** A media file's properties, in the inspector's place. `url` is the file itself on the film origin (Download). */
8
+ export function ResourceInspector({ file, url, width, onClose }: {
9
+ file: WorkspaceResource; url: string; width: number; onClose: () => void;
10
+ }) {
11
+ const t = useT();
12
+ const Icon = KIND_ICON[file.kind] ?? FileIcon;
13
+ return (
14
+ <InspectorPanel title={t('assets.props')} width={width} onClose={onClose}>
15
+ <div className="flex items-start gap-2.5 border-b border-[var(--border)] py-4">
16
+ <span className="flex size-9 shrink-0 items-center justify-center rounded-lg bg-[var(--fill-tsp)] text-[var(--text-muted)]"><Icon size={18} /></span>
17
+ <div className="min-w-0"><p className="whitespace-pre-wrap break-words text-[12px] font-medium">{file.name}</p><p className="mt-1 break-all text-[10px] leading-relaxed text-[var(--text-muted)]">{file.path}</p></div>
18
+ </div>
19
+ <AssetFields file={file} />
20
+ <div className="mt-3 flex flex-wrap gap-2 border-t border-[var(--border)] pt-4 text-[11px]">
21
+ <a href={url} download={file.name} className="flex items-center gap-1.5 rounded-md bg-[var(--fill-tsp)] px-2.5 py-2 hover:bg-[var(--bg-hover)]"><Download size={13} />{t('assets.download')}</a>
22
+ </div>
23
+ </InspectorPanel>
24
+ );
25
+ }
26
+
27
+ function fileExtension(name: string): string {
28
+ const cut = name.lastIndexOf('.');
29
+ return cut <= 0 ? '' : name.slice(cut + 1).toLowerCase();
30
+ }
31
+
32
+ function assetTypeText(file: WorkspaceResource, t: (path: string) => string): string {
33
+ const label = assetKindLabel(file.kind, t);
34
+ if (file.kind === 'mg') return label;
35
+ const ext = fileExtension(file.name);
36
+ return ext ? `${label} · ${ext.toUpperCase()}` : label;
37
+ }
38
+
39
+ function assetKindLabel(kind: WorkspaceResourceKind, t: (path: string) => string): string {
40
+ if (kind === 'mg') return t('assets.kindPage');
41
+ if (kind === 'video') return t('assets.catVideo');
42
+ if (kind === 'image') return t('assets.catImage');
43
+ if (kind === 'audio') return t('assets.kindAudio');
44
+ return t('assets.catOther');
45
+ }
46
+
47
+ /** Frame rate as people say it: 29.97 keeps two decimals, 30 is 30. */
48
+ function formatFps(fps: number): string {
49
+ return Number.isInteger(fps) ? String(fps) : fps.toFixed(2).replace(/\.?0+$/, '');
50
+ }
51
+
52
+ /**
53
+ * A file's facts, read-only: one per row and not drawn as inputs — anything that looks like an input gets clicked,
54
+ * and none of these can be changed. Everything comes from props, so the panel opens without a request.
55
+ */
56
+ function AssetFields({ file }: { file: WorkspaceResource }) {
57
+ const t = useT();
58
+ const rows: { label: string; value: string }[] = [
59
+ { label: t('assets.propType'), value: assetTypeText(file, t) },
60
+ { label: t('assets.propSize'), value: formatFileSize(file.size) },
61
+ ];
62
+ if (file.durationMs) {
63
+ rows.push({ label: t('assets.propDuration'), value: formatClipDuration(file.durationMs) });
64
+ }
65
+ if (file.w && file.h) {
66
+ rows.push({ label: t('assets.propDimensions'), value: `${file.w} × ${file.h}` });
67
+ }
68
+ /* only footage has a frame rate; 25 and 30 mixed in one film repeat a frame every few on export, and this is
69
+ where it shows before then */
70
+ if (file.kind === 'video' && file.fps) {
71
+ rows.push({ label: t('assets.propFps'), value: `${formatFps(file.fps)} fps` });
72
+ }
73
+ if (file.mtimeMs) {
74
+ rows.push({ label: t('assets.propModified'), value: new Date(file.mtimeMs).toLocaleString() });
75
+ }
76
+ /* heavy footage's proxy for the preview: being made, ready, or not to be */
77
+ if (file.proxy) {
78
+ const { state, ratio } = file.proxy;
79
+ rows.push({
80
+ label: t('assets.propProxy'),
81
+ value: state === 'making' ? t('media.proxyMaking').replace('{p}', String(Math.round((ratio ?? 0) * 100)))
82
+ : state === 'waiting' ? t('media.proxyWaiting') : state === 'failed' ? t('media.proxyFailed') : t('media.proxyReady'),
83
+ });
84
+ }
85
+ /* a link to media kept elsewhere (a NAS): where it is, or that it cannot be reached now */
86
+ if (file.link) {
87
+ rows.push({ label: t('assets.propLinked'), value: file.link.missing ? `${file.link.to} (${t('media.linkMissing')})` : file.link.to });
88
+ }
89
+
90
+ return (
91
+ <dl className="py-2">
92
+ {rows.map((row) => (
93
+ <div key={row.label} className="border-t border-[var(--border)] py-2 first:border-t-0 first:pt-1">
94
+ <dt className="text-[11px] leading-none text-[var(--text-muted)]">{row.label}</dt>
95
+ <dd className="mt-1 break-all text-[12px] leading-[1.45] text-[var(--text)]">{row.value}</dd>
96
+ </div>
97
+ ))}
98
+ </dl>
99
+ );
100
+ }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * The small bar that comes up beside what is selected (clips on the timeline, a layer or a clip on the picture, a
3
+ * range or a moment on the ruler): a row of icon buttons for what can be done to it there, the first always
4
+ * "Add to chat" when the app around Studio has one. The actions are the ones the menus already have; the bar only
5
+ * puts the usual few a click away.
6
+ *
7
+ * · **Beside it, never over it** (lib/selection-toolbar placeToolbar): above, else below, inside the area it belongs
8
+ * to, and clear of what its owner says it must not cover (the other clips on the timeline); placed again on every
9
+ * render of its owner, so it follows a scroll or a moved frame.
10
+ * · **At once.** It shows with the selection's own frame, in the same paint, and stays away while `hidden` (a
11
+ * marquee, dragging, trimming, cropping, typing, playing): its owner says when the selection is still moving.
12
+ * · **Esc puts it away** until the selection changes.
13
+ * · **On body**, like the menus: the timeline and the picture clip what spills out of them. Presses on it reach
14
+ * neither (they would select or drag what is under it in the component tree), and take no focus (Space plays).
15
+ * · `data-capture-hide`: the app hides it when it takes a picture of the viewer for a reference.
16
+ */
17
+ import * as React from 'react';
18
+ import { createPortal } from 'react-dom';
19
+
20
+ import { Tooltip } from '@/components/Tooltip';
21
+ import { inHostPanel } from '@/components/typing-target';
22
+ import { placeToolbar, type ScreenBox, type ToolbarPlacement } from '@/lib/selection-toolbar';
23
+
24
+ export interface SelectionTool {
25
+ id: string;
26
+ label: string;
27
+ icon: React.ReactNode;
28
+ /** Its key, already formatted by shortcutHint. */
29
+ shortcut?: string;
30
+ disabled?: boolean;
31
+ /** Why it cannot be pressed (said in its tooltip). */
32
+ hint?: string;
33
+ danger?: boolean;
34
+ /** `button`: where a menu it opens hangs from. */
35
+ onSelect: (button: HTMLButtonElement) => void;
36
+ }
37
+
38
+ export type SelectionToolEntry = SelectionTool | { id: string; separator: true };
39
+
40
+ export function SelectionToolbar({
41
+ selection, measure, hidden = false, lead, label, tools, ariaLabel,
42
+ }: {
43
+ /** What is selected, as a key (null: nothing). Esc hides the bar until it changes. */
44
+ selection: string | null;
45
+ /**
46
+ * Where the selection is on screen and the area the bar stays in (and the room between, when not the usual; what it
47
+ * must not cover); null when it cannot be seen.
48
+ */
49
+ measure: () => { anchor: ScreenBox; bounds: ScreenBox; gap?: number; avoid?: readonly ScreenBox[] } | null;
50
+ /** Kept away for now (a drag, playback): shown again when it ends. */
51
+ hidden?: boolean;
52
+ /** The first button, always: "Add to chat" (none without a chat). */
53
+ lead?: SelectionTool | null;
54
+ /** Said after the first button: how many are selected ("3 clips"). */
55
+ label?: string;
56
+ /** The rest, after a separator. */
57
+ tools: readonly SelectionToolEntry[];
58
+ ariaLabel: string;
59
+ }) {
60
+ const ref = React.useRef<HTMLDivElement | null>(null);
61
+ const [dismissed, setDismissed] = React.useState<string | null>(null);
62
+ const [pos, setPos] = React.useState<ToolbarPlacement | null>(null);
63
+ const [, setTick] = React.useState(0);
64
+
65
+ React.useEffect(() => { if (dismissed && dismissed !== selection) setDismissed(null); }, [dismissed, selection]);
66
+
67
+ const shown = !hidden && selection != null && dismissed !== selection && (Boolean(lead) || tools.length > 0);
68
+
69
+ React.useEffect(() => {
70
+ if (!shown) return undefined;
71
+ /* an Esc pressed in an app's own panel is the app's (its chat's: stop the agent) */
72
+ const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape' && !inHostPanel(e.target)) setDismissed(selection); };
73
+ const again = () => setTick((n) => n + 1);
74
+ window.addEventListener('keydown', onKey);
75
+ window.addEventListener('resize', again);
76
+ window.addEventListener('scroll', again, true);
77
+ return () => {
78
+ window.removeEventListener('keydown', onKey);
79
+ window.removeEventListener('resize', again);
80
+ window.removeEventListener('scroll', again, true);
81
+ };
82
+ }, [shown, selection]);
83
+
84
+ /* placed after every render (the owner re-renders as what it is about moves), measured first */
85
+ React.useLayoutEffect(() => {
86
+ if (!shown) { setPos(null); return; }
87
+ const m = measure();
88
+ const el = ref.current;
89
+ const next = m ? placeToolbar({
90
+ anchor: m.anchor, size: { width: el?.offsetWidth ?? 0, height: el?.offsetHeight ?? 0 }, bounds: m.bounds,
91
+ ...(m.gap != null ? { gap: m.gap } : {}), ...(m.avoid ? { avoid: m.avoid } : {}),
92
+ }) : null;
93
+ setPos((cur) => (cur && next && cur.left === next.left && cur.top === next.top && cur.side === next.side ? cur : next));
94
+ });
95
+
96
+ if (!shown || typeof document === 'undefined') return null;
97
+ const stop = (e: React.SyntheticEvent) => e.stopPropagation();
98
+ const side = pos?.side === 'below' ? 'bottom' : 'top';
99
+ const sep = (key: string) => <span key={key} aria-hidden className="mx-[3px] h-4 w-px shrink-0 bg-[var(--border)]" />;
100
+
101
+ return createPortal(
102
+ <div
103
+ ref={ref}
104
+ role="toolbar"
105
+ aria-label={ariaLabel}
106
+ data-selection-toolbar=""
107
+ data-capture-hide=""
108
+ onPointerDown={stop}
109
+ onPointerUp={stop}
110
+ onPointerMove={stop}
111
+ onClick={stop}
112
+ onDoubleClick={stop}
113
+ onContextMenu={(e) => { e.stopPropagation(); e.preventDefault(); }}
114
+ /* above the workspace, below menus and dialogs (starting at z-110) */
115
+ className="fixed z-[100] flex h-[28px] items-center gap-px rounded-[8px] border border-[var(--border)] bg-[var(--surface)] px-[2px] shadow-[var(--shadow-lg)]"
116
+ style={{
117
+ left: pos?.left ?? 0,
118
+ top: pos?.top ?? 0,
119
+ /* shown at once, where it goes: a rise would trail the click */
120
+ visibility: pos ? 'visible' : 'hidden',
121
+ }}
122
+ >
123
+ {lead ? <ToolbarButton tool={lead} side={side} /> : null}
124
+ {label ? <span className="shrink-0 whitespace-nowrap px-1.5 text-[11.5px] tabular-nums text-[var(--text-muted)]">{label}</span> : null}
125
+ {(lead || label) && tools.length ? sep('lead-sep') : null}
126
+ {tools.map((entry) => ('separator' in entry ? sep(entry.id) : <ToolbarButton key={entry.id} tool={entry} side={side} />))}
127
+ </div>,
128
+ document.body,
129
+ );
130
+ }
131
+
132
+ function ToolbarButton({ tool, side }: { tool: SelectionTool; side: 'top' | 'bottom' }) {
133
+ const tip = tool.disabled && tool.hint ? `${tool.label} · ${tool.hint}` : tool.label;
134
+ return (
135
+ <Tooltip label={tip} side={side} {...(tool.shortcut ? { shortcut: tool.shortcut } : {})}>
136
+ <button
137
+ type="button"
138
+ aria-label={tool.label}
139
+ /* aria-disabled, not disabled: a disabled button gets no hover, and its tooltip says why it cannot be pressed */
140
+ aria-disabled={tool.disabled || undefined}
141
+ data-tool={tool.id}
142
+ /* no focus from a click: the next Space plays instead of pressing this again */
143
+ onMouseDown={(e) => e.preventDefault()}
144
+ onClick={(e) => { if (!tool.disabled) tool.onSelect(e.currentTarget); }}
145
+ className={`flex h-[24px] w-[24px] shrink-0 items-center justify-center rounded-[6px] outline-none transition focus-visible:ring-1 focus-visible:ring-[var(--border-strong)] ${
146
+ tool.disabled
147
+ ? 'cursor-default text-[var(--text-faint)]'
148
+ : tool.danger
149
+ ? 'text-[var(--text-dim)] hover:bg-[var(--bg-hover)] hover:text-[var(--err)]'
150
+ : 'text-[var(--text-dim)] hover:bg-[var(--bg-hover)] hover:text-[var(--text)]'
151
+ }`}
152
+ >
153
+ {tool.icon}
154
+ </button>
155
+ </Tooltip>
156
+ );
157
+ }