@jini-ai/ui 0.6.15-rc.1 → 0.6.15

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 (195) hide show
  1. package/dist/browser/useDismissOnOutsideOrEscape.js +1 -1
  2. package/dist/browser/useGlobalKeydown.js +2 -2
  3. package/dist/browser/useModalWindowDragGuard.js +1 -1
  4. package/dist/core/theme/css-name.d.ts +3 -0
  5. package/dist/core/theme/css-name.js +5 -0
  6. package/dist/core/theme/stylesheet.js +2 -1
  7. package/dist/core/theme/validation.js +15 -4
  8. package/dist/features/about/rules.js +2 -1
  9. package/dist/features/admin-widgets/components/InfoTip.hooks.js +22 -12
  10. package/dist/features/admin-widgets/components/Lightbox/Lightbox.hooks.js +1 -1
  11. package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.hooks.js +1 -1
  12. package/dist/features/admin-widgets/components/SeeMore/SeeMore.hooks.js +2 -2
  13. package/dist/features/admin-widgets/components/SeeMore/SeeMore.js +4 -2
  14. package/dist/features/admin-widgets/components/Select/Select.hooks.js +2 -1
  15. package/dist/features/admin-widgets/components/Select/Select.js +2 -1
  16. package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.js +4 -2
  17. package/dist/features/admin-widgets/components/useDebouncedHtmlPreview.hooks.js +3 -2
  18. package/dist/features/asset-grid/react/components/AssetGrid.js +8 -3
  19. package/dist/features/asset-grid/react/hooks/useAssetGridKeyboardShortcuts.js +7 -6
  20. package/dist/features/asset-grid/react/hooks/useAssetGridLiveUpdates.js +8 -10
  21. package/dist/features/asset-grid/react/hooks/useRubberBandDrag.js +1 -1
  22. package/dist/features/asset-tree-browser/react/components/AssetTreeFileRow.js +6 -8
  23. package/dist/features/asset-tree-browser/react/hooks/useAssetTreeRowMenu.js +1 -1
  24. package/dist/features/asset-tree-browser/rules.js +1 -3
  25. package/dist/features/browser-chrome/rules.js +10 -8
  26. package/dist/features/charts/browser.js +2 -1
  27. package/dist/features/charts/react.js +3 -1
  28. package/dist/features/connectors/ConnectorsBrowser.js +3 -1
  29. package/dist/features/connectors/hooks/useConnectorAuthorization.js +1 -1
  30. package/dist/features/connectors/hooks/useConnectorCatalog.js +2 -2
  31. package/dist/features/connectors/rules.js +10 -14
  32. package/dist/features/execution/react/components/AgentDiagnosticRow.js +2 -0
  33. package/dist/features/execution/react/components/ByokProviderForm.js +1 -3
  34. package/dist/features/execution/react/components/LocalCliAgentSettings.js +2 -1
  35. package/dist/features/execution/react/hooks/use-byok-field-validation.hooks.js +6 -2
  36. package/dist/features/execution/react/hooks/useExecutionTab.js +9 -5
  37. package/dist/features/execution/react/hooks/useLocalCliAgentCard.js +3 -6
  38. package/dist/features/execution/react/hooks/useReasoningControl.js +3 -3
  39. package/dist/features/execution/rules.js +23 -18
  40. package/dist/features/external-mcp/react/components/ExternalMcpTab.js +10 -1
  41. package/dist/features/file-dropzone/rules.js +3 -2
  42. package/dist/features/html-editor/canvas-embed-placeholders.js +2 -3
  43. package/dist/features/html-editor/css.js +7 -5
  44. package/dist/features/html-editor/editor-blocks.js +60 -6
  45. package/dist/features/html-editor/protected-elements.js +5 -3
  46. package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.js +3 -3
  47. package/dist/features/html-editor/source-splice.js +6 -4
  48. package/dist/features/iframe-pool/react/components/IframeKeepAliveProvider.js +5 -5
  49. package/dist/features/iframe-pool/react/dom-sync.js +2 -2
  50. package/dist/features/integrations/react/components/IntegrationsTab.js +3 -1
  51. package/dist/features/integrations/rules.js +6 -4
  52. package/dist/features/interactive-ui/interactive-ui.css +1 -1
  53. package/dist/features/interactive-ui/providers/shadcn/button.d.ts +8 -6
  54. package/dist/features/interactive-ui/providers/shadcn/button.js +5 -6
  55. package/dist/features/interactive-ui/providers/shadcn/card.d.ts +3 -3
  56. package/dist/features/interactive-ui/providers/shadcn/card.js +3 -4
  57. package/dist/features/interactive-ui/providers/shadcn/data-table.js +10 -5
  58. package/dist/features/interactive-ui/providers/shadcn/select.d.ts +3 -5
  59. package/dist/features/interactive-ui/providers/shadcn/select.js +3 -7
  60. package/dist/features/interactive-ui/providers/shadcn/table.d.ts +4 -5
  61. package/dist/features/interactive-ui/providers/shadcn/table.js +4 -9
  62. package/dist/features/interactive-ui/table-data.js +1 -1
  63. package/dist/features/lexical-rich-text-editor/deserialize.js +2 -2
  64. package/dist/features/lexical-rich-text-editor/mention-parser.js +4 -4
  65. package/dist/features/lexical-rich-text-editor/react/components/RichTextInput.js +2 -1
  66. package/dist/features/lexical-rich-text-editor/react/hooks/useCaretFloatingLayerPosition.js +1 -1
  67. package/dist/features/lexical-rich-text-editor/react/hooks/useMentionColorStamping.js +1 -1
  68. package/dist/features/lexical-rich-text-editor/serialize.js +13 -10
  69. package/dist/features/list-detail-panel/react/components/ListDetailPanel.js +1 -1
  70. package/dist/features/mcp-ui/confirmation-store.js +3 -6
  71. package/dist/features/mcp-ui/surfaces/confirmation.d.ts +5 -0
  72. package/dist/features/mcp-ui/surfaces/confirmation.js +1 -0
  73. package/dist/features/mcp-ui/surfaces/document.d.ts +3 -15
  74. package/dist/features/mcp-ui/surfaces/document.js +37 -6
  75. package/dist/features/mcp-ui/surfaces/fields.js +2 -0
  76. package/dist/features/mcp-ui/surfaces/host-theme.js +2 -1
  77. package/dist/features/mcp-ui/surfaces/index.d.ts +1 -0
  78. package/dist/features/mcp-ui/surfaces/index.js +1 -0
  79. package/dist/features/mcp-ui/surfaces/outcome.js +1 -1
  80. package/dist/features/mcp-ui/surfaces/select.js +2 -1
  81. package/dist/features/mcp-ui/surfaces/structured-details.d.ts +40 -0
  82. package/dist/features/mcp-ui/surfaces/structured-details.js +159 -0
  83. package/dist/features/mcp-ui/surfaces/text-input.js +1 -1
  84. package/dist/features/mcp-ui/surfaces/tool-approval.d.ts +21 -0
  85. package/dist/features/mcp-ui/surfaces/tool-approval.js +41 -0
  86. package/dist/features/media-providers/react/hooks/useMediaProvidersTab.js +2 -1
  87. package/dist/features/memory/async-commit-guard.js +4 -1
  88. package/dist/features/memory/formatters.js +5 -1
  89. package/dist/features/memory/react/components/MemoryConnectedPanel.js +3 -1
  90. package/dist/features/memory/react/components/MemoryEntryCard.js +1 -1
  91. package/dist/features/memory/react/components/MemorySettingsPanel.js +3 -1
  92. package/dist/features/memory/react/hooks/useMemoryConfig.hooks.js +12 -10
  93. package/dist/features/memory/react/hooks/useMemoryConnectors.hooks.js +2 -5
  94. package/dist/features/memory/react/hooks/useMemoryConnectors.rules.js +10 -9
  95. package/dist/features/memory/react/hooks/useMemoryEntries.hooks.js +4 -2
  96. package/dist/features/memory/react/hooks/useMemoryFlash.hooks.js +1 -1
  97. package/dist/features/memory/rules.js +2 -1
  98. package/dist/features/panel-kit/fetch-query/adapter.react.js +3 -2
  99. package/dist/features/panel-kit/fetch-query/adapter.tanstack.js +3 -2
  100. package/dist/features/panel-kit/fetch-query/binding.react.js +2 -1
  101. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +13 -4
  102. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -1
  103. package/dist/features/panel-kit/hooks/use-focus-trap.hooks.js +1 -1
  104. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +14 -10
  105. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +2 -2
  106. package/dist/features/progress-card/components/ProgressCard.d.ts +1 -1
  107. package/dist/features/progress-card/reference-adapters.js +23 -13
  108. package/dist/features/progress-card/rules.d.ts +0 -1
  109. package/dist/features/progress-card/rules.js +14 -20
  110. package/dist/features/progress-card/types.d.ts +1 -1
  111. package/dist/features/project-locations/dependencies.js +6 -1
  112. package/dist/features/project-locations/react/components/ProjectLocationsTab.js +2 -1
  113. package/dist/features/project-locations/rules.js +2 -1
  114. package/dist/features/resource-dashboard/react/components/ResourceBoardToolbar.js +8 -1
  115. package/dist/features/resource-dashboard/react/components/ResourceBoardView.js +3 -2
  116. package/dist/features/resource-dashboard/react/components/ResourceBoardViewParts.d.ts +0 -2
  117. package/dist/features/resource-dashboard/react/components/ResourceBoardViewParts.js +0 -6
  118. package/dist/features/resource-dashboard/react/components/ResourceCardParts.js +2 -1
  119. package/dist/features/resource-dashboard/react/components/ResourceRowListItem.js +3 -1
  120. package/dist/features/resource-dashboard/react/components/ResourceRowListItemParts.js +2 -1
  121. package/dist/features/resource-dashboard/react/components/ResourceRowListView.js +1 -1
  122. package/dist/features/resource-dashboard/react/hooks/useResourceBoard.js +1 -1
  123. package/dist/features/schedule-picker/rules.js +13 -7
  124. package/dist/features/sketch-editor/constants.d.ts +0 -6
  125. package/dist/features/sketch-editor/constants.js +17 -15
  126. package/dist/features/sketch-editor/dom.js +4 -7
  127. package/dist/features/sketch-editor/react/components/SketchMainMenu.js +2 -1
  128. package/dist/features/sketch-editor/react/components/SketchSaveStateBadge.js +2 -1
  129. package/dist/features/sketch-editor/react/hooks/useSketchDomEnhancements.js +1 -1
  130. package/dist/features/sketch-editor/rules.js +13 -12
  131. package/dist/features/skills/dependencies.js +9 -4
  132. package/dist/features/skills/react/components/SkillRow.js +2 -1
  133. package/dist/features/skills/react/components/SkillsTab.js +2 -1
  134. package/dist/features/skills/rules.js +3 -3
  135. package/dist/features/source-config-list/agent-handles.js +4 -3
  136. package/dist/features/source-config-list/react/components/SourceConfigItemCard.js +3 -1
  137. package/dist/features/tab-strip/react/components/TabBar.hooks.js +4 -2
  138. package/dist/features/version-manager/react/components/VersionPreviewFrame.js +4 -1
  139. package/dist/features/version-manager/react/components/VersionSidebar.js +8 -6
  140. package/dist/features/version-manager/react/hooks/usePreviewCanvasSize.js +1 -1
  141. package/dist/features/version-manager/react/hooks/useVersionManager.js +5 -7
  142. package/dist/features/version-manager/rules.js +3 -5
  143. package/dist/features/viewer-shell/react/components/CommentSidePanel.js +2 -1
  144. package/dist/features/viewer-shell/react/components/SegmentedToggle.js +2 -1
  145. package/dist/features/viewer-shell/react/components/ViewerShell.js +2 -1
  146. package/dist/features/viewer-shell/rules.js +8 -7
  147. package/dist/react/components/BrandLogo.js +10 -2
  148. package/dist/react/components/CustomSelect.js +1 -1
  149. package/dist/react/components/EditorIcon.js +17 -14
  150. package/dist/react/components/Loading.js +2 -1
  151. package/dist/react/components/OnboardingChipField.js +5 -1
  152. package/dist/react/components/OnboardingDropdown.js +6 -4
  153. package/dist/react/components/PaletteTweaks.js +7 -6
  154. package/dist/react/components/PillButton.js +2 -1
  155. package/dist/react/components/PopoverMenu.js +2 -1
  156. package/dist/react/components/RemixIcon.js +2 -1
  157. package/dist/react/components/SecretInput.hooks.d.ts +2 -25
  158. package/dist/react/components/SecretInput.hooks.js +2 -24
  159. package/dist/react/components/SecretInput.js +5 -4
  160. package/dist/react/components/Toast.js +4 -2
  161. package/dist/react/components/TooltipLayer.js +4 -1
  162. package/dist/react/components/WorkingDirPicker.js +5 -4
  163. package/dist/react/components/remixicon-font-recovery.js +9 -1
  164. package/dist/react/components/select-popover.js +2 -2
  165. package/dist/react/hooks/useBrandFonts.js +1 -1
  166. package/dist/react/hooks/useEdgeAutoScroll.js +1 -1
  167. package/dist/react/hooks/useInView.js +9 -9
  168. package/dist/react/hooks/useResizableSplitPane.js +12 -20
  169. package/dist/react/mcp-ui/useHostSurfaceTokens.js +1 -1
  170. package/dist/renderers/annotation-canvas/react/components/AnnotationCanvas.js +7 -1
  171. package/dist/renderers/annotation-canvas/react/hooks/useAnnotationCanvas.controller.js +4 -2
  172. package/dist/renderers/annotation-canvas/react/hooks/useAnnotationCanvas.js +9 -5
  173. package/dist/renderers/annotation-canvas/rules.js +9 -13
  174. package/dist/renderers/annotation-canvas/theme.js +5 -2
  175. package/dist/renderers/renderers/index.js +1 -1
  176. package/dist/renderers/renderers/markdown.js +4 -6
  177. package/dist/theme/apply.js +7 -3
  178. package/dist/utils/class-names.d.ts +3 -0
  179. package/dist/utils/class-names.js +5 -0
  180. package/dist/utils/design-md.js +3 -3
  181. package/dist/utils/ime-composing.d.ts +1 -1
  182. package/dist/utils/ime-composing.js +1 -2
  183. package/dist/utils/localized-url.js +2 -11
  184. package/dist/utils/markdown-scroll-sync.js +1 -1
  185. package/dist/utils/omit-keys.d.ts +3 -0
  186. package/dist/utils/omit-keys.js +8 -0
  187. package/dist/utils/polygon-selection.js +1 -1
  188. package/dist/utils/sse.js +3 -0
  189. package/dist/utils/trim-chars.d.ts +13 -0
  190. package/dist/utils/trim-chars.js +30 -0
  191. package/dist/utils/unhandled-case.d.ts +10 -0
  192. package/dist/utils/unhandled-case.js +12 -0
  193. package/dist/utils/unit-bezier.js +1 -3
  194. package/dist/utils/zip.js +6 -10
  195. package/package.json +10 -9
