@jini-ai/ui 0.5.5 → 0.5.6

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 (263) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/core/theme/validation.js +40 -38
  3. package/dist/features/a2ui/renderer.js +15 -13
  4. package/dist/features/about/react/components/AboutTab.js +64 -34
  5. package/dist/features/about/rules.d.ts +2 -2
  6. package/dist/features/about/rules.js +156 -129
  7. package/dist/features/admin-widgets/components/ImagePreviewModal.d.ts +4 -3
  8. package/dist/features/admin-widgets/components/ImagePreviewModal.js +6 -10
  9. package/dist/features/admin-widgets/components/InfoTip.d.ts +3 -2
  10. package/dist/features/admin-widgets/components/InfoTip.hooks.js +2 -0
  11. package/dist/features/admin-widgets/components/InfoTip.js +34 -7
  12. package/dist/features/admin-widgets/components/InfoTip.rules.d.ts +13 -0
  13. package/dist/features/admin-widgets/components/InfoTip.rules.js +43 -10
  14. package/dist/features/admin-widgets/components/Lightbox/Lightbox.d.ts +30 -0
  15. package/dist/features/admin-widgets/components/Lightbox/Lightbox.hooks.d.ts +40 -0
  16. package/dist/features/admin-widgets/components/Lightbox/Lightbox.hooks.js +128 -0
  17. package/dist/features/admin-widgets/components/Lightbox/Lightbox.js +18 -0
  18. package/dist/features/admin-widgets/components/MiddleTruncate.d.ts +14 -0
  19. package/dist/features/admin-widgets/components/MiddleTruncate.js +12 -0
  20. package/dist/features/admin-widgets/components/MiddleTruncate.rules.d.ts +20 -0
  21. package/dist/features/admin-widgets/components/MiddleTruncate.rules.js +21 -0
  22. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.d.ts +18 -0
  23. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.hooks.d.ts +16 -0
  24. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.hooks.js +45 -0
  25. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.js +18 -0
  26. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.rules.d.ts +32 -0
  27. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.rules.js +26 -0
  28. package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.hooks.js +29 -14
  29. package/dist/features/admin-widgets/index.d.ts +14 -2
  30. package/dist/features/admin-widgets/index.js +7 -1
  31. package/dist/features/admin-widgets/styles/admin-widgets.css +1 -0
  32. package/dist/features/asset-grid/react/components/AssetCard.js +7 -1
  33. package/dist/features/asset-grid/react/components/AssetGrid.d.ts +1 -1
  34. package/dist/features/asset-grid/react/components/AssetGrid.js +38 -18
  35. package/dist/features/asset-grid/react/components/useAssetGridFilters.d.ts +21 -0
  36. package/dist/features/asset-grid/react/components/useAssetGridFilters.js +19 -0
  37. package/dist/features/asset-grid/react/hooks/useAssetGridKeyboardShortcuts.js +16 -11
  38. package/dist/features/asset-grid/react/hooks/useAssetGridLiveUpdates.d.ts +2 -0
  39. package/dist/features/asset-grid/react/hooks/useAssetGridLiveUpdates.js +35 -11
  40. package/dist/features/asset-tree-browser/react/components/AssetTreeBrowser.d.ts +6 -6
  41. package/dist/features/asset-tree-browser/react/components/AssetTreeBrowser.js +10 -129
  42. package/dist/features/asset-tree-browser/react/components/AssetTreeBrowserParts.d.ts +50 -0
  43. package/dist/features/asset-tree-browser/react/components/AssetTreeBrowserParts.js +95 -0
  44. package/dist/features/asset-tree-browser/react/components/useAssetTreeBrowserController.d.ts +56 -0
  45. package/dist/features/asset-tree-browser/react/components/useAssetTreeBrowserController.js +80 -0
  46. package/dist/features/asset-tree-browser/rules.js +40 -22
  47. package/dist/features/browser-chrome/rules.js +74 -70
  48. package/dist/features/charts/browser.js +11 -5
  49. package/dist/features/charts/react.js +2 -2
  50. package/dist/features/charts/table.js +2 -2
  51. package/dist/features/command-palette/react/hooks/useCommandPalette.js +3 -9
  52. package/dist/features/connectors/ConnectorsBrowser.d.ts +4 -4
  53. package/dist/features/connectors/ConnectorsBrowser.js +26 -14
  54. package/dist/features/connectors/components/ConnectorCard.js +67 -38
  55. package/dist/features/connectors/components/ConnectorDetailDrawer.js +62 -31
  56. package/dist/features/connectors/components/ConnectorGrid.js +11 -6
  57. package/dist/features/connectors/hooks/useConnectorAuthorization.js +6 -0
  58. package/dist/features/connectors/hooks/useConnectorCatalog.d.ts +2 -0
  59. package/dist/features/connectors/hooks/useConnectorCatalog.js +32 -13
  60. package/dist/features/connectors/hooks/useWiredConnectorsBrowser.d.ts +3 -3
  61. package/dist/features/connectors/rules.d.ts +8 -1
  62. package/dist/features/connectors/rules.js +42 -20
  63. package/dist/features/connectors/types.d.ts +11 -0
  64. package/dist/features/execution/constants.d.ts +4 -10
  65. package/dist/features/execution/constants.js +9 -94
  66. package/dist/features/execution/react/components/ByokProviderForm.d.ts +2 -4
  67. package/dist/features/execution/react/components/ByokProviderForm.js +140 -43
  68. package/dist/features/execution/react/components/ExecutionTab.d.ts +1 -2
  69. package/dist/features/execution/react/components/ExecutionTab.js +52 -30
  70. package/dist/features/execution/react/components/LocalCliAgentCard.js +12 -3
  71. package/dist/features/execution/react/components/LocalCliAgentList.js +14 -3
  72. package/dist/features/execution/react/components/LocalCliAgentSettings.js +14 -3
  73. package/dist/features/execution/react/hooks/useLocalCliAgentCard.js +54 -36
  74. package/dist/features/execution/react/hooks/useReasoningControl.js +114 -78
  75. package/dist/features/execution/rules.d.ts +2 -2
  76. package/dist/features/execution/rules.js +31 -15
  77. package/dist/features/execution/types.d.ts +4 -42
  78. package/dist/features/execution/types.js +0 -17
  79. package/dist/features/external-mcp/react/components/ExternalMcpTab.d.ts +6 -2
  80. package/dist/features/external-mcp/react/components/ExternalMcpTab.js +9 -2
  81. package/dist/features/file-dropzone/react/components/FileDropzone.js +24 -25
  82. package/dist/features/file-dropzone/react/components/FileDropzoneLightbox.d.ts +2 -2
  83. package/dist/features/file-dropzone/react/components/FileDropzoneLightbox.js +35 -30
  84. package/dist/features/file-dropzone/react/components/FileDropzoneThumbnailGrid.js +30 -24
  85. package/dist/features/file-dropzone/react/components/FileDropzoneZone.d.ts +14 -0
  86. package/dist/features/file-dropzone/react/components/FileDropzoneZone.js +26 -0
  87. package/dist/features/html-editor/canvas-content-wrapper.js +13 -13
  88. package/dist/features/html-editor/canvas-embed-placeholders.js +46 -37
  89. package/dist/features/html-editor/css.js +76 -69
  90. package/dist/features/html-editor/editor-blocks.js +47 -33
  91. package/dist/features/html-editor/editor-serialization.js +6 -5
  92. package/dist/features/html-editor/protected-elements.js +39 -28
  93. package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.js +4 -4
  94. package/dist/features/html-editor/source-splice.js +51 -30
  95. package/dist/features/i18n/dictionaries/settings-dialog.ja.js +1 -0
  96. package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +1 -0
  97. package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +1 -0
  98. package/dist/features/i18n/locale.js +20 -16
  99. package/dist/features/iframe-pool/react/dom-sync.js +8 -6
  100. package/dist/features/integrations/react/components/CodexInstallToggleButton.js +19 -4
  101. package/dist/features/integrations/react/components/IntegrationsTab.js +60 -14
  102. package/dist/features/interactive-ui/interactive-ui.css +1 -1
  103. package/dist/features/interactive-ui/providers/shadcn/data-table.js +10 -7
  104. package/dist/features/lexical-rich-text-editor/mention-parser.js +60 -57
  105. package/dist/features/lexical-rich-text-editor/react/hooks/useKeyboardCommands.js +33 -38
  106. package/dist/features/lexical-rich-text-editor/rules.js +87 -74
  107. package/dist/features/lexical-rich-text-editor/serialize.js +27 -25
  108. package/dist/features/mcp-ui/secret-card.js +111 -61
  109. package/dist/features/mcp-ui/surfaces/confirmation.js +39 -21
  110. package/dist/features/mcp-ui/surfaces/document.d.ts +1 -1
  111. package/dist/features/mcp-ui/surfaces/document.js +10 -2
  112. package/dist/features/mcp-ui/surfaces/fields.js +30 -50
  113. package/dist/features/mcp-ui/surfaces/form.d.ts +6 -0
  114. package/dist/features/mcp-ui/surfaces/form.js +43 -21
  115. package/dist/features/mcp-ui/surfaces/outcome.js +30 -20
  116. package/dist/features/mcp-ui/surfaces/text-input.js +33 -21
  117. package/dist/features/mcp-ui/surfaces/tokens.d.ts +6 -1
  118. package/dist/features/mcp-ui/surfaces/tokens.js +5 -2
  119. package/dist/features/media-providers/constants.d.ts +1 -0
  120. package/dist/features/media-providers/constants.js +2 -81
  121. package/dist/features/media-providers/index.d.ts +1 -1
  122. package/dist/features/media-providers/ports.d.ts +2 -0
  123. package/dist/features/media-providers/react/components/MediaProviderCard.d.ts +22 -0
  124. package/dist/features/media-providers/react/components/MediaProviderCard.js +50 -0
  125. package/dist/features/media-providers/react/components/MediaProvidersTab.d.ts +5 -6
  126. package/dist/features/media-providers/react/components/MediaProvidersTab.js +40 -72
  127. package/dist/features/media-providers/react/components/media-providers-tab-labels.d.ts +11 -0
  128. package/dist/features/media-providers/react/components/media-providers-tab-labels.js +57 -0
  129. package/dist/features/media-providers/react/hooks/useMediaProvidersTab.d.ts +4 -2
  130. package/dist/features/media-providers/react/hooks/useMediaProvidersTab.js +37 -23
  131. package/dist/features/media-providers/rules.js +17 -10
  132. package/dist/features/media-providers/types.d.ts +4 -22
  133. package/dist/features/media-providers/types.js +3 -8
  134. package/dist/features/memory/formatters.js +127 -92
  135. package/dist/features/memory/react/components/MemoryConnectedPanel.js +92 -36
  136. package/dist/features/memory/react/components/MemoryList.js +37 -18
  137. package/dist/features/memory/react/hooks/useMemoryExtractions.store.js +42 -36
  138. package/dist/features/notifications/react/components/NotificationsTab.js +28 -28
  139. package/dist/features/observability/long-task.d.ts +0 -12
  140. package/dist/features/observability/long-task.js +15 -19
  141. package/dist/features/observability/resource-error.d.ts +0 -10
  142. package/dist/features/observability/resource-error.js +18 -13
  143. package/dist/features/observability/stuck-run.js +10 -6
  144. package/dist/features/observability/telemetry-url.d.ts +6 -0
  145. package/dist/features/observability/telemetry-url.js +16 -0
  146. package/dist/features/observability/white-screen.js +12 -24
  147. package/dist/features/panel-kit/fetch-query/adapter.react.d.ts +2 -2
  148. package/dist/features/panel-kit/fetch-query/adapter.react.js +4 -2
  149. package/dist/features/panel-kit/fetch-query/adapter.tanstack.js +46 -33
  150. package/dist/features/panel-kit/helpers/standing-draft-local-backup.js +3 -1
  151. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +3 -1
  152. package/dist/features/privacy/react/components/PrivacyTab.js +24 -13
  153. package/dist/features/progress-card/components/ProgressCard.js +7 -3
  154. package/dist/features/progress-card/reference-adapters.js +59 -54
  155. package/dist/features/project-locations/react/components/ProjectLocationsTab.js +24 -15
  156. package/dist/features/project-locations/react/hooks/useProjectLocationsTab.d.ts +8 -0
  157. package/dist/features/project-locations/react/hooks/useProjectLocationsTab.js +31 -23
  158. package/dist/features/resource-dashboard/react/components/ResourceBoard.js +6 -47
  159. package/dist/features/resource-dashboard/react/components/ResourceBoardView.d.ts +2 -2
  160. package/dist/features/resource-dashboard/react/components/ResourceBoardView.js +3 -6
  161. package/dist/features/resource-dashboard/react/components/ResourceBoardViewParts.d.ts +15 -0
  162. package/dist/features/resource-dashboard/react/components/ResourceBoardViewParts.js +35 -0
  163. package/dist/features/resource-dashboard/react/components/ResourceCard.js +5 -16
  164. package/dist/features/resource-dashboard/react/components/ResourceCardParts.d.ts +34 -0
  165. package/dist/features/resource-dashboard/react/components/ResourceCardParts.js +34 -0
  166. package/dist/features/resource-dashboard/react/components/ResourceRowList.js +3 -1
  167. package/dist/features/resource-dashboard/react/components/ResourceRowListItem.js +2 -2
  168. package/dist/features/resource-dashboard/react/components/ResourceRowListItemParts.d.ts +19 -0
  169. package/dist/features/resource-dashboard/react/components/ResourceRowListItemParts.js +11 -0
  170. package/dist/features/resource-dashboard/react/components/ResourceRowListView.d.ts +1 -1
  171. package/dist/features/resource-dashboard/react/components/ResourceRowListView.js +4 -4
  172. package/dist/features/resource-dashboard/react/components/ResourceRowListViewParts.d.ts +10 -0
  173. package/dist/features/resource-dashboard/react/components/ResourceRowListViewParts.js +20 -0
  174. package/dist/features/resource-dashboard/react/components/resourceBoardProps.d.ts +48 -0
  175. package/dist/features/resource-dashboard/react/components/resourceBoardProps.js +70 -0
  176. package/dist/features/resource-dashboard/react/components/resourceRowListProps.d.ts +19 -0
  177. package/dist/features/resource-dashboard/react/components/resourceRowListProps.js +14 -0
  178. package/dist/features/resource-dashboard/react/components/translatedErrorLabelProps.d.ts +16 -0
  179. package/dist/features/resource-dashboard/react/components/translatedErrorLabelProps.js +13 -0
  180. package/dist/features/resource-dashboard/rules.js +2 -8
  181. package/dist/features/sketch-editor/dom.js +150 -99
  182. package/dist/features/sketch-editor/react/components/SketchEditor.js +26 -15
  183. package/dist/features/skills/react/components/SkillDraftForm.js +13 -19
  184. package/dist/features/skills/react/components/SkillDraftForm.labels.d.ts +31 -0
  185. package/dist/features/skills/react/components/SkillDraftForm.labels.js +54 -0
  186. package/dist/features/skills/react/components/SkillRow.js +38 -18
  187. package/dist/features/skills/react/components/SkillRow.labels.d.ts +7 -0
  188. package/dist/features/skills/react/components/SkillRow.labels.js +18 -0
  189. package/dist/features/skills/react/components/SkillsTab.js +24 -14
  190. package/dist/features/skills/react/components/SkillsTab.labels.d.ts +7 -0
  191. package/dist/features/skills/react/components/SkillsTab.labels.js +11 -0
  192. package/dist/features/skills/rules.js +32 -23
  193. package/dist/features/source-config-list/agent-handles.js +2 -1
  194. package/dist/features/source-config-list/dependencies.js +53 -49
  195. package/dist/features/source-config-list/index.d.ts +3 -3
  196. package/dist/features/source-config-list/react/components/SourceConfigAddForm.d.ts +6 -5
  197. package/dist/features/source-config-list/react/components/SourceConfigAddForm.js +38 -7
  198. package/dist/features/source-config-list/react/components/SourceConfigField.js +69 -25
  199. package/dist/features/source-config-list/react/components/SourceConfigItemCard.d.ts +6 -5
  200. package/dist/features/source-config-list/react/components/SourceConfigItemCard.js +76 -20
  201. package/dist/features/source-config-list/react/components/SourceConfigListView.d.ts +5 -3
  202. package/dist/features/source-config-list/react/components/SourceConfigListView.js +33 -3
  203. package/dist/features/source-config-list/react/components/SourceConfigTestControl.js +17 -1
  204. package/dist/features/source-config-list/react/components/SourceConfigTrustOptions.d.ts +11 -0
  205. package/dist/features/source-config-list/react/components/SourceConfigTrustOptions.js +12 -0
  206. package/dist/features/source-config-list/types.d.ts +14 -0
  207. package/dist/features/tab-launcher-menu/react/components/TabLauncherMenu.js +25 -1
  208. package/dist/features/tab-launcher-menu/react/hooks/useTabLauncherMenu.js +15 -16
  209. package/dist/features/tab-strip/react/components/TabBar.js +8 -1
  210. package/dist/features/tabbed-dialog/react/components/TabbedDialog.js +8 -30
  211. package/dist/features/tabbed-dialog/react/components/TabbedDialogChrome.d.ts +15 -0
  212. package/dist/features/tabbed-dialog/react/components/TabbedDialogChrome.js +8 -0
  213. package/dist/features/tabbed-dialog/react/components/TabbedDialogHeader.d.ts +12 -0
  214. package/dist/features/tabbed-dialog/react/components/TabbedDialogHeader.js +6 -0
  215. package/dist/features/tabbed-dialog/react/components/TabbedDialogSidebar.d.ts +27 -0
  216. package/dist/features/tabbed-dialog/react/components/TabbedDialogSidebar.js +12 -0
  217. package/dist/features/tabbed-dialog/react/components/tabbedDialogView.d.ts +43 -0
  218. package/dist/features/tabbed-dialog/react/components/tabbedDialogView.js +59 -0
  219. package/dist/features/version-manager/dependencies.d.ts +3 -1
  220. package/dist/features/version-manager/dependencies.js +3 -2
  221. package/dist/features/version-manager/react/components/VersionManagerModal.js +10 -6
  222. package/dist/features/version-manager/react/hooks/useVersionManager.js +7 -4
  223. package/dist/features/viewer-shell/react/components/CommentSidePanel.js +103 -50
  224. package/dist/features/viewer-shell/react/components/MarkdownSplitPane.js +28 -8
  225. package/dist/features/viewer-shell/react/hooks/useCommentReorder.js +22 -16
  226. package/dist/features/viewer-shell/rules.js +19 -23
  227. package/dist/react/components/CustomSelect.d.ts +38 -1
  228. package/dist/react/components/CustomSelect.js +94 -38
  229. package/dist/react/components/OnboardingDropdown.js +20 -8
  230. package/dist/react/components/RemixIcon.stylesheet.js +3 -1
  231. package/dist/react/components/Toast.js +13 -2
  232. package/dist/react/components/TooltipLayer.js +5 -14
  233. package/dist/react/components/WorkingDirPicker.js +78 -50
  234. package/dist/react/components/remixicon-font-recovery.js +38 -30
  235. package/dist/react/components/select-popover.d.ts +8 -6
  236. package/dist/react/components/select-popover.js +33 -29
  237. package/dist/react/mcp-ui/McpUiHost.d.ts +1 -1
  238. package/dist/react/mcp-ui/McpUiHost.js +10 -11
  239. package/dist/react/mcp-ui/host-message-source.d.ts +14 -2
  240. package/dist/react/mcp-ui/host-message-source.js +15 -5
  241. package/dist/react/mcp-ui/mcp-ui-host-frame.d.ts +41 -0
  242. package/dist/react/mcp-ui/mcp-ui-host-frame.js +39 -0
  243. package/dist/renderers/annotation-canvas/react/components/AnnotationCanvas.js +97 -69
  244. package/dist/renderers/preview-modal-shell/react/components/PreviewModalShell.js +59 -30
  245. package/dist/renderers/react/components/ArtifactView.js +9 -6
  246. package/dist/renderers/srcdoc/build.d.ts +4 -1
  247. package/dist/renderers/srcdoc/build.js +62 -48
  248. package/dist/styles/admin.css +29 -14
  249. package/dist/styles/lightbox.css +142 -0
  250. package/dist/styles/variables.css +128 -0
  251. package/dist/theme/apply.js +9 -5
  252. package/dist/utils/auto-open-file.d.ts +2 -1
  253. package/dist/utils/auto-open-file.js +69 -85
  254. package/dist/utils/markdown-scroll-sync.js +108 -87
  255. package/dist/utils/polygon-selection.js +25 -26
  256. package/dist/utils/smooth-scroll-to-top.d.ts +1 -1
  257. package/dist/utils/smooth-scroll-to-top.js +2 -32
  258. package/dist/utils/sse.js +26 -26
  259. package/dist/utils/unit-bezier.d.ts +7 -0
  260. package/dist/utils/unit-bezier.js +38 -0
  261. package/package.json +7 -10
  262. package/dist/features/admin-widgets/components/ImagePreviewModal.hooks.d.ts +0 -13
  263. package/dist/features/admin-widgets/components/ImagePreviewModal.hooks.js +0 -46
