@jini-ai/ui 0.5.4 → 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 (270) hide show
  1. package/CHANGELOG.md +10 -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/Select/Select.hooks.d.ts +21 -0
  29. package/dist/features/admin-widgets/components/Select/Select.hooks.js +24 -2
  30. package/dist/features/admin-widgets/components/Select/Select.js +4 -3
  31. package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.hooks.js +29 -14
  32. package/dist/features/admin-widgets/index.d.ts +14 -2
  33. package/dist/features/admin-widgets/index.js +7 -1
  34. package/dist/features/admin-widgets/styles/admin-widgets.css +1 -0
  35. package/dist/features/asset-grid/react/components/AssetCard.js +7 -1
  36. package/dist/features/asset-grid/react/components/AssetGrid.d.ts +1 -1
  37. package/dist/features/asset-grid/react/components/AssetGrid.js +38 -18
  38. package/dist/features/asset-grid/react/components/useAssetGridFilters.d.ts +21 -0
  39. package/dist/features/asset-grid/react/components/useAssetGridFilters.js +19 -0
  40. package/dist/features/asset-grid/react/hooks/useAssetGridKeyboardShortcuts.js +16 -11
  41. package/dist/features/asset-grid/react/hooks/useAssetGridLiveUpdates.d.ts +2 -0
  42. package/dist/features/asset-grid/react/hooks/useAssetGridLiveUpdates.js +35 -11
  43. package/dist/features/asset-tree-browser/react/components/AssetTreeBrowser.d.ts +6 -6
  44. package/dist/features/asset-tree-browser/react/components/AssetTreeBrowser.js +10 -129
  45. package/dist/features/asset-tree-browser/react/components/AssetTreeBrowserParts.d.ts +50 -0
  46. package/dist/features/asset-tree-browser/react/components/AssetTreeBrowserParts.js +95 -0
  47. package/dist/features/asset-tree-browser/react/components/useAssetTreeBrowserController.d.ts +56 -0
  48. package/dist/features/asset-tree-browser/react/components/useAssetTreeBrowserController.js +80 -0
  49. package/dist/features/asset-tree-browser/rules.js +40 -22
  50. package/dist/features/browser-chrome/rules.js +74 -70
  51. package/dist/features/charts/browser.js +11 -5
  52. package/dist/features/charts/react.js +2 -2
  53. package/dist/features/charts/table.js +2 -2
  54. package/dist/features/command-palette/react/hooks/useCommandPalette.js +3 -9
  55. package/dist/features/connectors/ConnectorsBrowser.d.ts +4 -4
  56. package/dist/features/connectors/ConnectorsBrowser.js +26 -14
  57. package/dist/features/connectors/components/ConnectorCard.js +67 -38
  58. package/dist/features/connectors/components/ConnectorDetailDrawer.js +62 -31
  59. package/dist/features/connectors/components/ConnectorGrid.js +11 -6
  60. package/dist/features/connectors/hooks/useConnectorAuthorization.js +6 -0
  61. package/dist/features/connectors/hooks/useConnectorCatalog.d.ts +2 -0
  62. package/dist/features/connectors/hooks/useConnectorCatalog.js +32 -13
  63. package/dist/features/connectors/hooks/useWiredConnectorsBrowser.d.ts +3 -3
  64. package/dist/features/connectors/rules.d.ts +8 -1
  65. package/dist/features/connectors/rules.js +42 -20
  66. package/dist/features/connectors/types.d.ts +11 -0
  67. package/dist/features/execution/constants.d.ts +4 -10
  68. package/dist/features/execution/constants.js +9 -94
  69. package/dist/features/execution/react/components/ByokProviderForm.d.ts +2 -4
  70. package/dist/features/execution/react/components/ByokProviderForm.js +140 -43
  71. package/dist/features/execution/react/components/ExecutionTab.d.ts +1 -2
  72. package/dist/features/execution/react/components/ExecutionTab.js +52 -30
  73. package/dist/features/execution/react/components/LocalCliAgentCard.js +12 -3
  74. package/dist/features/execution/react/components/LocalCliAgentList.js +14 -3
  75. package/dist/features/execution/react/components/LocalCliAgentSettings.js +14 -3
  76. package/dist/features/execution/react/hooks/useLocalCliAgentCard.js +54 -36
  77. package/dist/features/execution/react/hooks/useReasoningControl.d.ts +1 -1
  78. package/dist/features/execution/react/hooks/useReasoningControl.js +116 -75
  79. package/dist/features/execution/rules.d.ts +13 -2
  80. package/dist/features/execution/rules.js +51 -11
  81. package/dist/features/execution/types.d.ts +6 -42
  82. package/dist/features/execution/types.js +0 -17
  83. package/dist/features/external-mcp/react/components/ExternalMcpTab.d.ts +6 -2
  84. package/dist/features/external-mcp/react/components/ExternalMcpTab.js +9 -2
  85. package/dist/features/file-dropzone/react/components/FileDropzone.js +24 -25
  86. package/dist/features/file-dropzone/react/components/FileDropzoneLightbox.d.ts +2 -2
  87. package/dist/features/file-dropzone/react/components/FileDropzoneLightbox.js +35 -30
  88. package/dist/features/file-dropzone/react/components/FileDropzoneThumbnailGrid.js +30 -24
  89. package/dist/features/file-dropzone/react/components/FileDropzoneZone.d.ts +14 -0
  90. package/dist/features/file-dropzone/react/components/FileDropzoneZone.js +26 -0
  91. package/dist/features/html-editor/canvas-content-wrapper.js +13 -13
  92. package/dist/features/html-editor/canvas-embed-placeholders.js +46 -37
  93. package/dist/features/html-editor/css.js +76 -69
  94. package/dist/features/html-editor/editor-blocks.js +47 -33
  95. package/dist/features/html-editor/editor-serialization.js +6 -5
  96. package/dist/features/html-editor/protected-elements.js +39 -28
  97. package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.js +4 -4
  98. package/dist/features/html-editor/source-splice.js +51 -30
  99. package/dist/features/i18n/dictionaries/settings-dialog.ja.js +1 -0
  100. package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +1 -0
  101. package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +1 -0
  102. package/dist/features/i18n/locale.js +20 -16
  103. package/dist/features/iframe-pool/react/dom-sync.js +8 -6
  104. package/dist/features/integrations/react/components/CodexInstallToggleButton.js +19 -4
  105. package/dist/features/integrations/react/components/IntegrationsTab.js +60 -14
  106. package/dist/features/interactive-ui/interactive-ui.css +1 -1
  107. package/dist/features/interactive-ui/providers/native/data-table.manifest.d.ts +6 -6
  108. package/dist/features/interactive-ui/providers/shadcn/data-table.js +10 -7
  109. package/dist/features/interactive-ui/providers/shadcn/data-table.manifest.d.ts +6 -6
  110. package/dist/features/lexical-rich-text-editor/mention-parser.js +60 -57
  111. package/dist/features/lexical-rich-text-editor/react/hooks/useKeyboardCommands.js +33 -38
  112. package/dist/features/lexical-rich-text-editor/rules.js +87 -74
  113. package/dist/features/lexical-rich-text-editor/serialize.js +27 -25
  114. package/dist/features/mcp-ui/secret-card.js +111 -61
  115. package/dist/features/mcp-ui/surfaces/confirmation.js +39 -21
  116. package/dist/features/mcp-ui/surfaces/document.d.ts +1 -1
  117. package/dist/features/mcp-ui/surfaces/document.js +10 -2
  118. package/dist/features/mcp-ui/surfaces/fields.js +30 -50
  119. package/dist/features/mcp-ui/surfaces/form.d.ts +6 -0
  120. package/dist/features/mcp-ui/surfaces/form.js +43 -21
  121. package/dist/features/mcp-ui/surfaces/outcome.js +30 -20
  122. package/dist/features/mcp-ui/surfaces/text-input.js +33 -21
  123. package/dist/features/mcp-ui/surfaces/tokens.d.ts +6 -1
  124. package/dist/features/mcp-ui/surfaces/tokens.js +5 -2
  125. package/dist/features/media-providers/constants.d.ts +1 -0
  126. package/dist/features/media-providers/constants.js +2 -81
  127. package/dist/features/media-providers/index.d.ts +1 -1
  128. package/dist/features/media-providers/ports.d.ts +2 -0
  129. package/dist/features/media-providers/react/components/MediaProviderCard.d.ts +22 -0
  130. package/dist/features/media-providers/react/components/MediaProviderCard.js +50 -0
  131. package/dist/features/media-providers/react/components/MediaProvidersTab.d.ts +5 -6
  132. package/dist/features/media-providers/react/components/MediaProvidersTab.js +40 -72
  133. package/dist/features/media-providers/react/components/media-providers-tab-labels.d.ts +11 -0
  134. package/dist/features/media-providers/react/components/media-providers-tab-labels.js +57 -0
  135. package/dist/features/media-providers/react/hooks/useMediaProvidersTab.d.ts +4 -2
  136. package/dist/features/media-providers/react/hooks/useMediaProvidersTab.js +37 -23
  137. package/dist/features/media-providers/rules.js +17 -10
  138. package/dist/features/media-providers/types.d.ts +4 -22
  139. package/dist/features/media-providers/types.js +3 -8
  140. package/dist/features/memory/formatters.js +127 -92
  141. package/dist/features/memory/react/components/MemoryConnectedPanel.js +92 -36
  142. package/dist/features/memory/react/components/MemoryList.js +37 -18
  143. package/dist/features/memory/react/hooks/useMemoryExtractions.store.js +42 -36
  144. package/dist/features/notifications/react/components/NotificationsTab.js +28 -28
  145. package/dist/features/observability/long-task.d.ts +0 -12
  146. package/dist/features/observability/long-task.js +15 -19
  147. package/dist/features/observability/resource-error.d.ts +0 -10
  148. package/dist/features/observability/resource-error.js +18 -13
  149. package/dist/features/observability/stuck-run.js +10 -6
  150. package/dist/features/observability/telemetry-url.d.ts +6 -0
  151. package/dist/features/observability/telemetry-url.js +16 -0
  152. package/dist/features/observability/white-screen.js +12 -24
  153. package/dist/features/panel-kit/fetch-query/adapter.react.d.ts +2 -2
  154. package/dist/features/panel-kit/fetch-query/adapter.react.js +4 -2
  155. package/dist/features/panel-kit/fetch-query/adapter.tanstack.js +46 -33
  156. package/dist/features/panel-kit/helpers/standing-draft-local-backup.js +3 -1
  157. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +3 -1
  158. package/dist/features/privacy/react/components/PrivacyTab.js +24 -13
  159. package/dist/features/progress-card/components/ProgressCard.js +7 -3
  160. package/dist/features/progress-card/reference-adapters.js +59 -54
  161. package/dist/features/project-locations/react/components/ProjectLocationsTab.js +24 -15
  162. package/dist/features/project-locations/react/hooks/useProjectLocationsTab.d.ts +8 -0
  163. package/dist/features/project-locations/react/hooks/useProjectLocationsTab.js +31 -23
  164. package/dist/features/resource-dashboard/react/components/ResourceBoard.js +6 -47
  165. package/dist/features/resource-dashboard/react/components/ResourceBoardView.d.ts +2 -2
  166. package/dist/features/resource-dashboard/react/components/ResourceBoardView.js +3 -6
  167. package/dist/features/resource-dashboard/react/components/ResourceBoardViewParts.d.ts +15 -0
  168. package/dist/features/resource-dashboard/react/components/ResourceBoardViewParts.js +35 -0
  169. package/dist/features/resource-dashboard/react/components/ResourceCard.js +5 -16
  170. package/dist/features/resource-dashboard/react/components/ResourceCardParts.d.ts +34 -0
  171. package/dist/features/resource-dashboard/react/components/ResourceCardParts.js +34 -0
  172. package/dist/features/resource-dashboard/react/components/ResourceRowList.js +3 -1
  173. package/dist/features/resource-dashboard/react/components/ResourceRowListItem.js +2 -2
  174. package/dist/features/resource-dashboard/react/components/ResourceRowListItemParts.d.ts +19 -0
  175. package/dist/features/resource-dashboard/react/components/ResourceRowListItemParts.js +11 -0
  176. package/dist/features/resource-dashboard/react/components/ResourceRowListView.d.ts +1 -1
  177. package/dist/features/resource-dashboard/react/components/ResourceRowListView.js +4 -4
  178. package/dist/features/resource-dashboard/react/components/ResourceRowListViewParts.d.ts +10 -0
  179. package/dist/features/resource-dashboard/react/components/ResourceRowListViewParts.js +20 -0
  180. package/dist/features/resource-dashboard/react/components/resourceBoardProps.d.ts +48 -0
  181. package/dist/features/resource-dashboard/react/components/resourceBoardProps.js +70 -0
  182. package/dist/features/resource-dashboard/react/components/resourceRowListProps.d.ts +19 -0
  183. package/dist/features/resource-dashboard/react/components/resourceRowListProps.js +14 -0
  184. package/dist/features/resource-dashboard/react/components/translatedErrorLabelProps.d.ts +16 -0
  185. package/dist/features/resource-dashboard/react/components/translatedErrorLabelProps.js +13 -0
  186. package/dist/features/resource-dashboard/rules.js +2 -8
  187. package/dist/features/sketch-editor/dom.js +150 -99
  188. package/dist/features/sketch-editor/react/components/SketchEditor.js +26 -15
  189. package/dist/features/skills/react/components/SkillDraftForm.js +13 -19
  190. package/dist/features/skills/react/components/SkillDraftForm.labels.d.ts +31 -0
  191. package/dist/features/skills/react/components/SkillDraftForm.labels.js +54 -0
  192. package/dist/features/skills/react/components/SkillRow.js +38 -18
  193. package/dist/features/skills/react/components/SkillRow.labels.d.ts +7 -0
  194. package/dist/features/skills/react/components/SkillRow.labels.js +18 -0
  195. package/dist/features/skills/react/components/SkillsTab.js +24 -14
  196. package/dist/features/skills/react/components/SkillsTab.labels.d.ts +7 -0
  197. package/dist/features/skills/react/components/SkillsTab.labels.js +11 -0
  198. package/dist/features/skills/rules.js +32 -23
  199. package/dist/features/source-config-list/agent-handles.js +2 -1
  200. package/dist/features/source-config-list/dependencies.js +53 -49
  201. package/dist/features/source-config-list/index.d.ts +3 -3
  202. package/dist/features/source-config-list/react/components/SourceConfigAddForm.d.ts +6 -5
  203. package/dist/features/source-config-list/react/components/SourceConfigAddForm.js +38 -7
  204. package/dist/features/source-config-list/react/components/SourceConfigField.js +69 -25
  205. package/dist/features/source-config-list/react/components/SourceConfigItemCard.d.ts +6 -5
  206. package/dist/features/source-config-list/react/components/SourceConfigItemCard.js +76 -20
  207. package/dist/features/source-config-list/react/components/SourceConfigListView.d.ts +5 -3
  208. package/dist/features/source-config-list/react/components/SourceConfigListView.js +33 -3
  209. package/dist/features/source-config-list/react/components/SourceConfigTestControl.js +17 -1
  210. package/dist/features/source-config-list/react/components/SourceConfigTrustOptions.d.ts +11 -0
  211. package/dist/features/source-config-list/react/components/SourceConfigTrustOptions.js +12 -0
  212. package/dist/features/source-config-list/types.d.ts +14 -0
  213. package/dist/features/tab-launcher-menu/react/components/TabLauncherMenu.js +25 -1
  214. package/dist/features/tab-launcher-menu/react/hooks/useTabLauncherMenu.js +15 -16
  215. package/dist/features/tab-strip/react/components/TabBar.js +8 -1
  216. package/dist/features/tabbed-dialog/react/components/TabbedDialog.js +8 -30
  217. package/dist/features/tabbed-dialog/react/components/TabbedDialogChrome.d.ts +15 -0
  218. package/dist/features/tabbed-dialog/react/components/TabbedDialogChrome.js +8 -0
  219. package/dist/features/tabbed-dialog/react/components/TabbedDialogHeader.d.ts +12 -0
  220. package/dist/features/tabbed-dialog/react/components/TabbedDialogHeader.js +6 -0
  221. package/dist/features/tabbed-dialog/react/components/TabbedDialogSidebar.d.ts +27 -0
  222. package/dist/features/tabbed-dialog/react/components/TabbedDialogSidebar.js +12 -0
  223. package/dist/features/tabbed-dialog/react/components/tabbedDialogView.d.ts +43 -0
  224. package/dist/features/tabbed-dialog/react/components/tabbedDialogView.js +59 -0
  225. package/dist/features/version-manager/dependencies.d.ts +3 -1
  226. package/dist/features/version-manager/dependencies.js +3 -2
  227. package/dist/features/version-manager/react/components/VersionManagerModal.js +10 -6
  228. package/dist/features/version-manager/react/hooks/useVersionManager.js +7 -4
  229. package/dist/features/viewer-shell/react/components/CommentSidePanel.js +103 -50
  230. package/dist/features/viewer-shell/react/components/MarkdownSplitPane.js +28 -8
  231. package/dist/features/viewer-shell/react/hooks/useCommentReorder.js +22 -16
  232. package/dist/features/viewer-shell/rules.js +19 -23
  233. package/dist/react/components/CustomSelect.d.ts +38 -1
  234. package/dist/react/components/CustomSelect.js +94 -38
  235. package/dist/react/components/OnboardingDropdown.js +20 -8
  236. package/dist/react/components/RemixIcon.stylesheet.js +3 -1
  237. package/dist/react/components/Toast.js +13 -2
  238. package/dist/react/components/TooltipLayer.js +5 -14
  239. package/dist/react/components/WorkingDirPicker.js +78 -50
  240. package/dist/react/components/remixicon-font-recovery.js +38 -30
  241. package/dist/react/components/select-popover.d.ts +8 -6
  242. package/dist/react/components/select-popover.js +33 -29
  243. package/dist/react/mcp-ui/McpUiHost.d.ts +1 -1
  244. package/dist/react/mcp-ui/McpUiHost.js +10 -11
  245. package/dist/react/mcp-ui/host-message-source.d.ts +14 -2
  246. package/dist/react/mcp-ui/host-message-source.js +15 -5
  247. package/dist/react/mcp-ui/mcp-ui-host-frame.d.ts +41 -0
  248. package/dist/react/mcp-ui/mcp-ui-host-frame.js +39 -0
  249. package/dist/renderers/annotation-canvas/react/components/AnnotationCanvas.js +97 -69
  250. package/dist/renderers/preview-modal-shell/react/components/PreviewModalShell.js +59 -30
  251. package/dist/renderers/preview-modal-shell/react/components/icons.js +4 -1
  252. package/dist/renderers/react/components/ArtifactView.js +9 -6
  253. package/dist/renderers/srcdoc/build.d.ts +4 -1
  254. package/dist/renderers/srcdoc/build.js +62 -48
  255. package/dist/styles/admin.css +29 -14
  256. package/dist/styles/lightbox.css +142 -0
  257. package/dist/styles/variables.css +128 -0
  258. package/dist/theme/apply.js +9 -5
  259. package/dist/utils/auto-open-file.d.ts +2 -1
  260. package/dist/utils/auto-open-file.js +69 -85
  261. package/dist/utils/markdown-scroll-sync.js +108 -87
  262. package/dist/utils/polygon-selection.js +25 -26
  263. package/dist/utils/smooth-scroll-to-top.d.ts +1 -1
  264. package/dist/utils/smooth-scroll-to-top.js +2 -32
  265. package/dist/utils/sse.js +26 -26
  266. package/dist/utils/unit-bezier.d.ts +7 -0
  267. package/dist/utils/unit-bezier.js +38 -0
  268. package/package.json +7 -10
  269. package/dist/features/admin-widgets/components/ImagePreviewModal.hooks.d.ts +0 -13
  270. package/dist/features/admin-widgets/components/ImagePreviewModal.hooks.js +0 -46