@@ -36,21 +36,24 @@ const xcodePath = 'M19.06 5.3327c.4517-.1936.7744-.2581 1.097-.1936.5163.1291.77
36
36
  const webstormPath = 'M0 0v24h24V0H0zm17.889 2.889c1.444 0 2.667.444 3.667 1.278l-1.111 1.667c-.889-.611-1.722-1-2.556-1s-1.278.389-1.278.889v.056c0 .667.444.889 2.111 1.333 2 .556 3.111 1.278 3.111 3v.056c0 2-1.5 3.111-3.611 3.111-1.5-.056-3-.611-4.167-1.667l1.278-1.556c.889.722 1.833 1.222 2.944 1.222.889 0 1.389-.333 1.389-.944v-.056c0-.556-.333-.833-2-1.278-2-.5-3.222-1.056-3.222-3.056v-.056c0-1.833 1.444-3 3.444-3zm-16.111.222h2.278l1.5 5.778 1.722-5.778h1.667l1.667 5.778 1.5-5.778h2.333l-2.833 9.944H9.723L8.112 7.277l-1.667 5.778H4.612L1.779 3.111zm.5 16.389h9V21h-9v-1.5z';
37
37
  const ideaPath = 'M0 0v24h24V0zm3.723 3.111h5v1.834h-1.39v6.277h1.39v1.834h-5v-1.834h1.444V4.945H3.723zm11.055 0H17v6.5c0 .612-.055 1.111-.222 1.556-.167.444-.39.777-.723 1.11-.277.279-.666.557-1.11.668a3.933 3.933 0 0 1-1.445.278c-.778 0-1.444-.167-1.944-.445a4.81 4.81 0 0 1-1.279-1.056l1.39-1.555c.277.334.555.555.833.722.277.167.611.278.945.278.389 0 .721-.111 1-.389.221-.278.333-.667.333-1.278zM2.222 19.5h9V21h-9z';