@@ -145,3 +145,131 @@
145
145
  --jini-modal-padding: 24px;
146
146
  --jini-sidebar-width: 240px;
147
147
  }
148
+
149
+ /* Material/role contract (ADR-069). Theme inputs are owned by admin's default theme or
150
+ a host theme. Structural consumers opt into these roles at their existing paint owners.
151
+ Existing palette fallbacks above remain for hosts that have not imported a theme yet. */
152
+ :root, [data-admin-theme], [data-color-scheme] {
153
+ --jini-app-bg: var(--jini-theme-app-bg, var(--jini-default-app-bg)); /* Application canvas background. */
154
+ --jini-shell-bg: var(--jini-theme-shell-bg, var(--jini-bg)); /* Shell over the application canvas. */
155
+ --jini-panel-bg: var(--jini-theme-panel-bg, var(--jini-default-panel-bg)); /* Card/panel fill. */
156
+ --jini-panel-border: var(--jini-theme-panel-border, var(--jini-default-panel-border)); /* Card/panel border color. */
157
+ --jini-panel-shadow: var(--jini-theme-panel-shadow, var(--jini-default-panel-shadow)); /* Card/panel elevation. */
158
+ --jini-panel-backdrop: var(--jini-theme-panel-backdrop); /* Panel backdrop-filter. */
159
+ --jini-panel-texture: var(--jini-theme-panel-texture); /* Panel background-image layer. */
160
+ --jini-rail-bg: var(--jini-theme-rail-bg, var(--jini-panel-bg)); /* Navigation rail fill. */
161
+ --jini-rail-border: var(--jini-theme-rail-border, var(--jini-panel-border)); /* Navigation rail edge. */
162
+ --jini-rail-shadow: var(--jini-theme-rail-shadow, none); /* Navigation rail elevation. */
163
+ --jini-rail-backdrop: var(--jini-theme-rail-backdrop, var(--jini-panel-backdrop, none)); /* Rail frost layer. */
164
+ --jini-overlay-bg: var(--jini-theme-overlay-bg, var(--jini-default-overlay-bg)); /* Modal scrim background. */
165
+ --jini-overlay-backdrop: var(--jini-theme-overlay-backdrop); /* Modal scrim backdrop-filter. */
166
+ --jini-overlay-shadow: var(--jini-theme-overlay-shadow, var(--jini-default-overlay-shadow)); /* Floating dialog elevation. */
167
+ --jini-control-preview-backdrop: var(--jini-theme-control-preview-backdrop, blur(6px)); /* Preview action filter. */
168
+ --jini-control-bg: var(--jini-theme-control-bg, var(--jini-default-control-bg)); /* Neutral input/control fill. */
169
+ --jini-control-border: var(--jini-theme-control-border, var(--jini-default-control-border)); /* Neutral input/control border color. */
170
+ --jini-control-shadow: var(--jini-theme-control-shadow); /* Resting control shadow. */
171
+ --jini-control-shadow-hover: var(--jini-theme-control-shadow-hover); /* Hovered control shadow. */
172
+ --jini-control-shadow-pressed: var(--jini-theme-control-shadow-pressed); /* Pressed control shadow. */
173
+ --jini-control-shadow-inset: var(--jini-theme-control-shadow-inset); /* Inset control shadow. */
174
+ --jini-border-width: var(--jini-theme-border-width); /* Standard primitive border width. */
175
+ }
176
+
177
+ /* Additive paint/elevation roles (ADR-069 step 2). New roles have exact compatibility
178
+ fallbacks here; existing material defaults remain admin-owned and unchanged. Theme inputs
179
+ win over fallbacks, including when inherited. Keep complete shadows and SVG images intact:
180
+ custom properties cannot interpolate into a data-URL SVG's stroke attribute. */
181
+ :root, [data-admin-theme], [data-color-scheme] {
182
+ --jini-control-chevron-image: var(--jini-theme-control-chevron-image, url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14' fill='none'%3E%3Cpath d='M3.5 5.5 7 9l3.5-3.5' stroke='%23666C75' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"));
183
+ --jini-overlay-tooltip-bg: var(--jini-theme-overlay-tooltip-bg, #fff);
184
+ --jini-overlay-tooltip-text: var(--jini-theme-overlay-tooltip-text, #111);
185
+ --jini-overlay-tooltip-border: var(--jini-theme-overlay-tooltip-border, rgba(0, 0, 0, 0.1));
186
+ --jini-overlay-bg-preview: var(--jini-theme-overlay-bg-preview, oklch(0% 0 0 / 0.55));
187
+ --jini-overlay-bg-navigation: var(--jini-theme-overlay-bg-navigation, oklch(0% 0 0 / 0.4));
188
+ --jini-code-string: var(--jini-theme-code-string, #4caf7a);
189
+ --jini-code-number: var(--jini-theme-code-number, #d1975a);
190
+ --jini-control-thumb-bg: var(--jini-theme-control-thumb-bg, #fff);
191
+ --jini-control-solid-ink: var(--jini-theme-control-solid-ink, #fff);
192
+ --jini-overlay-bg-lightbox: var(--jini-theme-overlay-bg-lightbox, oklch(0% 0 0 / 0.6));
193
+ --jini-lightbox-bg: var(--jini-theme-lightbox-bg, oklch(23% 0.005 256));
194
+ --jini-lightbox-ink: var(--jini-theme-lightbox-ink, oklch(97% 0 0));
195
+ --jini-lightbox-ink-muted: var(--jini-theme-lightbox-ink-muted, oklch(97% 0 0 / 0.58));
196
+ --jini-lightbox-media-bg: var(--jini-theme-lightbox-media-bg, oklch(0% 0 0 / 0.25));
197
+ --jini-lightbox-document-bg: var(--jini-theme-lightbox-document-bg, #fff);
198
+ --jini-lightbox-button-border: var(--jini-theme-lightbox-button-border, oklch(100% 0 0 / 0.16));
199
+ --jini-lightbox-button-bg: var(--jini-theme-lightbox-button-bg, oklch(100% 0 0 / 0.07));
200
+ --jini-lightbox-button-bg-hover: var(--jini-theme-lightbox-button-bg-hover, oklch(100% 0 0 / 0.15));
201
+ --jini-lightbox-button-border-hover: var(--jini-theme-lightbox-button-border-hover, oklch(100% 0 0 / 0.28));
202
+ --jini-control-media-bg: var(--jini-theme-control-media-bg, oklch(0% 0 0 / 0.55));
203
+ --jini-control-media-ink: var(--jini-theme-control-media-ink, oklch(100% 0 0));
204
+ --jini-control-media-border: var(--jini-theme-control-media-border, oklch(100% 0 0 / 0.35));
205
+ --jini-control-media-bg-hover: var(--jini-theme-control-media-bg-hover, oklch(0% 0 0 / 0.72));
206
+ --jini-overlay-bg-source: var(--jini-theme-overlay-bg-source, oklch(0% 0 0 / 0.48));
207
+ --jini-file-icon-folder: var(--jini-theme-file-icon-folder, oklch(72% 0.12 75));
208
+ --jini-file-icon-json: var(--jini-theme-file-icon-json, oklch(72% 0.13 90));
209
+ --jini-file-icon-markdown: var(--jini-theme-file-icon-markdown, oklch(62% 0.12 245));
210
+ --jini-file-icon-javascript: var(--jini-theme-file-icon-javascript, oklch(76% 0.15 95));
211
+ --jini-file-icon-typescript: var(--jini-theme-file-icon-typescript, oklch(60% 0.13 250));
212
+ --jini-file-icon-config: var(--jini-theme-file-icon-config, oklch(62% 0.14 20));
213
+ --jini-file-icon-html: var(--jini-theme-file-icon-html, oklch(64% 0.17 40));
214
+ --jini-file-icon-css: var(--jini-theme-file-icon-css, oklch(62% 0.13 280));
215
+ --jini-file-icon-image: var(--jini-theme-file-icon-image, oklch(64% 0.12 160));
216
+ --jini-file-icon-shell: var(--jini-theme-file-icon-shell, oklch(64% 0.12 145));
217
+ --jini-status-complete-ink-fallback: var(--jini-theme-status-complete-ink-fallback, oklch(99% 0 none));
218
+ --jini-control-preview-ink: var(--jini-theme-control-preview-ink, #fff);
219
+ --jini-control-preview-bg: var(--jini-theme-control-preview-bg, rgba(17, 17, 17, 0.42));
220
+ --jini-control-preview-bg-hover: var(--jini-theme-control-preview-bg-hover, rgba(17, 17, 17, 0.72));
221
+ --jini-control-preview-tip-bg: var(--jini-theme-control-preview-tip-bg, rgba(17, 17, 17, 0.88));
222
+ --jini-panel-border-muted: var(--jini-theme-panel-border-muted, #d9d9d9);
223
+ --jini-control-composer-border-focus: var(--jini-theme-control-composer-border-focus, oklch(70% 0.012 255));
224
+ --jini-overlay-bg-chat: var(--jini-theme-overlay-bg-chat, rgb(0 0 0 / 45%));
225
+ --jini-control-hover-neutral-bg: var(--jini-theme-control-hover-neutral-bg, rgba(127, 127, 127, 0.15));
226
+ --jini-control-recording-bg: var(--jini-theme-control-recording-bg, #dc2626);
227
+ --jini-control-recording-ink: var(--jini-theme-control-recording-ink, #ffffff);
228
+ --jini-control-recording-bg-hover: var(--jini-theme-control-recording-bg-hover, #b91c1c);
229
+ --jini-control-shadow-focus: var(--jini-theme-control-shadow-focus, 0 0 0 3px var(--jini-accent-ring));
230
+ --jini-overlay-tooltip-shadow: var(--jini-theme-overlay-tooltip-shadow, 0 4px 16px rgba(0, 0, 0, 0.25));
231
+ --jini-control-shadow-focus-subtle: var(--jini-theme-control-shadow-focus-subtle, 0 0 0 2px var(--jini-neutral-accent-soft));
232
+ --jini-overlay-toast-shadow: var(--jini-theme-overlay-toast-shadow, 0 8px 24px rgb(0 0 0 / 22%));
233
+ --jini-status-danger-shadow: var(--jini-theme-status-danger-shadow, inset 3px 0 0 var(--jini-danger));
234
+ --jini-control-thumb-shadow-strong: var(--jini-theme-control-thumb-shadow-strong, 0 1px 2px rgba(28, 27, 26, 0.28));
235
+ --jini-panel-shadow-divider: var(--jini-theme-panel-shadow-divider, inset 0 -1px 0 var(--jini-border));
236
+ --jini-control-shadow-focus-accent: var(--jini-theme-control-shadow-focus-accent, 0 0 0 3px color-mix(in srgb, var(--jini-accent) 18%, transparent));
237
+ --jini-overlay-menu-shadow-above: var(--jini-theme-overlay-menu-shadow-above, 0 -4px 14px rgba(20,28,45,0.09), 0 -1px 4px rgba(20,28,45,0.06));
238
+ --jini-control-shadow-selected: var(--jini-theme-control-shadow-selected, 0 0 0 1px var(--jini-accent));
239
+ --jini-control-shadow-swatch-selected: var(--jini-theme-control-shadow-swatch-selected, 0 0 0 2px var(--jini-bg-panel), 0 0 0 4px var(--jini-accent));
240
+ --jini-control-shadow-segment: var(--jini-theme-control-shadow-segment, 0 1px 2px rgb(0 0 0 / 8%));
241
+ --jini-panel-shadow-sticky: var(--jini-theme-panel-shadow-sticky, 0 6px 8px -6px rgb(0 0 0 / 0.14));
242
+ --jini-overlay-menu-shadow: var(--jini-theme-overlay-menu-shadow, 0 12px 32px rgb(0 0 0 / 12%));
243
+ --jini-control-shadow-segment-raised: var(--jini-theme-control-shadow-segment-raised, 0 1px 2px rgb(0 0 0 / 12%));
244
+ --jini-control-thumb-shadow: var(--jini-theme-control-thumb-shadow, 0 1px 2px rgb(0 0 0 / 25%));
245
+ --jini-panel-shadow-banner: var(--jini-theme-panel-shadow-banner, 0 2px 8px rgb(0 0 0 / 0.12));
246
+ --jini-status-warning-shadow: var(--jini-theme-status-warning-shadow, inset 2px 0 0 var(--jini-warning));
247
+ --jini-panel-shadow-flat: var(--jini-theme-panel-shadow-flat, none);
248
+ }
249
+
250
+ /* Chat uses its own stylesheet so hosts can load it independently of the admin palette.
251
+ Aliases live on :root only so nested theme/scheme scopes inherit ChatPane's component-local
252
+ values. Moving that component wiring is a later step. Classic retains its existing scoped
253
+ host bridge in Tovu. */
254
+ :root {
255
+ --jini-chat-font-body: var(--jini-theme-chat-font-body, -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei UI", "Noto Sans", Roboto, "Helvetica Neue", Arial, sans-serif);
256
+ --jini-chat-font-mono: var(--jini-theme-chat-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
257
+ --jini-chat-popover-shadow: var(--jini-theme-chat-popover-shadow, 0 18px 46px rgba(13, 12, 10, .15), 0 2px 8px rgba(13, 12, 10, .06));
258
+ --jini-chat-bg: var(--jini-theme-chat-bg);
259
+ --jini-chat-panel: var(--jini-theme-chat-panel);
260
+ --jini-chat-text: var(--jini-theme-chat-text);
261
+ --jini-chat-text-strong: var(--jini-theme-chat-text-strong);
262
+ --jini-chat-muted: var(--jini-theme-chat-muted);
263
+ --jini-chat-faint: var(--jini-theme-chat-faint);
264
+ --jini-chat-border: var(--jini-theme-chat-border);
265
+ --jini-chat-border-strong: var(--jini-theme-chat-border-strong);
266
+ --jini-chat-border-soft: var(--jini-theme-chat-border-soft);
267
+ --jini-chat-subtle: var(--jini-theme-chat-subtle);
268
+ --jini-chat-accent: var(--jini-theme-chat-accent);
269
+ --jini-chat-accent-soft: var(--jini-theme-chat-accent-soft);
270
+ --jini-chat-danger: var(--jini-theme-chat-danger);
271
+ --jini-chat-radius: var(--jini-theme-chat-radius);
272
+ --jini-chat-radius-lg: var(--jini-theme-chat-radius-lg);
273
+ --jini-chat-user-bubble: var(--jini-theme-chat-user-bubble);
274
+ --jini-chat-user-bubble-ink: var(--jini-theme-chat-user-bubble-ink);
275
+ }
@@ -26,11 +26,7 @@ function acquireFont(document, href) {
26
26
  leases.delete(absolute);
27
27
  } };
28
28
  }
29
- /** Install both palettes on the injected scope and load fonts. Cleanup restores the host. */
30
- export function applyAdminTheme({ theme }, { target, document }) {
31
- const checked = validateAdminTheme({ theme });
32
- if (!checked.valid)
33
- throw new TypeError(checked.errors.join('; '));
29
+ function themeValues({ theme }, _options = {}) {
34
30
  const values = {};
35
31
  for (const scheme of ['light', 'dark'])
36
32
  for (const key of adminThemeColorKeys) {
@@ -41,6 +37,14 @@ export function applyAdminTheme({ theme }, { target, document }) {
41
37
  values['--jini-font-mono'] = theme.fonts.mono ?? defaultAdminTheme.fonts.mono;
42
38
  values['--jini-radius'] = theme.radius ?? defaultAdminTheme.radius;
43
39
  values['--jini-density'] = String(theme.density ?? defaultAdminTheme.density);
40
+ return values;
41
+ }
42
+ /** Install both palettes on the injected scope and load fonts. Cleanup restores the host. */
43
+ export function applyAdminTheme({ theme }, { target, document }) {
44
+ const checked = validateAdminTheme({ theme });
45
+ if (!checked.valid)
46
+ throw new TypeError(checked.errors.join('; '));
47
+ const values = themeValues({ theme });
44
48
  const before = Object.keys(values).map((key) => ({ key, value: target.style.getPropertyValue(key), priority: target.style.getPropertyPriority(key) }));
45
49
  const previousName = target.getAttribute('data-admin-theme');
46
50
  const releases = [];
@@ -7,10 +7,11 @@ interface CandidateFile {
7
7
  interface AutoOpenOptions {
8
8
  readonly moduleFileNames?: ReadonlySet<string>;
9
9
  }
10
- export declare function decideAutoOpenAfterWrite(filePath: string, nextFiles: ReadonlyArray<CandidateFile>, options?: AutoOpenOptions): {
10
+ type AutoOpenDecision = {
11
11
  shouldOpen: boolean;
12
12
  fileName: string | null;
13
13
  };
14
+ export declare function decideAutoOpenAfterWrite(filePath: string, nextFiles: ReadonlyArray<CandidateFile>, options?: AutoOpenOptions): AutoOpenDecision;
14
15
  export interface SelectAutoOpenOptions {
15
16
  readonly preferSiteEntry?: boolean;
16
17
  }
@@ -24,59 +24,51 @@ function basenameOf(p) {
24
24
  // rather than tested around.
25
25
  return p.split('/').pop();
26
26
  }
27
+ function decline() {
28
+ return { shouldOpen: false, fileName: null };
29
+ }
30
+ function relPathOf(file) {
31
+ return file.path ?? file.name;
32
+ }
33
+ // Does the agent's `filePath` positively identify the project file at `rel`?
34
+ function isPathSuffixMatch({ filePath, rel }) {
35
+ if (!rel)
36
+ return false;
37
+ // Require segment alignment: filePath ends with "/${rel}" so that
38
+ // "subdir/App.jsx" matches ".../subdir/App.jsx" but not
39
+ // ".../notsubdir/App.jsx".
40
+ return filePath === rel || filePath.endsWith('/' + rel);
41
+ }
42
+ // Resolve a positive identification into an open decision, declining files
43
+ // that are modules of a multi-file HTML entry rather than standalone pages.
44
+ // More than one match means multiple project files plausibly correspond to
45
+ // this path — refuse rather than open the wrong one.
46
+ function resolveUniqueMatch({ matches, moduleFileNames }) {
47
+ if (matches.length !== 1)
48
+ return decline();
49
+ const fileName = matches[0].name;
50
+ return moduleFileNames.has(fileName) ? decline() : { shouldOpen: true, fileName };
51
+ }
27
52
  export function decideAutoOpenAfterWrite(filePath, nextFiles, options = {}) {
28
53
  const moduleFileNames = options.moduleFileNames ?? NO_MODULES;
29
- // Resolve a positive identification into an open decision, declining files
30
- // that are modules of a multi-file HTML entry rather than standalone pages.
31
- const resolve = (fileName) => moduleFileNames.has(fileName)
32
- ? { shouldOpen: false, fileName: null }
33
- : { shouldOpen: true, fileName };
34
54
  if (!filePath)
35
- return { shouldOpen: false, fileName: null };
55
+ return decline();
36
56
  // 1) Path-suffix match against full project-relative paths.
37
- const suffixMatches = [];
38
- for (const f of nextFiles) {
39
- const rel = f.path ?? f.name;
40
- if (!rel)
41
- continue;
42
- if (filePath === rel) {
43
- suffixMatches.push(f);
44
- continue;
45
- }
46
- // Require segment alignment: filePath ends with "/${rel}" so that
47
- // "subdir/App.jsx" matches ".../subdir/App.jsx" but not
48
- // ".../notsubdir/App.jsx".
49
- if (filePath.length > rel.length && filePath.endsWith('/' + rel)) {
50
- suffixMatches.push(f);
51
- }
52
- }
53
- if (suffixMatches.length === 1) {
54
- return resolve(suffixMatches[0].name);
55
- }
56
- if (suffixMatches.length > 1) {
57
- // Multiple project files plausibly correspond to this path — refuse
58
- // rather than open the wrong one.
59
- return { shouldOpen: false, fileName: null };
60
- }
57
+ const suffixMatches = nextFiles.filter((f) => isPathSuffixMatch({ filePath, rel: relPathOf(f) }));
58
+ if (suffixMatches.length > 0)
59
+ return resolveUniqueMatch({ matches: suffixMatches, moduleFileNames });
61
60
  // 2) Basename fallback only when filePath itself is just a basename.
62
61
  // If filePath contains a slash but didn't path-suffix-match anything,
63
62
  // it's an external edit that happens to share a basename — declining
64
63
  // is the whole point of the guard.
65
- if (filePath.includes('/')) {
66
- return { shouldOpen: false, fileName: null };
67
- }
68
- const basenameMatches = nextFiles.filter((f) => {
69
- const rel = f.path ?? f.name;
70
- return rel ? basenameOf(rel) === filePath : false;
71
- });
72
- if (basenameMatches.length === 1) {
73
- return resolve(basenameMatches[0].name);
74
- }
75
- return { shouldOpen: false, fileName: null };
64
+ if (filePath.includes('/'))
65
+ return decline();
66
+ // A blank rel's basename is '' and can never equal the non-empty filePath.
67
+ const basenameMatches = nextFiles.filter((f) => basenameOf(relPathOf(f)) === filePath);
68
+ return resolveUniqueMatch({ matches: basenameMatches, moduleFileNames });
76
69
  }
77
70
  function isHtmlPreviewFile(file) {
78
- const path = file.path ?? file.name;
79
- return file.kind === 'html' || /\.html?$/i.test(path);
71
+ return file.kind === 'html' || /\.html?$/i.test(relPathOf(file));
80
72
  }
81
73
  // Markdown documents (plan.md, report.md, DESIGN.md, …) often render inline
82
74
  // in a viewer, so a turn that produces one should surface it just like an
@@ -84,8 +76,7 @@ function isHtmlPreviewFile(file) {
84
76
  // whose backend maps both `.md` and `.txt` to the same generic text kind
85
77
  // still opens markdown while deliberately leaving plain `.txt` alone.
86
78
  function isMarkdownPreviewFile(file) {
87
- const path = file.path ?? file.name;
88
- return /\.(md|markdown)$/i.test(path);
79
+ return /\.(md|markdown)$/i.test(relPathOf(file));
89
80
  }
90
81
  // Auto-open priority for a turn's produced files. Higher wins. HTML is the
91
82
  // primary visual deliverable, so when a turn writes both an HTML page and a
@@ -104,58 +95,51 @@ function autoOpenPreviewRank(file) {
104
95
  // files. Depth orders competing entries so the site root wins over a locale
105
96
  // or section subtree's own index.
106
97
  function siteEntryDepth(file) {
107
- const path = file.path ?? file.name;
98
+ const path = relPathOf(file);
108
99
  if (!/(^|\/)index\.html?$/i.test(path))
109
100
  return null;
110
101
  return path.split('/').length;
111
102
  }
103
+ // Missing or non-finite mtimes count as 0 for the newest-wins tie-break.
104
+ function finiteMtime(file) {
105
+ return typeof file.mtime === 'number' && Number.isFinite(file.mtime) ? file.mtime : 0;
106
+ }
107
+ // The file with the highest `score` (null = not a candidate); equal scores
108
+ // break to the newest mtime, and an mtime tie goes to the later file.
109
+ function pickHighestScoring({ files, score }) {
110
+ let best = null;
111
+ for (const file of files) {
112
+ const fileScore = score(file);
113
+ if (fileScore === null)
114
+ continue;
115
+ const beatsBest = best === null ||
116
+ fileScore > best.score ||
117
+ (fileScore === best.score && finiteMtime(file) >= finiteMtime(best.file));
118
+ if (beatsBest)
119
+ best = { file, score: fileScore };
120
+ }
121
+ return best?.file ?? null;
122
+ }
123
+ // Every site entry is an `index.html`/`.htm`, so it is always an HTML preview
124
+ // file too; the shallower entry scores higher.
125
+ function siteEntryScore(file) {
126
+ const depth = siteEntryDepth(file);
127
+ return depth === null ? null : -depth;
128
+ }
129
+ function previewRankScore(file) {
130
+ const rank = autoOpenPreviewRank(file);
131
+ return rank === 0 ? null : rank;
132
+ }
112
133
  // Pick which of a turn's produced files to auto-open in the viewer. Among
113
134
  // previewable files, a higher-priority kind always beats a lower one; ties
114
135
  // break to the most recently written file (newest mtime). Returns null when
115
136
  // the turn produced nothing previewable.
116
137
  export function selectAutoOpenProducedArtifact(producedFiles, options = {}) {
117
138
  if (options.preferSiteEntry) {
118
- let entry = null;
119
- let entryDepth = Number.POSITIVE_INFINITY;
120
- for (const file of producedFiles) {
121
- if (!isHtmlPreviewFile(file))
122
- continue;
123
- const depth = siteEntryDepth(file);
124
- if (depth === null)
125
- continue;
126
- if (depth < entryDepth) {
127
- entry = file;
128
- entryDepth = depth;
129
- continue;
130
- }
131
- if (depth > entryDepth || !entry)
132
- continue;
133
- const nextMtime = typeof file.mtime === 'number' && Number.isFinite(file.mtime) ? file.mtime : 0;
134
- const entryMtime = typeof entry.mtime === 'number' && Number.isFinite(entry.mtime) ? entry.mtime : 0;
135
- if (nextMtime >= entryMtime)
136
- entry = file;
137
- }
139
+ const entry = pickHighestScoring({ files: producedFiles, score: siteEntryScore });
138
140
  if (entry)
139
141
  return entry.name;
140
142
  }
141
- let selected = null;
142
- let selectedRank = 0;
143
- for (const file of producedFiles) {
144
- const rank = autoOpenPreviewRank(file);
145
- if (rank === 0)
146
- continue;
147
- if (!selected || rank > selectedRank) {
148
- selected = file;
149
- selectedRank = rank;
150
- continue;
151
- }
152
- if (rank < selectedRank)
153
- continue;
154
- const nextMtime = typeof file.mtime === 'number' && Number.isFinite(file.mtime) ? file.mtime : 0;
155
- const selectedMtime = typeof selected.mtime === 'number' && Number.isFinite(selected.mtime) ? selected.mtime : 0;
156
- if (nextMtime >= selectedMtime)
157
- selected = file;
158
- }
159
- return selected?.name ?? null;
143
+ return pickHighestScoring({ files: producedFiles, score: previewRankScore })?.name ?? null;
160
144
  }
161
145
  //# sourceMappingURL=auto-open-file.js.map
@@ -56,72 +56,50 @@ function hasVerticalProgression(offsets) {
56
56
  const first = offsets[0];
57
57
  return offsets.some((offset, index) => index > 0 && offset > first + 0.5);
58
58
  }
59
- /**
60
- * 1-based source start line of every top-level markdown block, in document
61
- * order. Nested blocks (list items, blockquote contents) collapse into their
62
- * top-level container so the result lines up 1:1 with the preview article's
63
- * direct element children.
64
- */
65
- export function extractMarkdownBlockLines(markdown) {
66
- if (!markdown)
67
- return [];
68
- let events;
59
+ function parseMarkdownEvents({ markdown }) {
69
60
  try {
70
- events = postprocess(parse({ extensions: [gfm()] })
61
+ return postprocess(parse({ extensions: [gfm()] })
71
62
  .document()
72
63
  .write(preprocess()(markdown, undefined, true)));
73
64
  }
74
65
  catch {
75
- return [];
66
+ return null;
76
67
  }
68
+ }
69
+ // Nesting depth after a container token's enter/exit event.
70
+ function nextContainerDepth({ phase, depth }) {
71
+ return phase === 'enter' ? depth + 1 : Math.max(0, depth - 1);
72
+ }
73
+ function collectTopLevelBlockLines({ events }) {
77
74
  const lines = [];
78
75
  let depth = 0;
79
- for (const event of events) {
80
- const phase = event[0];
81
- const token = event[1];
82
- const type = token.type;
83
- if (CONTAINER_TOKENS.has(type)) {
84
- if (phase === 'enter') {
85
- if (depth === 0)
86
- lines.push(token.start.line);
87
- depth += 1;
88
- }
89
- else {
90
- depth = Math.max(0, depth - 1);
91
- }
92
- continue;
93
- }
94
- if (phase === 'enter' && depth === 0 && FLOW_BLOCK_TOKENS.has(type)) {
76
+ for (const [phase, token] of events) {
77
+ const isContainer = CONTAINER_TOKENS.has(token.type);
78
+ const isTopLevelStart = phase === 'enter' && depth === 0;
79
+ if (isTopLevelStart && (isContainer || FLOW_BLOCK_TOKENS.has(token.type)))
95
80
  lines.push(token.start.line);
96
- }
81
+ if (isContainer)
82
+ depth = nextContainerDepth({ phase, depth });
97
83
  }
98
84
  return lines;
99
85
  }
86
+ /**
87
+ * 1-based source start line of every top-level markdown block, in document
88
+ * order. Nested blocks (list items, blockquote contents) collapse into their
89
+ * top-level container so the result lines up 1:1 with the preview article's
90
+ * direct element children.
91
+ */
92
+ export function extractMarkdownBlockLines(markdown) {
93
+ if (!markdown)
94
+ return [];
95
+ const events = parseMarkdownEvents({ markdown });
96
+ return events ? collectTopLevelBlockLines({ events }) : [];
97
+ }
100
98
  function escapeMirrorText(value) {
101
99
  return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
102
100
  }
103
- /**
104
- * Pixel offset (within the textarea's scrollable content) of each block's
105
- * start line, accounting for soft-wrapping. Measured with a hidden mirror that
106
- * mimics the textarea's wrapping so the offsets stay accurate even when long
107
- * lines wrap. Returns `null` when measurement is not possible.
108
- */
109
- export function measureEditorBlockOffsets(textarea, blockLines, text) {
110
- // The `typeof document === 'undefined'` half of this guard is real,
111
- // intentional SSR-safety API contract (documented in this function's own
112
- // JSDoc), but is empirically unreachable through this repo's actual call
113
- // graph today: its only caller (`useMarkdownScrollSync`) only ever
114
- // invokes it with an already browser-mounted `HTMLTextAreaElement` ref,
115
- // which cannot exist without `document` already existing. A real test
116
- // would require either reintroducing this package's own documented
117
- // `@vitest/coverage-v8` jsdom/node branch-merge bug (see
118
- // `features/html-viewer/dependencies`'s precedent in
119
- // packages/ui/archived provenance ledger) or hand-faking this function's full
120
- // `innerHTML`/`querySelectorAll`/`getComputedStyle` surface under plain
121
- // Node — disproportionate to a defensive guard with no reachable path.
122
- // See packages/ui/archived provenance ledger's 2026-07-22 dated entry.
123
- if (blockLines.length === 0 || typeof document === 'undefined')
124
- return null;
101
+ // A hidden off-screen div styled to wrap text exactly like `textarea`.
102
+ function createMirror({ textarea }) {
125
103
  const computed = window.getComputedStyle(textarea);
126
104
  const mirror = document.createElement('div');
127
105
  const style = mirror.style;
@@ -142,23 +120,30 @@ export function measureEditorBlockOffsets(textarea, blockLines, text) {
142
120
  style.width = `${textarea.clientWidth}px`;
143
121
  if (style.whiteSpace === 'normal' || !style.whiteSpace)
144
122
  style.whiteSpace = 'pre-wrap';
145
- const lines = text.split('\n');
123
+ return mirror;
124
+ }
125
+ // Block indices grouped by the (clamped, 0-based) source line they start on.
126
+ function groupBlocksByLine({ blockLines, lineCount }) {
146
127
  const markersByLine = new Map();
147
128
  blockLines.forEach((line, blockIndex) => {
148
- const lineIndex = Math.max(0, Math.min(lines.length - 1, line - 1));
129
+ const lineIndex = Math.max(0, Math.min(lineCount - 1, line - 1));
149
130
  const existing = markersByLine.get(lineIndex);
150
131
  if (existing)
151
132
  existing.push(blockIndex);
152
133
  else
153
134
  markersByLine.set(lineIndex, [blockIndex]);
154
135
  });
136
+ return markersByLine;
137
+ }
138
+ // The escaped `text` with an empty `data-md-block` marker span in front of
139
+ // every block's start line.
140
+ function buildMirrorMarkup({ blockLines, text }) {
141
+ const lines = text.split('\n');
142
+ const markersByLine = groupBlocksByLine({ blockLines, lineCount: lines.length });
155
143
  let buffer = '';
156
144
  for (let i = 0; i < lines.length; i += 1) {
157
- const markers = markersByLine.get(i);
158
- if (markers) {
159
- for (const blockIndex of markers)
160
- buffer += `<span data-md-block="${blockIndex}"></span>`;
161
- }
145
+ for (const blockIndex of markersByLine.get(i) ?? [])
146
+ buffer += `<span data-md-block="${blockIndex}"></span>`;
162
147
  // `text.split('\n')` always yields a dense array (never a hole), so
163
148
  // `lines[i]` for `i` in `[0, lines.length)` is always a defined string
164
149
  // (possibly `''`) — the `?? ''` fallback this used to have was dead
@@ -168,25 +153,51 @@ export function measureEditorBlockOffsets(textarea, blockLines, text) {
168
153
  if (i < lines.length - 1)
169
154
  buffer += '\n';
170
155
  }
171
- mirror.innerHTML = buffer;
156
+ return buffer;
157
+ }
158
+ // Lay the mirror out in the document just long enough to read each marker's
159
+ // offsetTop. Every marker span comes from `buildMirrorMarkup` (the text itself
160
+ // is escaped, so it cannot inject one), so each index is a valid block index.
161
+ function readMarkerOffsets({ mirror, blockCount }) {
172
162
  document.body.appendChild(mirror);
173
- const offsets = new Array(blockLines.length).fill(0);
163
+ const offsets = new Array(blockCount).fill(0);
174
164
  try {
175
- const markers = mirror.querySelectorAll('span[data-md-block]');
176
- for (const marker of Array.from(markers)) {
177
- const blockIndex = Number(marker.getAttribute('data-md-block'));
178
- if (Number.isInteger(blockIndex) && blockIndex >= 0 && blockIndex < offsets.length) {
179
- offsets[blockIndex] = marker.offsetTop;
180
- }
165
+ for (const marker of Array.from(mirror.querySelectorAll('span[data-md-block]'))) {
166
+ offsets[Number(marker.getAttribute('data-md-block'))] = marker.offsetTop;
181
167
  }
182
168
  }
183
169
  finally {
184
170
  document.body.removeChild(mirror);
185
171
  }
186
- if (!hasVerticalProgression(offsets))
187
- return null;
188
172
  return offsets;
189
173
  }
174
+ /**
175
+ * Pixel offset (within the textarea's scrollable content) of each block's
176
+ * start line, accounting for soft-wrapping. Measured with a hidden mirror that
177
+ * mimics the textarea's wrapping so the offsets stay accurate even when long
178
+ * lines wrap. Returns `null` when measurement is not possible.
179
+ */
180
+ export function measureEditorBlockOffsets(textarea, blockLines, text) {
181
+ // The `typeof document === 'undefined'` half of this guard is real,
182
+ // intentional SSR-safety API contract (documented in this function's own
183
+ // JSDoc), but is empirically unreachable through this repo's actual call
184
+ // graph today: its only caller (`useMarkdownScrollSync`) only ever
185
+ // invokes it with an already browser-mounted `HTMLTextAreaElement` ref,
186
+ // which cannot exist without `document` already existing. A real test
187
+ // would require either reintroducing this package's own documented
188
+ // `@vitest/coverage-v8` jsdom/node branch-merge bug (see
189
+ // `features/html-viewer/dependencies`'s precedent in
190
+ // packages/ui/archived provenance ledger) or hand-faking this function's full
191
+ // `innerHTML`/`querySelectorAll`/`getComputedStyle` surface under plain
192
+ // Node — disproportionate to a defensive guard with no reachable path.
193
+ // See packages/ui/archived provenance ledger's 2026-07-22 dated entry.
194
+ if (blockLines.length === 0 || typeof document === 'undefined')
195
+ return null;
196
+ const mirror = createMirror({ textarea });
197
+ mirror.innerHTML = buildMirrorMarkup({ blockLines, text });
198
+ const offsets = readMarkerOffsets({ mirror, blockCount: blockLines.length });
199
+ return hasVerticalProgression(offsets) ? offsets : null;
200
+ }
190
201
  /**
191
202
  * Pixel offset (within the preview pane's scrollable content) of each rendered
192
203
  * top-level block element. Returns `null` when the rendered article's direct
@@ -231,6 +242,25 @@ export function buildScrollAnchors(blockOffsets, scrollHeight) {
231
242
  }
232
243
  return anchors;
233
244
  }
245
+ // Anchor value at `index`, reading an array hole as 0.
246
+ function anchorAt({ anchors, index }) {
247
+ return anchors[index] ?? 0;
248
+ }
249
+ // Binary-search the lower index of the segment bracketing `value`, given
250
+ // `source[0] <= value < source[count - 1]`; the segment's upper index is
251
+ // always this plus one.
252
+ function findBracketLow({ value, source, count }) {
253
+ let low = 0;
254
+ let high = count - 1;
255
+ while (high - low > 1) {
256
+ const mid = (low + high) >> 1;
257
+ if (anchorAt({ anchors: source, index: mid }) <= value)
258
+ low = mid;
259
+ else
260
+ high = mid;
261
+ }
262
+ return low;
263
+ }
234
264
  /**
235
265
  * Map a scroll position from the source pane to the target pane using paired
236
266
  * anchor arrays (same length, both monotonic). Linear interpolation within the
@@ -240,25 +270,16 @@ export function mapScrollPosition(value, source, target) {
240
270
  const count = Math.min(source.length, target.length);
241
271
  if (count === 0)
242
272
  return value;
243
- if (count === 1)
244
- return target[0] ?? 0;
245
- if (value <= (source[0] ?? 0))
246
- return target[0] ?? 0;
247
- if (value >= (source[count - 1] ?? 0))
248
- return target[count - 1] ?? 0;
249
- let low = 0;
250
- let high = count - 1;
251
- while (high - low > 1) {
252
- const mid = (low + high) >> 1;
253
- if ((source[mid] ?? 0) <= value)
254
- low = mid;
255
- else
256
- high = mid;
257
- }
258
- const sourceLow = source[low] ?? 0;
259
- const sourceHigh = source[high] ?? 0;
260
- const targetLow = target[low] ?? 0;
261
- const targetHigh = target[high] ?? 0;
273
+ const last = count - 1;
274
+ if (count === 1 || value <= anchorAt({ anchors: source, index: 0 }))
275
+ return anchorAt({ anchors: target, index: 0 });
276
+ if (value >= anchorAt({ anchors: source, index: last }))
277
+ return anchorAt({ anchors: target, index: last });
278
+ const low = findBracketLow({ value, source, count });
279
+ const sourceLow = anchorAt({ anchors: source, index: low });
280
+ const sourceHigh = anchorAt({ anchors: source, index: low + 1 });
281
+ const targetLow = anchorAt({ anchors: target, index: low });
282
+ const targetHigh = anchorAt({ anchors: target, index: low + 1 });
262
283
  // `span` is provably > 0 here, for *any* input array (not just a
263
284
  // monotonic one): the early clamp checks above guarantee
264
285
  // `source[0] <= value < source[count - 1]` before the search loop ever