@puredesktop/puredesktop-ui-bridge 1.0.0-beta.1

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 (412) hide show
  1. package/package.json +96 -0
  2. package/src/agents/runtime/agentLiveReducer.test.ts +72 -0
  3. package/src/agents/runtime/agentLiveReducer.ts +139 -0
  4. package/src/agents/runtime/agentRunEventTypes.ts +115 -0
  5. package/src/agents/runtime/contextWindowUsage.test.ts +38 -0
  6. package/src/agents/runtime/contextWindowUsage.ts +25 -0
  7. package/src/agents/runtime/index.ts +24 -0
  8. package/src/agents/runtime/mapAgentMessages.ts +54 -0
  9. package/src/agents/runtime/mapAgentSnapshot.ts +71 -0
  10. package/src/agents/runtime/mergeUsageTurn.ts +23 -0
  11. package/src/agents/runtime/pendingToolCalls.test.ts +85 -0
  12. package/src/agents/runtime/pendingToolCalls.ts +14 -0
  13. package/src/agents/runtime/toolRequiresApproval.ts +20 -0
  14. package/src/agents/runtime/unresolvedToolCalls.ts +42 -0
  15. package/src/bridge/agentModelTiers.ts +50 -0
  16. package/src/bridge/agentToolHandlers.ts +23 -0
  17. package/src/bridge/agentToolHelpers.test.ts +27 -0
  18. package/src/bridge/agentToolHelpers.ts +20 -0
  19. package/src/bridge/agentTypes.ts +294 -0
  20. package/src/bridge/agents.d.mts +113 -0
  21. package/src/bridge/agents.mjs +233 -0
  22. package/src/bridge/agents.ts +62 -0
  23. package/src/bridge/appSettings.d.mts +9 -0
  24. package/src/bridge/appSettings.mjs +18 -0
  25. package/src/bridge/appSettings.ts +4 -0
  26. package/src/bridge/applyThemeFromHost.d.mts +7 -0
  27. package/src/bridge/applyThemeFromHost.mjs +33 -0
  28. package/src/bridge/assets.d.mts +66 -0
  29. package/src/bridge/assets.mjs +121 -0
  30. package/src/bridge/catalog.d.mts +5 -0
  31. package/src/bridge/catalog.mjs +24 -0
  32. package/src/bridge/catalog.ts +1 -0
  33. package/src/bridge/client.d.mts +19 -0
  34. package/src/bridge/client.mjs +147 -0
  35. package/src/bridge/collectionAssetAdapter.ts +63 -0
  36. package/src/bridge/collectionAssets.d.mts +1 -0
  37. package/src/bridge/collectionAssets.mjs +2 -0
  38. package/src/bridge/collectionAssets.ts +1 -0
  39. package/src/bridge/collectionDocumentHtml.test.ts +71 -0
  40. package/src/bridge/collectionDocumentHtml.ts +118 -0
  41. package/src/bridge/collectionImagePaste.test.ts +32 -0
  42. package/src/bridge/collectionImagePaste.ts +130 -0
  43. package/src/bridge/context.mjs +12 -0
  44. package/src/bridge/credentials.mjs +22 -0
  45. package/src/bridge/dialog.d.mts +5 -0
  46. package/src/bridge/dialog.mjs +13 -0
  47. package/src/bridge/events.d.mts +14 -0
  48. package/src/bridge/events.mjs +12 -0
  49. package/src/bridge/filePreviewKind.test.ts +43 -0
  50. package/src/bridge/filePreviewKind.ts +76 -0
  51. package/src/bridge/fs.d.mts +41 -0
  52. package/src/bridge/fs.mjs +82 -0
  53. package/src/bridge/fs.test.ts +16 -0
  54. package/src/bridge/fs.ts +10 -0
  55. package/src/bridge/index.ts +12 -0
  56. package/src/bridge/inlinePreview.d.mts +35 -0
  57. package/src/bridge/inlinePreview.mjs +99 -0
  58. package/src/bridge/inlinePreview.test.ts +56 -0
  59. package/src/bridge/inlinePreview.ts +13 -0
  60. package/src/bridge/methods.d.mts +115 -0
  61. package/src/bridge/methods.mjs +132 -0
  62. package/src/bridge/os.d.mts +11 -0
  63. package/src/bridge/os.mjs +26 -0
  64. package/src/bridge/plugins.d.mts +9 -0
  65. package/src/bridge/plugins.mjs +31 -0
  66. package/src/bridge/plugins.ts +5 -0
  67. package/src/bridge/preferences.d.mts +7 -0
  68. package/src/bridge/preferences.mjs +17 -0
  69. package/src/bridge/preferences.ts +13 -0
  70. package/src/bridge/printDocument.test.ts +47 -0
  71. package/src/bridge/printDocument.ts +75 -0
  72. package/src/bridge/pureRender/adapters.test.ts +144 -0
  73. package/src/bridge/pureRender/adapters.ts +175 -0
  74. package/src/bridge/pureRender/assets.test.ts +44 -0
  75. package/src/bridge/pureRender/base.css +76 -0
  76. package/src/bridge/pureRender/compiler.test.ts +279 -0
  77. package/src/bridge/pureRender/compiler.ts +660 -0
  78. package/src/bridge/pureRender/document.test.ts +283 -0
  79. package/src/bridge/pureRender/document.ts +35 -0
  80. package/src/bridge/pureRender/extraction.test.ts +432 -0
  81. package/src/bridge/pureRender/extraction.ts +1023 -0
  82. package/src/bridge/pureRender/flow.css +3 -0
  83. package/src/bridge/pureRender/index.ts +66 -0
  84. package/src/bridge/pureRender/normalize.test.ts +442 -0
  85. package/src/bridge/pureRender/normalize.ts +682 -0
  86. package/src/bridge/pureRender/presets.ts +365 -0
  87. package/src/bridge/pureRender/profiles/book.css +19 -0
  88. package/src/bridge/pureRender/profiles/manuscript.css +11 -0
  89. package/src/bridge/pureRender/profiles/writer.css +10 -0
  90. package/src/bridge/pureRender/reset.css +5 -0
  91. package/src/bridge/pureRender/samples.ts +94 -0
  92. package/src/bridge/pureRender/styles.ts +164 -0
  93. package/src/bridge/pureRender/theme.test.ts +53 -0
  94. package/src/bridge/pureRender/theme.ts +258 -0
  95. package/src/bridge/pureRender/tokens/default.css +36 -0
  96. package/src/bridge/pureRender/tokens/default.json +32 -0
  97. package/src/bridge/pureRender/types.ts +264 -0
  98. package/src/bridge/pureRender/visualLayoutInspection.test.ts +57 -0
  99. package/src/bridge/pureRender/visualLayoutInspection.ts +161 -0
  100. package/src/bridge/react/useDebouncedEffect.ts +21 -0
  101. package/src/bridge/react/useIpc.tsx +75 -0
  102. package/src/bridge/react/usePlatformAgentRunLiveState.tsx +78 -0
  103. package/src/bridge/react/usePlatformAgentSessionDrive.tsx +729 -0
  104. package/src/bridge/react/usePlatformAgentSessions.tsx +60 -0
  105. package/src/bridge/react/usePlatformAgentTools.tsx +82 -0
  106. package/src/bridge/react/usePlatformAppManifest.tsx +32 -0
  107. package/src/bridge/react/usePlatformBridge.tsx +56 -0
  108. package/src/bridge/react/usePlatformDeepLink.tsx +57 -0
  109. package/src/bridge/react/usePlatformFileBinaryDataUrl.tsx +54 -0
  110. package/src/bridge/react/usePlatformFilePreview.tsx +92 -0
  111. package/src/bridge/react/usePlatformPreferences.tsx +77 -0
  112. package/src/bridge/react/usePlatformThemeDocumentAttrs.ts +35 -0
  113. package/src/bridge/react/usePlatformThemeFromBridge.tsx +16 -0
  114. package/src/bridge/react/usePlatformViewportResource.test.tsx +106 -0
  115. package/src/bridge/react/usePlatformViewportResource.tsx +72 -0
  116. package/src/bridge/react/useScheduledTask.tsx +59 -0
  117. package/src/bridge/render.d.mts +31 -0
  118. package/src/bridge/render.mjs +62 -0
  119. package/src/bridge/render.ts +8 -0
  120. package/src/bridge/screenAnnotation.test.ts +103 -0
  121. package/src/bridge/screenAnnotation.ts +269 -0
  122. package/src/bridge/theme.d.mts +3 -0
  123. package/src/bridge/theme.mjs +9 -0
  124. package/src/bridge/types.ts +439 -0
  125. package/src/bridge/watch.d.mts +9 -0
  126. package/src/bridge/watch.mjs +34 -0
  127. package/src/bridge/watch.ts +1 -0
  128. package/src/bridge/window.d.mts +44 -0
  129. package/src/bridge/window.mjs +33 -0
  130. package/src/chrome/index.ts +1 -0
  131. package/src/components/accessibility/AccessibilityReviewDialog.test.tsx +104 -0
  132. package/src/components/accessibility/AccessibilityReviewDialog.tsx +350 -0
  133. package/src/components/accessibility/accessibilityReview.test.ts +101 -0
  134. package/src/components/accessibility/accessibilityReview.ts +639 -0
  135. package/src/components/accessibility/index.ts +16 -0
  136. package/src/components/agent-feedback/ScreenAnnotationAgentDialog.tsx +509 -0
  137. package/src/components/agent-feedback/index.ts +5 -0
  138. package/src/components/agents/AgentComposer.tsx +34 -0
  139. package/src/components/agents/AgentContextUsageBar.tsx +127 -0
  140. package/src/components/agents/AgentDrawerPanel.tsx +82 -0
  141. package/src/components/agents/AgentMarkdown.tsx +12 -0
  142. package/src/components/agents/AgentMessageBubble.tsx +82 -0
  143. package/src/components/agents/AgentMessageList.tsx +47 -0
  144. package/src/components/agents/AgentModelMenu.tsx +55 -0
  145. package/src/components/agents/AgentThinkingBlock.tsx +66 -0
  146. package/src/components/agents/AgentToolApprovalActions.tsx +65 -0
  147. package/src/components/agents/AgentToolCallCard.tsx +26 -0
  148. package/src/components/agents/AgentToolPendingList.tsx +39 -0
  149. package/src/components/agents/AgentToolPendingReview.tsx +118 -0
  150. package/src/components/agents/agentPanelStyles.ts +79 -0
  151. package/src/components/agents/agentTypes.ts +96 -0
  152. package/src/components/agents/formatToolCallArguments.test.ts +16 -0
  153. package/src/components/agents/formatToolCallArguments.ts +9 -0
  154. package/src/components/agents/index.ts +40 -0
  155. package/src/components/assets/AssetLibraryDialog.tsx +5 -0
  156. package/src/components/assets/asset-library/AssetLibraryDialog.tsx +43 -0
  157. package/src/components/assets/asset-library/AssetLibraryWorkspace.tsx +54 -0
  158. package/src/components/assets/asset-library/detail/AssetLibraryDetailActions.tsx +68 -0
  159. package/src/components/assets/asset-library/detail/AssetLibraryDetailHeader.tsx +56 -0
  160. package/src/components/assets/asset-library/detail/AssetLibraryDetailPane.tsx +90 -0
  161. package/src/components/assets/asset-library/detail/AssetLibraryDetailPreview.tsx +67 -0
  162. package/src/components/assets/asset-library/detail/AssetLibraryInlinePreview.tsx +87 -0
  163. package/src/components/assets/asset-library/detail/AssetLibraryInlineSourceField.tsx +29 -0
  164. package/src/components/assets/asset-library/detail/AssetLibraryMetadataFields.tsx +34 -0
  165. package/src/components/assets/asset-library/detail/AssetLibraryUsagePanel.tsx +46 -0
  166. package/src/components/assets/asset-library/detail/AssetLibraryVersionsPanel.tsx +145 -0
  167. package/src/components/assets/asset-library/helpers/assetKind.ts +74 -0
  168. package/src/components/assets/asset-library/helpers.ts +41 -0
  169. package/src/components/assets/asset-library/hooks/useAssetDetailDraft.ts +81 -0
  170. package/src/components/assets/asset-library/hooks/useAssetLibraryDialog.ts +233 -0
  171. package/src/components/assets/asset-library/hooks/useAssetLibraryMutations.ts +266 -0
  172. package/src/components/assets/asset-library/hooks/useAssetLibrarySelectionSync.ts +24 -0
  173. package/src/components/assets/asset-library/hooks/useAssetLibraryVersions.ts +145 -0
  174. package/src/components/assets/asset-library/hooks/useInlineAssetPreview.ts +44 -0
  175. package/src/components/assets/asset-library/index.ts +19 -0
  176. package/src/components/assets/asset-library/layout/assetLibraryLayout.ts +201 -0
  177. package/src/components/assets/asset-library/shared/fieldStyles.ts +62 -0
  178. package/src/components/assets/asset-library/sidebar/AssetLibraryFilterPills.tsx +89 -0
  179. package/src/components/assets/asset-library/sidebar/AssetLibraryList.tsx +91 -0
  180. package/src/components/assets/asset-library/sidebar/AssetLibraryListRow.tsx +99 -0
  181. package/src/components/assets/asset-library/sidebar/AssetLibraryListStatus.tsx +24 -0
  182. package/src/components/assets/asset-library/sidebar/AssetLibrarySidebar.tsx +48 -0
  183. package/src/components/assets/asset-library/sidebar/AssetLibrarySidebarActions.tsx +38 -0
  184. package/src/components/assets/asset-library/sidebar/AssetLibrarySidebarToolbar.tsx +78 -0
  185. package/src/components/assets/asset-library/sidebar/AssetListThumbnail.tsx +86 -0
  186. package/src/components/assets/asset-library/types.ts +128 -0
  187. package/src/components/assets/asset-library/utils/assetLibraryUtils.ts +181 -0
  188. package/src/components/assets/asset-library/utils/pathBasename.ts +5 -0
  189. package/src/components/assets/asset-library/utils/sanitizeUploadFileName.ts +7 -0
  190. package/src/components/assets/asset-library/utils/uploadCollectionAsset.ts +52 -0
  191. package/src/components/chrome/OverlayActionCapsule.test.tsx +82 -0
  192. package/src/components/chrome/OverlayActionCapsule.tsx +90 -0
  193. package/src/components/chrome/PanelTabStrip.tsx +113 -0
  194. package/src/components/chrome/PlatformIcon.tsx +27 -0
  195. package/src/components/chrome/PureAppWordmark.tsx +89 -0
  196. package/src/components/chrome/ShellWindowControls.tsx +51 -0
  197. package/src/components/chrome/ThemeModeButton.tsx +16 -0
  198. package/src/components/chrome/Titlebar.tsx +79 -0
  199. package/src/components/chrome/WorkspaceTabStrip.tsx +300 -0
  200. package/src/components/chrome/index.ts +31 -0
  201. package/src/components/chrome/workspaceTabTypes.ts +29 -0
  202. package/src/components/common/buttons/Button.tsx +241 -0
  203. package/src/components/common/buttons/ChoiceChip.tsx +73 -0
  204. package/src/components/common/buttons/CleanButton.tsx +34 -0
  205. package/src/components/common/buttons/IconButton.tsx +89 -0
  206. package/src/components/common/buttons/QuietAction.tsx +29 -0
  207. package/src/components/common/buttons/SegmentedControl.tsx +67 -0
  208. package/src/components/common/buttons/TextActionButton.tsx +55 -0
  209. package/src/components/common/buttons/TextLink.tsx +38 -0
  210. package/src/components/common/canvas/CanvasElementFrame.test.tsx +79 -0
  211. package/src/components/common/canvas/CanvasElementFrame.tsx +182 -0
  212. package/src/components/common/canvas/canvasGeometry.test.ts +66 -0
  213. package/src/components/common/canvas/canvasGeometry.ts +133 -0
  214. package/src/components/common/canvas/index.ts +27 -0
  215. package/src/components/common/canvas/useCanvasDragResize.test.tsx +152 -0
  216. package/src/components/common/canvas/useCanvasDragResize.ts +161 -0
  217. package/src/components/common/chat/ChatAssistantMessage.tsx +81 -0
  218. package/src/components/common/chat/ChatBox.tsx +189 -0
  219. package/src/components/common/chat/ChatMarkdown.tsx +109 -0
  220. package/src/components/common/chat/ChatModelMenu.tsx +258 -0
  221. package/src/components/common/chat/ChatStatusRow.tsx +24 -0
  222. package/src/components/common/chat/ChatThinkingIndicator.tsx +54 -0
  223. package/src/components/common/chat/ChatThread.tsx +59 -0
  224. package/src/components/common/chat/ChatToolRow.tsx +141 -0
  225. package/src/components/common/chat/ChatUserBubble.tsx +45 -0
  226. package/src/components/common/chat/chatUtils.ts +7 -0
  227. package/src/components/common/chat/index.ts +24 -0
  228. package/src/components/common/connections/ConnectionSetup.tsx +529 -0
  229. package/src/components/common/connections/index.ts +26 -0
  230. package/src/components/common/containers/AppFrame.test.tsx +47 -0
  231. package/src/components/common/containers/AppFrame.tsx +65 -0
  232. package/src/components/common/containers/AppHeader.tsx +60 -0
  233. package/src/components/common/containers/AutoFillGrid.tsx +37 -0
  234. package/src/components/common/containers/Collapsible.tsx +43 -0
  235. package/src/components/common/containers/CollapsibleGrid.tsx +174 -0
  236. package/src/components/common/containers/MotionScope.tsx +32 -0
  237. package/src/components/common/containers/PageHeader.tsx +35 -0
  238. package/src/components/common/containers/PanelToolbarHeader.tsx +113 -0
  239. package/src/components/common/containers/QuietDisclosure.tsx +91 -0
  240. package/src/components/common/containers/QuietSurface.tsx +41 -0
  241. package/src/components/common/containers/ScrollPanel.tsx +33 -0
  242. package/src/components/common/containers/StatusBar.tsx +42 -0
  243. package/src/components/common/containers/SurfacePanel.tsx +66 -0
  244. package/src/components/common/containers/ToolbarStrip.tsx +80 -0
  245. package/src/components/common/containers/flex.ts +117 -0
  246. package/src/components/common/containers/useOpenTransition.ts +33 -0
  247. package/src/components/common/context/ContextSelector.tsx +269 -0
  248. package/src/components/common/context/index.ts +7 -0
  249. package/src/components/common/desk/ActionReader.tsx +270 -0
  250. package/src/components/common/desk/ActivityLedger.tsx +143 -0
  251. package/src/components/common/desk/CreationDesk.tsx +139 -0
  252. package/src/components/common/desk/DailyBrief.tsx +160 -0
  253. package/src/components/common/desk/DeskCard.tsx +157 -0
  254. package/src/components/common/desk/index.ts +30 -0
  255. package/src/components/common/dropdown/ContextMenu.tsx +87 -0
  256. package/src/components/common/dropdown/MenuButtonDropdown.tsx +121 -0
  257. package/src/components/common/dropdown/MenuDropdown.tsx +63 -0
  258. package/src/components/common/dropdown/MenuDropdownEntry.tsx +51 -0
  259. package/src/components/common/dropdown/MenuDropdownItem.tsx +82 -0
  260. package/src/components/common/dropdown/MenuDropdownSubmenu.tsx +145 -0
  261. package/src/components/common/dropdown/MenuPanel.tsx +69 -0
  262. package/src/components/common/dropdown/MenuPortal.tsx +162 -0
  263. package/src/components/common/dropdown/MenuPortalLayer.tsx +6 -0
  264. package/src/components/common/dropdown/SelectMenu.tsx +243 -0
  265. package/src/components/common/dropdown/SelectMenuControl.tsx +23 -0
  266. package/src/components/common/dropdown/SelectMenuOptions.tsx +38 -0
  267. package/src/components/common/dropdown/menuPortalConstants.ts +11 -0
  268. package/src/components/common/dropdown/menuTypes.ts +71 -0
  269. package/src/components/common/dropdown/menuViewportPosition.test.ts +139 -0
  270. package/src/components/common/dropdown/menuViewportPosition.ts +122 -0
  271. package/src/components/common/dropdown/selectMenuLayout.test.ts +42 -0
  272. package/src/components/common/dropdown/selectMenuLayout.ts +123 -0
  273. package/src/components/common/dropdown/useMenuAnchorPosition.ts +109 -0
  274. package/src/components/common/dropdown/useSelectMenuAnchor.ts +30 -0
  275. package/src/components/common/evidence/StructuredEvidenceView.test.tsx +77 -0
  276. package/src/components/common/evidence/StructuredEvidenceView.tsx +312 -0
  277. package/src/components/common/evidence/index.ts +13 -0
  278. package/src/components/common/feedback/Badge.tsx +33 -0
  279. package/src/components/common/feedback/CapabilityPills.tsx +36 -0
  280. package/src/components/common/feedback/EmptyState.tsx +132 -0
  281. package/src/components/common/feedback/InfoHint.tsx +67 -0
  282. package/src/components/common/feedback/InlineBanner.tsx +82 -0
  283. package/src/components/common/feedback/PlatformChip.tsx +104 -0
  284. package/src/components/common/feedback/QuietPill.tsx +75 -0
  285. package/src/components/common/feedback/WorkProgressPanel.tsx +156 -0
  286. package/src/components/common/feedback/index.ts +33 -0
  287. package/src/components/common/inputs/BareInput.tsx +31 -0
  288. package/src/components/common/inputs/BareTextarea.tsx +10 -0
  289. package/src/components/common/inputs/Checkbox.tsx +13 -0
  290. package/src/components/common/inputs/FieldControlShell.tsx +49 -0
  291. package/src/components/common/inputs/FormField.tsx +40 -0
  292. package/src/components/common/inputs/HybridSearchField.tsx +280 -0
  293. package/src/components/common/inputs/InlineComposer.tsx +197 -0
  294. package/src/components/common/inputs/PaletteCirclePicker.tsx +26 -0
  295. package/src/components/common/inputs/PaletteDropdown.tsx +413 -0
  296. package/src/components/common/inputs/Radio.tsx +19 -0
  297. package/src/components/common/inputs/SearchField.tsx +128 -0
  298. package/src/components/common/inputs/SelectField.tsx +169 -0
  299. package/src/components/common/inputs/SurfacePalettePicker.tsx +54 -0
  300. package/src/components/common/inputs/TextAreaField.tsx +22 -0
  301. package/src/components/common/inputs/TextField.tsx +51 -0
  302. package/src/components/common/inputs/ToggleSwitch.tsx +110 -0
  303. package/src/components/common/inputs/WorkspacePicker.tsx +349 -0
  304. package/src/components/common/inputs/fieldStyles.ts +137 -0
  305. package/src/components/common/layout/PanelGroup.tsx +479 -0
  306. package/src/components/common/layout/index.ts +31 -0
  307. package/src/components/common/layout/panelLayoutConstants.ts +7 -0
  308. package/src/components/common/layout/panelLayoutPersistence.ts +35 -0
  309. package/src/components/common/layout/panelLayoutState.test.ts +38 -0
  310. package/src/components/common/layout/panelLayoutState.ts +174 -0
  311. package/src/components/common/layout/panelLayoutTypes.ts +19 -0
  312. package/src/components/common/layout/tabs/Tabs.tsx +136 -0
  313. package/src/components/common/lists/listRowStyles.ts +32 -0
  314. package/src/components/common/lists/quiet-ledger/QuietActivityList.tsx +64 -0
  315. package/src/components/common/lists/quiet-ledger/QuietLedgerRow.tsx +119 -0
  316. package/src/components/common/lists/quiet-ledger/index.ts +9 -0
  317. package/src/components/common/lists/reorderable/DragHandle.tsx +65 -0
  318. package/src/components/common/lists/reorderable/ReorderableList.tsx +280 -0
  319. package/src/components/common/lists/reorderable/ReorderableListRow.tsx +102 -0
  320. package/src/components/common/lists/reorderable/ReorderableSections.tsx +344 -0
  321. package/src/components/common/lists/reorderable/ReorderableSectionsDemo.tsx +94 -0
  322. package/src/components/common/lists/reorderable/index.ts +15 -0
  323. package/src/components/common/lists/reorderable/reorderableUtils.test.ts +28 -0
  324. package/src/components/common/lists/reorderable/reorderableUtils.ts +23 -0
  325. package/src/components/common/lists/reorderable/types.ts +40 -0
  326. package/src/components/common/lists/reorderable/useReorderableDragState.ts +49 -0
  327. package/src/components/common/media/ImageReferencePicker.tsx +242 -0
  328. package/src/components/common/menubar/MenuBar.tsx +78 -0
  329. package/src/components/common/menubar/MenuBarTrigger.tsx +70 -0
  330. package/src/components/common/narrative/NarrativeMention.tsx +74 -0
  331. package/src/components/common/navigation/PathBreadcrumbs.tsx +78 -0
  332. package/src/components/common/navigation/UnderlineTabs.tsx +83 -0
  333. package/src/components/common/overlays/InstallAppPromptDialog.tsx +59 -0
  334. package/src/components/common/overlays/Modal.tsx +285 -0
  335. package/src/components/common/overlays/ModalChromeHeader.tsx +105 -0
  336. package/src/components/common/overlays/ObjectActionStrip.tsx +80 -0
  337. package/src/components/common/people/AutoUpdateReviewList.tsx +182 -0
  338. package/src/components/common/people/EntityList.tsx +122 -0
  339. package/src/components/common/people/EntityPortrait.tsx +64 -0
  340. package/src/components/common/people/LinkedDossierList.tsx +99 -0
  341. package/src/components/common/people/OpportunityList.tsx +94 -0
  342. package/src/components/common/people/PeopleSourceList.tsx +145 -0
  343. package/src/components/common/people/RelationshipDossier.tsx +168 -0
  344. package/src/components/common/people/RelationshipMap.tsx +362 -0
  345. package/src/components/common/people/index.ts +40 -0
  346. package/src/components/common/preview/FileAudioPreview.tsx +89 -0
  347. package/src/components/common/preview/FileImagePreview.tsx +88 -0
  348. package/src/components/common/preview/FilePdfPreview.tsx +87 -0
  349. package/src/components/common/preview/FileTextPreview.tsx +96 -0
  350. package/src/components/common/preview/FileVideoPreview.tsx +87 -0
  351. package/src/components/common/preview/PlatformFilePreview.tsx +166 -0
  352. package/src/components/common/preview/previewLayout.ts +27 -0
  353. package/src/components/common/render/Each.tsx +42 -0
  354. package/src/components/common/research/EvidenceDossier.tsx +894 -0
  355. package/src/components/common/research/index.ts +7 -0
  356. package/src/components/common/tiles/CatalogCard.tsx +116 -0
  357. package/src/components/common/tiles/Tile.tsx +45 -0
  358. package/src/components/common/tiles/TileGrid.tsx +13 -0
  359. package/src/components/common/typography/Heading.tsx +27 -0
  360. package/src/components/common/typography/Kicker.tsx +30 -0
  361. package/src/components/common/typography/Text.tsx +80 -0
  362. package/src/components/credentials/CredentialsPanel.tsx +169 -0
  363. package/src/components/credentials/credentialsTypes.ts +14 -0
  364. package/src/components/data/DataChart.tsx +913 -0
  365. package/src/components/data/dataTable.ts +99 -0
  366. package/src/components/editor/EditorCommentPromptDialog.tsx +124 -0
  367. package/src/components/editor/EditorLinkPromptDialog.tsx +65 -0
  368. package/src/components/print-design/PrintDesignPanel.test.tsx +1446 -0
  369. package/src/components/print-design/PrintDesignPanel.tsx +3043 -0
  370. package/src/components/print-design/index.ts +9 -0
  371. package/src/components/proofing/PagedProofDialog.tsx +453 -0
  372. package/src/components/proofing/index.ts +4 -0
  373. package/src/editor/DocumentEditor.tsx +28 -0
  374. package/src/editor/EditorViewModeToggle.tsx +35 -0
  375. package/src/editor/index.ts +97 -0
  376. package/src/editor/toolbarOverlays.ts +8 -0
  377. package/src/index.ts +209 -0
  378. package/src/theme/ThemeProvider.tsx +113 -0
  379. package/src/theme/appAccents.ts +87 -0
  380. package/src/theme/buildThemeBridgePayload.ts +23 -0
  381. package/src/theme/composePlatformTheme.test.ts +30 -0
  382. package/src/theme/composePlatformTheme.ts +23 -0
  383. package/src/theme/platformFont.d.mts +7 -0
  384. package/src/theme/platformFont.mjs +20 -0
  385. package/src/theme/platformFont.ts +6 -0
  386. package/src/theme/reportsShellChromeSelectors.ts +136 -0
  387. package/src/theme/styled.d.ts +5 -0
  388. package/src/theme/surfacePalettes.ts +178 -0
  389. package/src/theme/surfaceRoleSelectors.ts +113 -0
  390. package/src/theme/themeVars.ts +13 -0
  391. package/src/theme/themes/dark.ts +59 -0
  392. package/src/theme/themes/light.ts +60 -0
  393. package/src/theme/themes/palette.ts +44 -0
  394. package/src/theme/themes/shared.ts +68 -0
  395. package/src/theme/tokens.ts +114 -0
  396. package/src/theme/utils/GlobalStyles.ts +88 -0
  397. package/src/theme/utils/appFrameDocumentStyles.d.mts +5 -0
  398. package/src/theme/utils/appFrameDocumentStyles.mjs +89 -0
  399. package/src/theme/utils/appFrameDocumentStyles.ts +5 -0
  400. package/src/theme/utils/applyTheme.ts +50 -0
  401. package/src/theme/utils/cssVariables.ts +49 -0
  402. package/src/theme/utils/useInjectThemeIntoIframe.ts +40 -0
  403. package/src/utils/extensionIconMap.ts +123 -0
  404. package/src/utils/generalUtils.ts +157 -0
  405. package/src/writing/DraftAssistantDialog.tsx +279 -0
  406. package/src/writing/WritingStyleManager.tsx +416 -0
  407. package/src/writing/condenseWritingStyle.ts +103 -0
  408. package/src/writing/draftTextWithAgent.ts +134 -0
  409. package/src/writing/index.ts +39 -0
  410. package/src/writing/styleStore.ts +32 -0
  411. package/src/writing/writingStyle.test.ts +122 -0
  412. package/src/writing/writingStyle.ts +217 -0