38
38
  const warpPath = 'M12.035 2.723h9.253A2.712 2.712 0 0 1 24 5.435v10.529a2.712 2.712 0 0 1-2.712 2.713H8.047Zm-1.681 2.6L6.766 19.677h5.598l-.399 1.6H2.712A2.712 2.712 0 0 1 0 18.565V8.036a2.712 2.712 0 0 1 2.712-2.712Z';
39
+ const PRIMARY = 'var(--jini-primary)';
40
+ const BG = 'var(--jini-bg)';
41
+ const TEXT = 'var(--jini-text)';
39
42
  const EDITORS = {
40
- vscode: { bg: 'var(--jini-primary)', fg: 'var(--jini-bg)', glyph: vscodeLogo },
41
- cursor: { bg: 'var(--jini-text)', fg: 'var(--jini-bg)', glyph: simplePath(cursorPath) },
42
- windsurf: { bg: 'var(--jini-bg)', fg: 'var(--jini-text)', glyph: simplePath(windsurfPath) },
43
- zed: { bg: 'var(--jini-primary)', fg: 'var(--jini-bg)', glyph: simplePath(zedPath) },
44
- qoder: { bg: 'var(--jini-warning)', fg: 'var(--jini-text)', glyph: qoderLogo },
45
- antigravity: { bg: 'var(--jini-bg)', fg: 'var(--jini-text)', glyph: antigravityLogo },
46
- webstorm: { bg: 'var(--jini-text)', fg: 'var(--jini-bg)', glyph: simplePath(webstormPath) },
47
- idea: { bg: 'var(--jini-text)', fg: 'var(--jini-bg)', glyph: simplePath(ideaPath) },
48
- xcode: { bg: 'var(--jini-primary)', fg: 'var(--jini-bg)', glyph: simplePath(xcodePath) },
49
- finder: { bg: 'var(--jini-primary)', fg: 'var(--jini-bg)', glyph: finderLogo },
50
- explorer: { bg: 'var(--jini-warning)', fg: 'var(--jini-text)', glyph: folderLogo },
51
- 'file-manager': { bg: 'var(--jini-muted)', fg: 'var(--jini-bg)', glyph: folderLogo },
52
- terminal: { bg: 'var(--jini-text)', fg: 'var(--jini-success)', glyph: terminalLogo },
53
- warp: { bg: 'var(--jini-primary)', fg: 'var(--jini-bg)', glyph: simplePath(warpPath) },
43
+ vscode: { bg: PRIMARY, fg: BG, glyph: vscodeLogo },
44
+ cursor: { bg: TEXT, fg: BG, glyph: simplePath(cursorPath) },
45
+ windsurf: { bg: BG, fg: TEXT, glyph: simplePath(windsurfPath) },
46
+ zed: { bg: PRIMARY, fg: BG, glyph: simplePath(zedPath) },
47
+ qoder: { bg: 'var(--jini-warning)', fg: TEXT, glyph: qoderLogo },
48
+ antigravity: { bg: BG, fg: TEXT, glyph: antigravityLogo },
49
+ webstorm: { bg: TEXT, fg: BG, glyph: simplePath(webstormPath) },
50
+ idea: { bg: TEXT, fg: BG, glyph: simplePath(ideaPath) },
51
+ xcode: { bg: PRIMARY, fg: BG, glyph: simplePath(xcodePath) },
52
+ finder: { bg: PRIMARY, fg: BG, glyph: finderLogo },
53
+ explorer: { bg: 'var(--jini-warning)', fg: TEXT, glyph: folderLogo },
54
+ 'file-manager': { bg: 'var(--jini-muted)', fg: BG, glyph: folderLogo },
55
+ terminal: { bg: TEXT, fg: 'var(--jini-success)', glyph: terminalLogo },
56
+ warp: { bg: PRIMARY, fg: BG, glyph: simplePath(warpPath) },
54
57
  };