@@ -5,7 +5,8 @@ import { useInfoTip } from "./InfoTip.hooks.js";
5
5
  * and no touch visibility. Hover/focus opens this affordance; Escape dismisses without moving
6
6
  * focus, which onBlur alone cannot do.
7
7
  * A body portal escapes overflow:hidden/scrolling ancestors: z-index cannot rescue an absolutely
8
- * positioned descendant from clipping. Measured viewport coordinates position it above by
8
+ * positioned descendant from clipping. Inside a modal <dialog> it portals into that dialog
9
+ * instead, because the dialog's top layer covers everything in body. Measured viewport coordinates position it above by
9
10
  * preference, with the hook's headroom guard choosing below near the viewport's top; near a side
10
11
  * edge (or a host panel marked `data-jini-tip-bounds`) it shifts by its real width, never squeezes.
11
12
  * Keep state and DOM measurement in the injectable hook so rendering can use fixed fake state.
@@ -13,7 +14,7 @@ import { useInfoTip } from "./InfoTip.hooks.js";
13
14
  * tap, blur or Escape; a hover-opened tip still closes when the pointer leaves.
14
15
  */
15
16
  export interface InfoTipProps {
16
- /** The explanation the bubble shows. */
17
+ /** The explanation the bubble shows; newlines separate lines, and "• " lines show as a hanging list. */
17
18
  label: string;
18
19
  /**
19
20
  * Short accessible name for the icon ("About subagents"). Omitted, the icon is named by `label`
@@ -78,6 +78,8 @@ export function useInfoTip(_input = {}, { placement: preferredPlacement } = {})
78
78
  }, [open]);
79
79
  function handleIconKeyDown(e) {
80
80
  if (e.key === "Escape" && open) {
81
+ // preventDefault also stops an enclosing modal <dialog> from closing on the same keypress.
82
+ e.preventDefault();
81
83
  e.stopPropagation();
82
84
  hide();
83
85
  return;
@@ -3,18 +3,45 @@ import { useId } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
  import { agentHandle } from "@jini-ai/agentic";
5
5
  import { useInfoTip } from "./InfoTip.hooks.js";
6
- import { infoTipBubbleStyle } from "./InfoTip.rules.js";
6
+ import { infoTipBubbleStyle, infoTipLines } from "./InfoTip.rules.js";
7
7
  /** Focus/hover tooltip that portals outside clipping ancestors; Escape dismisses in place. */
8
8
  export function InfoTip({ label, triggerLabel, descriptionId, placement: preferredPlacement, useTip = useInfoTip, agentHandle: handle, icon }) {
9
- const { open, placement, coords, iconRef, bubbleRef, show, hide, leave, toggle, handleIconKeyDown } = useTip({}, { placement: preferredPlacement === "inline" ? undefined : preferredPlacement });
9
+ const inline = preferredPlacement === "inline";
10
+ const { open, placement, coords, iconRef, bubbleRef, show, hide, leave, toggle, handleIconKeyDown } = useTip({}, { placement: inline ? undefined : preferredPlacement });
10
11
  const autoId = useId();
11
- const name = triggerLabel ?? label;
12
+ const { name, textId, describedBy } = infoTipNaming({ label, autoId }, { triggerLabel, descriptionId });
13
+ return (_jsxs("span", { className: inline ? "info-tip info-tip--inline" : "info-tip", children: [_jsx("span", { ref: iconRef, className: icon ? "info-tip-icon info-tip-icon--custom" : "info-tip-icon", role: "button", tabIndex: 0, "aria-label": name, "aria-expanded": open, ...(describedBy ? { "aria-describedby": describedBy } : {}), onMouseEnter: show, onMouseLeave: leave, onFocus: show, onBlur: hide, onClick: toggle, onKeyDown: handleIconKeyDown, ...(handle ? agentHandle({ handle }, { role: "button", label: name }) : {}), children: icon ?? "ⓘ" }), textId ? _jsx("span", { id: textId, hidden: true, children: label }) : null, open ? _jsx(InfoTipOpenCopy, { label: label, inline: inline, placement: placement, coords: coords, iconRef: iconRef, bubbleRef: bubbleRef }) : null] }));
14
+ }
15
+ /**
16
+ * The icon's accessible name and the id of the hidden copy of `label` (if any).
17
+ * @returns `name` for `aria-label`, `textId` for the hidden copy, `describedBy` for `aria-describedby`.
18
+ * @complexity O(1).
19
+ */
20
+ function infoTipNaming({ label, autoId }, { triggerLabel, descriptionId }) {
12
21
  // With no short name the icon is already named by the full text; describing it again would
13
22
  // make a screen reader read it twice.
14
23
  const textId = descriptionId ?? (triggerLabel ? autoId : undefined);
15
- const describedBy = triggerLabel ? textId : undefined;
16
- return (_jsxs("span", { className: preferredPlacement === "inline" ? "info-tip info-tip--inline" : "info-tip", children: [_jsx("span", { ref: iconRef, className: icon ? "info-tip-icon info-tip-icon--custom" : "info-tip-icon", role: "button", tabIndex: 0, "aria-label": name, "aria-expanded": open, ...(describedBy ? { "aria-describedby": describedBy } : {}), onMouseEnter: show, onMouseLeave: leave, onFocus: show, onBlur: hide, onClick: toggle, onKeyDown: handleIconKeyDown, ...(handle ? agentHandle({ handle: handle }, { role: "button", label: name }) : {}), children: icon ?? "ⓘ" }), textId ? _jsx("span", { id: textId, hidden: true, children: label }) : null, open && preferredPlacement === "inline" ? _jsx("span", { className: "info-tip-inline-copy", "aria-hidden": "true", children: label }) : open
17
- ? createPortal(_jsx("span", { ref: bubbleRef, className: placement === "below" ? "info-tip-bubble info-tip-bubble-below" : "info-tip-bubble", role: "presentation", "aria-hidden": "true", style: infoTipBubbleStyle(coords), children: label }), document.body)
18
- : null] }));
24
+ return { name: triggerLabel ?? label, textId, describedBy: triggerLabel ? textId : undefined };
25
+ }
26
+ /** The open tip: inline copy beside the icon, or the bubble portaled out of clipping ancestors. */
27
+ function InfoTipOpenCopy({ label, inline, placement, coords, iconRef, bubbleRef }) {
28
+ if (inline)
29
+ return _jsx("span", { className: "info-tip-inline-copy", "aria-hidden": "true", children: label });
30
+ return createPortal(_jsx("span", { ref: bubbleRef, className: placement === "below" ? "info-tip-bubble info-tip-bubble-below" : "info-tip-bubble", role: "presentation", "aria-hidden": "true", style: { ...infoTipBubbleStyle(coords), whiteSpace: "pre-line" }, children: _jsx(InfoTipBubbleText, { label: label }) }),
31
+ // A modal <dialog> sits in the browser's top layer, above anything portaled to body.
32
+ iconRef.current?.closest("dialog") ?? document.body);
33
+ }
34
+ /** The bubble's text: a hanging list when any line is a "• " bullet, else the label as-is. */
35
+ function InfoTipBubbleText({ label }) {
36
+ const lines = infoTipLines(label);
37
+ if (!lines)
38
+ return label;
39
+ // Keep the label's text separators; flex layout alone loses them from textContent.
40
+ return lines.map((line, index) => {
41
+ const separator = index < lines.length - 1 ? "\n" : null;
42
+ return line.bullet
43
+ ? _jsxs("span", { className: "info-tip-bullet", children: [_jsx("span", { "aria-hidden": "true", children: "\u2022" }), " ", _jsx("span", { children: line.text }), separator] }, index)
44
+ : _jsxs("span", { className: "info-tip-line", children: [line.text, separator] }, index);
45
+ });
19
46
  }
20
47
  //# sourceMappingURL=InfoTip.js.map
@@ -69,4 +69,17 @@ export declare function infoTipBubbleStyle(coords: {
69
69
  left: number;
70
70
  arrowLeft?: number | undefined;
71
71
  }): CSSProperties;
72
+ /** One line of a bubble that holds a bulleted list. */
73
+ export interface InfoTipLine {
74
+ text: string;
75
+ bullet: boolean;
76
+ }
77
+ /**
78
+ * Splits a label into lines when any line is a "• " bullet (e.g. a settings group's one tip, a line
79
+ * per option), so each bullet's wrapped text can hang past its dot; `pre-line` alone wraps under it.
80
+ * @param label The tip text; newlines separate lines.
81
+ * @returns The lines with their "• " removed, or `null` when the label has no bullet line.
82
+ * @complexity O(n) in the label's length.
83
+ */
84
+ export declare function infoTipLines(label: string): InfoTipLine[] | null;
72
85
  //# sourceMappingURL=InfoTip.rules.d.ts.map
@@ -32,25 +32,45 @@ const ARROW_INSET_PX = 10;
32
32
  export function placeInfoTipBubble(input, { bounds, preferred } = {}) {
33
33
  const { anchor, bubble, viewport } = input;
34
34
  const width = bubble && bubble.width > 0 ? bubble.width : MAX_BUBBLE_WIDTH_PX;
35
+ const height = bubble && bubble.height > 0 ? bubble.height : 0;
36
+ const { left, arrowLeft } = placeHorizontally({ anchor, width, viewportWidth: viewport.width }, { bounds });
37
+ const placement = placeVertically({ anchor, height, viewportHeight: viewport.height }, { preferred });
38
+ return { placement, top: placement === "above" ? anchor.top : anchor.top + anchor.height, left, arrowLeft };
39
+ }
40
+ /**
41
+ * The horizontal half of {@link placeInfoTipBubble}: the bubble's centre x and the arrow's offset.
42
+ * @param input.width The bubble's measured width, or the worst-case estimate before measurement.
43
+ * @param options.bounds A host panel's limits; used only when the bubble fits inside them.
44
+ * @returns The bubble's centre x and the arrow's x inside the bubble.
45
+ * @complexity O(1).
46
+ */
47
+ function placeHorizontally({ anchor, width, viewportWidth }, { bounds }) {
35
48
  const anchorCentre = anchor.left + anchor.width / 2;
36
- const viewportSpan = { lo: EDGE_MARGIN_PX, hi: viewport.width - EDGE_MARGIN_PX };
49
+ const viewportSpan = { lo: EDGE_MARGIN_PX, hi: viewportWidth - EDGE_MARGIN_PX };
37
50
  const boundsSpan = bounds
38
- ? { lo: Math.max(bounds.left, 0) + EDGE_MARGIN_PX, hi: Math.min(bounds.right, viewport.width) - EDGE_MARGIN_PX }
51
+ ? { lo: Math.max(bounds.left, 0) + EDGE_MARGIN_PX, hi: Math.min(bounds.right, viewportWidth) - EDGE_MARGIN_PX }
39
52
  : null;
40
53
  const span = boundsSpan && boundsSpan.hi - boundsSpan.lo >= width ? boundsSpan : viewportSpan;
41
54
  const left = span.hi - span.lo < width
42
- ? viewport.width / 2
55
+ ? viewportWidth / 2
43
56
  : Math.min(Math.max(anchorCentre, span.lo + width / 2), span.hi - width / 2);
44
57
  const bubbleLeftEdge = left - width / 2;
45
58
  const arrowLeft = Math.min(Math.max(anchorCentre - bubbleLeftEdge, ARROW_INSET_PX), width - ARROW_INSET_PX);
46
- const anchorBottom = anchor.top + anchor.height;
47
- const height = bubble && bubble.height > 0 ? bubble.height : 0;
59
+ return { left, arrowLeft };
60
+ }
61
+ /**
62
+ * The vertical half of {@link placeInfoTipBubble}: which side of the icon the bubble opens on.
63
+ * @param input.height The bubble's measured height, or 0 before measurement.
64
+ * @param options.preferred A host's preferred side; only "bottom" changes the default above-first rule.
65
+ * @returns The side the bubble opens on.
66
+ * @complexity O(1).
67
+ */
68
+ function placeVertically({ anchor, height, viewportHeight }, { preferred }) {
48
69
  const fitsAbove = height > 0 ? anchor.top >= height + GAP_PX : anchor.top >= ABOVE_HEADROOM_PX;
49
- const fitsBelow = height === 0 || anchorBottom + GAP_PX + height <= viewport.height;
50
- const placement = preferred === "bottom"
51
- ? (!fitsBelow && fitsAbove ? "above" : "below")
52
- : fitsAbove ? "above" : "below";
53
- return { placement, top: placement === "above" ? anchor.top : anchorBottom, left, arrowLeft };
70
+ if (preferred !== "bottom")
71
+ return fitsAbove ? "above" : "below";
72
+ const fitsBelow = height === 0 || anchor.top + anchor.height + GAP_PX + height <= viewportHeight;
73
+ return !fitsBelow && fitsAbove ? "above" : "below";
54
74
  }
55
75
  /**
56
76
  * The portal's inline style: position plus, once measured, the arrow offset as a CSS variable
@@ -65,4 +85,17 @@ export function infoTipBubbleStyle(coords) {
65
85
  style["--info-tip-arrow-left"] = `${coords.arrowLeft}px`;
66
86
  return style;
67
87
  }
88
+ /**
89
+ * Splits a label into lines when any line is a "• " bullet (e.g. a settings group's one tip, a line
90
+ * per option), so each bullet's wrapped text can hang past its dot; `pre-line` alone wraps under it.
91
+ * @param label The tip text; newlines separate lines.
92
+ * @returns The lines with their "• " removed, or `null` when the label has no bullet line.
93
+ * @complexity O(n) in the label's length.
94
+ */
95
+ export function infoTipLines(label) {
96
+ const lines = label.split("\n");
97
+ if (!lines.some((line) => line.startsWith("• ")))
98
+ return null;
99
+ return lines.map((line) => line.startsWith("• ") ? { text: line.slice(2), bullet: true } : { text: line, bullet: false });
100
+ }
68
101
  //# sourceMappingURL=InfoTip.rules.js.map
@@ -0,0 +1,30 @@
1
+ import { type ReactNode } from "react";
2
+ import { useLightbox } from "./Lightbox.hooks.js";
3
+ type LightboxPart = "dialog" | "close" | "previous" | "next";
4
+ export interface LightboxProps {
5
+ open: boolean;
6
+ /** Shown above the media (usually the file name) and used as the dialog's accessible name. */
7
+ title: string;
8
+ onClose: () => void;
9
+ /** The media itself. `img`/`video` children are contain-fit to the viewport and never upscaled. */
10
+ children?: ReactNode;
11
+ /** Zero-based position and set size; with `onNavigate` and `count > 1` the arrow pair renders. */
12
+ index?: number | undefined;
13
+ count?: number | undefined;
14
+ onNavigate?: ((index: number) => void) | undefined;
15
+ /** Caller-specific controls (download, copy, metadata), placed bottom-left opposite the arrows. */
16
+ actions?: ReactNode;
17
+ /** No locale is owned here; a translated caller passes its own accessible labels. */
18
+ labels?: Partial<Record<"close" | "previous" | "next", string>> | undefined;
19
+ /** Extra class on the `<dialog>` so a caller can scope its own media rules. */
20
+ className?: string | undefined;
21
+ /** Per-part attributes (agent handles, `data-jini-part` markers) without the shell knowing them. */
22
+ partAttrs?: Partial<Record<LightboxPart, Record<string, string>>> | undefined;
23
+ /** A fake controller can exercise rendering without a real native-dialog lifecycle. */
24
+ useLightboxHook?: typeof useLightbox | undefined;
25
+ }
26
+ /** Full-viewport media lightbox: title and counter top-left, close top-right, the media as large
27
+ * as fits, caller actions bottom-left and the previous/next pair together bottom-right. */
28
+ export declare function Lightbox(props: LightboxProps): import("react").JSX.Element;
29
+ export {};
30
+ //# sourceMappingURL=Lightbox.d.ts.map
@@ -0,0 +1,40 @@
1
+ import { type MouseEvent, type RefObject, type SyntheticEvent } from "react";
2
+ /**
3
+ * Lifecycle for the shared `Lightbox`: native `<dialog>` open/close, focus capture and return,
4
+ * arrow-key navigation, Escape and backdrop dismissal. Absorbs the open/close and focus logic the
5
+ * media lightboxes and `ImagePreviewModal` each carried, so every caller gets the same behavior.
6
+ */
7
+ /** Idempotent native open. `showModal()` throws on an already-open dialog in real browsers; the
8
+ * attribute fallback covers DOM environments without it (jsdom). */
9
+ export declare function openLightboxDialog(dialog: HTMLDialogElement): void;
10
+ /** The close-side mirror of {@link openLightboxDialog} — same fallback and already-closed guard. */
11
+ export declare function closeLightboxDialog(dialog: HTMLDialogElement): void;
12
+ export interface LightboxHookInput {
13
+ open: boolean;
14
+ onClose: () => void;
15
+ /** Zero-based position in the set; with `count > 1` and `onNavigate` the arrow pair renders. */
16
+ index?: number | undefined;
17
+ count?: number | undefined;
18
+ onNavigate?: ((index: number) => void) | undefined;
19
+ }
20
+ export interface LightboxController {
21
+ dialogRef: RefObject<HTMLDialogElement | null>;
22
+ closeRef: RefObject<HTMLButtonElement | null>;
23
+ prevRef: RefObject<HTMLButtonElement | null>;
24
+ nextRef: RefObject<HTMLButtonElement | null>;
25
+ /** Whether the arrow pair renders at all (a single item has nowhere to go). */
26
+ navigable: boolean;
27
+ hasPrev: boolean;
28
+ hasNext: boolean;
29
+ /** "3 / 12", or "" when there is no set to count. */
30
+ counter: string;
31
+ previous: () => void;
32
+ next: () => void;
33
+ handleCancel: (event: SyntheticEvent<HTMLDialogElement>) => void;
34
+ handleClick: (event: MouseEvent<HTMLDialogElement>) => void;
35
+ }
36
+ /** Marks the stage's empty area: the dialog fills the viewport, so "backdrop" is any click that
37
+ * lands on the dialog itself or the stage around the media rather than on a control or the media. */
38
+ export declare const LIGHTBOX_DISMISS_ATTR = "data-jini-lightbox-dismiss";
39
+ export declare function useLightbox(input: LightboxHookInput): LightboxController;
40
+ //# sourceMappingURL=Lightbox.hooks.d.ts.map
@@ -0,0 +1,128 @@
1
+ import { useEffect, useRef } from "react";
2
+ /**
3
+ * Lifecycle for the shared `Lightbox`: native `<dialog>` open/close, focus capture and return,
4
+ * arrow-key navigation, Escape and backdrop dismissal. Absorbs the open/close and focus logic the
5
+ * media lightboxes and `ImagePreviewModal` each carried, so every caller gets the same behavior.
6
+ */
7
+ /** Idempotent native open. `showModal()` throws on an already-open dialog in real browsers; the
8
+ * attribute fallback covers DOM environments without it (jsdom). */
9
+ export function openLightboxDialog(dialog) {
10
+ if (typeof dialog.showModal === "function") {
11
+ if (!dialog.open)
12
+ dialog.showModal();
13
+ return;
14
+ }
15
+ dialog.setAttribute("open", "");
16
+ }
17
+ /** The close-side mirror of {@link openLightboxDialog} — same fallback and already-closed guard. */
18
+ export function closeLightboxDialog(dialog) {
19
+ if (typeof dialog.close === "function") {
20
+ if (dialog.open)
21
+ dialog.close();
22
+ return;
23
+ }
24
+ dialog.removeAttribute("open");
25
+ }
26
+ /** Marks the stage's empty area: the dialog fills the viewport, so "backdrop" is any click that
27
+ * lands on the dialog itself or the stage around the media rather than on a control or the media. */
28
+ export const LIGHTBOX_DISMISS_ATTR = "data-jini-lightbox-dismiss";
29
+ function isDismissTarget(target) {
30
+ return target instanceof Element && (target.tagName === "DIALOG" || target.hasAttribute(LIGHTBOX_DISMISS_ATTR));
31
+ }
32
+ function navState(input) {
33
+ const count = input.count ?? 0;
34
+ const index = input.index ?? 0;
35
+ const navigable = count > 1 && typeof input.onNavigate === "function";
36
+ return { count, index, navigable, hasPrev: navigable && index > 0, hasNext: navigable && index + 1 < count };
37
+ }
38
+ export function useLightbox(input) {
39
+ const dialogRef = useRef(null);
40
+ const closeRef = useRef(null);
41
+ const prevRef = useRef(null);
42
+ const nextRef = useRef(null);
43
+ // Captured when the dialog opens, before focus moves inside: by construction the element that
44
+ // opened it (a card's expand button). Restored on close so keyboard users land where they were.
45
+ const triggerRef = useRef(null);
46
+ const pendingFocusRef = useRef(null);
47
+ const nav = navState(input);
48
+ useEffect(() => {
49
+ pendingFocusRef.current?.current?.focus({ preventScroll: true });
50
+ pendingFocusRef.current = null;
51
+ }, [input.index]);
52
+ useEffect(() => {
53
+ const dialog = dialogRef.current;
54
+ if (!dialog)
55
+ return;
56
+ if (input.open) {
57
+ triggerRef.current = dialog.ownerDocument.activeElement;
58
+ openLightboxDialog(dialog);
59
+ // Close exists at every position (a single item has no arrows), so it is the stable initial
60
+ // focus target. Only opening focuses it; navigation preserves the operator's current focus.
61
+ closeRef.current?.focus({ preventScroll: true });
62
+ return;
63
+ }
64
+ closeLightboxDialog(dialog);
65
+ const trigger = triggerRef.current;
66
+ trigger?.focus?.({ preventScroll: true });
67
+ // Keyed on `open`, not `index`: navigating while open must not reopen the dialog (showModal on
68
+ // an open dialog throws) or recapture its trigger from inside the lightbox.
69
+ // eslint-disable-next-line react-hooks/exhaustive-deps
70
+ }, [input.open]);
71
+ function go(target) {
72
+ // Clamps rather than wrapping: staying on the last asset is the less surprising default for an
73
+ // operator navigating a specific set of uploads, not a slideshow.
74
+ if (!nav.navigable || target < 0 || target >= nav.count)
75
+ return;
76
+ // An arrow becomes disabled at its end, and a disabled focused button drops focus out of the
77
+ // dialog. Hand focus to its neighbour first; interior moves leave focus where the operator put it.
78
+ // The neighbour may itself be disabled until the caller re-renders, so the move is applied
79
+ // after the index changes.
80
+ const active = dialogRef.current?.ownerDocument.activeElement;
81
+ if (target === 0 && active === prevRef.current)
82
+ pendingFocusRef.current = nextRef;
83
+ if (target === nav.count - 1 && active === nextRef.current)
84
+ pendingFocusRef.current = prevRef;
85
+ input.onNavigate?.(target);
86
+ }
87
+ // Listens on the document rather than the dialog: when the focused media is swapped out (an
88
+ // image that failed over to a video) focus drops to the body, and arrow keys must keep working.
89
+ useEffect(() => {
90
+ const dialog = dialogRef.current;
91
+ if (!dialog || !input.open)
92
+ return;
93
+ const onKey = (event) => {
94
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight")
95
+ return;
96
+ // Known limitation: a focused `<video controls>` also seeks on these keys natively, so the
97
+ // press both seeks and navigates. Callers know their media; the shell does not.
98
+ event.preventDefault();
99
+ go(nav.index + (event.key === "ArrowRight" ? 1 : -1));
100
+ };
101
+ dialog.ownerDocument.addEventListener("keydown", onKey);
102
+ return () => dialog.ownerDocument.removeEventListener("keydown", onKey);
103
+ // eslint-disable-next-line react-hooks/exhaustive-deps
104
+ }, [input.open, nav.index, nav.count, input.onNavigate]);
105
+ return {
106
+ dialogRef,
107
+ closeRef,
108
+ prevRef,
109
+ nextRef,
110
+ navigable: nav.navigable,
111
+ hasPrev: nav.hasPrev,
112
+ hasNext: nav.hasNext,
113
+ counter: nav.count > 1 ? `${nav.index + 1} / ${nav.count}` : "",
114
+ previous: () => go(nav.index - 1),
115
+ next: () => go(nav.index + 1),
116
+ // Always prevented so the `open` prop stays the single source of truth; Escape routes through
117
+ // onClose and the caller's state change is what actually closes the dialog.
118
+ handleCancel: (event) => {
119
+ event.preventDefault();
120
+ input.onClose();
121
+ },
122
+ handleClick: (event) => {
123
+ if (isDismissTarget(event.target))
124
+ input.onClose();
125
+ },
126
+ };
127
+ }
128
+ //# sourceMappingURL=Lightbox.hooks.js.map
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useId } from "react";
3
+ import { Icon } from "../../../../react/components/Icon.js";
4
+ import { LIGHTBOX_DISMISS_ATTR, useLightbox } from "./Lightbox.hooks.js";
5
+ const DEFAULT_LABELS = { close: "Close preview", previous: "Previous", next: "Next" };
6
+ // Native dialog supplies the focus trap, Escape and top-layer stacking, so there is no hand-built
7
+ // overlay or z-index policy. The dialog itself fills the viewport as the dark gray stage (rather
8
+ // than a card over a dim backdrop) so the media can take nearly all of the screen.
9
+ /** Full-viewport media lightbox: title and counter top-left, close top-right, the media as large
10
+ * as fits, caller actions bottom-left and the previous/next pair together bottom-right. */
11
+ export function Lightbox(props) {
12
+ const { open, title, children, actions, className, partAttrs = {}, useLightboxHook = useLightbox } = props;
13
+ const labels = { ...DEFAULT_LABELS, ...props.labels };
14
+ const titleId = useId();
15
+ const vm = useLightboxHook({ open, onClose: props.onClose, index: props.index, count: props.count, onNavigate: props.onNavigate });
16
+ return (_jsx("dialog", { ref: vm.dialogRef, className: className ? `jini-lightbox ${className}` : "jini-lightbox", "aria-labelledby": open ? titleId : undefined, onCancel: vm.handleCancel, onClick: vm.handleClick, ...partAttrs.dialog, children: open ? (_jsxs(_Fragment, { children: [_jsxs("header", { className: "jini-lightbox-header", children: [_jsx("h2", { id: titleId, className: "jini-lightbox-title", title: title, children: title }), vm.counter ? _jsx("span", { className: "jini-lightbox-counter", children: vm.counter }) : null, _jsx("button", { type: "button", ref: vm.closeRef, className: "jini-lightbox-button jini-lightbox-close", "aria-label": labels.close, onClick: props.onClose, ...partAttrs.close, children: _jsx(Icon, { name: "close", size: 16, strokeWidth: 1.8 }) })] }), _jsx("div", { className: "jini-lightbox-stage", [LIGHTBOX_DISMISS_ATTR]: "", children: children }), actions || vm.navigable ? (_jsxs("footer", { className: "jini-lightbox-footer", children: [_jsx("div", { className: "jini-lightbox-actions", children: actions }), vm.navigable ? (_jsxs("div", { className: "jini-lightbox-nav", children: [_jsx("button", { type: "button", ref: vm.prevRef, className: "jini-lightbox-button", "aria-label": labels.previous, disabled: !vm.hasPrev, onClick: vm.previous, ...partAttrs.previous, children: _jsx(Icon, { name: "chevron-left", size: 18, strokeWidth: 1.8 }) }), _jsx("button", { type: "button", ref: vm.nextRef, className: "jini-lightbox-button", "aria-label": labels.next, disabled: !vm.hasNext, onClick: vm.next, ...partAttrs.next, children: _jsx(Icon, { name: "chevron-right", size: 18, strokeWidth: 1.8 }) })] })) : null] })) : null] })) : null }));
17
+ }
18
+ //# sourceMappingURL=Lightbox.js.map
@@ -0,0 +1,14 @@
1
+ export interface MiddleTruncateProps {
2
+ /** The full text, e.g. a file name. */
3
+ text: string;
4
+ /** Stem characters kept beside the extension; see `splitMiddleTruncation`. */
5
+ keepChars?: number | undefined;
6
+ className?: string | undefined;
7
+ }
8
+ /**
9
+ * One line of text that ellipsizes in the middle, keeping the end (a file's extension) visible.
10
+ * The two halves are separate boxes, so a screen reader may read them as two pieces: name the
11
+ * enclosing control with the full text (`aria-label`) rather than relying on this content.
12
+ */
13
+ export declare function MiddleTruncate({ text, keepChars, className }: MiddleTruncateProps): import("react").JSX.Element;
14
+ //# sourceMappingURL=MiddleTruncate.d.ts.map
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { splitMiddleTruncation } from "./MiddleTruncate.rules.js";
3
+ /**
4
+ * One line of text that ellipsizes in the middle, keeping the end (a file's extension) visible.
5
+ * The two halves are separate boxes, so a screen reader may read them as two pieces: name the
6
+ * enclosing control with the full text (`aria-label`) rather than relying on this content.
7
+ */
8
+ export function MiddleTruncate({ text, keepChars, className }) {
9
+ const { head, tail } = splitMiddleTruncation({ text }, keepChars === undefined ? {} : { keepChars });
10
+ return (_jsxs("span", { className: className ? `jini-middle-truncate ${className}` : "jini-middle-truncate", children: [_jsx("span", { className: "jini-middle-truncate__head", children: head }), tail ? _jsx("span", { className: "jini-middle-truncate__tail", children: tail }) : null] }));
11
+ }
12
+ //# sourceMappingURL=MiddleTruncate.js.map
@@ -0,0 +1,20 @@
1
+ export interface MiddleTruncationParts {
2
+ /** The part that shrinks with an ellipsis when space runs out. */
3
+ head: string;
4
+ /** The part that always stays visible: the extension plus a few characters before it. */
5
+ tail: string;
6
+ }
7
+ /**
8
+ * Splits a name so CSS can cut it in the MIDDLE: `head` ellipsizes, `tail` never shrinks, so
9
+ * `woodnest-cabin-booking.mp4` narrows to `woodnest-cabin-bo…ing.mp4` and the extension stays
10
+ * readable. CSS does the measuring, so the same split works at any card width.
11
+ * @param required.text The full name.
12
+ * @param optional.keepChars Characters of the stem kept beside the extension.
13
+ * @complexity O(n) in the name's length.
14
+ */
15
+ export declare function splitMiddleTruncation({ text }: {
16
+ text: string;
17
+ }, { keepChars }?: {
18
+ keepChars?: number;
19
+ }): MiddleTruncationParts;
20
+ //# sourceMappingURL=MiddleTruncate.rules.d.ts.map
@@ -0,0 +1,21 @@
1
+ /** Longest trailing ".ext" still treated as an extension; a longer dotted tail is just text. */
2
+ const MAX_EXTENSION_CHARS = 10;
3
+ /**
4
+ * Splits a name so CSS can cut it in the MIDDLE: `head` ellipsizes, `tail` never shrinks, so
5
+ * `woodnest-cabin-booking.mp4` narrows to `woodnest-cabin-bo…ing.mp4` and the extension stays
6
+ * readable. CSS does the measuring, so the same split works at any card width.
7
+ * @param required.text The full name.
8
+ * @param optional.keepChars Characters of the stem kept beside the extension.
9
+ * @complexity O(n) in the name's length.
10
+ */
11
+ export function splitMiddleTruncation({ text }, { keepChars = 3 } = {}) {
12
+ const dot = text.lastIndexOf(".");
13
+ // A leading dot (".env") is a hidden-file name, not an extension.
14
+ const extensionChars = dot > 0 && text.length - dot <= MAX_EXTENSION_CHARS ? text.length - dot : 0;
15
+ const tailChars = extensionChars + keepChars;
16
+ // No extension to protect, or a name too short to cut: one piece that ellipsizes at its end.
17
+ if (extensionChars === 0 || text.length <= tailChars + 1)
18
+ return { head: text, tail: "" };
19
+ return { head: text.slice(0, text.length - tailChars), tail: text.slice(text.length - tailChars) };
20
+ }
21
+ //# sourceMappingURL=MiddleTruncate.rules.js.map
@@ -0,0 +1,18 @@
1
+ import { useScrollJump } from "./ScrollJump.hooks.js";
2
+ export interface ScrollJumpButtonsProps {
3
+ /** The scrolling element the buttons control (an admin shell's `<main>`); `null` renders nothing. */
4
+ container: HTMLElement | null;
5
+ /** Accessible names, already translated by the host. */
6
+ labels?: {
7
+ toTop: string;
8
+ toBottom: string;
9
+ } | undefined;
10
+ useJump?: typeof useScrollJump | undefined;
11
+ }
12
+ /**
13
+ * A quiet ↑/↓ pair for long pages. Hidden on short pages; ↑ hides at the top and ↓ at the bottom.
14
+ * Position it with the host's CSS (`--jini-scroll-jump-right`/`-bottom`/`-direction`) so it
15
+ * clears that host's own floating controls.
16
+ */
17
+ export declare function ScrollJumpButtons({ container, labels, useJump }: ScrollJumpButtonsProps): import("react").JSX.Element | null;
18
+ //# sourceMappingURL=ScrollJump.d.ts.map
@@ -0,0 +1,16 @@
1
+ import { type ScrollJumpVisibility } from "./ScrollJump.rules.js";
2
+ export interface ScrollJumpController extends ScrollJumpVisibility {
3
+ jump: (edge: "top" | "bottom") => void;
4
+ }
5
+ /**
6
+ * Tracks whether `container` is tall enough for jump buttons and which edges it is away from.
7
+ * Scroll events alone miss content that loads or grows without scrolling (a media grid filling in),
8
+ * so a ResizeObserver watches the container and each direct child, and a MutationObserver
9
+ * re-observes when a route swaps the children out.
10
+ */
11
+ export declare function useScrollJump({ container }: {
12
+ container: HTMLElement | null;
13
+ }, { reducedMotion }?: {
14
+ reducedMotion?: () => boolean;
15
+ }): ScrollJumpController;
16
+ //# sourceMappingURL=ScrollJump.hooks.d.ts.map
@@ -0,0 +1,45 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { scrollJumpBehavior, scrollJumpTarget, scrollJumpVisibility } from "./ScrollJump.rules.js";
3
+ const HIDDEN = { showTop: false, showBottom: false };
4
+ const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
5
+ const prefersReducedMotion = () => typeof window !== "undefined" && (window.matchMedia?.(REDUCED_MOTION_QUERY).matches ?? false);
6
+ /**
7
+ * Tracks whether `container` is tall enough for jump buttons and which edges it is away from.
8
+ * Scroll events alone miss content that loads or grows without scrolling (a media grid filling in),
9
+ * so a ResizeObserver watches the container and each direct child, and a MutationObserver
10
+ * re-observes when a route swaps the children out.
11
+ */
12
+ export function useScrollJump({ container }, { reducedMotion = prefersReducedMotion } = {}) {
13
+ const [visibility, setVisibility] = useState(HIDDEN);
14
+ useEffect(() => {
15
+ if (!container)
16
+ return;
17
+ // Same-value updates keep the previous object so a scroll that changes nothing does not re-render.
18
+ const update = () => setVisibility((previous) => {
19
+ const next = scrollJumpVisibility(container);
20
+ return next.showTop === previous.showTop && next.showBottom === previous.showBottom ? previous : next;
21
+ });
22
+ update();
23
+ container.addEventListener("scroll", update, { passive: true });
24
+ const resize = typeof ResizeObserver === "function" ? new ResizeObserver(update) : null;
25
+ const observeChildren = () => {
26
+ resize?.disconnect();
27
+ resize?.observe(container);
28
+ for (const child of Array.from(container.children))
29
+ resize?.observe(child);
30
+ };
31
+ observeChildren();
32
+ const mutation = typeof MutationObserver === "function" ? new MutationObserver(() => { observeChildren(); update(); }) : null;
33
+ mutation?.observe(container, { childList: true });
34
+ return () => {
35
+ container.removeEventListener("scroll", update);
36
+ resize?.disconnect();
37
+ mutation?.disconnect();
38
+ };
39
+ }, [container]);
40
+ const jump = useCallback((edge) => {
41
+ container?.scrollTo({ top: scrollJumpTarget({ edge, scrollHeight: container.scrollHeight }), behavior: scrollJumpBehavior({ reducedMotion: reducedMotion() }) });
42
+ }, [container, reducedMotion]);
43
+ return { ...(container ? visibility : HIDDEN), jump };
44
+ }
45
+ //# sourceMappingURL=ScrollJump.hooks.js.map
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useScrollJump } from "./ScrollJump.hooks.js";
3
+ const DEFAULT_LABELS = { toTop: "Scroll to top", toBottom: "Scroll to bottom" };
4
+ /** An edge button that does not apply keeps its slot (CSS `visibility: hidden`, which also drops it
5
+ * from the tab order), so the other button never slides under the pointer that just clicked. */
6
+ const offProps = (show) => (show ? {} : { "data-off": "" });
7
+ /**
8
+ * A quiet ↑/↓ pair for long pages. Hidden on short pages; ↑ hides at the top and ↓ at the bottom.
9
+ * Position it with the host's CSS (`--jini-scroll-jump-right`/`-bottom`/`-direction`) so it
10
+ * clears that host's own floating controls.
11
+ */
12
+ export function ScrollJumpButtons({ container, labels = DEFAULT_LABELS, useJump = useScrollJump }) {
13
+ const { showTop, showBottom, jump } = useJump({ container });
14
+ if (!showTop && !showBottom)
15
+ return null;
16
+ return (_jsxs("div", { className: "jini-scroll-jump", children: [_jsx("button", { type: "button", className: "jini-scroll-jump__btn", ...offProps(showTop), "aria-label": labels.toTop, title: labels.toTop, onClick: () => jump("top"), children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "m18 15-6-6-6 6" }) }) }), _jsx("button", { type: "button", className: "jini-scroll-jump__btn", ...offProps(showBottom), "aria-label": labels.toBottom, title: labels.toBottom, onClick: () => jump("bottom"), children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: _jsx("path", { d: "m6 9 6 6 6-6" }) }) })] }));
17
+ }
18
+ //# sourceMappingURL=ScrollJump.js.map
@@ -0,0 +1,32 @@
1
+ /** A page must be at least this many viewports tall before the jump buttons are worth their space. */
2
+ export declare const SCROLL_JUMP_MIN_VIEWPORTS = 2;
3
+ /** Within this many px of an edge counts as AT that edge (fractional scroll positions never hit 0 exactly). */
4
+ export declare const SCROLL_JUMP_EDGE_SLACK_PX = 8;
5
+ export interface ScrollMetrics {
6
+ scrollTop: number;
7
+ scrollHeight: number;
8
+ clientHeight: number;
9
+ }
10
+ export interface ScrollJumpVisibility {
11
+ showTop: boolean;
12
+ showBottom: boolean;
13
+ }
14
+ /**
15
+ * Which jump buttons a scroll container shows: none on a short page, ↑ only once scrolled away from
16
+ * the top, ↓ only while there is more below.
17
+ * @complexity O(1).
18
+ */
19
+ export declare function scrollJumpVisibility({ scrollTop, scrollHeight, clientHeight }: ScrollMetrics, { minViewports, edgeSlackPx }?: {
20
+ minViewports?: number;
21
+ edgeSlackPx?: number;
22
+ }): ScrollJumpVisibility;
23
+ /** Reduced-motion users get an instant jump instead of a long animated scroll. */
24
+ export declare function scrollJumpBehavior({ reducedMotion }: {
25
+ reducedMotion: boolean;
26
+ }): ScrollBehavior;
27
+ /** The `scrollTo` target for an edge: 0 for the top, the full content height for the bottom. */
28
+ export declare function scrollJumpTarget({ edge, scrollHeight }: {
29
+ edge: "top" | "bottom";
30
+ scrollHeight: number;
31
+ }): number;
32
+ //# sourceMappingURL=ScrollJump.rules.d.ts.map