@@ -0,0 +1,3043 @@
1
+ import { Check, Download, FileImage, WandSparkles, X } from 'lucide-react'
2
+ import type { CSSProperties, ReactNode } from 'react'
3
+ import { useEffect, useId, useMemo, useRef, useState } from 'react'
4
+ import { styled } from 'styled-components'
5
+ import {
6
+ DEFAULT_PURE_RENDER_THEME,
7
+ PRINT_STYLE_PROFILE_SCHEMA,
8
+ PRINT_TOKEN_EXTRACTION_PROMPT,
9
+ PURE_RENDER_THEME_PRESETS,
10
+ buildPrintStyleProfilePrompt,
11
+ compilePrintTheme,
12
+ parsePureRenderThemeJson,
13
+ type PrintStyleProfile,
14
+ type PureRenderTheme,
15
+ } from '../../bridge/pureRender/index.js'
16
+ import { PlatformIcon } from '../chrome/PlatformIcon.js'
17
+ import { PanelTabStrip } from '../chrome/PanelTabStrip.js'
18
+ import { Button } from '../common/buttons/Button.js'
19
+ import { FlexRow } from '../common/containers/flex.js'
20
+ import { TextAreaField } from '../common/inputs/TextAreaField.js'
21
+ import { Text } from '../common/typography/Text.js'
22
+
23
+ export type PrintDesignMode =
24
+ | 'preset'
25
+ | 'vision'
26
+ | 'manual'
27
+ | 'json'
28
+ | 'image'
29
+ | 'description'
30
+ export type PrintDesignPanelVariant = 'sidebar' | 'popover' | 'workspace'
31
+ export type PrintDesignPanelDensity = 'comfortable' | 'compact'
32
+
33
+ export interface PrintDesignThemePreset {
34
+ id: string
35
+ label: string
36
+ theme: PureRenderTheme
37
+ }
38
+
39
+ export interface PrintDesignPanelAction {
40
+ id: string
41
+ label: string
42
+ icon?: ReactNode
43
+ disabled?: boolean
44
+ busy?: boolean
45
+ busyLabel?: string
46
+ variant?: 'primary' | 'subtle' | 'ghost' | 'text'
47
+ onClick: () => void
48
+ }
49
+
50
+ export interface PrintDesignExtractionCapability {
51
+ image?: boolean
52
+ description?: boolean
53
+ reason?: string
54
+ imageReason?: string
55
+ descriptionReason?: string
56
+ }
57
+
58
+ export interface PrintDesignVisionTypesetting {
59
+ enabled?: boolean
60
+ loading?: boolean
61
+ status?: string
62
+ progress?: PrintDesignVisionProgress
63
+ report?: readonly [string, string][]
64
+ evidence?: readonly PrintDesignVisionEvidence[]
65
+ snapshotUrl?: string
66
+ snapshotAlt?: string
67
+ onSnapshotOpen?: () => void
68
+ maxFixesPerIssue?: number
69
+ onMaxFixesPerIssueChange?: (value: number) => void
70
+ exportLabel?: string
71
+ exportBusy?: boolean
72
+ exportDisabled?: boolean
73
+ onToggle?: (enabled: boolean) => void | Promise<void>
74
+ onExport?: () => void
75
+ }
76
+
77
+ export interface PrintDesignVisionProgress {
78
+ state: 'idle' | 'running' | 'complete' | 'stopped' | 'pass'
79
+ label: string
80
+ }
81
+
82
+ export interface PrintDesignVisionEvidence {
83
+ id: string
84
+ label: string
85
+ snapshots?: readonly PrintDesignVisionSnapshot[]
86
+ imageUrl?: string
87
+ imageAlt?: string
88
+ note?: string
89
+ tone?: 'neutral' | 'warning' | 'candidate' | 'pass'
90
+ onOpen?: () => void
91
+ comparison?: PrintDesignVisionComparison
92
+ }
93
+
94
+ export interface PrintDesignVisionComparison {
95
+ title: string
96
+ issueLabel: string
97
+ fixLabel: string
98
+ resultSummary: string
99
+ baseline: PrintDesignVisionComparisonSide
100
+ fixed: PrintDesignVisionComparisonSide
101
+ }
102
+
103
+ export interface PrintDesignVisionComparisonSide {
104
+ label: string
105
+ note?: string
106
+ pdfPath?: string
107
+ snapshots: readonly PrintDesignVisionSnapshot[]
108
+ }
109
+
110
+ export interface PrintDesignVisionSnapshot {
111
+ id?: string
112
+ page: number
113
+ pageCount?: number
114
+ imageUrl: string
115
+ imageAlt?: string
116
+ label?: string
117
+ role?: 'problem' | 'figure' | 'image' | 'caption' | 'sample' | 'candidate'
118
+ onOpen?: () => void
119
+ }
120
+
121
+ export interface PrintDesignPanelProps {
122
+ presets?: readonly PrintDesignThemePreset[]
123
+ value?: PureRenderTheme
124
+ variant?: PrintDesignPanelVariant
125
+ density?: PrintDesignPanelDensity
126
+ headerAccessory?: ReactNode
127
+ modes?: readonly PrintDesignMode[]
128
+ actions?: readonly PrintDesignPanelAction[]
129
+ extractionCapability?: PrintDesignExtractionCapability
130
+ visionTypesetting?: PrintDesignVisionTypesetting
131
+ onThemeChange?: (theme: PureRenderTheme) => void
132
+ onProfileExtractRequest?: (input: {
133
+ mode: Extract<PrintDesignMode, 'image' | 'description'>
134
+ prompt: string
135
+ promptTemplate: string
136
+ schema: typeof PRINT_STYLE_PROFILE_SCHEMA
137
+ modelPrompt: string
138
+ image?: File
139
+ images?: readonly File[]
140
+ }) => Promise<PrintStyleProfile>
141
+ }
142
+
143
+ export interface PrintDesignPopoverProps {
144
+ open: boolean
145
+ children: ReactNode
146
+ title?: ReactNode
147
+ className?: string
148
+ style?: CSSProperties
149
+ onClose?: () => void
150
+ }
151
+
152
+ const DEFAULT_PRESETS: readonly PrintDesignThemePreset[] =
153
+ PURE_RENDER_THEME_PRESETS
154
+
155
+ const DEFAULT_VISION_SAMPLE_REPORT = [
156
+ ['sample', 'No export inspection has run yet.'],
157
+ [
158
+ 'inspect',
159
+ 'The next beta export will render with Paged.js and capture a real page snapshot.',
160
+ ],
161
+ ] as const
162
+
163
+ const MODE_OPTIONS = [
164
+ { value: 'preset', label: 'Design' },
165
+ { value: 'vision', label: 'Beta vision typesetting' },
166
+ { value: 'manual', label: 'Tune' },
167
+ { value: 'image', label: 'Image' },
168
+ { value: 'description', label: 'Describe' },
169
+ { value: 'json', label: 'JSON' },
170
+ ] as const
171
+
172
+ const MAX_IMAGE_BYTES = 8 * 1024 * 1024
173
+ const IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp'])
174
+ const IMAGE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'webp'])
175
+
176
+ function isSupportedImageFile(file: File): boolean {
177
+ if (IMAGE_TYPES.has(file.type)) return true
178
+ if (file.type) return false
179
+ const extension = file.name.split('.').pop()?.toLowerCase()
180
+ return Boolean(extension && IMAGE_EXTENSIONS.has(extension))
181
+ }
182
+
183
+ function evidenceSnapshots(
184
+ evidence: PrintDesignVisionEvidence,
185
+ ): readonly PrintDesignVisionSnapshot[] {
186
+ if (evidence.snapshots?.length) return evidence.snapshots
187
+ if (!evidence.imageUrl) return []
188
+ return [
189
+ {
190
+ page: 1,
191
+ imageUrl: evidence.imageUrl,
192
+ imageAlt: evidence.imageAlt,
193
+ label: evidence.label,
194
+ onOpen: evidence.onOpen,
195
+ },
196
+ ]
197
+ }
198
+
199
+ function snapshotDisplayLabel(
200
+ evidence: PrintDesignVisionEvidence,
201
+ snapshot: PrintDesignVisionSnapshot,
202
+ ): string {
203
+ const role = snapshot.role ? `${snapshot.role} · ` : ''
204
+ const page = snapshot.pageCount
205
+ ? `page ${snapshot.page} of ${snapshot.pageCount}`
206
+ : `page ${snapshot.page}`
207
+ return snapshot.label ?? `${evidence.label} · ${role}${page}`
208
+ }
209
+
210
+ const Panel = styled.section<{
211
+ $variant: PrintDesignPanelVariant
212
+ $density: PrintDesignPanelDensity
213
+ }>`
214
+ --print-design-page-surface: #fff;
215
+ --platform-panel-tab-surface: var(--print-design-page-surface);
216
+
217
+ display: grid;
218
+ gap: ${({ $variant }) =>
219
+ $variant === 'popover'
220
+ ? 'var(--platform-spacing-xs)'
221
+ : 'var(--platform-spacing-sm)'};
222
+ min-width: 0;
223
+ min-height: 0;
224
+ background: var(--print-design-page-surface);
225
+ font-size: var(--platform-typography-font-size-sm);
226
+
227
+ ${({ $variant }) =>
228
+ $variant === 'workspace'
229
+ ? `
230
+ flex: 1 1 auto;
231
+ grid-template-columns: minmax(0, 1fr) minmax(380px, 440px);
232
+ grid-template-rows: auto minmax(0, 1fr);
233
+ align-content: stretch;
234
+ align-items: stretch;
235
+ gap: 0 var(--platform-spacing-md);
236
+ overflow: hidden;
237
+ `
238
+ : $variant === 'popover'
239
+ ? `
240
+ flex: 1 1 auto;
241
+ grid-template-columns: minmax(0, 1fr) minmax(380px, 460px);
242
+ align-content: start;
243
+ align-items: start;
244
+ gap: var(--platform-spacing-md);
245
+ `
246
+ : ''}
247
+
248
+ ${({ $density }) =>
249
+ $density === 'compact'
250
+ ? `
251
+ gap: var(--platform-spacing-xs);
252
+ font-size: var(--platform-typography-font-size-xs);
253
+ `
254
+ : ''}
255
+ `
256
+
257
+ const Popover = styled.div`
258
+ --print-design-page-surface: #fff;
259
+ --platform-panel-tab-surface: var(--print-design-page-surface);
260
+
261
+ display: flex;
262
+ flex-direction: column;
263
+ box-sizing: border-box;
264
+ position: fixed;
265
+ inset: 0;
266
+ z-index: 90;
267
+ width: 100vw;
268
+ height: 100vh;
269
+ transform: none;
270
+ overflow: hidden;
271
+ padding: var(--platform-spacing-sm) var(--platform-spacing-md)
272
+ var(--platform-spacing-sm);
273
+ border: 0;
274
+ border-radius: 0;
275
+ background: var(--print-design-page-surface);
276
+ box-shadow: none;
277
+
278
+ @media (max-width: 720px) {
279
+ padding: var(--platform-spacing-sm);
280
+ }
281
+ `
282
+
283
+ const PopoverChrome = styled.div`
284
+ position: absolute;
285
+ top: var(--platform-spacing-sm);
286
+ right: var(--platform-spacing-sm);
287
+ z-index: 2;
288
+ display: flex;
289
+ justify-content: flex-end;
290
+ `
291
+
292
+ const PopoverIconButton = styled.button`
293
+ display: grid;
294
+ place-items: center;
295
+ width: 26px;
296
+ height: 26px;
297
+ border: none;
298
+ border-radius: var(--platform-radius-sm);
299
+ background: transparent;
300
+ color: var(--platform-colors-text-secondary);
301
+ cursor: pointer;
302
+
303
+ &:hover {
304
+ background: var(--platform-colors-surface-hover);
305
+ color: var(--platform-colors-text);
306
+ }
307
+
308
+ &:focus-visible {
309
+ outline: 1px solid var(--platform-colors-border-strong);
310
+ outline-offset: 2px;
311
+ }
312
+ `
313
+
314
+ const PopoverTitleBar = styled.div`
315
+ display: flex;
316
+ align-items: center;
317
+ min-height: 34px;
318
+ padding: 0 42px var(--platform-spacing-sm) 0;
319
+ border-bottom: 1px solid var(--platform-colors-divider);
320
+ `
321
+
322
+ const PopoverTitle = styled.div`
323
+ margin: 0;
324
+ color: var(--platform-colors-text);
325
+ font-size: var(--platform-typography-font-size-md);
326
+ font-weight: var(--platform-typography-font-weight-semibold);
327
+ line-height: 1.25;
328
+ `
329
+
330
+ const HeaderRow = styled(FlexRow)<{ $variant: PrintDesignPanelVariant }>`
331
+ grid-column: 1 / -1;
332
+ min-width: 0;
333
+ align-items: center;
334
+ padding-right: 42px;
335
+ border-bottom: 1px solid var(--platform-colors-divider);
336
+
337
+ ${({ $variant }) =>
338
+ $variant === 'workspace'
339
+ ? `
340
+ display: grid;
341
+ grid-template-columns: minmax(0, 1fr) minmax(220px, max-content);
342
+ grid-template-areas:
343
+ 'identity actions'
344
+ 'nav actions';
345
+ align-items: end;
346
+ column-gap: var(--platform-spacing-md);
347
+ row-gap: 0;
348
+ min-height: 64px;
349
+ padding: 4px 42px 0 0;
350
+
351
+ @media (max-width: 960px) {
352
+ grid-template-columns: minmax(0, 1fr) auto;
353
+ grid-template-areas:
354
+ 'identity actions'
355
+ 'nav nav';
356
+ row-gap: var(--platform-spacing-xs);
357
+ }
358
+ `
359
+ : ''}
360
+ `
361
+
362
+ const WorkspaceIdentity = styled.div`
363
+ display: flex;
364
+ align-items: center;
365
+ grid-area: identity;
366
+ min-width: 220px;
367
+ max-width: 560px;
368
+ gap: var(--platform-spacing-sm);
369
+ `
370
+
371
+ const WorkspaceTitle = styled.div`
372
+ color: var(--platform-colors-text);
373
+ font-size: var(--platform-typography-font-size-md);
374
+ font-weight: var(--platform-typography-font-weight-semibold);
375
+ line-height: 1.2;
376
+ `
377
+
378
+ const WorkspaceSummary = styled.div`
379
+ display: inline-flex;
380
+ align-items: center;
381
+ gap: var(--platform-spacing-xs);
382
+ min-width: 0;
383
+ color: var(--platform-colors-text-secondary);
384
+ font-size: var(--platform-typography-font-size-xs);
385
+ white-space: nowrap;
386
+
387
+ @media (max-width: 1180px) {
388
+ display: none;
389
+ }
390
+ `
391
+
392
+ const WorkspaceSummaryText = styled.span`
393
+ min-width: 0;
394
+ max-width: 260px;
395
+ overflow: hidden;
396
+ text-overflow: ellipsis;
397
+ `
398
+
399
+ const WorkspaceSummarySwatches = styled.span`
400
+ display: inline-flex;
401
+ align-items: center;
402
+ gap: 4px;
403
+ `
404
+
405
+ const HeaderActions = styled.div`
406
+ grid-area: actions;
407
+ display: inline-flex;
408
+ align-self: center;
409
+ align-items: center;
410
+ justify-self: end;
411
+ gap: var(--platform-spacing-xs);
412
+ min-width: 0;
413
+ margin-left: auto;
414
+ `
415
+
416
+ const HeaderActionButton = styled(Button)`
417
+ flex: 0 0 auto;
418
+ min-height: 28px;
419
+ padding-inline: var(--platform-spacing-sm);
420
+ white-space: nowrap;
421
+ `
422
+
423
+ const SectionTitle = styled.div`
424
+ color: var(--platform-colors-text-secondary);
425
+ font-size: var(--platform-typography-font-size-xs);
426
+ font-weight: var(--platform-typography-font-weight-semibold);
427
+ line-height: 1.2;
428
+ `
429
+
430
+ const DesignSection = styled.section<{ $variant: PrintDesignPanelVariant }>`
431
+ display: grid;
432
+ min-width: 0;
433
+ gap: var(--platform-spacing-sm);
434
+ padding: ${({ $variant }) =>
435
+ $variant === 'popover' || $variant === 'workspace'
436
+ ? '0'
437
+ : 'var(--platform-spacing-sm)'};
438
+ border: ${({ $variant }) =>
439
+ $variant === 'popover' || $variant === 'workspace'
440
+ ? '0'
441
+ : '1px solid var(--platform-colors-divider)'};
442
+ border-radius: ${({ $variant }) =>
443
+ $variant === 'popover' || $variant === 'workspace'
444
+ ? '0'
445
+ : 'var(--platform-radius-md)'};
446
+ background: var(--print-design-page-surface);
447
+
448
+ ${({ $variant }) =>
449
+ $variant === 'popover' || $variant === 'workspace'
450
+ ? `
451
+ padding-top: var(--platform-spacing-md);
452
+ border-top: 1px solid var(--platform-colors-divider);
453
+
454
+ &:first-child {
455
+ padding-top: 0;
456
+ border-top: 0;
457
+ }
458
+ `
459
+ : ''}
460
+ `
461
+
462
+ const SectionHeader = styled.div`
463
+ display: flex;
464
+ align-items: baseline;
465
+ justify-content: space-between;
466
+ min-width: 0;
467
+ gap: var(--platform-spacing-sm);
468
+ `
469
+
470
+ const DesignMain = styled.div<{
471
+ $variant: PrintDesignPanelVariant
472
+ $mode: PrintDesignMode
473
+ }>`
474
+ display: grid;
475
+ min-width: 0;
476
+ min-height: 0;
477
+ gap: var(--platform-spacing-sm);
478
+ background: var(--print-design-page-surface);
479
+
480
+ ${({ $variant, $mode }) =>
481
+ $variant === 'workspace'
482
+ ? `
483
+ grid-column: 1 / 2;
484
+ grid-row: 2 / 3;
485
+ grid-template-columns: ${
486
+ $mode === 'vision'
487
+ ? 'minmax(0, 1fr)'
488
+ : 'minmax(0, 1fr)'
489
+ };
490
+ grid-template-rows: ${
491
+ $mode === 'vision' ? 'minmax(0, 1fr)' : 'minmax(0, 1fr) auto'
492
+ };
493
+ gap: ${$mode === 'vision' ? '0' : 'var(--platform-spacing-md)'};
494
+ min-height: 0;
495
+ overflow: hidden;
496
+ `
497
+ : $variant === 'popover'
498
+ ? `
499
+ align-content: start;
500
+ gap: var(--platform-spacing-md);
501
+ `
502
+ : ''}
503
+ `
504
+
505
+ const WorkspaceControls = styled.div<{
506
+ $variant: PrintDesignPanelVariant
507
+ $mode: PrintDesignMode
508
+ }>`
509
+ ${({ $variant }) =>
510
+ $variant === 'workspace'
511
+ ? `
512
+ display: grid;
513
+ align-content: start;
514
+ gap: var(--platform-spacing-md);
515
+ min-width: 0;
516
+ min-height: 0;
517
+ overflow: auto;
518
+ padding-right: var(--platform-spacing-md);
519
+ border-right: 1px solid var(--platform-colors-divider);
520
+ `
521
+ : `
522
+ display: contents;
523
+ `}
524
+
525
+ ${({ $variant, $mode }) =>
526
+ $variant === 'workspace' && $mode !== 'vision'
527
+ ? `
528
+ grid-column: 1 / 2;
529
+ grid-row: 1 / 2;
530
+ padding-right: var(--platform-spacing-md);
531
+ border-right: 0;
532
+ `
533
+ : ''}
534
+
535
+ ${({ $variant, $mode }) =>
536
+ $variant === 'workspace' && $mode === 'vision'
537
+ ? `
538
+ display: none;
539
+ `
540
+ : ''}
541
+ `
542
+
543
+ const WorkspaceFooter = styled.div<{
544
+ $variant: PrintDesignPanelVariant
545
+ $mode: PrintDesignMode
546
+ }>`
547
+ ${({ $variant }) =>
548
+ $variant === 'workspace'
549
+ ? `
550
+ display: grid;
551
+ align-self: end;
552
+ gap: var(--platform-spacing-sm);
553
+ min-width: 0;
554
+ padding-right: var(--platform-spacing-md);
555
+ border-right: 1px solid var(--platform-colors-divider);
556
+ `
557
+ : `
558
+ display: contents;
559
+ `}
560
+
561
+ ${({ $variant, $mode }) =>
562
+ $variant === 'workspace' && $mode === 'vision'
563
+ ? `
564
+ display: none;
565
+ `
566
+ : ''}
567
+
568
+ ${({ $variant, $mode }) =>
569
+ $variant === 'workspace' && $mode !== 'vision'
570
+ ? `
571
+ grid-column: 1 / 2;
572
+ grid-row: 2 / 3;
573
+ padding-right: var(--platform-spacing-md);
574
+ border-right: 0;
575
+ `
576
+ : ''}
577
+ `
578
+
579
+ const WorkspaceCanvas = styled.div<{
580
+ $variant: PrintDesignPanelVariant
581
+ $mode: PrintDesignMode
582
+ }>`
583
+ ${({ $variant }) =>
584
+ $variant === 'workspace'
585
+ ? `
586
+ display: grid;
587
+ align-content: start;
588
+ gap: var(--platform-spacing-md);
589
+ min-width: 0;
590
+ min-height: 0;
591
+ grid-column: 2 / 3;
592
+ grid-row: 1 / 3;
593
+ overflow: auto;
594
+ padding-right: var(--platform-spacing-xs);
595
+ `
596
+ : `
597
+ display: contents;
598
+ `}
599
+
600
+ ${({ $variant, $mode }) =>
601
+ $variant === 'workspace' && $mode !== 'vision'
602
+ ? `
603
+ display: none;
604
+ `
605
+ : ''}
606
+
607
+ ${({ $variant, $mode }) =>
608
+ $variant === 'workspace' && $mode === 'vision'
609
+ ? `
610
+ grid-column: 1 / 2;
611
+ grid-row: 1 / 2;
612
+ padding-right: var(--platform-spacing-md);
613
+ `
614
+ : ''}
615
+ `
616
+
617
+ const DesignReadout = styled.aside<{ $variant: PrintDesignPanelVariant }>`
618
+ display: grid;
619
+ min-width: 0;
620
+ min-height: 0;
621
+ gap: var(--platform-spacing-sm);
622
+
623
+ ${({ $variant }) =>
624
+ $variant === 'workspace'
625
+ ? `
626
+ grid-column: 2 / 3;
627
+ grid-row: 2 / 3;
628
+ overflow: auto;
629
+ padding-left: var(--platform-spacing-md);
630
+ border-left: 1px solid var(--platform-colors-divider);
631
+ `
632
+ : $variant === 'popover'
633
+ ? `
634
+ position: sticky;
635
+ top: 0;
636
+ max-height: calc(90vh - 104px);
637
+ overflow: auto;
638
+ padding-left: var(--platform-spacing-md);
639
+ border-left: 1px solid var(--platform-colors-divider);
640
+ `
641
+ : ''}
642
+
643
+ ${({ $variant }) =>
644
+ $variant === 'sidebar'
645
+ ? `
646
+ display: contents;
647
+ `
648
+ : ''}
649
+ `
650
+
651
+ const ModeTabs = styled(PanelTabStrip<PrintDesignMode>)`
652
+ grid-area: nav;
653
+ justify-self: start;
654
+ min-width: 0;
655
+ width: fit-content;
656
+ max-width: 100%;
657
+
658
+ @media (max-width: 780px) {
659
+ width: 100%;
660
+ }
661
+ `
662
+
663
+ const Swatch = styled.span<{ $color: string }>`
664
+ width: 10px;
665
+ height: 10px;
666
+ border: 1px solid var(--platform-colors-border);
667
+ border-radius: 3px;
668
+ background: ${({ $color }) => $color};
669
+ `
670
+
671
+ const PresetList = styled.div<{ $variant: PrintDesignPanelVariant }>`
672
+ display: ${({ $variant }) =>
673
+ $variant === 'popover' ? 'flex' : 'grid'};
674
+ grid-template-columns: ${({ $variant }) =>
675
+ $variant === 'workspace'
676
+ ? 'repeat(auto-fit, minmax(172px, 1fr))'
677
+ : 'minmax(0, 1fr)'};
678
+ flex-wrap: wrap;
679
+ gap: ${({ $variant }) =>
680
+ $variant === 'popover' ? 'var(--platform-spacing-xs)' : '4px'};
681
+
682
+ @media (max-width: 720px) {
683
+ display: grid;
684
+ }
685
+ `
686
+
687
+ const PresetOption = styled.button<{
688
+ $active: boolean
689
+ $density: PrintDesignPanelDensity
690
+ $variant: PrintDesignPanelVariant
691
+ }>`
692
+ display: grid;
693
+ grid-template-columns: 1fr auto;
694
+ align-items: center;
695
+ gap: var(--platform-spacing-2xs);
696
+ min-width: 0;
697
+ width: ${({ $variant }) => ($variant === 'popover' ? 'auto' : '100%')};
698
+ flex: ${({ $variant }) =>
699
+ $variant === 'popover' ? '0 1 148px' : '1 1 auto'};
700
+ max-width: ${({ $variant }) => ($variant === 'popover' ? '166px' : 'none')};
701
+ padding: ${({ $density }) =>
702
+ $density === 'compact' ? '3px 8px' : '5px 9px'};
703
+ border: 1px solid var(--platform-colors-border);
704
+ border-color: ${({ $active }) =>
705
+ $active
706
+ ? 'var(--platform-colors-accent, var(--platform-colors-border))'
707
+ : 'var(--platform-colors-border)'};
708
+ border-radius: var(--platform-radius-sm);
709
+ background: var(--print-design-page-surface);
710
+ color: var(--platform-colors-text);
711
+ font: inherit;
712
+ font-size: var(--platform-typography-font-size-xs);
713
+ line-height: 1.25;
714
+ text-align: left;
715
+
716
+ &:hover {
717
+ background: var(--print-design-page-surface);
718
+ }
719
+ `
720
+
721
+ const PresetLabel = styled.span`
722
+ min-width: 0;
723
+ overflow: hidden;
724
+ text-overflow: ellipsis;
725
+ white-space: nowrap;
726
+ `
727
+
728
+ const PresetPreview = styled.span`
729
+ display: inline-flex;
730
+ align-items: center;
731
+ gap: 4px;
732
+ color: var(--platform-colors-text-secondary);
733
+ font-size: var(--platform-typography-font-size-xs);
734
+ `
735
+
736
+ const PreviewLetter = styled.span<{ $family: string }>`
737
+ font-family: ${({ $family }) => $family};
738
+ font-size: 11px;
739
+ line-height: 1;
740
+ `
741
+
742
+ const ControlStack = styled.div`
743
+ display: grid;
744
+ gap: var(--platform-spacing-xs);
745
+ `
746
+
747
+ const ReferenceGrid = styled.div<{ $variant: PrintDesignPanelVariant }>`
748
+ display: grid;
749
+ grid-template-columns: ${({ $variant }) =>
750
+ $variant === 'popover' || $variant === 'workspace'
751
+ ? 'minmax(240px, 0.72fr) minmax(360px, 1.28fr)'
752
+ : 'minmax(0, 1fr)'};
753
+ gap: var(--platform-spacing-sm);
754
+ align-items: start;
755
+
756
+ @media (max-width: 860px) {
757
+ grid-template-columns: minmax(0, 1fr);
758
+ }
759
+ `
760
+
761
+ const ReferencePane = styled.div<{ $variant: PrintDesignPanelVariant }>`
762
+ display: grid;
763
+ min-width: 0;
764
+ gap: var(--platform-spacing-xs);
765
+ align-content: start;
766
+ min-height: ${({ $variant }) => ($variant === 'popover' ? '0' : '128px')};
767
+ padding: ${({ $variant }) =>
768
+ $variant === 'popover' ? '0' : 'var(--platform-spacing-sm)'};
769
+ border: ${({ $variant }) =>
770
+ $variant === 'popover' ? '0' : '1px solid var(--platform-colors-divider)'};
771
+ border-radius: ${({ $variant }) =>
772
+ $variant === 'popover' ? '0' : 'var(--platform-radius-sm)'};
773
+ background: var(--print-design-page-surface);
774
+ `
775
+
776
+ const DescribeStack = styled.div`
777
+ display: grid;
778
+ gap: var(--platform-spacing-xs);
779
+ `
780
+
781
+ const VisionPanel = styled.div<{ $variant: PrintDesignPanelVariant }>`
782
+ display: grid;
783
+ gap: var(--platform-spacing-sm);
784
+ align-content: start;
785
+ min-width: 0;
786
+ min-height: 0;
787
+
788
+ ${({ $variant }) =>
789
+ $variant === 'workspace'
790
+ ? `
791
+ grid-template-columns: minmax(0, 1fr);
792
+ align-items: start;
793
+ gap: var(--platform-spacing-sm);
794
+ `
795
+ : ''}
796
+ `
797
+
798
+ const VisionSettingsRail = styled.div<{ $variant: PrintDesignPanelVariant }>`
799
+ display: grid;
800
+ gap: var(--platform-spacing-sm);
801
+ min-width: 0;
802
+ align-content: start;
803
+
804
+ ${({ $variant }) =>
805
+ $variant === 'workspace'
806
+ ? `
807
+ padding: 0 0 var(--platform-spacing-sm);
808
+ border-bottom: 1px solid var(--platform-colors-divider);
809
+ `
810
+ : ''}
811
+ `
812
+
813
+ const VisionEvidenceColumn = styled.div`
814
+ display: grid;
815
+ gap: var(--platform-spacing-sm);
816
+ min-width: 0;
817
+ min-height: 0;
818
+ align-content: start;
819
+ `
820
+
821
+ const VisionSettingsStrip = styled.div`
822
+ display: flex;
823
+ flex-wrap: wrap;
824
+ align-items: center;
825
+ gap: var(--platform-spacing-sm);
826
+ min-width: 0;
827
+ `
828
+
829
+ const VisionToggleRow = styled.label`
830
+ display: grid;
831
+ grid-template-columns: minmax(0, 1fr) auto;
832
+ align-items: start;
833
+ gap: var(--platform-spacing-sm);
834
+ min-width: 0;
835
+ flex: 1 1 280px;
836
+ padding: 0;
837
+ border: 1px solid var(--platform-colors-border);
838
+ border: 0;
839
+ border-radius: 0;
840
+ background: var(--print-design-page-surface);
841
+ color: var(--platform-colors-text);
842
+ font-size: var(--platform-typography-font-size-xs);
843
+ `
844
+
845
+ const VisionToggleCopy = styled.span`
846
+ display: grid;
847
+ min-width: 0;
848
+ gap: 2px;
849
+ `
850
+
851
+ const VisionToggleTitle = styled.span`
852
+ font-weight: var(--platform-typography-font-weight-semibold);
853
+ `
854
+
855
+ const VisionToggleStatus = styled.span`
856
+ color: var(--platform-colors-text-secondary);
857
+ font-size: var(--platform-typography-font-size-xs);
858
+ `
859
+
860
+ const VisionCheckbox = styled.input`
861
+ flex: 0 0 auto;
862
+ width: 18px;
863
+ height: 18px;
864
+ accent-color: var(--platform-colors-accent, var(--platform-colors-text));
865
+ `
866
+
867
+ const VisionToggleControls = styled.span`
868
+ display: inline-flex;
869
+ align-items: center;
870
+ gap: var(--platform-spacing-sm);
871
+ flex: 0 0 auto;
872
+ `
873
+
874
+ const VisionProgressStrip = styled.div<{
875
+ $state: PrintDesignVisionProgress['state']
876
+ }>`
877
+ display: flex;
878
+ align-items: center;
879
+ gap: var(--platform-spacing-xs);
880
+ min-width: 0;
881
+ padding: 8px 10px;
882
+ border: 1px solid
883
+ ${({ $state }) =>
884
+ $state === 'running'
885
+ ? '#9eb9df'
886
+ : $state === 'stopped'
887
+ ? '#dfad59'
888
+ : $state === 'complete' || $state === 'pass'
889
+ ? '#8cc7a0'
890
+ : 'var(--platform-colors-border)'};
891
+ border-radius: var(--platform-radius-sm);
892
+ background: ${({ $state }) =>
893
+ $state === 'running'
894
+ ? '#f8fbff'
895
+ : $state === 'stopped'
896
+ ? '#fffaf0'
897
+ : $state === 'complete' || $state === 'pass'
898
+ ? '#f8fff9'
899
+ : '#fff'};
900
+ color: var(--platform-colors-text);
901
+ font-size: var(--platform-typography-font-size-sm);
902
+ `
903
+
904
+ const VisionProgressState = styled.span<{
905
+ $state: PrintDesignVisionProgress['state']
906
+ }>`
907
+ flex: 0 0 auto;
908
+ color: ${({ $state }) =>
909
+ $state === 'running'
910
+ ? '#426489'
911
+ : $state === 'stopped'
912
+ ? '#8a5a10'
913
+ : $state === 'complete' || $state === 'pass'
914
+ ? '#2f744b'
915
+ : 'var(--platform-colors-text-secondary)'};
916
+ font-size: var(--platform-typography-font-size-xs);
917
+ font-weight: var(--platform-typography-font-weight-semibold);
918
+ text-transform: uppercase;
919
+ `
920
+
921
+ const VisionProgressLabel = styled.span`
922
+ min-width: 0;
923
+ overflow: hidden;
924
+ text-overflow: ellipsis;
925
+ white-space: nowrap;
926
+ `
927
+
928
+ const VisionMaxFixesField = styled.label`
929
+ display: inline-flex;
930
+ align-items: center;
931
+ gap: var(--platform-spacing-2xs);
932
+ flex: 0 0 auto;
933
+ color: var(--platform-colors-text-secondary);
934
+ font-size: var(--platform-typography-font-size-xs);
935
+ `
936
+
937
+ const VisionMaxFixesInput = styled.input`
938
+ width: 46px;
939
+ height: 26px;
940
+ box-sizing: border-box;
941
+ padding: 0 6px;
942
+ border: 1px solid var(--platform-colors-border);
943
+ border-radius: var(--platform-radius-xs);
944
+ background: #fff;
945
+ color: var(--platform-colors-text);
946
+ font: inherit;
947
+ `
948
+
949
+ const VisionSnapshotGrid = styled.div<{ $hasEvidence: boolean }>`
950
+ display: grid;
951
+ grid-template-columns: ${({ $hasEvidence }) =>
952
+ $hasEvidence ? 'minmax(0, 1fr)' : 'minmax(260px, 340px) minmax(0, 1fr)'};
953
+ gap: var(--platform-spacing-md);
954
+ align-items: start;
955
+
956
+ @media (max-width: 780px) {
957
+ grid-template-columns: minmax(0, 1fr);
958
+ }
959
+ `
960
+
961
+ const VisionSnapshotFrame = styled.div`
962
+ display: grid;
963
+ min-width: 0;
964
+ gap: var(--platform-spacing-xs);
965
+ `
966
+
967
+ const VisionEvidenceList = styled.div`
968
+ display: grid;
969
+ gap: var(--platform-spacing-sm);
970
+ max-height: calc(100vh - 236px);
971
+ padding-right: var(--platform-spacing-2xs);
972
+ overflow: auto;
973
+ `
974
+
975
+ const VisionEvidenceCard = styled.div<{
976
+ $tone: NonNullable<PrintDesignVisionEvidence['tone']>
977
+ }>`
978
+ display: grid;
979
+ grid-template-columns: minmax(300px, 0.78fr) minmax(260px, 0.62fr);
980
+ gap: var(--platform-spacing-md);
981
+ align-items: start;
982
+ padding: var(--platform-spacing-sm) 0 var(--platform-spacing-sm)
983
+ var(--platform-spacing-sm);
984
+ border-left: 2px solid
985
+ ${({ $tone }) =>
986
+ $tone === 'warning'
987
+ ? '#d38a16'
988
+ : $tone === 'candidate'
989
+ ? '#3268c7'
990
+ : $tone === 'pass'
991
+ ? '#2f8b57'
992
+ : 'var(--platform-colors-border)'};
993
+ background: #fff;
994
+
995
+ @media (max-width: 640px) {
996
+ grid-template-columns: minmax(0, 1fr);
997
+ }
998
+ `
999
+
1000
+ const VisionEvidenceCaption = styled.div`
1001
+ display: grid;
1002
+ gap: var(--platform-spacing-2xs);
1003
+ min-width: 0;
1004
+ padding-top: var(--platform-spacing-2xs);
1005
+ `
1006
+
1007
+ const VisionEvidenceTitle = styled.div`
1008
+ color: var(--platform-colors-text);
1009
+ font-size: var(--platform-typography-font-size-xs);
1010
+ font-weight: var(--platform-typography-font-weight-semibold);
1011
+ line-height: 1.25;
1012
+ `
1013
+
1014
+ const VisionEvidenceNote = styled.div`
1015
+ color: var(--platform-colors-text-secondary);
1016
+ font-size: var(--platform-typography-font-size-xs);
1017
+ line-height: 1.35;
1018
+ max-width: 48ch;
1019
+ `
1020
+
1021
+ const VisionEvidenceActions = styled.div`
1022
+ display: flex;
1023
+ flex-wrap: wrap;
1024
+ gap: var(--platform-spacing-2xs);
1025
+ padding-top: var(--platform-spacing-2xs);
1026
+ `
1027
+
1028
+ const VisionEvidenceActionButton = styled.button`
1029
+ appearance: none;
1030
+ display: inline-flex;
1031
+ align-items: center;
1032
+ min-height: 24px;
1033
+ padding: 2px 10px;
1034
+ border: 1px solid #8cc7a0;
1035
+ border-radius: 999px;
1036
+ background: #f8fff9;
1037
+ color: #2f744b;
1038
+ font: inherit;
1039
+ font-size: var(--platform-typography-font-size-xs);
1040
+ cursor: pointer;
1041
+ `
1042
+
1043
+ const VisionCarousel = styled.div`
1044
+ display: grid;
1045
+ gap: var(--platform-spacing-2xs);
1046
+ min-width: 0;
1047
+ `
1048
+
1049
+ const VisionCarouselControls = styled.div`
1050
+ display: flex;
1051
+ align-items: center;
1052
+ gap: var(--platform-spacing-2xs);
1053
+ min-width: 0;
1054
+ `
1055
+
1056
+ const VisionCarouselButton = styled.button`
1057
+ appearance: none;
1058
+ display: inline-grid;
1059
+ place-items: center;
1060
+ width: 24px;
1061
+ height: 24px;
1062
+ padding: 0;
1063
+ border: 1px solid var(--platform-colors-border);
1064
+ border-radius: 999px;
1065
+ background: #fff;
1066
+ color: var(--platform-colors-text);
1067
+ cursor: pointer;
1068
+
1069
+ &:disabled {
1070
+ cursor: default;
1071
+ opacity: 0.38;
1072
+ }
1073
+ `
1074
+
1075
+ const VisionCarouselLabel = styled.div`
1076
+ flex: 1 1 auto;
1077
+ min-width: 0;
1078
+ color: var(--platform-colors-text-secondary);
1079
+ font-size: var(--platform-typography-font-size-xs);
1080
+ overflow: hidden;
1081
+ text-overflow: ellipsis;
1082
+ white-space: nowrap;
1083
+ `
1084
+
1085
+ const VisionCarouselChips = styled.div`
1086
+ display: flex;
1087
+ flex-wrap: wrap;
1088
+ gap: 4px;
1089
+ `
1090
+
1091
+ const VisionCarouselChip = styled.button<{ $active: boolean }>`
1092
+ appearance: none;
1093
+ min-width: 24px;
1094
+ height: 20px;
1095
+ padding: 0 7px;
1096
+ border: 1px solid
1097
+ ${({ $active }) =>
1098
+ $active
1099
+ ? 'var(--platform-colors-accent, var(--platform-colors-text))'
1100
+ : 'var(--platform-colors-border)'};
1101
+ border-radius: 999px;
1102
+ background: ${({ $active }) =>
1103
+ $active
1104
+ ? 'rgb(from var(--platform-colors-accent, #3454d1) r g b / 0.08)'
1105
+ : '#fff'};
1106
+ color: var(--platform-colors-text-secondary);
1107
+ font-size: var(--platform-typography-font-size-xs);
1108
+ cursor: pointer;
1109
+ `
1110
+
1111
+ const VisionSnapshotLabel = styled.div`
1112
+ color: var(--platform-colors-text-secondary);
1113
+ font-size: var(--platform-typography-font-size-xs);
1114
+ font-weight: var(--platform-typography-font-weight-semibold);
1115
+ `
1116
+
1117
+ const VisionSnapshotPage = styled.div`
1118
+ position: relative;
1119
+ display: grid;
1120
+ gap: 7px;
1121
+ aspect-ratio: 0.72;
1122
+ width: min(100%, 300px);
1123
+ padding: 20px 18px;
1124
+ border: 1px solid var(--platform-colors-border);
1125
+ border-radius: var(--platform-radius-sm);
1126
+ background: #fff;
1127
+ box-shadow: 0 10px 28px rgb(from var(--platform-colors-text) r g b / 0.08);
1128
+ `
1129
+
1130
+ const VisionSnapshotImage = styled.img`
1131
+ display: block;
1132
+ width: 100%;
1133
+ max-height: min(680px, calc(100vh - 280px));
1134
+ object-fit: contain;
1135
+ border: 1px solid var(--platform-colors-border);
1136
+ border-radius: var(--platform-radius-sm);
1137
+ background: #fff;
1138
+ `
1139
+
1140
+ const VisionSnapshotButton = styled.button`
1141
+ appearance: none;
1142
+ display: inline-flex;
1143
+ width: 100%;
1144
+ padding: 0;
1145
+ border: 0;
1146
+ background: transparent;
1147
+ cursor: pointer;
1148
+
1149
+ &:focus-visible {
1150
+ outline: 2px solid var(--platform-colors-accent);
1151
+ outline-offset: 3px;
1152
+ }
1153
+ `
1154
+
1155
+ const VisionPageHeading = styled.div`
1156
+ width: 72%;
1157
+ height: 12px;
1158
+ border-radius: 2px;
1159
+ background: var(--platform-colors-text);
1160
+ `
1161
+
1162
+ const VisionPageLine = styled.div<{ $width?: string }>`
1163
+ width: ${({ $width }) => $width ?? '100%'};
1164
+ height: 4px;
1165
+ border-radius: 2px;
1166
+ background: rgb(from var(--platform-colors-text) r g b / 0.34);
1167
+ `
1168
+
1169
+ const VisionPageObject = styled.div`
1170
+ width: 78%;
1171
+ height: 54px;
1172
+ margin: 5px auto 2px;
1173
+ border: 1px solid var(--platform-colors-border);
1174
+ border-radius: 3px;
1175
+ background: linear-gradient(
1176
+ 135deg,
1177
+ rgb(from var(--platform-colors-accent, #3454d1) r g b / 0.16),
1178
+ rgb(from var(--platform-colors-accent, #3454d1) r g b / 0.04)
1179
+ ),
1180
+ #fff;
1181
+ `
1182
+
1183
+ const VisionSnapshotCopy = styled.div`
1184
+ display: grid;
1185
+ gap: var(--platform-spacing-xs);
1186
+ color: var(--platform-colors-text-secondary);
1187
+ font-size: var(--platform-typography-font-size-xs);
1188
+ line-height: 1.45;
1189
+ `
1190
+
1191
+ const VisionSnapshotTitle = styled.div`
1192
+ color: var(--platform-colors-text);
1193
+ font-size: var(--platform-typography-font-size-sm);
1194
+ font-weight: var(--platform-typography-font-weight-semibold);
1195
+ `
1196
+
1197
+ const ImageChoiceButton = styled(Button)`
1198
+ width: 100%;
1199
+ justify-content: center;
1200
+ `
1201
+
1202
+ const ImageSelectionMeta = styled.div`
1203
+ display: flex;
1204
+ align-items: center;
1205
+ gap: var(--platform-spacing-2xs);
1206
+ min-width: 0;
1207
+ color: var(--platform-colors-text-secondary);
1208
+ font-size: var(--platform-typography-font-size-sm);
1209
+ line-height: var(--platform-typography-line-height-base);
1210
+ `
1211
+
1212
+ const ImageSelectionName = styled.span`
1213
+ min-width: 0;
1214
+ overflow: hidden;
1215
+ text-overflow: ellipsis;
1216
+ white-space: nowrap;
1217
+ `
1218
+
1219
+ const ManualGrid = styled.div<{
1220
+ $density: PrintDesignPanelDensity
1221
+ $variant: PrintDesignPanelVariant
1222
+ }>`
1223
+ display: grid;
1224
+ grid-template-columns: ${({ $density, $variant }) =>
1225
+ $variant === 'popover'
1226
+ ? 'repeat(3, minmax(132px, 156px))'
1227
+ : $variant === 'workspace'
1228
+ ? 'repeat(3, minmax(160px, 1fr))'
1229
+ : $density === 'compact'
1230
+ ? 'repeat(2, minmax(0, 1fr))'
1231
+ : 'repeat(3, minmax(0, 1fr))'};
1232
+ gap: ${({ $density, $variant }) =>
1233
+ $variant === 'popover'
1234
+ ? 'var(--platform-spacing-xs) var(--platform-spacing-sm)'
1235
+ : $density === 'compact'
1236
+ ? 'var(--platform-spacing-xs)'
1237
+ : 'var(--platform-spacing-sm)'};
1238
+ justify-content: start;
1239
+ max-width: ${({ $variant }) => ($variant === 'popover' ? '504px' : 'none')};
1240
+
1241
+ @media (max-width: 980px) {
1242
+ grid-template-columns: ${({ $variant }) =>
1243
+ $variant === 'popover'
1244
+ ? 'repeat(2, minmax(132px, 156px))'
1245
+ : 'repeat(2, minmax(0, 1fr))'};
1246
+ }
1247
+
1248
+ @media (max-width: 560px) {
1249
+ grid-template-columns: minmax(0, 1fr);
1250
+ }
1251
+ `
1252
+
1253
+ const ManualField = styled.label<{ $span?: 'wide' }>`
1254
+ display: grid;
1255
+ min-width: 0;
1256
+ gap: 3px;
1257
+ color: var(--platform-colors-text-secondary);
1258
+ font-size: var(--platform-typography-font-size-xs);
1259
+ line-height: 1.2;
1260
+
1261
+ ${({ $span }) =>
1262
+ $span === 'wide'
1263
+ ? `
1264
+ grid-column: span 2;
1265
+
1266
+ @media (max-width: 560px) {
1267
+ grid-column: auto;
1268
+ }
1269
+ `
1270
+ : ''}
1271
+ `
1272
+
1273
+ const ManualInput = styled.input`
1274
+ width: 100%;
1275
+ min-width: 0;
1276
+ min-height: 28px;
1277
+ padding: 0 7px;
1278
+ border: 1px solid var(--platform-colors-border);
1279
+ border-radius: var(--platform-radius-sm);
1280
+ background: var(--print-design-page-surface);
1281
+ color: var(--platform-colors-text);
1282
+ font: inherit;
1283
+ font-size: var(--platform-typography-font-size-xs);
1284
+ line-height: 1.2;
1285
+
1286
+ &[type='color'] {
1287
+ height: 28px;
1288
+ padding: 2px;
1289
+ }
1290
+ `
1291
+
1292
+ const ManualSelect = styled.select`
1293
+ width: 100%;
1294
+ min-width: 0;
1295
+ min-height: 28px;
1296
+ padding: 0 7px;
1297
+ border: 1px solid var(--platform-colors-border);
1298
+ border-radius: var(--platform-radius-sm);
1299
+ background: var(--print-design-page-surface);
1300
+ color: var(--platform-colors-text);
1301
+ font: inherit;
1302
+ font-size: var(--platform-typography-font-size-xs);
1303
+ line-height: 1.2;
1304
+ `
1305
+
1306
+ const ReferenceActionRow = styled.div`
1307
+ display: flex;
1308
+ flex-wrap: wrap;
1309
+ align-items: center;
1310
+ gap: var(--platform-spacing-xs);
1311
+ `
1312
+
1313
+ const ReferenceButton = styled(Button)`
1314
+ flex: 0 1 auto;
1315
+ `
1316
+
1317
+ const CompactTextArea = styled(TextAreaField)`
1318
+ font-size: var(--platform-typography-font-size-sm);
1319
+
1320
+ textarea {
1321
+ min-height: 0;
1322
+ }
1323
+ `
1324
+
1325
+ const FooterRow = styled(FlexRow)`
1326
+ min-width: 0;
1327
+ `
1328
+
1329
+ const ActionRow = styled(FlexRow)<{ $variant: PrintDesignPanelVariant }>`
1330
+ min-width: 0;
1331
+ padding-top: var(--platform-spacing-2xs);
1332
+ border-top: 1px solid var(--platform-colors-divider);
1333
+ flex-wrap: wrap;
1334
+ `
1335
+
1336
+ const ActionButton = styled(Button)`
1337
+ flex: 1 1 auto;
1338
+ min-width: 0;
1339
+ white-space: nowrap;
1340
+ `
1341
+
1342
+ const StatusText = styled.span`
1343
+ min-width: 0;
1344
+ overflow: hidden;
1345
+ text-overflow: ellipsis;
1346
+ white-space: nowrap;
1347
+ color: var(--platform-colors-text-secondary);
1348
+ font-size: var(--platform-typography-font-size-sm);
1349
+ line-height: var(--platform-typography-line-height-base);
1350
+ `
1351
+
1352
+ const DecisionStream = styled.div`
1353
+ color: var(--platform-colors-text-secondary);
1354
+ font-size: var(--platform-typography-font-size-xs);
1355
+ padding-top: var(--platform-spacing-2xs);
1356
+ `
1357
+
1358
+ const DecisionList = styled.div`
1359
+ display: grid;
1360
+ gap: var(--platform-spacing-2xs);
1361
+ max-height: calc(100vh - 190px);
1362
+ margin-top: var(--platform-spacing-xs);
1363
+ padding-right: var(--platform-spacing-2xs);
1364
+ overflow: auto;
1365
+ overflow-wrap: break-word;
1366
+ `
1367
+
1368
+ const DecisionItem = styled.div<{ $type: string; $startsGroup: boolean }>`
1369
+ display: grid;
1370
+ grid-template-columns: 54px minmax(0, 1fr);
1371
+ align-items: start;
1372
+ column-gap: var(--platform-spacing-sm);
1373
+ padding: ${({ $startsGroup }) =>
1374
+ $startsGroup ? '8px 0 6px' : '5px 0 6px'};
1375
+ border-top: 1px solid var(--platform-colors-divider);
1376
+ background: ${({ $type }) => decisionBackground($type)};
1377
+
1378
+ &:first-child {
1379
+ padding: 7px 0 8px;
1380
+ border-top: 0;
1381
+ border-bottom: 1px solid var(--platform-colors-divider);
1382
+ background: ${({ $type }) => currentDecisionBackground($type)};
1383
+
1384
+ > span:first-child {
1385
+ box-shadow: 0 0 0 1px ${({ $type }) => decisionBorder($type)} inset;
1386
+ }
1387
+ }
1388
+ `
1389
+
1390
+ const DecisionGroupLabel = styled.div`
1391
+ grid-column: 1 / -1;
1392
+ margin-bottom: 3px;
1393
+ color: var(--platform-colors-text);
1394
+ font-size: 10px;
1395
+ font-weight: var(--platform-typography-font-weight-semibold);
1396
+ letter-spacing: 0;
1397
+ line-height: 1.2;
1398
+ `
1399
+
1400
+ const DecisionType = styled.span<{ $type: string }>`
1401
+ display: inline-flex;
1402
+ align-items: center;
1403
+ justify-content: center;
1404
+ width: 54px;
1405
+ max-width: 54px;
1406
+ min-height: 18px;
1407
+ padding: 1px 4px;
1408
+ border: 1px solid ${({ $type }) => decisionBorder($type)};
1409
+ border-radius: var(--platform-radius-sm);
1410
+ color: ${({ $type }) => decisionText($type)};
1411
+ background: ${({ $type }) => decisionBadgeBackground($type)};
1412
+ font-size: 8.5px;
1413
+ line-height: 1.25;
1414
+ text-align: center;
1415
+ text-transform: uppercase;
1416
+ overflow: hidden;
1417
+ text-overflow: ellipsis;
1418
+ white-space: nowrap;
1419
+ `
1420
+
1421
+ const DecisionMessage = styled(Text)`
1422
+ min-width: 0;
1423
+ display: block;
1424
+ padding-top: 1px;
1425
+ color: var(--platform-colors-text-secondary);
1426
+ font-size: var(--platform-typography-font-size-xs);
1427
+ line-height: 1.55;
1428
+ overflow-wrap: break-word;
1429
+ word-break: break-word;
1430
+ white-space: normal;
1431
+ `
1432
+
1433
+ const DecisionColor = styled.span<{ $color: string }>`
1434
+ display: inline-flex;
1435
+ align-items: center;
1436
+ gap: 3px;
1437
+ margin: 0 2px;
1438
+ white-space: nowrap;
1439
+
1440
+ &::before {
1441
+ content: '';
1442
+ width: 0.75em;
1443
+ height: 0.75em;
1444
+ border: 1px solid var(--platform-colors-border);
1445
+ border-radius: 2px;
1446
+ background: ${({ $color }) => $color};
1447
+ }
1448
+ `
1449
+
1450
+ const DecisionLink = styled.a`
1451
+ color: var(--platform-colors-accent);
1452
+ text-decoration: none;
1453
+
1454
+ &:hover {
1455
+ text-decoration: underline;
1456
+ }
1457
+ `
1458
+
1459
+ const HiddenFile = styled.input`
1460
+ width: 1px;
1461
+ height: 1px;
1462
+ opacity: 0;
1463
+ position: absolute;
1464
+ pointer-events: none;
1465
+ `
1466
+
1467
+ export function PrintDesignPopover({
1468
+ open,
1469
+ children,
1470
+ title = 'Design - PDF Typesetter',
1471
+ className,
1472
+ style,
1473
+ onClose,
1474
+ }: PrintDesignPopoverProps): React.ReactElement | null {
1475
+ const popoverRef = useRef<HTMLDivElement | null>(null)
1476
+
1477
+ useEffect(() => {
1478
+ if (!open || !onClose) return
1479
+ const closeOnOutsidePointerDown = (event: PointerEvent): void => {
1480
+ const target = event.target
1481
+ if (target instanceof Node && popoverRef.current?.contains(target)) {
1482
+ return
1483
+ }
1484
+ onClose()
1485
+ }
1486
+ window.addEventListener('pointerdown', closeOnOutsidePointerDown)
1487
+ return () =>
1488
+ window.removeEventListener('pointerdown', closeOnOutsidePointerDown)
1489
+ }, [onClose, open])
1490
+
1491
+ if (!open) return null
1492
+ return (
1493
+ <Popover
1494
+ ref={popoverRef}
1495
+ className={className}
1496
+ style={style}
1497
+ onClick={event => event.stopPropagation()}
1498
+ >
1499
+ <PopoverChrome>
1500
+ {onClose ? (
1501
+ <PopoverIconButton
1502
+ type="button"
1503
+ title="Close print design"
1504
+ aria-label="Close print design"
1505
+ onClick={onClose}
1506
+ >
1507
+ <X size={14} aria-hidden="true" />
1508
+ </PopoverIconButton>
1509
+ ) : null}
1510
+ </PopoverChrome>
1511
+ {title ? (
1512
+ <PopoverTitleBar>
1513
+ <PopoverTitle>{title}</PopoverTitle>
1514
+ </PopoverTitleBar>
1515
+ ) : null}
1516
+ {children}
1517
+ </Popover>
1518
+ )
1519
+ }
1520
+
1521
+ async function sampleImageThemeHints(file: File): Promise<
1522
+ | {
1523
+ ink: string
1524
+ paper: string
1525
+ accent: string
1526
+ }
1527
+ | undefined
1528
+ > {
1529
+ if (typeof createImageBitmap !== 'function') return undefined
1530
+ const bitmap = await createImageBitmap(file).catch(() => undefined)
1531
+ if (!bitmap) return undefined
1532
+ try {
1533
+ const size = 48
1534
+ const canvas = document.createElement('canvas')
1535
+ canvas.width = size
1536
+ canvas.height = size
1537
+ const context = canvas.getContext('2d', { willReadFrequently: true })
1538
+ if (!context) return undefined
1539
+ context.drawImage(bitmap, 0, 0, size, size)
1540
+ const pixels = context.getImageData(0, 0, size, size).data
1541
+ const colors: Array<{
1542
+ hex: string
1543
+ luminance: number
1544
+ saturation: number
1545
+ }> = []
1546
+ for (let index = 0; index < pixels.length; index += 16) {
1547
+ const alpha = pixels[index + 3] ?? 0
1548
+ if (alpha < 180) continue
1549
+ const red = pixels[index] ?? 0
1550
+ const green = pixels[index + 1] ?? 0
1551
+ const blue = pixels[index + 2] ?? 0
1552
+ colors.push({
1553
+ hex: rgbToHex(red, green, blue),
1554
+ luminance: relativeLuminance(red, green, blue),
1555
+ saturation: saturation(red, green, blue),
1556
+ })
1557
+ }
1558
+ if (colors.length < 3) return undefined
1559
+ const sorted = [...colors].sort((a, b) => a.luminance - b.luminance)
1560
+ const accent =
1561
+ [...colors].sort((a, b) => b.saturation - a.saturation)[0]?.hex ??
1562
+ DEFAULT_PURE_RENDER_THEME.color.accent
1563
+ return {
1564
+ ink: sorted[0]?.hex ?? DEFAULT_PURE_RENDER_THEME.color.ink,
1565
+ paper:
1566
+ sorted[sorted.length - 1]?.hex ??
1567
+ DEFAULT_PURE_RENDER_THEME.color.paper ??
1568
+ '#FFFFFF',
1569
+ accent,
1570
+ }
1571
+ } finally {
1572
+ bitmap.close()
1573
+ }
1574
+ }
1575
+
1576
+ function rgbToHex(red: number, green: number, blue: number): string {
1577
+ return `#${[red, green, blue]
1578
+ .map(channel =>
1579
+ Math.max(0, Math.min(255, channel)).toString(16).padStart(2, '0'),
1580
+ )
1581
+ .join('')}`
1582
+ }
1583
+
1584
+ function relativeLuminance(red: number, green: number, blue: number): number {
1585
+ return (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255
1586
+ }
1587
+
1588
+ function saturation(red: number, green: number, blue: number): number {
1589
+ const max = Math.max(red, green, blue)
1590
+ const min = Math.min(red, green, blue)
1591
+ return max === 0 ? 0 : (max - min) / max
1592
+ }
1593
+
1594
+ function fontStack(
1595
+ role: 'serif' | 'sans',
1596
+ purpose: 'display' | 'body' | 'label',
1597
+ ): string {
1598
+ if (role === 'sans') return '"Source Sans 3", system-ui, sans-serif'
1599
+ if (purpose === 'display') return '"Newsreader", Georgia, serif'
1600
+ return '"Source Serif 4", Georgia, serif'
1601
+ }
1602
+
1603
+ function themeMatchesPreset(
1604
+ theme: PureRenderTheme,
1605
+ presetTheme: PureRenderTheme,
1606
+ ): boolean {
1607
+ return (
1608
+ theme.name === presetTheme.name &&
1609
+ theme.color.ink === presetTheme.color.ink &&
1610
+ theme.color.accent === presetTheme.color.accent &&
1611
+ (theme.color.paper ?? theme.color.surface) ===
1612
+ (presetTheme.color.paper ?? presetTheme.color.surface) &&
1613
+ theme.type.fonts.display === presetTheme.type.fonts.display &&
1614
+ theme.type.fonts.serif === presetTheme.type.fonts.serif &&
1615
+ theme.type.fonts.sans === presetTheme.type.fonts.sans &&
1616
+ theme.type.measure === presetTheme.type.measure &&
1617
+ theme.type.lead === presetTheme.type.lead &&
1618
+ theme.setting?.align === presetTheme.setting?.align
1619
+ )
1620
+ }
1621
+
1622
+ function paperColorForTheme(theme: PureRenderTheme): string {
1623
+ return theme.color.paper ?? theme.color.surface ?? '#FFFFFF'
1624
+ }
1625
+
1626
+ function fontRoleForFamily(family: string | undefined): 'serif' | 'sans' {
1627
+ return /sans|inter|arial|helvetica|system-ui/i.test(family ?? '')
1628
+ ? 'sans'
1629
+ : 'serif'
1630
+ }
1631
+
1632
+ function manualAlignForTheme(
1633
+ align: string | undefined,
1634
+ ): 'justified' | 'flush-left' {
1635
+ return align === 'flush-left' ? 'flush-left' : 'justified'
1636
+ }
1637
+
1638
+ function VisionEvidenceCarousel({
1639
+ evidence,
1640
+ }: {
1641
+ evidence: PrintDesignVisionEvidence
1642
+ }): React.ReactElement | null {
1643
+ const snapshots = evidenceSnapshots(evidence)
1644
+ const [activeIndex, setActiveIndex] = useState(0)
1645
+ const clampedIndex = Math.min(activeIndex, Math.max(snapshots.length - 1, 0))
1646
+ const active = snapshots[clampedIndex]
1647
+
1648
+ useEffect(() => {
1649
+ if (activeIndex > snapshots.length - 1) {
1650
+ setActiveIndex(Math.max(snapshots.length - 1, 0))
1651
+ }
1652
+ }, [activeIndex, snapshots.length])
1653
+
1654
+ if (!active) return null
1655
+
1656
+ const image = (
1657
+ <VisionSnapshotImage
1658
+ src={active.imageUrl}
1659
+ alt={active.imageAlt ?? `${evidence.label} page snapshot`}
1660
+ />
1661
+ )
1662
+
1663
+ return (
1664
+ <VisionCarousel>
1665
+ {active.onOpen ? (
1666
+ <VisionSnapshotButton
1667
+ type="button"
1668
+ onClick={active.onOpen}
1669
+ aria-label={`Open ${snapshotDisplayLabel(evidence, active)}`}
1670
+ >
1671
+ {image}
1672
+ </VisionSnapshotButton>
1673
+ ) : (
1674
+ image
1675
+ )}
1676
+ {snapshots.length > 1 ? (
1677
+ <>
1678
+ <VisionCarouselControls>
1679
+ <VisionCarouselButton
1680
+ type="button"
1681
+ disabled={clampedIndex === 0}
1682
+ aria-label={`Previous page for ${evidence.label}`}
1683
+ onClick={() => setActiveIndex(Math.max(clampedIndex - 1, 0))}
1684
+ >
1685
+
1686
+ </VisionCarouselButton>
1687
+ <VisionCarouselLabel>
1688
+ {snapshotDisplayLabel(evidence, active)}
1689
+ </VisionCarouselLabel>
1690
+ <VisionCarouselButton
1691
+ type="button"
1692
+ disabled={clampedIndex === snapshots.length - 1}
1693
+ aria-label={`Next page for ${evidence.label}`}
1694
+ onClick={() =>
1695
+ setActiveIndex(Math.min(clampedIndex + 1, snapshots.length - 1))
1696
+ }
1697
+ >
1698
+
1699
+ </VisionCarouselButton>
1700
+ </VisionCarouselControls>
1701
+ <VisionCarouselChips aria-label={`${evidence.label} pages`}>
1702
+ {snapshots.map((snapshot, index) => (
1703
+ <VisionCarouselChip
1704
+ key={snapshot.id ?? `${snapshot.page}-${index}`}
1705
+ type="button"
1706
+ $active={index === clampedIndex}
1707
+ onClick={() => setActiveIndex(index)}
1708
+ >
1709
+ {snapshot.page}
1710
+ </VisionCarouselChip>
1711
+ ))}
1712
+ </VisionCarouselChips>
1713
+ </>
1714
+ ) : null}
1715
+ </VisionCarousel>
1716
+ )
1717
+ }
1718
+
1719
+ function openVisionComparisonWindow(
1720
+ comparison: PrintDesignVisionComparison,
1721
+ ): void {
1722
+ const target = window.open('', '_blank')
1723
+ if (!target) return
1724
+ target.document.write(visionComparisonHtml(comparison))
1725
+ target.document.close()
1726
+ }
1727
+
1728
+ function visionComparisonHtml(comparison: PrintDesignVisionComparison): string {
1729
+ const payload = JSON.stringify(comparison).replace(/</g, '\\u003c')
1730
+ return `<!doctype html>
1731
+ <html>
1732
+ <head>
1733
+ <meta charset="utf-8" />
1734
+ <title>${escapeHtml(comparison.title)}</title>
1735
+ <style>
1736
+ :root { color-scheme: light; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #202428; background: #f5f5f2; }
1737
+ body { margin: 0; }
1738
+ header { padding: 20px 24px 14px; border-bottom: 1px solid #ddd; background: #fff; }
1739
+ h1 { margin: 0 0 8px; font-size: 20px; font-weight: 650; }
1740
+ .meta { display: grid; gap: 4px; color: #60666d; font-size: 13px; line-height: 1.4; }
1741
+ main { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 18px; padding: 18px 24px 24px; }
1742
+ section { min-width: 0; background: #fff; border: 1px solid #ddd; border-radius: 10px; padding: 14px; }
1743
+ h2 { margin: 0 0 10px; font-size: 15px; }
1744
+ .note { min-height: 44px; margin-bottom: 12px; color: #60666d; font-size: 13px; line-height: 1.4; }
1745
+ .stage { display: grid; gap: 10px; }
1746
+ img { display: block; width: 100%; max-height: 72vh; object-fit: contain; background: #fff; border: 1px solid #e2e2e2; border-radius: 8px; }
1747
+ .controls { display: flex; align-items: center; gap: 8px; }
1748
+ button { border: 1px solid #ccc; background: #fff; border-radius: 999px; min-width: 28px; height: 28px; cursor: pointer; }
1749
+ button:disabled { opacity: .35; cursor: default; }
1750
+ .label { flex: 1; min-width: 0; color: #60666d; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1751
+ .pdf { margin-top: 8px; font-size: 12px; }
1752
+ .pdf a { color: #2457a6; }
1753
+ @media (max-width: 900px) { main { grid-template-columns: minmax(0, 1fr); } }
1754
+ </style>
1755
+ </head>
1756
+ <body>
1757
+ <header>
1758
+ <h1>${escapeHtml(comparison.title)}</h1>
1759
+ <div class="meta">
1760
+ <div>${escapeHtml(comparison.issueLabel)}</div>
1761
+ <div>${escapeHtml(comparison.fixLabel)}</div>
1762
+ <div>${escapeHtml(comparison.resultSummary)}</div>
1763
+ </div>
1764
+ </header>
1765
+ <main>
1766
+ <section data-side="baseline"></section>
1767
+ <section data-side="fixed"></section>
1768
+ </main>
1769
+ <script>
1770
+ const comparison = ${payload};
1771
+ const state = { baseline: 0, fixed: 0 };
1772
+ function sideLabel(side, snapshot) {
1773
+ const role = snapshot.role ? snapshot.role + " · " : "";
1774
+ const page = snapshot.pageCount ? "page " + snapshot.page + " of " + snapshot.pageCount : "page " + snapshot.page;
1775
+ return snapshot.label || side.label + " · " + role + page;
1776
+ }
1777
+ function renderSide(key, side) {
1778
+ const host = document.querySelector('[data-side="' + key + '"]');
1779
+ const snapshots = side.snapshots || [];
1780
+ const index = Math.min(state[key], Math.max(snapshots.length - 1, 0));
1781
+ const snapshot = snapshots[index];
1782
+ host.innerHTML = "";
1783
+ const title = document.createElement("h2");
1784
+ title.textContent = side.label;
1785
+ host.appendChild(title);
1786
+ const note = document.createElement("div");
1787
+ note.className = "note";
1788
+ note.textContent = side.note || "";
1789
+ host.appendChild(note);
1790
+ if (!snapshot) return;
1791
+ const stage = document.createElement("div");
1792
+ stage.className = "stage";
1793
+ const image = document.createElement("img");
1794
+ image.src = snapshot.imageUrl;
1795
+ image.alt = snapshot.imageAlt || side.label;
1796
+ stage.appendChild(image);
1797
+ if (snapshots.length > 1) {
1798
+ const controls = document.createElement("div");
1799
+ controls.className = "controls";
1800
+ const previous = document.createElement("button");
1801
+ previous.textContent = "<";
1802
+ previous.disabled = index === 0;
1803
+ previous.onclick = () => { state[key] = Math.max(0, index - 1); renderSide(key, side); };
1804
+ const label = document.createElement("div");
1805
+ label.className = "label";
1806
+ label.textContent = sideLabel(side, snapshot);
1807
+ const next = document.createElement("button");
1808
+ next.textContent = ">";
1809
+ next.disabled = index === snapshots.length - 1;
1810
+ next.onclick = () => { state[key] = Math.min(snapshots.length - 1, index + 1); renderSide(key, side); };
1811
+ controls.append(previous, label, next);
1812
+ stage.appendChild(controls);
1813
+ } else {
1814
+ const label = document.createElement("div");
1815
+ label.className = "label";
1816
+ label.textContent = sideLabel(side, snapshot);
1817
+ stage.appendChild(label);
1818
+ }
1819
+ if (side.pdfPath) {
1820
+ const pdf = document.createElement("div");
1821
+ pdf.className = "pdf";
1822
+ const link = document.createElement("a");
1823
+ link.href = ${JSON.stringify(
1824
+ 'file://',
1825
+ )} + side.pdfPath.split('/').map(encodeURIComponent).join('/');
1826
+ link.textContent = side.pdfPath.split('/').pop() || side.pdfPath;
1827
+ pdf.append("PDF: ", link);
1828
+ stage.appendChild(pdf);
1829
+ }
1830
+ host.appendChild(stage);
1831
+ }
1832
+ renderSide("baseline", comparison.baseline);
1833
+ renderSide("fixed", comparison.fixed);
1834
+ </script>
1835
+ </body>
1836
+ </html>`
1837
+ }
1838
+
1839
+ export function PrintDesignPanel({
1840
+ presets = DEFAULT_PRESETS,
1841
+ value,
1842
+ variant = 'sidebar',
1843
+ density,
1844
+ headerAccessory,
1845
+ modes,
1846
+ actions = [],
1847
+ extractionCapability,
1848
+ visionTypesetting,
1849
+ onThemeChange,
1850
+ onProfileExtractRequest,
1851
+ }: PrintDesignPanelProps): React.ReactElement {
1852
+ const imageInputId = useId()
1853
+ const [mode, setMode] = useState<PrintDesignMode>('preset')
1854
+ const [presetId, setPresetId] = useState(presets[0]?.id ?? 'default')
1855
+ const [description, setDescription] = useState('')
1856
+ const [themeJson, setThemeJson] = useState('')
1857
+ const [images, setImages] = useState<readonly File[]>([])
1858
+ const [manualInk, setManualInk] = useState(
1859
+ DEFAULT_PURE_RENDER_THEME.color.ink,
1860
+ )
1861
+ const [manualPaper, setManualPaper] = useState(
1862
+ DEFAULT_PURE_RENDER_THEME.color.paper ??
1863
+ DEFAULT_PURE_RENDER_THEME.color.surface ??
1864
+ '#FFFFFF',
1865
+ )
1866
+ const [manualAccent, setManualAccent] = useState(
1867
+ DEFAULT_PURE_RENDER_THEME.color.accent,
1868
+ )
1869
+ const [manualDisplayFont, setManualDisplayFont] = useState<'serif' | 'sans'>(
1870
+ 'serif',
1871
+ )
1872
+ const [manualBodyFont, setManualBodyFont] = useState<'serif' | 'sans'>(
1873
+ 'serif',
1874
+ )
1875
+ const [manualLabelFont, setManualLabelFont] = useState<'serif' | 'sans'>(
1876
+ 'sans',
1877
+ )
1878
+ const [manualMeasure, setManualMeasure] = useState('34rem')
1879
+ const [manualLead, setManualLead] = useState('1.5')
1880
+ const [manualAlign, setManualAlign] = useState<'justified' | 'flush-left'>(
1881
+ 'justified',
1882
+ )
1883
+ const [decisions, setDecisions] = useState<readonly [string, string][]>([])
1884
+ const [status, setStatus] = useState<string>(
1885
+ onProfileExtractRequest ? 'Ready' : 'Preset/manual only',
1886
+ )
1887
+ const panelDensity: PrintDesignPanelDensity =
1888
+ density ??
1889
+ (variant === 'popover' || variant === 'workspace'
1890
+ ? 'compact'
1891
+ : 'comfortable')
1892
+ const modeOptions = useMemo(() => {
1893
+ const defaultModes = MODE_OPTIONS.filter(
1894
+ option => option.value === 'preset' || option.value === 'vision',
1895
+ )
1896
+ const allowed = new Set(modes ?? defaultModes.map(option => option.value))
1897
+ return MODE_OPTIONS.filter(option => allowed.has(option.value))
1898
+ }, [modes])
1899
+
1900
+ useEffect(() => {
1901
+ if (modeOptions.some(option => option.value === mode)) return
1902
+ setMode(modeOptions[0]?.value ?? 'preset')
1903
+ }, [mode, modeOptions])
1904
+
1905
+ useEffect(() => {
1906
+ if (!value) return
1907
+ const matchingPreset = presets.find(preset =>
1908
+ themeMatchesPreset(value, preset.theme),
1909
+ )
1910
+ if (matchingPreset) setPresetId(matchingPreset.id)
1911
+ }, [presets, value])
1912
+
1913
+ const activeTheme = useMemo(
1914
+ () =>
1915
+ value ??
1916
+ presets.find(preset => preset.id === presetId)?.theme ??
1917
+ DEFAULT_PURE_RENDER_THEME,
1918
+ [presetId, presets, value],
1919
+ )
1920
+ const activePresetLabel =
1921
+ presets.find(preset => themeMatchesPreset(activeTheme, preset.theme))
1922
+ ?.label ??
1923
+ activeTheme.name ??
1924
+ 'Custom theme'
1925
+ const activeDesignSummary = `${activePresetLabel} · ${
1926
+ activeTheme.setting?.align === 'flush-left' ? 'Flush left' : 'Justified'
1927
+ } · ${activeTheme.type.measure}`
1928
+
1929
+ useEffect(() => {
1930
+ setManualInk(activeTheme.color.ink)
1931
+ setManualPaper(paperColorForTheme(activeTheme))
1932
+ setManualAccent(activeTheme.color.accent)
1933
+ setManualDisplayFont(fontRoleForFamily(activeTheme.type.fonts.display))
1934
+ setManualBodyFont(fontRoleForFamily(activeTheme.type.fonts.serif))
1935
+ setManualLabelFont(
1936
+ activeTheme.profile_rules?.caption?.label_style === 'plain'
1937
+ ? 'serif'
1938
+ : 'sans',
1939
+ )
1940
+ setManualMeasure(activeTheme.type.measure)
1941
+ setManualLead(String(activeTheme.type.lead))
1942
+ setManualAlign(manualAlignForTheme(activeTheme.setting?.align))
1943
+ }, [activeTheme])
1944
+
1945
+ function extractionUnavailableReason(
1946
+ targetMode: PrintDesignMode,
1947
+ ): string | undefined {
1948
+ if (targetMode !== 'image' && targetMode !== 'description') {
1949
+ return undefined
1950
+ }
1951
+ if (!onProfileExtractRequest) return 'Extraction unavailable'
1952
+ if (targetMode === 'image' && extractionCapability?.image === false) {
1953
+ return (
1954
+ extractionCapability.imageReason ??
1955
+ extractionCapability.reason ??
1956
+ 'Image extraction unavailable for the selected model'
1957
+ )
1958
+ }
1959
+ if (
1960
+ targetMode === 'description' &&
1961
+ extractionCapability?.description === false
1962
+ ) {
1963
+ return (
1964
+ extractionCapability.descriptionReason ??
1965
+ extractionCapability.reason ??
1966
+ 'Description extraction unavailable for the selected model'
1967
+ )
1968
+ }
1969
+ return undefined
1970
+ }
1971
+
1972
+ const unavailableReason = extractionUnavailableReason(mode)
1973
+ const displayedStatus =
1974
+ unavailableReason && (mode === 'image' || mode === 'description')
1975
+ ? unavailableReason
1976
+ : status
1977
+ const imageChoiceLabel =
1978
+ images.length > 1
1979
+ ? `${images.length} images`
1980
+ : images[0]?.name ?? 'Choose image'
1981
+ const imageButtonLabel = images.length > 0 ? 'Replace image' : 'Choose image'
1982
+ const unifiedDesignMode = mode === 'preset'
1983
+ const rawVisionReport = visionTypesetting?.report?.length
1984
+ ? visionTypesetting.report
1985
+ : []
1986
+ const visionReport = rawVisionReport
1987
+ const sampleVisionReport =
1988
+ mode === 'vision' && rawVisionReport.length === 0
1989
+ ? DEFAULT_VISION_SAMPLE_REPORT
1990
+ : []
1991
+ const liveReadoutEntries = [
1992
+ ...sampleVisionReport,
1993
+ ...visionReport,
1994
+ ...decisions,
1995
+ ]
1996
+ const displayedReadoutEntries =
1997
+ variant === 'workspace'
1998
+ ? [...liveReadoutEntries].reverse()
1999
+ : liveReadoutEntries
2000
+ const visionEvidence = visionTypesetting?.evidence ?? []
2001
+ const imageSectionEnabled = !modes || modes.includes('image')
2002
+ const descriptionSectionEnabled = !modes || modes.includes('description')
2003
+ const showPresets = unifiedDesignMode
2004
+ const showManualControls = unifiedDesignMode || mode === 'manual'
2005
+ const showReferenceInputs =
2006
+ (unifiedDesignMode && (imageSectionEnabled || descriptionSectionEnabled)) ||
2007
+ mode === 'image' ||
2008
+ mode === 'description'
2009
+ const hasHostExportAction = actions.some(
2010
+ action => /export/i.test(action.id) || /export\s+pdf/i.test(action.label),
2011
+ )
2012
+ const exportAction = actions.find(
2013
+ action => /export/i.test(action.id) || /export\s+pdf/i.test(action.label),
2014
+ )
2015
+ const secondaryActions = actions.filter(action => action !== exportAction)
2016
+ const routeExportThroughVision =
2017
+ visionTypesetting?.enabled === true &&
2018
+ visionTypesetting.onExport !== undefined &&
2019
+ exportAction !== undefined
2020
+ const headerExportDisabled =
2021
+ exportAction?.disabled ||
2022
+ exportAction?.busy ||
2023
+ (routeExportThroughVision &&
2024
+ (visionTypesetting?.exportDisabled || visionTypesetting?.exportBusy))
2025
+ const headerExportBusy = routeExportThroughVision
2026
+ ? visionTypesetting?.exportBusy
2027
+ : exportAction?.busy
2028
+ const headerExportLabel =
2029
+ headerExportBusy && exportAction
2030
+ ? exportAction.busyLabel ?? exportAction.label
2031
+ : (routeExportThroughVision
2032
+ ? visionTypesetting?.exportLabel
2033
+ : exportAction?.label) ?? exportAction?.label
2034
+ const showModeActionButton =
2035
+ mode !== 'preset' &&
2036
+ mode !== 'manual' &&
2037
+ (mode !== 'vision' || !hasHostExportAction)
2038
+
2039
+ function applyPreset(id: string): void {
2040
+ setPresetId(id)
2041
+ const preset = presets.find(item => item.id === id)
2042
+ if (preset) {
2043
+ setDecisions([['apply', `${preset.label} preset selected`]])
2044
+ onThemeChange?.(preset.theme)
2045
+ setStatus('Ready')
2046
+ }
2047
+ }
2048
+
2049
+ async function extract(input?: {
2050
+ mode?: Extract<PrintDesignMode, 'image' | 'description'>
2051
+ images?: readonly File[]
2052
+ prompt?: string
2053
+ }): Promise<void> {
2054
+ const targetMode =
2055
+ input?.mode ?? (mode === 'image' || mode === 'description' ? mode : null)
2056
+ if (!targetMode) return
2057
+ const targetImages = input?.images ?? images
2058
+ const targetPrompt = input?.prompt ?? description
2059
+ const unavailable = extractionUnavailableReason(targetMode)
2060
+ if (unavailable) {
2061
+ setStatus(unavailable)
2062
+ return
2063
+ }
2064
+ if (targetMode === 'image' && targetImages.length === 0) {
2065
+ setStatus('Choose image')
2066
+ return
2067
+ }
2068
+ const requestProfile = onProfileExtractRequest
2069
+ if (!requestProfile) {
2070
+ setStatus('Extraction unavailable')
2071
+ return
2072
+ }
2073
+ setStatus('Extracting')
2074
+ try {
2075
+ const profile = await requestProfile({
2076
+ mode: targetMode,
2077
+ prompt: targetPrompt,
2078
+ promptTemplate: PRINT_TOKEN_EXTRACTION_PROMPT,
2079
+ schema: PRINT_STYLE_PROFILE_SCHEMA,
2080
+ modelPrompt: buildPrintStyleProfilePrompt({
2081
+ mode: targetMode === 'image' ? 'image' : 'description',
2082
+ concerns: targetPrompt,
2083
+ imageName:
2084
+ targetImages.map(file => file.name).join(', ') || undefined,
2085
+ imageMimeType:
2086
+ targetImages.length === 1
2087
+ ? targetImages[0]?.type
2088
+ : 'multiple images',
2089
+ }),
2090
+ image: targetImages[0],
2091
+ images: targetImages,
2092
+ })
2093
+ const result = compilePrintTheme(profile)
2094
+ setDecisions(result.decisions)
2095
+ if (result.theme) {
2096
+ onThemeChange?.(result.theme)
2097
+ setStatus('Compiled')
2098
+ } else {
2099
+ setStatus(result.decisions[0]?.[1] ?? 'Profile rejected')
2100
+ }
2101
+ } catch (error) {
2102
+ setStatus(error instanceof Error ? error.message : String(error))
2103
+ }
2104
+ }
2105
+
2106
+ function applyThemeJson(): void {
2107
+ try {
2108
+ const theme = parsePureRenderThemeJson(themeJson)
2109
+ setDecisions([['apply', `${theme.name} JSON selected`]])
2110
+ onThemeChange?.(theme)
2111
+ setStatus('Applied')
2112
+ } catch (error) {
2113
+ setStatus(error instanceof Error ? error.message : String(error))
2114
+ }
2115
+ }
2116
+
2117
+ function applyManualTheme(
2118
+ overrides: Partial<{
2119
+ ink: string
2120
+ paper: string
2121
+ accent: string
2122
+ displayFont: 'serif' | 'sans'
2123
+ bodyFont: 'serif' | 'sans'
2124
+ labelFont: 'serif' | 'sans'
2125
+ measure: string
2126
+ lead: string
2127
+ align: 'justified' | 'flush-left'
2128
+ }> = {},
2129
+ ): void {
2130
+ const ink = overrides.ink ?? manualInk
2131
+ const paper = overrides.paper ?? manualPaper
2132
+ const accent = overrides.accent ?? manualAccent
2133
+ const displayRole = overrides.displayFont ?? manualDisplayFont
2134
+ const bodyRole = overrides.bodyFont ?? manualBodyFont
2135
+ const labelRole = overrides.labelFont ?? manualLabelFont
2136
+ const measure = overrides.measure ?? manualMeasure
2137
+ const align = overrides.align ?? manualAlign
2138
+ const leadText = overrides.lead ?? manualLead
2139
+ const lead = Number.parseFloat(leadText)
2140
+ const nextLead = Number.isFinite(lead) && lead >= 1.2 ? lead : 1.5
2141
+ const displayFont = fontStack(displayRole, 'display')
2142
+ const bodyFont = fontStack(bodyRole, 'body')
2143
+ const labelFont = fontStack(labelRole, 'label')
2144
+ const theme: PureRenderTheme = {
2145
+ ...activeTheme,
2146
+ name: 'Manual Theme',
2147
+ color: {
2148
+ ...activeTheme.color,
2149
+ ink,
2150
+ 'ink-soft': activeTheme.color['ink-soft'],
2151
+ surface: paper,
2152
+ paper,
2153
+ accent,
2154
+ 'accent-ink': accent,
2155
+ },
2156
+ type: {
2157
+ ...activeTheme.type,
2158
+ lead: nextLead,
2159
+ measure: measure.trim() || activeTheme.type.measure,
2160
+ fonts: {
2161
+ ...activeTheme.type.fonts,
2162
+ display: displayFont,
2163
+ serif: bodyFont,
2164
+ sans: labelFont,
2165
+ },
2166
+ },
2167
+ rhythm: `${(10.6 * nextLead).toFixed(1)}pt`,
2168
+ setting: {
2169
+ ...activeTheme.setting,
2170
+ align,
2171
+ hyphenate: align === 'justified',
2172
+ },
2173
+ profile_rules: {
2174
+ ...activeTheme.profile_rules,
2175
+ caption: {
2176
+ ...activeTheme.profile_rules?.caption,
2177
+ label_style: labelRole === 'sans' ? 'small-caps' : 'plain',
2178
+ },
2179
+ },
2180
+ enforced_rules: [
2181
+ ...(activeTheme.enforced_rules ?? []),
2182
+ 'Manual theme controls produced token JSON only.',
2183
+ ],
2184
+ }
2185
+ setDecisions([
2186
+ ['tune', `ink ${ink}, paper ${paper}, accent ${accent}`],
2187
+ ['tune', `${displayRole} display, ${bodyRole} body, ${labelRole} labels`],
2188
+ ['tune', `${measure}, lead ${nextLead}, ${align}`],
2189
+ ['note', 'manual controls emit theme tokens, not CSS'],
2190
+ ])
2191
+ onThemeChange?.(theme)
2192
+ setStatus('Ready')
2193
+ }
2194
+
2195
+ async function chooseImages(files: readonly File[]): Promise<void> {
2196
+ if (files.length === 0) {
2197
+ setImages([])
2198
+ setStatus('Choose image')
2199
+ return
2200
+ }
2201
+ const valid: File[] = []
2202
+ for (const file of files) {
2203
+ if (!isSupportedImageFile(file)) {
2204
+ setImages([])
2205
+ setStatus('Use PNG, JPEG, or WebP')
2206
+ return
2207
+ }
2208
+ if (file.size > MAX_IMAGE_BYTES) {
2209
+ setImages([])
2210
+ setStatus('Image too large')
2211
+ return
2212
+ }
2213
+ valid.push(file)
2214
+ }
2215
+ setImages(valid)
2216
+ setStatus(
2217
+ onProfileExtractRequest
2218
+ ? 'Extracting'
2219
+ : valid.length > 1
2220
+ ? `${valid.length} images ready`
2221
+ : 'Image ready',
2222
+ )
2223
+ const sampled = await sampleImageThemeHints(valid[0]!)
2224
+ if (sampled) {
2225
+ setManualInk(sampled.ink)
2226
+ setManualPaper(sampled.paper)
2227
+ setManualAccent(sampled.accent)
2228
+ applyManualTheme({
2229
+ ink: sampled.ink,
2230
+ paper: sampled.paper,
2231
+ accent: sampled.accent,
2232
+ })
2233
+ setDecisions([
2234
+ ['sample', `ink ${sampled.ink}, paper ${sampled.paper}`],
2235
+ ['sample', `accent ${sampled.accent} from local image colors`],
2236
+ ])
2237
+ setStatus('Image sampled')
2238
+ }
2239
+ if (onProfileExtractRequest) {
2240
+ await extract({ mode: 'image', images: valid, prompt: description })
2241
+ }
2242
+ }
2243
+
2244
+ return (
2245
+ <Panel
2246
+ $variant={variant}
2247
+ $density={panelDensity}
2248
+ data-density={panelDensity}
2249
+ data-panel-variant={variant}
2250
+ aria-label="Print design"
2251
+ >
2252
+ <HeaderRow
2253
+ $variant={variant}
2254
+ $align="center"
2255
+ $justify="start"
2256
+ $gap="sm"
2257
+ >
2258
+ {variant === 'workspace' ? (
2259
+ <WorkspaceIdentity>
2260
+ <WorkspaceTitle>Design - PDF Typesetter</WorkspaceTitle>
2261
+ <WorkspaceSummary aria-label="Active print design">
2262
+ <WorkspaceSummaryText title={activeDesignSummary}>
2263
+ {activeDesignSummary}
2264
+ </WorkspaceSummaryText>
2265
+ <WorkspaceSummarySwatches aria-hidden="true">
2266
+ <Swatch $color={activeTheme.color.ink} />
2267
+ <Swatch $color={activeTheme.color.accent} />
2268
+ <Swatch $color={paperColorForTheme(activeTheme)} />
2269
+ </WorkspaceSummarySwatches>
2270
+ </WorkspaceSummary>
2271
+ </WorkspaceIdentity>
2272
+ ) : null}
2273
+ <ModeTabs
2274
+ items={modeOptions.map(option => ({
2275
+ id: option.value,
2276
+ label: option.label,
2277
+ }))}
2278
+ activeId={mode}
2279
+ onSelect={setMode}
2280
+ data-mode-count={modeOptions.length}
2281
+ aria-label="Print design source"
2282
+ />
2283
+ {exportAction || headerAccessory ? (
2284
+ <HeaderActions>
2285
+ {headerAccessory}
2286
+ {exportAction ? (
2287
+ <HeaderActionButton
2288
+ type="button"
2289
+ variant={exportAction.variant ?? 'primary'}
2290
+ disabled={headerExportDisabled}
2291
+ onClick={() => {
2292
+ if (routeExportThroughVision) {
2293
+ visionTypesetting?.onExport?.()
2294
+ } else {
2295
+ exportAction.onClick()
2296
+ }
2297
+ }}
2298
+ >
2299
+ {exportAction.icon}
2300
+ {headerExportLabel}
2301
+ </HeaderActionButton>
2302
+ ) : null}
2303
+ </HeaderActions>
2304
+ ) : null}
2305
+ </HeaderRow>
2306
+
2307
+ <DesignMain $variant={variant} $mode={mode}>
2308
+ <WorkspaceControls $variant={variant} $mode={mode}>
2309
+ {showReferenceInputs ? (
2310
+ <DesignSection $variant={variant}>
2311
+ <SectionHeader>
2312
+ <SectionTitle>Reference</SectionTitle>
2313
+ </SectionHeader>
2314
+ <ReferenceGrid $variant={variant}>
2315
+ {(unifiedDesignMode && imageSectionEnabled) ||
2316
+ mode === 'image' ? (
2317
+ <ReferencePane $variant={variant}>
2318
+ <ControlStack>
2319
+ <SectionTitle>Image</SectionTitle>
2320
+ <ImageChoiceButton
2321
+ type="button"
2322
+ variant="subtle"
2323
+ onClick={() =>
2324
+ document.getElementById(imageInputId)?.click()
2325
+ }
2326
+ >
2327
+ <PlatformIcon icon={FileImage} size={14} />
2328
+ {imageButtonLabel}
2329
+ </ImageChoiceButton>
2330
+ {images.length > 0 ? (
2331
+ <ImageSelectionMeta title={imageChoiceLabel}>
2332
+ <PlatformIcon icon={FileImage} size={14} />
2333
+ <ImageSelectionName>
2334
+ {imageChoiceLabel}
2335
+ </ImageSelectionName>
2336
+ </ImageSelectionMeta>
2337
+ ) : null}
2338
+ <HiddenFile
2339
+ id={imageInputId}
2340
+ type="file"
2341
+ accept="image/*"
2342
+ multiple
2343
+ onChange={event => {
2344
+ void chooseImages(
2345
+ Array.from(event.currentTarget.files ?? []),
2346
+ )
2347
+ }}
2348
+ />
2349
+ </ControlStack>
2350
+ </ReferencePane>
2351
+ ) : null}
2352
+ {(unifiedDesignMode && descriptionSectionEnabled) ||
2353
+ mode === 'description' ? (
2354
+ <ReferencePane $variant={variant}>
2355
+ <DescribeStack>
2356
+ <SectionTitle>Describe</SectionTitle>
2357
+ <CompactTextArea
2358
+ rows={panelDensity === 'compact' ? 5 : 6}
2359
+ value={description}
2360
+ placeholder={
2361
+ 'Example: I would like an academic article feel. Tables should have a blue header. Keep the body quiet and readable.'
2362
+ }
2363
+ onChange={event =>
2364
+ setDescription(event.currentTarget.value)
2365
+ }
2366
+ />
2367
+ <ReferenceActionRow>
2368
+ <ReferenceButton
2369
+ type="button"
2370
+ variant="subtle"
2371
+ disabled={
2372
+ extractionUnavailableReason('description') !==
2373
+ undefined
2374
+ }
2375
+ onClick={() => void extract({ mode: 'description' })}
2376
+ >
2377
+ <PlatformIcon icon={WandSparkles} size={14} />
2378
+ Extract from description
2379
+ </ReferenceButton>
2380
+ </ReferenceActionRow>
2381
+ </DescribeStack>
2382
+ </ReferencePane>
2383
+ ) : null}
2384
+ </ReferenceGrid>
2385
+ </DesignSection>
2386
+ ) : null}
2387
+
2388
+ {showPresets ? (
2389
+ <DesignSection $variant={variant}>
2390
+ <SectionHeader>
2391
+ <SectionTitle>Presets</SectionTitle>
2392
+ </SectionHeader>
2393
+ <PresetList $variant={variant} aria-label="Print theme presets">
2394
+ {presets.map(preset => (
2395
+ <PresetOption
2396
+ key={preset.id}
2397
+ type="button"
2398
+ $active={preset.id === presetId}
2399
+ $density={panelDensity}
2400
+ $variant={variant}
2401
+ aria-pressed={preset.id === presetId}
2402
+ onClick={() => applyPreset(preset.id)}
2403
+ >
2404
+ <PresetLabel>{preset.label}</PresetLabel>
2405
+ <PresetPreview aria-hidden="true">
2406
+ <PreviewLetter
2407
+ $family={
2408
+ preset.theme.type.fonts.display ??
2409
+ preset.theme.type.fonts.sans
2410
+ }
2411
+ >
2412
+ Aa
2413
+ </PreviewLetter>
2414
+ <Swatch $color={preset.theme.color.ink} />
2415
+ <Swatch $color={preset.theme.color.accent} />
2416
+ {preset.id === presetId ? (
2417
+ <PlatformIcon icon={Check} size={12} />
2418
+ ) : null}
2419
+ </PresetPreview>
2420
+ </PresetOption>
2421
+ ))}
2422
+ </PresetList>
2423
+ </DesignSection>
2424
+ ) : null}
2425
+
2426
+ {mode === 'json' ? (
2427
+ <CompactTextArea
2428
+ rows={panelDensity === 'compact' ? 6 : 8}
2429
+ value={themeJson}
2430
+ placeholder="Paste theme JSON"
2431
+ onChange={event => setThemeJson(event.currentTarget.value)}
2432
+ />
2433
+ ) : null}
2434
+
2435
+ {showManualControls ? (
2436
+ <DesignSection $variant={variant}>
2437
+ <SectionHeader>
2438
+ <SectionTitle>Tune</SectionTitle>
2439
+ </SectionHeader>
2440
+ <ManualGrid
2441
+ $density={panelDensity}
2442
+ $variant={variant}
2443
+ aria-label="Manual theme controls"
2444
+ >
2445
+ <ManualField>
2446
+ Ink
2447
+ <ManualInput
2448
+ type="color"
2449
+ value={manualInk}
2450
+ onChange={event => {
2451
+ const ink = event.currentTarget.value
2452
+ setManualInk(ink)
2453
+ applyManualTheme({ ink })
2454
+ }}
2455
+ />
2456
+ </ManualField>
2457
+ <ManualField>
2458
+ Paper
2459
+ <ManualInput
2460
+ type="color"
2461
+ value={manualPaper}
2462
+ onChange={event => {
2463
+ const paper = event.currentTarget.value
2464
+ setManualPaper(paper)
2465
+ applyManualTheme({ paper })
2466
+ }}
2467
+ />
2468
+ </ManualField>
2469
+ <ManualField>
2470
+ Accent
2471
+ <ManualInput
2472
+ type="color"
2473
+ value={manualAccent}
2474
+ onChange={event => {
2475
+ const accent = event.currentTarget.value
2476
+ setManualAccent(accent)
2477
+ applyManualTheme({ accent })
2478
+ }}
2479
+ />
2480
+ </ManualField>
2481
+ <ManualField>
2482
+ Display
2483
+ <ManualSelect
2484
+ value={manualDisplayFont}
2485
+ onChange={event => {
2486
+ const displayFont = event.currentTarget.value as
2487
+ | 'serif'
2488
+ | 'sans'
2489
+ setManualDisplayFont(displayFont)
2490
+ applyManualTheme({ displayFont })
2491
+ }}
2492
+ >
2493
+ <option value="serif">Serif</option>
2494
+ <option value="sans">Sans</option>
2495
+ </ManualSelect>
2496
+ </ManualField>
2497
+ <ManualField>
2498
+ Body
2499
+ <ManualSelect
2500
+ value={manualBodyFont}
2501
+ onChange={event => {
2502
+ const bodyFont = event.currentTarget.value as
2503
+ | 'serif'
2504
+ | 'sans'
2505
+ setManualBodyFont(bodyFont)
2506
+ applyManualTheme({ bodyFont })
2507
+ }}
2508
+ >
2509
+ <option value="serif">Serif</option>
2510
+ <option value="sans">Sans</option>
2511
+ </ManualSelect>
2512
+ </ManualField>
2513
+ <ManualField>
2514
+ Labels
2515
+ <ManualSelect
2516
+ value={manualLabelFont}
2517
+ onChange={event => {
2518
+ const labelFont = event.currentTarget.value as
2519
+ | 'serif'
2520
+ | 'sans'
2521
+ setManualLabelFont(labelFont)
2522
+ applyManualTheme({ labelFont })
2523
+ }}
2524
+ >
2525
+ <option value="sans">Sans</option>
2526
+ <option value="serif">Serif</option>
2527
+ </ManualSelect>
2528
+ </ManualField>
2529
+ <ManualField>
2530
+ Measure
2531
+ <ManualInput
2532
+ value={manualMeasure}
2533
+ onChange={event => {
2534
+ const measure = event.currentTarget.value
2535
+ setManualMeasure(measure)
2536
+ applyManualTheme({ measure })
2537
+ }}
2538
+ />
2539
+ </ManualField>
2540
+ <ManualField>
2541
+ Lead
2542
+ <ManualInput
2543
+ value={manualLead}
2544
+ onChange={event => {
2545
+ const lead = event.currentTarget.value
2546
+ setManualLead(lead)
2547
+ applyManualTheme({ lead })
2548
+ }}
2549
+ />
2550
+ </ManualField>
2551
+ <ManualField>
2552
+ Align
2553
+ <ManualSelect
2554
+ value={manualAlign}
2555
+ onChange={event => {
2556
+ const align = event.currentTarget.value as
2557
+ | 'justified'
2558
+ | 'flush-left'
2559
+ setManualAlign(align)
2560
+ applyManualTheme({ align })
2561
+ }}
2562
+ >
2563
+ <option value="justified">Justified</option>
2564
+ <option value="flush-left">Flush left</option>
2565
+ </ManualSelect>
2566
+ </ManualField>
2567
+ </ManualGrid>
2568
+ </DesignSection>
2569
+ ) : null}
2570
+ </WorkspaceControls>
2571
+
2572
+ <WorkspaceCanvas $variant={variant} $mode={mode}>
2573
+ {mode === 'vision' ? (
2574
+ <VisionPanel $variant={variant} aria-label="Beta vision typesetting">
2575
+ <VisionSettingsRail $variant={variant}>
2576
+ <VisionSettingsStrip>
2577
+ <SectionTitle>Beta settings</SectionTitle>
2578
+ <VisionToggleRow>
2579
+ <VisionToggleCopy>
2580
+ <VisionToggleTitle>
2581
+ Use beta vision typesetting
2582
+ </VisionToggleTitle>
2583
+ <VisionToggleStatus>
2584
+ {visionTypesetting?.status ??
2585
+ (visionTypesetting?.enabled
2586
+ ? 'Vision beta enabled.'
2587
+ : 'Vision beta off.')}
2588
+ </VisionToggleStatus>
2589
+ </VisionToggleCopy>
2590
+ <VisionToggleControls>
2591
+ <VisionCheckbox
2592
+ type="checkbox"
2593
+ checked={visionTypesetting?.enabled === true}
2594
+ disabled={
2595
+ visionTypesetting?.loading ||
2596
+ visionTypesetting?.onToggle === undefined
2597
+ }
2598
+ onChange={event => {
2599
+ void visionTypesetting?.onToggle?.(
2600
+ event.currentTarget.checked,
2601
+ )
2602
+ }}
2603
+ />
2604
+ </VisionToggleControls>
2605
+ </VisionToggleRow>
2606
+ <VisionMaxFixesField>
2607
+ Max fixes per issue
2608
+ <VisionMaxFixesInput
2609
+ type="number"
2610
+ min={0}
2611
+ max={5}
2612
+ value={visionTypesetting?.maxFixesPerIssue ?? 2}
2613
+ disabled={
2614
+ visionTypesetting?.loading ||
2615
+ visionTypesetting?.onMaxFixesPerIssueChange ===
2616
+ undefined
2617
+ }
2618
+ onChange={event => {
2619
+ const value = Math.max(
2620
+ 0,
2621
+ Math.min(
2622
+ 5,
2623
+ Number.parseInt(event.currentTarget.value, 10) ||
2624
+ 0,
2625
+ ),
2626
+ )
2627
+ visionTypesetting?.onMaxFixesPerIssueChange?.(value)
2628
+ }}
2629
+ />
2630
+ </VisionMaxFixesField>
2631
+ </VisionSettingsStrip>
2632
+ </VisionSettingsRail>
2633
+ <VisionEvidenceColumn>
2634
+ {visionTypesetting?.progress ? (
2635
+ <VisionProgressStrip
2636
+ $state={visionTypesetting.progress.state}
2637
+ >
2638
+ <VisionProgressState
2639
+ $state={visionTypesetting.progress.state}
2640
+ >
2641
+ {progressStateLabel(visionTypesetting.progress.state)}
2642
+ </VisionProgressState>
2643
+ <VisionProgressLabel
2644
+ title={visionTypesetting.progress.label}
2645
+ >
2646
+ {visionTypesetting.progress.label}
2647
+ </VisionProgressLabel>
2648
+ </VisionProgressStrip>
2649
+ ) : null}
2650
+ <VisionSnapshotGrid $hasEvidence={visionEvidence.length > 0}>
2651
+ <VisionSnapshotFrame>
2652
+ <VisionSnapshotLabel>
2653
+ Sample page snapshot
2654
+ </VisionSnapshotLabel>
2655
+ {visionEvidence.length > 0 ? (
2656
+ <VisionEvidenceList>
2657
+ {visionEvidence.map(evidence => (
2658
+ <VisionEvidenceCard
2659
+ key={evidence.id}
2660
+ $tone={evidence.tone ?? 'neutral'}
2661
+ >
2662
+ <VisionEvidenceCarousel evidence={evidence} />
2663
+ <VisionEvidenceCaption>
2664
+ <VisionEvidenceTitle>
2665
+ {evidence.label}
2666
+ </VisionEvidenceTitle>
2667
+ {evidence.note ? (
2668
+ <VisionEvidenceNote>
2669
+ {evidence.note}
2670
+ </VisionEvidenceNote>
2671
+ ) : null}
2672
+ {evidence.comparison ? (
2673
+ <VisionEvidenceActions>
2674
+ <VisionEvidenceActionButton
2675
+ type="button"
2676
+ onClick={() =>
2677
+ openVisionComparisonWindow(
2678
+ evidence.comparison!,
2679
+ )
2680
+ }
2681
+ >
2682
+ View fix comparison
2683
+ </VisionEvidenceActionButton>
2684
+ </VisionEvidenceActions>
2685
+ ) : null}
2686
+ </VisionEvidenceCaption>
2687
+ </VisionEvidenceCard>
2688
+ ))}
2689
+ </VisionEvidenceList>
2690
+ ) : visionTypesetting?.snapshotUrl ? (
2691
+ visionTypesetting.onSnapshotOpen ? (
2692
+ <VisionSnapshotButton
2693
+ type="button"
2694
+ onClick={visionTypesetting.onSnapshotOpen}
2695
+ aria-label="Open page snapshot preview"
2696
+ >
2697
+ <VisionSnapshotImage
2698
+ src={visionTypesetting.snapshotUrl}
2699
+ alt={
2700
+ visionTypesetting.snapshotAlt ??
2701
+ 'Visual typesetting page snapshot'
2702
+ }
2703
+ />
2704
+ </VisionSnapshotButton>
2705
+ ) : (
2706
+ <VisionSnapshotImage
2707
+ src={visionTypesetting.snapshotUrl}
2708
+ alt={
2709
+ visionTypesetting.snapshotAlt ??
2710
+ 'Visual typesetting page snapshot'
2711
+ }
2712
+ />
2713
+ )
2714
+ ) : (
2715
+ <VisionSnapshotPage aria-label="Sample PDF page snapshot">
2716
+ <VisionPageHeading />
2717
+ <VisionPageLine />
2718
+ <VisionPageLine $width="92%" />
2719
+ <VisionPageLine $width="98%" />
2720
+ <VisionPageObject />
2721
+ <VisionPageLine $width="88%" />
2722
+ <VisionPageLine />
2723
+ <VisionPageLine $width="72%" />
2724
+ <VisionPageLine $width="94%" />
2725
+ <VisionPageLine $width="84%" />
2726
+ </VisionSnapshotPage>
2727
+ )}
2728
+ </VisionSnapshotFrame>
2729
+ {visionEvidence.length > 0 ? null : (
2730
+ <VisionSnapshotCopy>
2731
+ <VisionSnapshotTitle>
2732
+ Visual preflight sample
2733
+ </VisionSnapshotTitle>
2734
+ <span>
2735
+ When no export inspection has run yet, the typesetter
2736
+ keeps a sample page in view and reports the checks it
2737
+ will run against the next rendered PDF.
2738
+ </span>
2739
+ <span>
2740
+ After a beta export, this area can be replaced by the
2741
+ live page snapshot from the PDF inspection pass.
2742
+ </span>
2743
+ </VisionSnapshotCopy>
2744
+ )}
2745
+ </VisionSnapshotGrid>
2746
+ </VisionEvidenceColumn>
2747
+ </VisionPanel>
2748
+ ) : null}
2749
+ </WorkspaceCanvas>
2750
+
2751
+ <WorkspaceFooter $variant={variant} $mode={mode}>
2752
+ <DesignSection $variant={variant}>
2753
+ <FooterRow $align="center" $justify="between" $gap="sm">
2754
+ <StatusText title={displayedStatus}>{displayedStatus}</StatusText>
2755
+ {showModeActionButton ? (
2756
+ <Button
2757
+ type="button"
2758
+ variant="primary"
2759
+ disabled={
2760
+ (mode === 'image' || mode === 'description') &&
2761
+ unavailableReason !== undefined
2762
+ ? true
2763
+ : mode === 'vision'
2764
+ ? visionTypesetting?.exportDisabled ||
2765
+ visionTypesetting?.exportBusy ||
2766
+ visionTypesetting?.onExport === undefined
2767
+ : false
2768
+ }
2769
+ onClick={() => {
2770
+ if (mode === 'vision') visionTypesetting?.onExport?.()
2771
+ else if (mode === 'json') applyThemeJson()
2772
+ else if (mode === 'image' && images.length === 0)
2773
+ document.getElementById(imageInputId)?.click()
2774
+ else void extract()
2775
+ }}
2776
+ >
2777
+ <PlatformIcon
2778
+ icon={
2779
+ mode === 'json'
2780
+ ? WandSparkles
2781
+ : mode === 'vision'
2782
+ ? Download
2783
+ : WandSparkles
2784
+ }
2785
+ size={14}
2786
+ />
2787
+ {mode === 'json'
2788
+ ? 'Use JSON'
2789
+ : mode === 'vision'
2790
+ ? visionTypesetting?.exportBusy
2791
+ ? 'Exporting…'
2792
+ : visionTypesetting?.exportLabel ?? 'Export PDF'
2793
+ : mode === 'image' && images.length === 0
2794
+ ? 'Choose image'
2795
+ : mode === 'image'
2796
+ ? 'Update'
2797
+ : 'Extract'}
2798
+ </Button>
2799
+ ) : null}
2800
+ </FooterRow>
2801
+
2802
+ {secondaryActions.length > 0 ? (
2803
+ <ActionRow
2804
+ $variant={variant}
2805
+ $align="center"
2806
+ $justify="start"
2807
+ $gap="xs"
2808
+ >
2809
+ {secondaryActions.map(action => (
2810
+ <ActionButton
2811
+ key={action.id}
2812
+ type="button"
2813
+ variant={action.variant ?? 'subtle'}
2814
+ disabled={action.disabled || action.busy}
2815
+ onClick={action.onClick}
2816
+ >
2817
+ {action.icon}
2818
+ {action.busy
2819
+ ? action.busyLabel ?? action.label
2820
+ : action.label}
2821
+ </ActionButton>
2822
+ ))}
2823
+ </ActionRow>
2824
+ ) : null}
2825
+
2826
+ {liveReadoutEntries.length > 0 && variant === 'sidebar' ? (
2827
+ <DecisionReadout entries={displayedReadoutEntries} />
2828
+ ) : null}
2829
+ </DesignSection>
2830
+ </WorkspaceFooter>
2831
+ </DesignMain>
2832
+
2833
+ {variant !== 'sidebar' ? (
2834
+ <DesignReadout $variant={variant} aria-label="Live design readout">
2835
+ <DesignSection $variant={variant}>
2836
+ <SectionHeader>
2837
+ <SectionTitle>Live readout</SectionTitle>
2838
+ <StatusText title={displayedStatus}>{displayedStatus}</StatusText>
2839
+ </SectionHeader>
2840
+ {displayedReadoutEntries.length > 0 ? (
2841
+ <DecisionReadout entries={displayedReadoutEntries} />
2842
+ ) : (
2843
+ <StatusText>No design decisions yet.</StatusText>
2844
+ )}
2845
+ </DesignSection>
2846
+ </DesignReadout>
2847
+ ) : null}
2848
+ </Panel>
2849
+ )
2850
+ }
2851
+
2852
+ function DecisionReadout({
2853
+ entries,
2854
+ }: {
2855
+ entries: readonly (readonly [string, string])[]
2856
+ }): React.ReactElement {
2857
+ let previousGroup: string | undefined
2858
+ return (
2859
+ <DecisionStream>
2860
+ <DecisionList>
2861
+ {entries.map(([type, message], index) => {
2862
+ const group = decisionGroupLabel(message)
2863
+ const startsGroup = group !== undefined && group !== previousGroup
2864
+ if (group) previousGroup = group
2865
+ return (
2866
+ <DecisionItem
2867
+ key={`${type}-${index}`}
2868
+ $type={type}
2869
+ $startsGroup={startsGroup}
2870
+ >
2871
+ {startsGroup ? (
2872
+ <DecisionGroupLabel aria-label={`${group} event group`}>
2873
+ {group}
2874
+ </DecisionGroupLabel>
2875
+ ) : null}
2876
+ <DecisionType $type={type}>{type.toUpperCase()}</DecisionType>
2877
+ <DecisionMessage size="sm" tone="secondary">
2878
+ {formatDecisionMessage(message)}
2879
+ </DecisionMessage>
2880
+ </DecisionItem>
2881
+ )
2882
+ })}
2883
+ </DecisionList>
2884
+ </DecisionStream>
2885
+ )
2886
+ }
2887
+
2888
+ function formatDecisionMessage(message: string): ReactNode {
2889
+ const pdfMatch = message.match(/^PDF(?:\s+\((.*?)\))?:\s+(.+\.pdf)$/i)
2890
+ if (pdfMatch) {
2891
+ const timestamp = pdfMatch[1]
2892
+ const pdfPath = pdfMatch[2] ?? ''
2893
+ return (
2894
+ <DecisionLink href={fileUrlForPath(pdfPath)}>
2895
+ {timestamp
2896
+ ? `${timestamp} · ${fileNameFromPath(pdfPath)}`
2897
+ : fileNameFromPath(pdfPath)}
2898
+ </DecisionLink>
2899
+ )
2900
+ }
2901
+ const parts = message.split(/(#[0-9a-fA-F]{6}\b)/g)
2902
+ return parts.map((part, index) =>
2903
+ /^#[0-9a-fA-F]{6}$/.test(part) ? (
2904
+ <DecisionColor key={`${part}-${index}`} $color={part}>
2905
+ {part}
2906
+ </DecisionColor>
2907
+ ) : (
2908
+ part
2909
+ ),
2910
+ )
2911
+ }
2912
+
2913
+ function decisionGroupLabel(message: string): string | undefined {
2914
+ const passMatch = message.match(/\bPass\s+(\d+)\b/i)
2915
+ if (!passMatch) return undefined
2916
+ return `Pass ${passMatch[1]}`
2917
+ }
2918
+
2919
+ function fileNameFromPath(path: string): string {
2920
+ return path.split('/').filter(Boolean).pop() ?? path
2921
+ }
2922
+
2923
+ function fileUrlForPath(path: string): string {
2924
+ return `file://${path
2925
+ .split('/')
2926
+ .map(part => encodeURIComponent(part))
2927
+ .join('/')}`
2928
+ }
2929
+
2930
+ function escapeHtml(value: string): string {
2931
+ return value
2932
+ .replace(/&/g, '&amp;')
2933
+ .replace(/</g, '&lt;')
2934
+ .replace(/>/g, '&gt;')
2935
+ .replace(/"/g, '&quot;')
2936
+ }
2937
+
2938
+ function progressStateLabel(state: PrintDesignVisionProgress['state']): string {
2939
+ switch (state) {
2940
+ case 'running':
2941
+ return 'In flight'
2942
+ case 'complete':
2943
+ return 'Complete'
2944
+ case 'stopped':
2945
+ return 'Stopped'
2946
+ case 'pass':
2947
+ return 'Pass'
2948
+ default:
2949
+ return 'Ready'
2950
+ }
2951
+ }
2952
+
2953
+ function decisionTone(type: string): 'warn' | 'good' | 'info' | 'neutral' {
2954
+ const normalized = type.toLowerCase()
2955
+ if (
2956
+ ['warn', 'warning', 'reject', 'fallback', 'stopped', 'issue'].includes(
2957
+ normalized,
2958
+ )
2959
+ ) {
2960
+ return 'warn'
2961
+ }
2962
+ if (['pass', 'apply', 'fix'].includes(normalized)) return 'good'
2963
+ if (
2964
+ [
2965
+ 'candidate',
2966
+ 'vision',
2967
+ 'inspect',
2968
+ 'snapshot',
2969
+ 'pdf',
2970
+ 'sample',
2971
+ 'check',
2972
+ 'progress',
2973
+ ].includes(normalized)
2974
+ ) {
2975
+ return 'info'
2976
+ }
2977
+ return 'neutral'
2978
+ }
2979
+
2980
+ function decisionBadgeBackground(type: string): string {
2981
+ switch (decisionTone(type)) {
2982
+ case 'warn':
2983
+ return '#fff6e5'
2984
+ case 'good':
2985
+ return '#effaf3'
2986
+ case 'info':
2987
+ return '#f1f6ff'
2988
+ default:
2989
+ return 'var(--print-design-page-surface)'
2990
+ }
2991
+ }
2992
+
2993
+ function decisionBackground(type: string): string {
2994
+ switch (decisionTone(type)) {
2995
+ case 'warn':
2996
+ return 'linear-gradient(90deg, #fffaf0, transparent 44%)'
2997
+ case 'good':
2998
+ return 'linear-gradient(90deg, #f8fff9, transparent 36%)'
2999
+ case 'info':
3000
+ return 'linear-gradient(90deg, #f8fbff, transparent 36%)'
3001
+ default:
3002
+ return 'transparent'
3003
+ }
3004
+ }
3005
+
3006
+ function currentDecisionBackground(type: string): string {
3007
+ switch (decisionTone(type)) {
3008
+ case 'warn':
3009
+ return 'linear-gradient(90deg, #fff4dc, transparent 58%)'
3010
+ case 'good':
3011
+ return 'linear-gradient(90deg, #effaf3, transparent 52%)'
3012
+ case 'info':
3013
+ return 'linear-gradient(90deg, #eef5ff, transparent 52%)'
3014
+ default:
3015
+ return 'var(--print-design-page-surface)'
3016
+ }
3017
+ }
3018
+
3019
+ function decisionBorder(type: string): string {
3020
+ switch (decisionTone(type)) {
3021
+ case 'warn':
3022
+ return '#dfad59'
3023
+ case 'good':
3024
+ return '#8cc7a0'
3025
+ case 'info':
3026
+ return '#9eb9df'
3027
+ default:
3028
+ return 'var(--platform-colors-border)'
3029
+ }
3030
+ }
3031
+
3032
+ function decisionText(type: string): string {
3033
+ switch (decisionTone(type)) {
3034
+ case 'warn':
3035
+ return '#8a5a10'
3036
+ case 'good':
3037
+ return '#2f744b'
3038
+ case 'info':
3039
+ return '#426489'
3040
+ default:
3041
+ return 'var(--platform-colors-text-secondary)'
3042
+ }
3043
+ }