55
58
  /**
56
59
  * Renders an editor/target glyph by string key using the host's theme, with a
@@ -1,10 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Icon } from './Icon.js';
3
+ import { withClassName } from '../../utils/class-names.js';
3
4
  export function Spinner({ size = 14, label }) {
4
5
  return (_jsxs("span", { className: "loading-spinner", role: "status", "aria-live": "polite", children: [_jsx(Icon, { name: "spinner", size: size }), label ? _jsx("span", { className: "loading-spinner-label", children: label }) : null] }));
5
6
  }
6
7
  export function Skeleton({ width, height = 14, radius = 6, className }) {
7
- return (_jsx("span", { className: `skeleton-block${className ? ` ${className}` : ''}`, style: { width, height, borderRadius: radius }, "aria-hidden": true }));
8
+ return (_jsx("span", { className: withClassName('skeleton-block', className), style: { width, height, borderRadius: radius }, "aria-hidden": true }));
8
9
  }
9
10
  /**
10
11
  * Card-shaped skeleton for grid layouts (thumbnail area over a row of meta
@@ -1,7 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** A single-select value as a selection list; an empty string selects nothing. */
3
+ function singleSelection(value) {
4
+ return value ? [value] : [];
5
+ }
2
6
  export function OnboardingChipField(props) {
3
7
  const { label, options, className } = props;
4
- const selected = props.multiple ? props.value : props.value ? [props.value] : [];
8
+ const selected = props.multiple ? props.value : singleSelection(props.value);
5
9
  return (_jsxs("div", { className: ['onboarding-chip-field', className].filter(Boolean).join(' '), children: [_jsx("span", { className: "onboarding-chip-field__label", children: label }), _jsx("div", { className: "onboarding-chip-field__chips", children: options.map((option) => {
6
10
  const active = selected.includes(option.value);
7
11
  return (_jsx("button", { type: "button", className: `onboarding-chip${active ? ' is-selected' : ''}`, "aria-pressed": active, onClick: () => {
@@ -51,7 +51,9 @@ export const ONBOARDING_MENU_FALLBACK_VIEWPORT = 720;
51
51
  * string (or otherwise falsy value) becomes an empty selection.
52
52
  */
53
53
  export function resolveOnboardingSelectedValues(value) {
54
- return Array.isArray(value) ? value : value ? [value] : [];
54
+ if (Array.isArray(value))
55
+ return value;
56
+ return value ? [value] : [];
55
57
  }
56
58
  /**
57
59
  * Compute the trigger's visible label. In multiple mode it joins every
@@ -121,7 +123,7 @@ export function useOnboardingDropdownPlacement(rootRef, open, placement, optionC
121
123
  });
122
124
  useLayoutEffect(() => {
123
125
  if (!open)
124
- return;
126
+ return undefined;
125
127
  function measureMenu() {
126
128
  const node = rootRef.current;
127
129
  if (!node)
@@ -153,11 +155,11 @@ export function useOnboardingDropdown(props) {
153
155
  const [open, setOpen] = useState(false);
154
156
  const [query, setQuery] = useState('');
155
157
  const rootRef = useRef(null);
156
- const dropdownIdRef = useRef(`onboarding-dropdown-${Math.random().toString(36).slice(2)}`);
158
+ const dropdownIdRef = useRef(`onboarding-dropdown-${globalThis.crypto.getRandomValues(new Uint32Array(1))[0].toString(36)}`);
157
159
  const { placement: resolvedPlacement, maxHeight: menuMaxHeight } = useOnboardingDropdownPlacement(rootRef, open, placement, options.length);
158
160
  useEffect(() => {
159
161
  if (!open)
160
- return;
162
+ return undefined;
161
163
  function handlePointerDown(event) {
162
164
  if (!rootRef.current?.contains(event.target)) {
163
165
  setOpen(false);
@@ -10,12 +10,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  // hook-testable in isolation.
11
11
  import { useCallback, useEffect, useRef, useState } from 'react';
12
12
  import { Icon } from './Icon.js';
13
+ const PRIMARY = 'var(--jini-primary)';
13
14
  export const PALETTE_TWEAKS_SWATCHES = [
14
- { id: 'coral', label: 'Coral - default', stripe: ['var(--jini-primary)', 'var(--jini-primary)', 'var(--jini-primary)', 'var(--jini-primary)'] },
15
- { id: 'electric', label: 'Electric', stripe: ['var(--jini-primary)', 'var(--jini-primary)', 'var(--jini-primary)', 'var(--jini-primary)'] },
16
- { id: 'acid-forest', label: 'Acid forest', stripe: ['var(--jini-primary)', 'var(--jini-primary)', 'var(--jini-primary)', 'var(--jini-primary)'] },
17
- { id: 'risograph', label: 'Risograph', stripe: ['var(--jini-primary)', 'var(--jini-primary)', 'var(--jini-primary)', 'var(--jini-primary)'] },
18
- { id: 'mono-noir', label: 'Mono noir', stripe: ['var(--jini-text)', 'var(--jini-primary)', 'var(--jini-primary)', 'var(--jini-primary)'] },
15
+ { id: 'coral', label: 'Coral - default', stripe: [PRIMARY, PRIMARY, PRIMARY, PRIMARY] },
16
+ { id: 'electric', label: 'Electric', stripe: [PRIMARY, PRIMARY, PRIMARY, PRIMARY] },
17
+ { id: 'acid-forest', label: 'Acid forest', stripe: [PRIMARY, PRIMARY, PRIMARY, PRIMARY] },
18
+ { id: 'risograph', label: 'Risograph', stripe: [PRIMARY, PRIMARY, PRIMARY, PRIMARY] },
19
+ { id: 'mono-noir', label: 'Mono noir', stripe: ['var(--jini-text)', PRIMARY, PRIMARY, PRIMARY] },
19
20
  ];
20
21
  // --- Pure derivations (module-level, directly unit-testable) --------------
21
22
  /**
@@ -71,7 +72,7 @@ export function usePaletteDismiss(options) {
71
72
  const rootRef = useRef(null);
72
73
  useEffect(() => {
73
74
  if (!open)
74
- return;
75
+ return undefined;
75
76
  function onDoc(ev) {
76
77
  if (!rootRef.current)
77
78
  return;
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { withClassName } from '../../utils/class-names.js';
2
3
  /**
3
4
  * Generic labeled trigger pill: icon + label (+ optional trailing icon),
4
5
  * togglable `active` state, and an optional anchored children slot for a
@@ -9,6 +10,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
10
  * (recon r6 §1.19) — generic popover-trigger chrome, no product types.
10
11
  */
11
12
  export function PillButton({ icon, label, active = false, 'aria-label': ariaLabel, trailingIcon, onClick, disabled = false, className, children, }) {
12
- return (_jsxs("div", { className: `jini-pill-wrap${className ? ` ${className}` : ''}`, children: [_jsxs("button", { type: "button", className: `jini-pill${active ? ' is-active' : ''}`, "aria-label": ariaLabel, "aria-expanded": children ? active : undefined, onClick: onClick, disabled: disabled, children: [icon, _jsx("span", { className: "jini-pill__label", children: label }), trailingIcon] }), children] }));
13
+ return (_jsxs("div", { className: withClassName('jini-pill-wrap', className), children: [_jsxs("button", { type: "button", className: `jini-pill${active ? ' is-active' : ''}`, "aria-label": ariaLabel, "aria-expanded": children ? active : undefined, onClick: onClick, disabled: disabled, children: [icon, _jsx("span", { className: "jini-pill__label", children: label }), trailingIcon] }), children] }));
13
14
  }
14
15
  //# sourceMappingURL=PillButton.js.map
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { withClassName } from '../../utils/class-names.js';
2
3
  /**
3
4
  * Generic popover panel shell: a plain positioned wrapper around a list of
4
5
  * `PopoverItem`s (or any other content). No open/closed state of its own —
@@ -8,6 +9,6 @@ import { jsx as _jsx } from "react/jsx-runtime";
8
9
  * (recon r6 §1.19) — generic, no product types.
9
10
  */
10
11
  export function PopoverMenu({ children, className }) {
11
- return _jsx("div", { className: `jini-popover${className ? ` ${className}` : ''}`, children: children });
12
+ return _jsx("div", { className: withClassName('jini-popover', className), children: children });
12
13
  }
13
14
  //# sourceMappingURL=PopoverMenu.js.map
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect } from 'react';
3
3
  import { ensureRemixIconStylesheet } from './RemixIcon.stylesheet.js';
4
+ import { withClassName } from '../../utils/class-names.js';
4
5
  export { installRemixIconStylesheet, REMIXICON_STYLESHEET_MARKER } from './RemixIcon.stylesheet.js';
5
6
  /**
6
7
  * Thin wrapper around a RemixIcon webfont glyph (`ri-<name>`). Loads this package's own default
@@ -12,7 +13,7 @@ export function RemixIcon({ name, size = 14, className, style }) {
12
13
  useEffect(() => {
13
14
  ensureRemixIconStylesheet({});
14
15
  }, []);
15
- return (_jsx("i", { className: `ri-${name}${className ? ` ${className}` : ''}`, "aria-hidden": "true", style: {
16
+ return (_jsx("i", { className: withClassName(`ri-${name}`, className), "aria-hidden": "true", style: {
16
17
  fontSize: size,
17
18
  lineHeight: 1,
18
19
  width: size,
@@ -1,26 +1,3 @@
1
- export interface SecretRevealRequired {
2
- /** The input's current value. A transition to `''` re-hides (the usual sign a save succeeded). */
3
- readonly value: unknown;
4
- /** The toggle's full accessible names, translated ("Show API key" / "Hide API key"). */
5
- readonly labels: {
6
- readonly show: string;
7
- readonly hide: string;
8
- };
9
- }
10
- export interface SecretRevealOptional {
11
- /** Controlled mode: the host owns the shown/hidden state (e.g. to reset it per provider). */
12
- readonly revealed?: boolean;
13
- readonly onRevealedChange?: (revealed: boolean) => void;
14
- }
15
- /**
16
- * The show/hide state behind `SecretInput`'s eye toggle. Starts hidden. Uncontrolled, it re-hides on
17
- * the value's transition to empty only — an empty field may still be revealed before pasting.
18
- * @complexity O(1).
19
- */
20
- export declare function useSecretReveal(required: SecretRevealRequired, optional?: SecretRevealOptional): {
21
- readonly revealed: boolean;
22
- readonly inputType: "text" | "password";
23
- readonly toggleLabel: string;
24
- readonly toggle: () => void;
25
- };
1
+ /** Compatibility entry; ui-kit owns the shared secret reveal lifecycle. */
2
+ export { useSecretReveal, type SecretRevealRequired, type SecretRevealOptional } from "@jini-ai/ui-kit/react";
26
3
  //# sourceMappingURL=SecretInput.hooks.d.ts.map
@@ -1,25 +1,3 @@
1
- import { useState } from 'react';
2
- /**
3
- * The show/hide state behind `SecretInput`'s eye toggle. Starts hidden. Uncontrolled, it re-hides on
4
- * the value's transition to empty only — an empty field may still be revealed before pasting.
5
- * @complexity O(1).
6
- */
7
- export function useSecretReveal(required, optional = {}) {
8
- const [ownRevealed, setOwnRevealed] = useState(false);
9
- const [lastValue, setLastValue] = useState(required.value);
10
- if (required.value !== lastValue) {
11
- setLastValue(required.value);
12
- if (required.value === '')
13
- setOwnRevealed(false);
14
- }
15
- const revealed = optional.revealed ?? ownRevealed;
16
- const toggleLabel = revealed ? required.labels.hide : required.labels.show;
17
- const toggle = () => {
18
- if (optional.onRevealedChange)
19
- optional.onRevealedChange(!revealed);
20
- else
21
- setOwnRevealed(!revealed);
22
- };
23
- return { revealed, inputType: revealed ? 'text' : 'password', toggleLabel, toggle };
24
- }
1
+ /** Compatibility entry; ui-kit owns the shared secret reveal lifecycle. */
2
+ export { useSecretReveal } from "@jini-ai/ui-kit/react";
25
3
  //# sourceMappingURL=SecretInput.hooks.js.map
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef } from 'react';
3
+ import { SecretInput as KitSecretInput } from '@jini-ai/ui-kit/react';
3
4
  import { useT } from '../../features/i18n/index.js';
4
5
  import { Icon } from './Icon.js';
5
6
  import { useSecretReveal } from './SecretInput.hooks.js';
@@ -13,10 +14,10 @@ function useSecretToggle({ label, toggleLabels, toggleAttrs, revealed, onReveale
13
14
  return { reveal, button };
14
15
  }
15
16
  export const SecretInput = forwardRef(function SecretInput(props, ref) {
16
- // The toggle props are pulled off so they never reach the DOM `<input>`.
17
- const { label, toggleLabels, toggleAttrs, revealed, onRevealedChange, ...inputProps } = props;
18
- const { reveal, button } = useSecretToggle(props, { value: inputProps.value, disabled: inputProps.disabled });
19
- return (_jsxs("span", { className: "jini-secret", children: [_jsx("input", { ...inputProps, spellCheck: false, ref: ref, type: reveal.inputType }), button] }));
17
+ const t = useT();
18
+ return _jsx(KitSecretInput, { ...props, ref: ref, toggleLabels: props.toggleLabels ?? {
19
+ show: `${t('Show')} ${props.label}`, hide: `${t('Hide')} ${props.label}`,
20
+ } });
20
21
  });
21
22
  export const SecretTextarea = forwardRef(function SecretTextarea(props, ref) {
22
23
  const { label, toggleLabels, toggleAttrs, revealed, onRevealedChange, style, maskedValue, ...textareaProps } = props;
@@ -15,6 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
15
  // hook-testable in isolation.
16
16
  import { useCallback, useEffect, useRef, useState, } from 'react';
17
17
  import { Icon } from './Icon.js';
18
+ import { withClassName } from '../../utils/class-names.js';
18
19
  export const TOAST_DEFAULT_TTL = 4000;
19
20
  // Exit fade duration — kept in sync with the `.jini-toast.leaving` CSS
20
21
  // animation the host supplies. The fade plays inside the TTL window (it
@@ -65,7 +66,8 @@ export function toastFadeDelay(effectiveTtl) {
65
66
  /** The root `className` string, composed from tone/placement/extra/leaving. */
66
67
  export function toastClassName(options) {
67
68
  const { tone, placement, className, leaving } = options;
68
- return `jini-toast tone-${tone} placement-${placement}${className ? ` ${className}` : ''}${leaving ? ' leaving' : ''}`;
69
+ const base = withClassName(`jini-toast tone-${tone} placement-${placement}`, className);
70
+ return leaving ? `${base} leaving` : base;
69
71
  }
70
72
  // --- Hooks (co-located, exported) -----------------------------------------
71
73
  /**
@@ -105,7 +107,7 @@ export function useToastAutoDismiss(options) {
105
107
  // prior leaving state before re-arming the timers.
106
108
  setLeaving(false);
107
109
  if (!toastShouldAutoDismiss(hasDismiss, effectiveTtl))
108
- return;
110
+ return undefined;
109
111
  const fadeId = window.setTimeout(() => setLeaving(true), toastFadeDelay(effectiveTtl));
110
112
  const dismissId = window.setTimeout(fireDismiss, effectiveTtl);
111
113
  return () => {
@@ -46,6 +46,9 @@ export function positionTooltip(target, tooltip, placement) {
46
46
  left = rect.right + TOOLTIP_GAP;
47
47
  top = rect.top + rect.height / 2 - tip.height / 2;
48
48
  }
49
+ else {
50
+ // 'top': the centred-above position computed above already applies.
51
+ }
49
52
  return {
50
53
  x: Math.round(clamp(left, TOOLTIP_MARGIN, maxLeft)),
51
54
  y: Math.round(clamp(top, TOOLTIP_MARGIN, maxTop)),
@@ -180,7 +183,7 @@ export function useTooltipLayer() {
180
183
  }, [restoreNativeTitle]);
181
184
  useEffect(() => {
182
185
  if (!state?.target || typeof MutationObserver === 'undefined')
183
- return;
186
+ return undefined;
184
187
  const target = state.target;
185
188
  const observer = new MutationObserver(() => {
186
189
  if (!isTooltipTarget(target)) {
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useId, useRef, useState, } from 'react';
3
3
  import { Icon } from './Icon.js';
4
+ import { withClassName } from '../../utils/class-names.js';
4
5
  export const DEFAULT_WORKING_DIR_LABELS = {
5
6
  missing: 'Directory no longer exists',
6
7
  hint: 'Select a working directory',
@@ -45,7 +46,7 @@ export function useDismissablePanel(onOpen) {
45
46
  }, [open, onOpen]);
46
47
  useEffect(() => {
47
48
  if (!open)
48
- return;
49
+ return undefined;
49
50
  function onPointer(event) {
50
51
  if (wrapRef.current?.contains(event.target))
51
52
  return;
@@ -175,10 +176,10 @@ function useWorkingDirFocusHandoff({ open, triggerRef, panelRef }) {
175
176
  if (open) {
176
177
  wasOpenRef.current = true;
177
178
  panelRef.current?.querySelector(MENU_ITEM_SELECTOR)?.focus();
178
- return;
179
+ return undefined;
179
180
  }
180
181
  if (!wasOpenRef.current)
181
- return;
182
+ return undefined;
182
183
  wasOpenRef.current = false;
183
184
  const timeout = window.setTimeout(() => triggerRef.current?.focus(), 0);
184
185
  return () => window.clearTimeout(timeout);
@@ -212,6 +213,6 @@ export function WorkingDirPicker({ workingDir, recentDirs, onPickDirectory, onSe
212
213
  return;
213
214
  handleRecentFlyoutKey({ event, showRecent, hideRecent });
214
215
  }, [hideRecent, showRecent]);
215
- return (_jsxs("div", { ref: wrapRef, className: `jini-working-dir-picker${className ? ` ${className}` : ''}`, "data-testid": "working-dir-picker", children: [_jsx(WorkingDirTrigger, { triggerRef: triggerRef, workingDir: workingDir, invalid: invalid, open: open, panelId: panelId, t: t, toggle: toggle }), open ? (_jsx(WorkingDirPanel, { panelRef: panelRef, panelId: panelId, placement: placement, workingDir: workingDir, recentDirs: recentDirs, canClear: Boolean(workingDir && onClear), t: t, picker: picker, onKeyDown: handlePanelKeyDown })) : null] }));
216
+ return (_jsxs("div", { ref: wrapRef, className: withClassName('jini-working-dir-picker', className), "data-testid": "working-dir-picker", children: [_jsx(WorkingDirTrigger, { triggerRef: triggerRef, workingDir: workingDir, invalid: invalid, open: open, panelId: panelId, t: t, toggle: toggle }), open ? (_jsx(WorkingDirPanel, { panelRef: panelRef, panelId: panelId, placement: placement, workingDir: workingDir, recentDirs: recentDirs, canClear: Boolean(workingDir && onClear), t: t, picker: picker, onKeyDown: handlePanelKeyDown })) : null] }));
216
217
  }
217
218
  //# sourceMappingURL=WorkingDirPicker.js.map
@@ -85,7 +85,15 @@ function isRemixIconFaceRule(rule) {
85
85
  function findRemixIconFontSource({ rules }) {
86
86
  const face = Array.from(rules).find(isRemixIconFaceRule);
87
87
  // This package owns one url(...) WOFF2 source, not a general CSS font-source parser.
88
- return face?.style.getPropertyValue('src').match(/url\(\s*["']?([^"')]+)["']?\s*\)/)?.[1];
88
+ const source = face?.style.getPropertyValue('src');
89
+ if (!source)
90
+ return undefined;
91
+ const open = source.indexOf('url(');
92
+ const close = source.indexOf(')', open + 4);
93
+ if (open < 0 || close < 0)
94
+ return undefined;
95
+ // URL construction below already discards surrounding whitespace. Keep quotes outside the URL.
96
+ return /^["']?([^"']+)["']?$/.exec(source.slice(open + 4, close).trim())?.[1];
89
97
  }
90
98
  /** The source URL tagged with the attempt number; only http(s) URLs accept a query parameter. */
91
99
  function retryFontUrl({ url, attempt }) {
@@ -103,7 +103,7 @@ export function useSelectPopoverPosition(required, optional = {}) {
103
103
  // biome-ignore lint/correctness/useExhaustiveDependencies: scroll/resize listener wiring — see comment above; onOutOfView is read through latest; only lifecycle/ref changes re-arm it.
104
104
  useEffect(() => {
105
105
  if (!open || !portal)
106
- return;
106
+ return undefined;
107
107
  function reposition() {
108
108
  const trigger = triggerRef.current;
109
109
  if (!trigger)
@@ -146,7 +146,7 @@ export function useSelectPopover(required, optional = {}) {
146
146
  }
147
147
  useEffect(() => {
148
148
  if (!open)
149
- return;
149
+ return undefined;
150
150
  function onOutside(event) {
151
151
  if (isSelectEventInside({ target: event.target, trigger: triggerRef.current, panel: panelRef.current }))
152
152
  return;
@@ -33,7 +33,7 @@ export function useBrandFonts({ fonts }, options = {}) {
33
33
  }, [googleUrls, document]);
34
34
  useEffect(() => {
35
35
  if (!projectId || !resolveProjectAssetUrl)
36
- return;
36
+ return undefined;
37
37
  let cancelled = false;
38
38
  let styleEl = null;
39
39
  void (async () => {
@@ -52,7 +52,7 @@ export function useEdgeAutoScroll(contentKey) {
52
52
  useEffect(() => {
53
53
  const el = scrollRef.current;
54
54
  if (!el)
55
- return;
55
+ return undefined;
56
56
  const updateEdges = () => {
57
57
  const maxScroll = el.scrollWidth - el.clientWidth;
58
58
  setEdges({
@@ -10,21 +10,21 @@ export function useInView(options = {}) {
10
10
  useEffect(() => {
11
11
  const node = ref.current;
12
12
  if (!node)
13
- return;
13
+ return undefined;
14
14
  if (typeof IntersectionObserver === 'undefined') {
15
15
  setInView(true);
16
- return;
16
+ return undefined;
17
17
  }
18
18
  const observer = new IntersectionObserver((entries) => {
19
19
  for (const entry of entries) {
20
- if (entry.isIntersecting) {
21
- setInView(true);
22
- if (options.once !== false) {
23
- observer.disconnect();
24
- }
20
+ if (!entry.isIntersecting) {
21
+ if (options.once === false)
22
+ setInView(false);
23
+ continue;
25
24
  }
26
- else if (options.once === false) {
27
- setInView(false);
25
+ setInView(true);
26
+ if (options.once !== false) {
27
+ observer.disconnect();
28
28
  }
29
29
  }
30
30
  }, {
@@ -114,12 +114,9 @@ export function useResizableSplitPane(options = {}) {
114
114
  return undefined;
115
115
  const updateAllowedWidth = () => {
116
116
  const containerWidth = container.clientWidth;
117
- const nextSecondaryMin = !Number.isFinite(containerWidth) || containerWidth <= 0
118
- ? minSecondaryWidth
119
- : containerWidth < minNormalContainerWidth
120
- ? 0
121
- : minSecondaryWidth;
122
- const nextMax = !Number.isFinite(containerWidth) || containerWidth <= 0
117
+ const unmeasured = !Number.isFinite(containerWidth) || containerWidth <= 0;
118
+ const nextSecondaryMin = !unmeasured && containerWidth < minNormalContainerWidth ? 0 : minSecondaryWidth;
119
+ const nextMax = unmeasured
123
120
  ? maxWidthOption
124
121
  : Math.max(0, Math.min(maxWidthOption, Math.floor(containerWidth - handleWidth - nextSecondaryMin)));
125
122
  maxWidthRef.current = nextMax;
@@ -219,22 +216,17 @@ export function useResizableSplitPane(options = {}) {
219
216
  finishResize(false);
220
217
  }, [finishResize, renderPreferredWidth]);
221
218
  const onHandleKeyDown = useCallback((event) => {
222
- let nextWidth = null;
223
219
  const container = containerRef.current;
224
220
  const isRtl = container ? window.getComputedStyle(container).direction === 'rtl' : false;
225
- if (event.key === 'ArrowLeft') {
226
- nextWidth = widthRef.current + (isRtl ? 1 : -1) * keyboardStep;
227
- }
228
- else if (event.key === 'ArrowRight') {
229
- nextWidth = widthRef.current + (isRtl ? -1 : 1) * keyboardStep;
230
- }
231
- else if (event.key === 'Home') {
232
- nextWidth = minWidth;
233
- }
234
- else if (event.key === 'End') {
235
- nextWidth = maxWidthRef.current;
236
- }
237
- if (nextWidth === null)
221
+ // ArrowRight grows the pane in LTR and shrinks it in RTL; ArrowLeft is the mirror.
222
+ const rightStep = (isRtl ? -1 : 1) * keyboardStep;
223
+ const nextWidth = new Map([
224
+ ['ArrowLeft', widthRef.current - rightStep],
225
+ ['ArrowRight', widthRef.current + rightStep],
226
+ ['Home', minWidth],
227
+ ['End', maxWidthRef.current],
228
+ ]).get(event.key);
229
+ if (nextWidth === undefined)
238
230
  return;
239
231
  event.preventDefault();
240
232
  const next = applyWidth(nextWidth);
@@ -26,7 +26,7 @@ export function useHostSurfaceTokens(ref) {
26
26
  const view = element?.ownerDocument.defaultView;
27
27
  if (element === null || element === undefined || view === null || view === undefined) {
28
28
  setTokens({});
29
- return;
29
+ return undefined;
30
30
  }
31
31
  let last;
32
32
  const read = () => {
@@ -16,6 +16,12 @@ import { useT } from '../../../react/i18n.js';
16
16
  function maybePortal(node, host) {
17
17
  return host ? createPortal(node, host) : node;
18
18
  }
19
+ /** Crosshair for drawing tools, an I-beam for the text tool, the default pointer while inactive. */
20
+ function annotationCursor(active, markTool) {
21
+ if (!active)
22
+ return 'default';
23
+ return markTool === 'text' ? 'text' : 'crosshair';
24
+ }
19
25
  export function AnnotationCanvas(props) {
20
26
  const { children, toolbarHost, icons: iconOverrides, className } = props;
21
27
  const icons = { ...DEFAULT_ANNOTATION_CANVAS_ICONS, ...iconOverrides };
@@ -39,7 +45,7 @@ function DrawingCanvas({ c, active }) {
39
45
  position: 'absolute',
40
46
  inset: 0,
41
47
  pointerEvents: active ? 'auto' : 'none',
42
- cursor: active ? (c.markTool === 'text' ? 'text' : 'crosshair') : 'default',
48
+ cursor: annotationCursor(active, c.markTool),
43
49
  visibility: c.chromeHidden ? 'hidden' : 'visible',
44
50
  touchAction: 'none',
45
51
  userSelect: 'none',
@@ -19,7 +19,8 @@ export function deriveUndoRedoAvailability(input) {
19
19
  /** Shared by the hook's `canSubmit` value and `send()`'s own gate — both computed the identical chain before this extraction. */
20
20
  export function deriveSendGate(input) {
21
21
  const { hasInk, hasBox, hasText, hasCaptureTarget, captureViewport, noteTrimmed, extraFilesCount } = input;
22
- const shouldCapture = hasInk || hasBox || hasText || hasCaptureTarget || captureViewport;
22
+ const hasAnnotation = hasInk || hasBox || hasText;
23
+ const shouldCapture = hasAnnotation || hasCaptureTarget || captureViewport;
23
24
  const canSubmitNow = shouldCapture || Boolean(noteTrimmed) || extraFilesCount > 0;
24
25
  return { shouldCapture, canSubmitNow };
25
26
  }
@@ -35,7 +36,8 @@ export function canAttemptSend(input) {
35
36
  /** Picks which of the two "no screenshot" warnings applies — the whole-viewport-request wording, or the mark-specific one. */
36
37
  export function selectNoScreenshotMessage(input, t) {
37
38
  const { captureViewport, hasInk, hasBox, hasCaptureTarget } = input;
38
- return captureViewport && !hasInk && !hasBox && !hasCaptureTarget
39
+ const hasMark = hasInk || hasBox || hasCaptureTarget;
40
+ return captureViewport && !hasMark
39
41
  ? t('No screenshot was captured for this annotation.')
40
42
  : t('No screenshot was captured for this mark.');
41
43
  }
@@ -25,6 +25,8 @@ const DEFAULT_DOCKED_STYLE = {
25
25
  maxWidth: 'min(760px, calc(100% - 144px))',
26
26
  };
27
27
  const SUBMIT_TIMEOUT_MS = 60_000;
28
+ /** The analytics toolbar element each mark tool reports as. */
29
+ const TOOLBAR_TOOL_FOR_MARK_TOOL = { box: 'rect', text: 'text' };
28
30
  export function useAnnotationCanvas(options) {
29
31
  const { active, onActiveChange, captureViewport = false, captureTarget = null, filePath, sendDisabled = false, hideChrome = false, onToolbarClick, port } = options;
30
32
  const t = useT();
@@ -155,7 +157,7 @@ export function useAnnotationCanvas(options) {
155
157
  }
156
158
  window.addEventListener('keydown', onKey);
157
159
  return () => window.removeEventListener('keydown', onKey);
158
- // eslint-disable-next-line react-hooks/exhaustive-deps
160
+ // Dependency list intentionally follows the lifecycle rationale above.
159
161
  }, [active, onActiveChange, sending]);
160
162
  // Keyboard shortcut #2: Escape closes the staged-image preview modal first (capture phase).
161
163
  useEffect(() => {
@@ -368,7 +370,7 @@ export function useAnnotationCanvas(options) {
368
370
  syncHistoryState();
369
371
  redraw();
370
372
  bumpLayoutRevision();
371
- // eslint-disable-next-line react-hooks/exhaustive-deps
373
+ // Dependency list intentionally follows the lifecycle rationale above.
372
374
  }, [active, redraw]);
373
375
  const autosizeTextArea = useCallback((el) => {
374
376
  if (!el)
@@ -509,7 +511,7 @@ export function useAnnotationCanvas(options) {
509
511
  };
510
512
  }, [extraFiles]);
511
513
  function selectMarkTool(nextTool) {
512
- onToolbarClick?.(nextTool === 'box' ? 'rect' : nextTool === 'text' ? 'text' : 'pen');
514
+ onToolbarClick?.(TOOLBAR_TOOL_FOR_MARK_TOOL[nextTool] ?? 'pen');
513
515
  setMarkTool(nextTool);
514
516
  setMarkToolMenuOpen(false);
515
517
  }
@@ -598,7 +600,9 @@ export function useAnnotationCanvas(options) {
598
600
  }
599
601
  function waitForOverlayHidden() {
600
602
  return new Promise((resolve) => {
601
- requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
603
+ const finish = () => resolve();
604
+ const afterNextFrame = () => requestAnimationFrame(finish);
605
+ requestAnimationFrame(afterNextFrame);
602
606
  });
603
607
  }
604
608
  async function requestSnapshot() {
@@ -735,7 +739,7 @@ export function useAnnotationCanvas(options) {
735
739
  anchor,
736
740
  });
737
741
  setDockPlacement((current) => (dockPlacementEquals(current, next) ? current : next));
738
- // eslint-disable-next-line react-hooks/exhaustive-deps
742
+ // Dependency list intentionally follows the lifecycle rationale above.
739
743
  }, [active, captureTarget, layoutRevision, imagePreviews.length, captureWarning?.message]);
740
744
  useLayoutEffect(() => {
741
745
  if (!active || typeof ResizeObserver === 'undefined')