@jini-ai/ui 0.4.4 → 0.4.5

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 (165) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +20 -0
  3. package/dist/core/theme/default.d.ts +4 -0
  4. package/dist/core/theme/default.js +11 -0
  5. package/dist/core/theme/stylesheet.d.ts +8 -0
  6. package/dist/core/theme/stylesheet.js +14 -0
  7. package/dist/core/theme/types.d.ts +39 -0
  8. package/dist/core/theme/types.js +2 -0
  9. package/dist/core/theme/validation.d.ts +15 -0
  10. package/dist/core/theme/validation.js +55 -0
  11. package/dist/features/admin-widgets/components/Select/Select.d.ts +11 -3
  12. package/dist/features/admin-widgets/components/Select/Select.hooks.d.ts +8 -13
  13. package/dist/features/admin-widgets/components/Select/Select.hooks.js +37 -215
  14. package/dist/features/admin-widgets/components/Select/Select.js +4 -3
  15. package/dist/features/appearance/constants.js +1 -1
  16. package/dist/features/connectors/rules.d.ts +1 -6
  17. package/dist/features/connectors/types.d.ts +1 -6
  18. package/dist/features/connectors/types.js +1 -6
  19. package/dist/features/execution/http-adapter.d.ts +111 -0
  20. package/dist/features/execution/http-adapter.js +160 -0
  21. package/dist/features/execution/index.d.ts +4 -0
  22. package/dist/features/execution/index.js +2 -0
  23. package/dist/features/execution/react/hooks/use-execution-credential.hooks.d.ts +232 -0
  24. package/dist/features/execution/react/hooks/use-execution-credential.hooks.js +217 -0
  25. package/dist/features/html-editor/canvas-style.d.ts +1 -1
  26. package/dist/features/html-editor/canvas-style.js +2 -2
  27. package/dist/features/i18n/dictionaries/settings-dialog.ar.js +2 -0
  28. package/dist/features/i18n/dictionaries/settings-dialog.bn.js +2 -0
  29. package/dist/features/i18n/dictionaries/settings-dialog.de.js +2 -0
  30. package/dist/features/i18n/dictionaries/settings-dialog.en.js +2 -0
  31. package/dist/features/i18n/dictionaries/settings-dialog.es.js +2 -0
  32. package/dist/features/i18n/dictionaries/settings-dialog.fa.js +2 -0
  33. package/dist/features/i18n/dictionaries/settings-dialog.fr.js +2 -0
  34. package/dist/features/i18n/dictionaries/settings-dialog.hi.js +2 -0
  35. package/dist/features/i18n/dictionaries/settings-dialog.hu.js +2 -0
  36. package/dist/features/i18n/dictionaries/settings-dialog.id.js +2 -0
  37. package/dist/features/i18n/dictionaries/settings-dialog.it.js +2 -0
  38. package/dist/features/i18n/dictionaries/settings-dialog.ja.js +2 -0
  39. package/dist/features/i18n/dictionaries/settings-dialog.ko.js +2 -0
  40. package/dist/features/i18n/dictionaries/settings-dialog.pl.js +2 -0
  41. package/dist/features/i18n/dictionaries/settings-dialog.pt-BR.js +2 -0
  42. package/dist/features/i18n/dictionaries/settings-dialog.ru.js +2 -0
  43. package/dist/features/i18n/dictionaries/settings-dialog.th.js +2 -0
  44. package/dist/features/i18n/dictionaries/settings-dialog.tr.js +2 -0
  45. package/dist/features/i18n/dictionaries/settings-dialog.uk.js +2 -0
  46. package/dist/features/i18n/dictionaries/settings-dialog.ur.js +2 -0
  47. package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +2 -0
  48. package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +2 -0
  49. package/dist/features/i18n/dictionary-translator.d.ts +5 -2
  50. package/dist/features/i18n/dictionary-translator.js +1 -1
  51. package/dist/features/i18n/index.d.ts +1 -1
  52. package/dist/features/i18n/index.js +1 -1
  53. package/dist/features/i18n/template-i18n.d.ts +11 -0
  54. package/dist/features/i18n/template-i18n.js +12 -0
  55. package/dist/features/interactive-ui/providers/recharts/chart-tokens.d.ts +1 -1
  56. package/dist/features/interactive-ui/providers/recharts/chart-tokens.js +1 -1
  57. package/dist/features/lexical-rich-text-editor/deserialize.d.ts +0 -5
  58. package/dist/features/lexical-rich-text-editor/deserialize.js +0 -5
  59. package/dist/features/lexical-rich-text-editor/mention-node.d.ts +5 -17
  60. package/dist/features/lexical-rich-text-editor/mention-node.js +5 -17
  61. package/dist/features/lexical-rich-text-editor/mention-parser.d.ts +2 -4
  62. package/dist/features/lexical-rich-text-editor/serialize.d.ts +0 -4
  63. package/dist/features/lexical-rich-text-editor/serialize.js +0 -4
  64. package/dist/features/lexical-rich-text-editor/types.d.ts +3 -13
  65. package/dist/features/lexical-rich-text-editor/types.js +3 -13
  66. package/dist/features/mcp-ui/early-message-buffer.d.ts +1 -2
  67. package/dist/features/mcp-ui/early-message-buffer.js +1 -2
  68. package/dist/features/mcp-ui/escape.d.ts +4 -0
  69. package/dist/features/mcp-ui/escape.js +2 -9
  70. package/dist/features/mcp-ui/index.d.ts +1 -0
  71. package/dist/features/mcp-ui/index.js +1 -0
  72. package/dist/features/mcp-ui/protocol.d.ts +2 -5
  73. package/dist/features/mcp-ui/protocol.js +2 -5
  74. package/dist/features/mcp-ui/resource.d.ts +47 -7
  75. package/dist/features/mcp-ui/resource.js +5 -8
  76. package/dist/features/mcp-ui/secret-card.d.ts +115 -0
  77. package/dist/features/mcp-ui/secret-card.js +137 -0
  78. package/dist/features/mcp-ui/surfaces/confirmation.js +5 -0
  79. package/dist/features/mcp-ui/surfaces/fields.d.ts +5 -2
  80. package/dist/features/mcp-ui/surfaces/fields.js +3 -1
  81. package/dist/features/mcp-ui/surfaces/form.d.ts +4 -1
  82. package/dist/features/mcp-ui/surfaces/form.js +65 -3
  83. package/dist/features/mcp-ui/surfaces/outcome.d.ts +2 -0
  84. package/dist/features/mcp-ui/surfaces/text-input.d.ts +14 -6
  85. package/dist/features/mcp-ui/surfaces/text-input.js +16 -2
  86. package/dist/features/mcp-ui/surfaces/tokens.js +1 -1
  87. package/dist/features/memory/react/components/MemoryConnectedPanel.d.ts +1 -1
  88. package/dist/features/memory/react/components/MemoryConnectedPanel.js +0 -1
  89. package/dist/features/panel-kit/helpers/format-timestamp.d.ts +5 -3
  90. package/dist/features/panel-kit/helpers/format-timestamp.js +13 -4
  91. package/dist/features/panel-kit/helpers/permissions.js +2 -0
  92. package/dist/features/panel-kit/helpers/resolve-active-tab-id.js +2 -0
  93. package/dist/features/panel-kit/helpers/standing-draft-local-backup.d.ts +51 -0
  94. package/dist/features/panel-kit/helpers/standing-draft-local-backup.js +77 -0
  95. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.d.ts +8 -0
  96. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +10 -0
  97. package/dist/features/panel-kit/hooks/use-async-action.hooks.d.ts +1 -0
  98. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.d.ts +1 -0
  99. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -0
  100. package/dist/features/panel-kit/hooks/use-serial-writes.hooks.d.ts +8 -0
  101. package/dist/features/panel-kit/hooks/use-serial-writes.hooks.js +3 -0
  102. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.d.ts +211 -0
  103. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +382 -0
  104. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.d.ts +179 -0
  105. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +300 -0
  106. package/dist/features/panel-kit/index.d.ts +7 -1
  107. package/dist/features/panel-kit/index.js +5 -1
  108. package/dist/features/resource-dashboard/types.d.ts +8 -41
  109. package/dist/features/resource-dashboard/types.js +3 -26
  110. package/dist/features/tab-strip/constants.d.ts +3 -4
  111. package/dist/features/tab-strip/constants.js +3 -4
  112. package/dist/features/tab-strip/dependencies.d.ts +1 -6
  113. package/dist/features/tab-strip/dependencies.js +1 -6
  114. package/dist/features/tab-strip/react/components/TabBar.d.ts +33 -1
  115. package/dist/features/tab-strip/react/components/TabBar.hooks.d.ts +57 -5
  116. package/dist/features/tab-strip/react/components/TabBar.hooks.js +63 -5
  117. package/dist/features/tab-strip/react/components/TabBar.js +12 -1
  118. package/dist/features/tab-strip/rules.d.ts +4 -17
  119. package/dist/features/tab-strip/rules.js +4 -17
  120. package/dist/features/tab-strip/types.d.ts +2 -13
  121. package/dist/icon-name.d.ts +1 -1
  122. package/dist/icon-name.js +1 -0
  123. package/dist/index.d.ts +42 -417
  124. package/dist/index.js +42 -430
  125. package/dist/react/components/CustomSelect.d.ts +6 -2
  126. package/dist/react/components/CustomSelect.js +20 -91
  127. package/dist/react/components/Icon.d.ts +6 -0
  128. package/dist/react/components/Icon.js +12 -0
  129. package/dist/react/components/select-popover.d.ts +106 -0
  130. package/dist/react/components/select-popover.js +210 -0
  131. package/dist/renderers/sandbox-scripts.d.ts +29 -0
  132. package/dist/renderers/sandbox-scripts.js +127 -0
  133. package/dist/renderers/sandboxed-document.js +3 -86
  134. package/dist/renderers/srcdoc/build.d.ts +10 -17
  135. package/dist/renderers/srcdoc/build.js +31 -234
  136. package/dist/styles/variables.css +2 -2
  137. package/dist/theme/apply.d.ts +1 -1
  138. package/dist/theme/apply.js +2 -2
  139. package/dist/theme/color-scheme.d.ts +1 -1
  140. package/dist/theme/default.d.ts +1 -3
  141. package/dist/theme/default.js +2 -10
  142. package/dist/theme/index.d.ts +3 -3
  143. package/dist/theme/index.js +2 -2
  144. package/dist/theme/stylesheet.d.ts +1 -7
  145. package/dist/theme/stylesheet.js +2 -13
  146. package/dist/theme/types.d.ts +1 -38
  147. package/dist/theme/types.js +2 -1
  148. package/dist/theme/validation.d.ts +1 -14
  149. package/dist/theme/validation.js +2 -54
  150. package/dist/utils/endpoint-policy.d.ts +21 -39
  151. package/dist/utils/endpoint-policy.js +19 -107
  152. package/dist/utils/file-system-errors.d.ts +1 -2
  153. package/dist/utils/file-system-errors.js +1 -2
  154. package/dist/utils/ime-composing.d.ts +0 -3
  155. package/dist/utils/ime-composing.js +0 -3
  156. package/dist/utils/platform.d.ts +1 -4
  157. package/dist/utils/platform.js +1 -4
  158. package/dist/utils/smooth-scroll-to-top.d.ts +1 -3
  159. package/dist/utils/smooth-scroll-to-top.js +3 -13
  160. package/dist/utils/timezone.js +1 -5
  161. package/dist/utils/uuid.d.ts +0 -3
  162. package/dist/utils/uuid.js +0 -3
  163. package/dist/utils/visual-stability.d.ts +1 -3
  164. package/dist/utils/visual-stability.js +1 -3
  165. package/package.json +74 -15
@@ -8,8 +8,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  // pattern (see TooltipLayer.tsx). Every seam is exported: pure helpers, the
9
9
  // `useCustomSelect` hook that owns all state/refs/effects/handlers, the
10
10
  // `CustomSelectOptionButton` leaf, and the dumb `CustomSelect` render.
11
- import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
11
+ import { useEffect, useId, useMemo, useRef, useState } from 'react';
12
12
  import { createPortal } from 'react-dom';
13
+ import { useSelectPopover, isSelectEventInside, openSelectOnKey, applySelectNavigationKey, resolveSelectPortalContainer } from './select-popover.js';
13
14
  import { Icon } from './Icon.js';
14
15
  /** Space between the trigger and the menu. */
15
16
  export const CUSTOM_SELECT_MENU_GAP = 4;
@@ -88,7 +89,7 @@ export function reconcileCustomSelectActiveValue(input) {
88
89
  /** Pure: is a pointerdown target inside the trigger or the (possibly absent)
89
90
  * menu? Used to decide whether an outside click should close the menu. */
90
91
  export function isCustomSelectEventInside(target, button, menu) {
91
- return Boolean(button?.contains(target)) || Boolean(menu?.contains(target));
92
+ return isSelectEventInside({ target, trigger: button, panel: menu });
92
93
  }
93
94
  /**
94
95
  * All of CustomSelect's behavior — ids, refs, open/active/position state, the
@@ -98,24 +99,14 @@ export function isCustomSelectEventInside(target, button, menu) {
98
99
  * logic is testable in isolation from the DOM; `CustomSelect` is the dumb
99
100
  * consumer.
100
101
  */
101
- export function useCustomSelect({ value, options, onChange, portal, placeholder, onOpenChange, }) {
102
+ export function useCustomSelect({ value, options, onChange, portal, placeholder, onOpenChange, }, _optional = {}) {
102
103
  const reactId = useId();
103
104
  const idBase = reactId.replace(/:/g, '');
104
- const buttonRef = useRef(null);
105
- const menuRef = useRef(null);
106
105
  const wasOpenRef = useRef(false);
107
106
  const activeSourceValueRef = useRef(value);
108
- const [open, setOpen] = useState(false);
109
- // Stored in a ref (not a `useEffect` dependency) so an unmemoized caller
110
- // passing a fresh function identity every render never re-fires this for
111
- // any reason OTHER than `open` actually changing.
112
- const onOpenChangeRef = useRef(onOpenChange);
113
- onOpenChangeRef.current = onOpenChange;
114
107
  const [activeValue, setActiveValue] = useState(value);
115
- const [position, setPosition] = useState(null);
116
- useEffect(() => {
117
- onOpenChangeRef.current?.(open);
118
- }, [open]);
108
+ const popover = useSelectPopover({ measurePosition: ({ trigger }) => computeCustomSelectMenuPosition(trigger.getBoundingClientRect(), { width: window.innerWidth, height: window.innerHeight }) }, { portal, onOpenChange });
109
+ const { open, position, triggerRef: buttonRef, panelRef: menuRef, updatePosition } = popover;
119
110
  const flatOptions = useMemo(() => flattenCustomSelectOptions(options), [options]);
120
111
  const selected = flatOptions.find((option) => option.value === value);
121
112
  const selectedLabel = selected?.label ?? placeholder ?? value;
@@ -126,24 +117,6 @@ export function useCustomSelect({ value, options, onChange, portal, placeholder,
126
117
  enabledOptionsRef.current = enabledOptions;
127
118
  const optionIdByValue = useMemo(() => new Map(flatOptions.map((option, index) => [option.value, `${idBase}-option-${index}`])), [flatOptions, idBase]);
128
119
  const activeOptionId = open && activeValue ? optionIdByValue.get(activeValue) : undefined;
129
- const updatePosition = useCallback(() => {
130
- if (!buttonRef.current)
131
- return;
132
- const rect = buttonRef.current.getBoundingClientRect();
133
- setPosition(computeCustomSelectMenuPosition(rect, {
134
- width: window.innerWidth,
135
- height: window.innerHeight,
136
- }));
137
- }, []);
138
- useEffect(() => {
139
- if (!portal)
140
- return;
141
- if (!open) {
142
- setPosition(null);
143
- return;
144
- }
145
- updatePosition();
146
- }, [open, portal, updatePosition]);
147
120
  useEffect(() => {
148
121
  const result = reconcileCustomSelectActiveValue({
149
122
  open,
@@ -158,71 +131,31 @@ export function useCustomSelect({ value, options, onChange, portal, placeholder,
158
131
  if (result.nextActiveValue !== undefined)
159
132
  setActiveValue(result.nextActiveValue);
160
133
  }, [open, value]);
161
- useEffect(() => {
162
- if (!open)
163
- return;
164
- const onPointerDown = (event) => {
165
- const target = event.target;
166
- if (isCustomSelectEventInside(target, buttonRef.current, menuRef.current))
167
- return;
168
- setOpen(false);
169
- };
170
- const onScrollOrResize = () => {
171
- if (portal)
172
- updatePosition();
173
- };
174
- document.addEventListener('mousedown', onPointerDown);
175
- window.addEventListener('resize', onScrollOrResize);
176
- window.addEventListener('scroll', onScrollOrResize, true);
177
- return () => {
178
- document.removeEventListener('mousedown', onPointerDown);
179
- window.removeEventListener('resize', onScrollOrResize);
180
- window.removeEventListener('scroll', onScrollOrResize, true);
181
- };
182
- }, [open, portal, updatePosition]);
183
134
  const choose = (nextValue) => {
184
135
  const next = flatOptions.find((option) => option.value === nextValue);
185
136
  if (!next || next.disabled)
186
137
  return;
187
138
  onChange(next.value);
188
- setOpen(false);
189
- buttonRef.current?.focus();
139
+ popover.closePanel({ refocusTrigger: true });
190
140
  };
191
141
  const moveActive = (direction) => {
192
142
  const next = nextCustomSelectActiveValue(enabledOptions, activeValue, direction);
193
143
  if (next !== null)
194
144
  setActiveValue(next);
195
145
  };
196
- const toggleOpen = () => setOpen((current) => !current);
197
- const closeAndRefocusTrigger = () => {
198
- setOpen(false);
199
- buttonRef.current?.focus();
200
- };
146
+ const toggleOpen = () => open ? popover.closePanel({ refocusTrigger: false }) : popover.openPanel();
201
147
  const onButtonKeyDown = (event) => {
202
- if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
203
- event.preventDefault();
204
- if (!open) {
205
- setOpen(true);
206
- return;
207
- }
208
- moveActive(event.key === 'ArrowDown' ? 1 : -1);
148
+ if (popover.handleDismissKey(event))
209
149
  return;
210
- }
211
- if (event.key === 'Enter' || event.key === ' ') {
212
- event.preventDefault();
213
- if (open) {
214
- choose(activeValue || value);
215
- }
216
- else {
217
- setOpen(true);
218
- }
150
+ if (openSelectOnKey({ event, open, openPanel: popover.openPanel }))
219
151
  return;
220
- }
221
- if (event.key === 'Escape' && open) {
222
- event.preventDefault();
223
- event.stopPropagation();
224
- closeAndRefocusTrigger();
225
- }
152
+ if (!open)
153
+ return;
154
+ applySelectNavigationKey({ key: event.key, actions: {
155
+ preventDefault: () => event.preventDefault(), move: moveActive,
156
+ edge: (edge) => setActiveValue((edge === 'first' ? enabledOptions[0] : enabledOptions.at(-1))?.value ?? ''),
157
+ choose: () => choose(activeValue || value),
158
+ } }, { spaceChooses: true });
226
159
  };
227
160
  // Escape closes even when focus has moved into custom `menuHeader` content
228
161
  // (e.g. `SearchableModelSelect`'s search input) — `onButtonKeyDown` alone
@@ -232,11 +165,7 @@ export function useCustomSelect({ value, options, onChange, portal, placeholder,
232
165
  // take real focus). Attached to the menu container, not the document, so it
233
166
  // never fires for an unrelated Escape press elsewhere on the page.
234
167
  const onMenuKeyDown = (event) => {
235
- if (event.key !== 'Escape')
236
- return;
237
- event.preventDefault();
238
- event.stopPropagation();
239
- closeAndRefocusTrigger();
168
+ popover.handleDismissKey(event);
240
169
  };
241
170
  return {
242
171
  idBase,
@@ -262,7 +191,7 @@ export function useCustomSelect({ value, options, onChange, portal, placeholder,
262
191
  * {@link useCustomSelect}; this component only wires the hook's state to the
263
192
  * trigger button and the listbox menu.
264
193
  */
265
- export function CustomSelect({ value, options, onChange, ariaLabel, labelledBy, className, triggerClassName, menuClassName, disabled = false, placeholder, portal = true, title, onFocus, menuHeader, onOpenChange, useCustomSelect: useCustomSelectHook = useCustomSelect, }) {
194
+ export function CustomSelect({ value, options, onChange, ariaLabel, labelledBy, className, triggerClassName, menuClassName, disabled = false, placeholder, portal = true, portalContainer, title, onFocus, menuHeader, onOpenChange, useCustomSelect: useCustomSelectHook = useCustomSelect, }, _optional = {}) {
266
195
  const { idBase, buttonRef, menuRef, open, activeValue, position, selectedLabel, optionIdByValue, activeOptionId, choose, setActiveValue, toggleOpen, onButtonKeyDown, onMenuKeyDown, } = useCustomSelectHook({ value, options, onChange, portal, placeholder, onOpenChange });
267
196
  /**
268
197
  * The theme of the place this menu was opened FROM.
@@ -301,7 +230,7 @@ export function CustomSelect({ value, options, onChange, ariaLabel, labelledBy,
301
230
  }
302
231
  return (_jsx(CustomSelectOptionButton, { option: item, selected: item.value === value, active: item.value === activeValue, id: optionIdByValue.get(item.value), onChoose: choose, onActive: setActiveValue }, item.value));
303
232
  })] }));
304
- return (_jsxs("div", { className: ['jini-select', className].filter(Boolean).join(' '), children: [_jsxs("button", { ref: buttonRef, type: "button", className: ['jini-select-trigger', triggerClassName].filter(Boolean).join(' '), role: "combobox", value: value, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": `${idBase}-menu`, "aria-activedescendant": activeOptionId, "aria-describedby": labelledBy, "aria-label": `${ariaLabel}: ${selectedLabel}`, disabled: disabled, title: title, onClick: toggleOpen, onKeyDown: onButtonKeyDown, onFocus: onFocus, children: [_jsx("span", { id: `${idBase}-value`, className: "jini-select-value", children: selectedLabel }), _jsx(Icon, { name: "chevron-down", size: 14 })] }), open ? (portal ? (position ? createPortal(menu, document.body) : null) : menu) : null] }));
233
+ return (_jsxs("div", { className: ['jini-select', className].filter(Boolean).join(' '), children: [_jsxs("button", { ref: buttonRef, type: "button", className: ['jini-select-trigger', triggerClassName].filter(Boolean).join(' '), role: "combobox", value: value, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": `${idBase}-menu`, "aria-activedescendant": activeOptionId, "aria-describedby": labelledBy, "aria-label": `${ariaLabel}: ${selectedLabel}`, disabled: disabled, title: title, onClick: toggleOpen, onKeyDown: onButtonKeyDown, onFocus: onFocus, children: [_jsx("span", { id: `${idBase}-value`, className: "jini-select-value", children: selectedLabel }), _jsx(Icon, { name: "chevron-down", size: 14 })] }), open ? (portal ? (position ? createPortal(menu, resolveSelectPortalContainer({ trigger: buttonRef.current }, { container: portalContainer })) : null) : menu) : null] }));
305
234
  }
306
235
  /** One row in the listbox: a selectable option button that reports hover
307
236
  * (activation) and click (choice) back to {@link CustomSelect}. */
@@ -1,6 +1,12 @@
1
1
  import type { ReactElement, SVGProps } from 'react';
2
2
  import type { IconName } from '../../icon-name.js';
3
3
  export type { IconName } from '../../icon-name.js';
4
+ /** Exact shared geometry for outlines that feature artwork decorates with different inner marks.
5
+ * Keep the marks with their feature; a shared outline does not make two whole icons equivalent. */
6
+ export declare const ICON_PATH_DATA: {
7
+ readonly 'document-outline-compact': "M6.5 3.5h7.6l3.9 3.9v13.1H6.5z";
8
+ readonly 'folder-outline-compact': "M3.5 7.5V6A1.5 1.5 0 0 1 5 4.5h4l2 2h8A1.5 1.5 0 0 1 20.5 8v10A1.5 1.5 0 0 1 19 19.5H5A1.5 1.5 0 0 1 3.5 18z";
9
+ };
4
10
  interface Props extends Omit<SVGProps<SVGSVGElement>, 'name'> {
5
11
  name: IconName;
6
12
  size?: number | string;
@@ -1,9 +1,21 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** Exact shared geometry for outlines that feature artwork decorates with different inner marks.
3
+ * Keep the marks with their feature; a shared outline does not make two whole icons equivalent. */
4
+ export const ICON_PATH_DATA = {
5
+ 'document-outline-compact': 'M6.5 3.5h7.6l3.9 3.9v13.1H6.5z',
6
+ 'folder-outline-compact': 'M3.5 7.5V6A1.5 1.5 0 0 1 5 4.5h4l2 2h8A1.5 1.5 0 0 1 20.5 8v10A1.5 1.5 0 0 1 19 19.5H5A1.5 1.5 0 0 1 3.5 18z',
7
+ };
2
8
  // Exported as a test seam only — call it directly to assert key-set
3
9
  // completeness and per-entry validity without rendering. NOT part of the
4
10
  // package's public API: do not re-export it from `packages/ui/src/index.ts`
5
11
  // or any entry point.
6
12
  export const ICON_RENDERERS = {
13
+ 'package-check': (common) => (_jsxs("svg", { ...common, children: [_jsx("path", { d: "M20.5 8.2v5.1L12 17.9l-8.5-4.6V8.2" }), _jsx("path", { d: "M3.5 8.2 12 3.6l8.5 4.6L12 12.8z" }), _jsx("path", { d: "M15.6 19.4l1.9 1.9 3.4-3.9" })] })),
14
+ 'storefront': (common) => (_jsxs("svg", { ...common, children: [_jsx("path", { d: "M3.5 8.5h17l-1.2-4H4.7z" }), _jsx("path", { d: "M5 8.5v11h14v-11" }), _jsx("path", { d: "M9.75 19.5v-5.5h4.5v5.5" })] })),
15
+ 'package-outline': (common) => (_jsxs("svg", { ...common, children: [_jsx("path", { d: "M3.5 8.2 12 3.6l8.5 4.6v7.6L12 20.4l-8.5-4.6z" }), _jsx("path", { d: "M3.5 8.2 12 12.8l8.5-4.6M12 12.8v7.6" })] })),
16
+ 'trash-compact': (common) => (_jsxs("svg", { ...common, children: [_jsx("path", { d: "M4.5 7h15M9.5 7V4.6h5V7" }), _jsx("path", { d: "M6.6 7l.9 12.4h9l.9-12.4" }), _jsx("path", { d: "M10.4 10.5v6M13.6 10.5v6" })] })),
17
+ 'chevron-right-wide': (common) => (_jsx("svg", { ...common, children: _jsx("path", { d: "M9 5.5 15.5 12 9 18.5" }) })),
18
+ 'check-long': (common) => (_jsx("svg", { ...common, children: _jsx("path", { d: "m5 13 4.5 4.5L19 6.5" }) })),
7
19
  'alert-triangle': (common) => (_jsxs("svg", { ...common, children: [_jsx("path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }), _jsx("path", { d: "M12 9v4" }), _jsx("path", { d: "M12 17h.01" })] })),
8
20
  'arrow-left': (common) => (_jsxs("svg", { ...common, children: [_jsx("path", { d: "M19 12H5" }), _jsx("path", { d: "m12 19-7-7 7-7" })] })),
9
21
  'arrow-up': (common) => (_jsxs("svg", { ...common, children: [_jsx("path", { d: "M12 19V5" }), _jsx("path", { d: "m5 12 7-7 7 7" })] })),
@@ -0,0 +1,106 @@
1
+ import type { KeyboardEvent, RefObject } from 'react';
2
+ /** DOM-order focus candidates within root, excluding the floating panel's own controls. */
3
+ export declare function focusableInDomOrder({ exclude }: {
4
+ exclude: HTMLElement | null;
5
+ }, { root }?: {
6
+ root?: ParentNode | undefined;
7
+ }): HTMLElement[];
8
+ /** Find the trigger's Tab neighbour, wrapping within its modal dialog to avoid inert page controls. */
9
+ export declare function resolveTabTarget({ panel, trigger, shiftKey }: {
10
+ panel: HTMLElement | null;
11
+ trigger: HTMLElement | null;
12
+ shiftKey: boolean;
13
+ }, _optional?: Record<string, never>): HTMLElement | null;
14
+ /** Resolve a portal inside the trigger's native dialog so its menu stays above inert page content. */
15
+ export declare function resolveSelectPortalContainer({ trigger }: {
16
+ trigger: HTMLElement | null;
17
+ }, { container }?: {
18
+ container?: Element | DocumentFragment | null | undefined;
19
+ }): Element | DocumentFragment;
20
+ /** Pure containment decision; outside clicks must neither select an option nor steal focus. */
21
+ export declare function isSelectEventInside({ target, trigger, panel }: {
22
+ target: Node;
23
+ trigger: HTMLElement | null;
24
+ panel: HTMLElement | null;
25
+ }, _optional?: Record<string, never>): boolean;
26
+ /** Apply the variant's geometry port; widget selects additionally dismiss a clipped/obscured trigger. */
27
+ export declare function repositionSelectPopover<P>({ trigger, measurePosition, setPosition }: {
28
+ trigger: HTMLElement;
29
+ measurePosition: (input: {
30
+ trigger: HTMLElement;
31
+ }) => P;
32
+ setPosition: (position: P) => void;
33
+ }, { onOutOfView }?: {
34
+ onOutOfView?: (() => void) | undefined;
35
+ }): void;
36
+ /** Measure before mounting/focusing a portal, then keep it anchored until close; listeners are disposed on close/unmount.
37
+ * Geometry is O(1); focus/visibility effects use the live DOM. The port retains each variant's existing placement bounds.
38
+ */
39
+ export declare function useSelectPopoverPosition<P>(required: {
40
+ open: boolean;
41
+ triggerRef: RefObject<HTMLButtonElement | null>;
42
+ panelRef: RefObject<HTMLDivElement | null>;
43
+ measurePosition: (input: {
44
+ trigger: HTMLElement;
45
+ }) => P;
46
+ }, optional?: {
47
+ portal?: boolean | undefined;
48
+ focusInitial?: ((input: {
49
+ panel: HTMLDivElement | null;
50
+ }) => void) | undefined;
51
+ onOutOfView?: (() => void) | undefined;
52
+ }): {
53
+ position: P | null;
54
+ setPosition: import("react").Dispatch<import("react").SetStateAction<P | null>>;
55
+ updatePosition: () => void;
56
+ };
57
+ /** Open/close, outside dismissal and Escape/Tab focus handling shared by the public select variants.
58
+ * Tab scans the focusable DOM in O(n); other lifecycle transitions are O(1).
59
+ * @example useSelectPopover({ measurePosition }, { focusInitial: ({ panel }) => panel?.focus() })
60
+ */
61
+ export declare function useSelectPopover<P>(required: {
62
+ measurePosition: (input: {
63
+ trigger: HTMLElement;
64
+ }) => P;
65
+ }, optional?: {
66
+ portal?: boolean | undefined;
67
+ disabled?: boolean | undefined;
68
+ dismissWhenHidden?: boolean | undefined;
69
+ focusInitial?: ((input: {
70
+ panel: HTMLDivElement | null;
71
+ }) => void) | undefined;
72
+ onOpenChange?: ((open: boolean) => void) | undefined;
73
+ }): {
74
+ openPanel: () => void;
75
+ closePanel: ({ refocusTrigger }: {
76
+ refocusTrigger: boolean;
77
+ }) => void;
78
+ handleDismissKey: (event: KeyboardEvent<HTMLElement>) => boolean;
79
+ position: P | null;
80
+ setPosition: import("react").Dispatch<import("react").SetStateAction<P | null>>;
81
+ updatePosition: () => void;
82
+ open: boolean;
83
+ triggerRef: RefObject<HTMLButtonElement | null>;
84
+ panelRef: RefObject<HTMLDivElement | null>;
85
+ };
86
+ /** Open-key decision shared by variants; returns false for unrelated keys or an already-open/disabled trigger. */
87
+ export declare function openSelectOnKey({ event, open, openPanel }: {
88
+ event: KeyboardEvent<HTMLElement>;
89
+ open: boolean;
90
+ openPanel: () => void;
91
+ }, { disabled }?: {
92
+ disabled?: boolean | undefined;
93
+ }): boolean;
94
+ /** Shared keyboard dispatch; adapters map navigation onto their enabled-value or filtered-index contract. */
95
+ export declare function applySelectNavigationKey({ key, actions }: {
96
+ key: string;
97
+ actions: {
98
+ preventDefault: () => void;
99
+ move: (delta: 1 | -1) => void;
100
+ edge: (edge: 'first' | 'last') => void;
101
+ choose: () => void;
102
+ };
103
+ }, { spaceChooses }?: {
104
+ spaceChooses?: boolean;
105
+ }): boolean;
106
+ //# sourceMappingURL=select-popover.d.ts.map
@@ -0,0 +1,210 @@
1
+ /** Shared DOM lifecycle for the two custom select variants. Option/search policy stays in each adapter. */
2
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
3
+ // The panel is portaled to the end of document.body, its dialog, or a host container. Native Tab
4
+ // would follow that DOM position toward browser chrome; walk the trigger real DOM-order
5
+ // neighbours instead, staying inside a modal dialog when one owns the trigger.
6
+ const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
7
+ /** DOM-order focus candidates within root, excluding the floating panel's own controls. */
8
+ export function focusableInDomOrder({ exclude }, { root = document } = {}) {
9
+ return Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).filter((el) => !exclude || !exclude.contains(el));
10
+ }
11
+ /** Find the trigger's Tab neighbour, wrapping within its modal dialog to avoid inert page controls. */
12
+ export function resolveTabTarget({ panel, trigger, shiftKey }, _optional = {}) {
13
+ const dialog = trigger?.closest("dialog[open]");
14
+ // An open non-modal dialog permits focus outside it; only showModal() creates inert neighbours.
15
+ const modalDialog = dialog?.matches(":modal") ? dialog : null;
16
+ const nodes = focusableInDomOrder({ exclude: panel }, { root: modalDialog ?? document });
17
+ const triggerIndex = trigger ? nodes.indexOf(trigger) : -1;
18
+ if (triggerIndex < 0)
19
+ return null;
20
+ const nextIndex = triggerIndex + (shiftKey ? -1 : 1);
21
+ return nodes[modalDialog ? (nextIndex + nodes.length) % nodes.length : nextIndex] ?? null;
22
+ }
23
+ /** Resolve a portal inside the trigger's native dialog so its menu stays above inert page content. */
24
+ export function resolveSelectPortalContainer({ trigger }, { container } = {}) {
25
+ return container ?? trigger?.closest('dialog') ?? document.body;
26
+ }
27
+ /** Pure containment decision; outside clicks must neither select an option nor steal focus. */
28
+ export function isSelectEventInside({ target, trigger, panel }, _optional = {}) {
29
+ return Boolean(trigger?.contains(target) || panel?.contains(target));
30
+ }
31
+ /** Apply the variant's geometry port; widget selects additionally dismiss a clipped/obscured trigger. */
32
+ export function repositionSelectPopover({ trigger, measurePosition, setPosition }, { onOutOfView } = {}) {
33
+ if (onOutOfView) {
34
+ const rect = trigger.getBoundingClientRect();
35
+ const outOfViewport = rect.bottom <= 0 || rect.top >= window.innerHeight || rect.right <= 0 || rect.left >= window.innerWidth;
36
+ if (outOfViewport) {
37
+ onOutOfView();
38
+ return;
39
+ }
40
+ if (typeof document.elementFromPoint === 'function') {
41
+ const topmostAtCenter = document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
42
+ const obscured = !topmostAtCenter || !(trigger.contains(topmostAtCenter) || topmostAtCenter.contains(trigger));
43
+ if (obscured) {
44
+ onOutOfView();
45
+ return;
46
+ }
47
+ }
48
+ }
49
+ setPosition(measurePosition({ trigger }));
50
+ }
51
+ /** Measure before mounting/focusing a portal, then keep it anchored until close; listeners are disposed on close/unmount.
52
+ * Geometry is O(1); focus/visibility effects use the live DOM. The port retains each variant's existing placement bounds.
53
+ */
54
+ export function useSelectPopoverPosition(required, optional = {}) {
55
+ const { open, triggerRef, panelRef } = required, portal = optional.portal !== false;
56
+ const latest = useRef({ required, optional });
57
+ latest.current = { required, optional };
58
+ const [position, setPosition] = useState(null);
59
+ const updatePosition = useCallback(() => {
60
+ const trigger = triggerRef.current;
61
+ if (trigger)
62
+ setPosition(latest.current.required.measurePosition({ trigger }));
63
+ }, [triggerRef]);
64
+ // Two phases, deliberately in one effect rather than two: (1) `position` starts `null` on every
65
+ // open (`closePanel` resets it), so the panel/`createPortal` call below isn't rendered at all yet
66
+ // — `panelRef`/`searchInputRef` are still unattached, so focusing them here would silently no-op.
67
+ // Computing `position` schedules a state update; because this is a `useLayoutEffect`, React
68
+ // re-renders and re-runs layout effects synchronously before the browser paints, rather than
69
+ // waiting a frame. (2) On that second pass `position` is non-null, the portal now exists in the
70
+ // DOM, and the refs are populated — only now is it correct to move focus into it. Found live via
71
+ // this component's own tests: every keyboard-driven test failed until this was split out, because
72
+ // `panelRef.current`/`searchInputRef.current` were both still `null` when focus was attempted in
73
+ // the same pass that first computed the position.
74
+ // biome-ignore lint/correctness/useExhaustiveDependencies: two-phase focus effect — see comment above; changing search/focus callbacks are read through latest and intentionally do not redrive it.
75
+ useLayoutEffect(() => {
76
+ if (!open) {
77
+ setPosition(null);
78
+ return;
79
+ }
80
+ if (portal && !position) {
81
+ updatePosition();
82
+ return;
83
+ }
84
+ latest.current.optional.focusInitial?.({ panel: panelRef.current });
85
+ }, [open, position, portal, updatePosition, panelRef]);
86
+ // Keeps the panel anchored to the trigger if the surrounding page/dialog scrolls or resizes while
87
+ // it's open — `.widget-picker-body`'s own `overflow-y: auto` made this a real, live scenario
88
+ // rather than a hypothetical one. `scroll` needs the capture phase: an inner element's own scroll
89
+ // does not bubble to `window` in the bubble phase. The decision logic itself is
90
+ // `repositionSelectPopover`, above; this effect is only the DOM listener wiring.
91
+ //
92
+ // Found live in a real browser, not by this component's own test suite: scrolling a host's
93
+ // scrolling ancestor far enough that the trigger scrolls out of its own visible clip used to leave
94
+ // this effect happily recomputing a mathematically-correct `position` for it — the panel just
95
+ // ended up following the trigger to an off-screen (or behind-the-header, obscured) spot, floating
96
+ // there indefinitely, nominally still "open" with no visible way to tell.
97
+ // biome-ignore lint/correctness/useExhaustiveDependencies: scroll/resize listener wiring — see comment above; onOutOfView is read through latest; only lifecycle/ref changes re-arm it.
98
+ useEffect(() => {
99
+ if (!open || !portal)
100
+ return;
101
+ function reposition() {
102
+ const trigger = triggerRef.current;
103
+ if (!trigger)
104
+ return;
105
+ repositionSelectPopover({ trigger, measurePosition: latest.current.required.measurePosition, setPosition }, { onOutOfView: latest.current.optional.onOutOfView });
106
+ }
107
+ window.addEventListener('scroll', reposition, true);
108
+ window.addEventListener('resize', reposition);
109
+ return () => {
110
+ window.removeEventListener('scroll', reposition, true);
111
+ window.removeEventListener('resize', reposition);
112
+ };
113
+ }, [open, portal, triggerRef]);
114
+ return { position, setPosition, updatePosition };
115
+ }
116
+ /** Open/close, outside dismissal and Escape/Tab focus handling shared by the public select variants.
117
+ * Tab scans the focusable DOM in O(n); other lifecycle transitions are O(1).
118
+ * @example useSelectPopover({ measurePosition }, { focusInitial: ({ panel }) => panel?.focus() })
119
+ */
120
+ export function useSelectPopover(required, optional = {}) {
121
+ const [open, setOpen] = useState(false);
122
+ const triggerRef = useRef(null), panelRef = useRef(null);
123
+ // Stored in a ref (not a `useEffect` dependency) so an unmemoized caller
124
+ // passing a fresh function identity every render never re-fires this for
125
+ // any reason OTHER than `open` actually changing.
126
+ const latest = useRef(optional);
127
+ latest.current = optional;
128
+ useEffect(() => { latest.current.onOpenChange?.(open); }, [open]);
129
+ const placement = useSelectPopoverPosition({ open, triggerRef, panelRef, measurePosition: required.measurePosition }, {
130
+ portal: optional.portal, focusInitial: optional.focusInitial,
131
+ onOutOfView: optional.dismissWhenHidden ? () => closePanel({ refocusTrigger: false }) : undefined,
132
+ });
133
+ function openPanel() { if (!latest.current.disabled)
134
+ setOpen(true); }
135
+ function closePanel({ refocusTrigger }) {
136
+ setOpen(false);
137
+ placement.setPosition(null); // forces the effect through its "measure, then focus" two-phase sequence again on the next open, rather than focusing at a stale, pre-close position
138
+ if (refocusTrigger)
139
+ triggerRef.current?.focus();
140
+ }
141
+ useEffect(() => {
142
+ if (!open)
143
+ return;
144
+ function onOutside(event) {
145
+ if (isSelectEventInside({ target: event.target, trigger: triggerRef.current, panel: panelRef.current }))
146
+ return;
147
+ closePanel({ refocusTrigger: false });
148
+ }
149
+ document.addEventListener('mousedown', onOutside);
150
+ return () => document.removeEventListener('mousedown', onOutside);
151
+ }, [open]);
152
+ function handleDismissKey(event) {
153
+ if (!open)
154
+ return false;
155
+ if (event.key === 'Escape') {
156
+ // Must not bubble: a host dialog (`WidgetPickerDialog`) listens for Escape on `document`
157
+ // to cancel the whole modal. Without stopping propagation here, closing just this dropdown
158
+ // would also close the dialog underneath it — the regression `Select.unit.test.tsx` pins.
159
+ event.preventDefault();
160
+ event.stopPropagation();
161
+ closePanel({ refocusTrigger: true });
162
+ return true;
163
+ }
164
+ if (event.key === 'Tab') {
165
+ // See `resolveTabTarget`'s own comment: walk the trigger's real DOM-order neighbours rather
166
+ // than let native Tab handling run, since this event is bubbling from a panel portaled to
167
+ // the end of `document.body`, its dialog, or a host container, not sitting next to the
168
+ // trigger in the DOM. Dialog-local traversal avoids focusing inert page neighbours.
169
+ if (optional.portal !== false) {
170
+ const target = resolveTabTarget({ panel: panelRef.current, trigger: triggerRef.current, shiftKey: event.shiftKey });
171
+ event.preventDefault();
172
+ closePanel({ refocusTrigger: false });
173
+ target?.focus();
174
+ }
175
+ else
176
+ closePanel({ refocusTrigger: false });
177
+ return true;
178
+ }
179
+ return false;
180
+ }
181
+ return { open, triggerRef, panelRef, ...placement, openPanel, closePanel, handleDismissKey };
182
+ }
183
+ /** Open-key decision shared by variants; returns false for unrelated keys or an already-open/disabled trigger. */
184
+ export function openSelectOnKey({ event, open, openPanel }, { disabled } = {}) {
185
+ if (disabled || open || !['Enter', ' ', 'ArrowDown', 'ArrowUp'].includes(event.key))
186
+ return false;
187
+ event.preventDefault();
188
+ openPanel();
189
+ return true;
190
+ }
191
+ /** Shared keyboard dispatch; adapters map navigation onto their enabled-value or filtered-index contract. */
192
+ export function applySelectNavigationKey({ key, actions }, { spaceChooses = false } = {}) {
193
+ if (key === 'ArrowDown' || key === 'ArrowUp') {
194
+ actions.preventDefault();
195
+ actions.move(key === 'ArrowDown' ? 1 : -1);
196
+ return true;
197
+ }
198
+ if (key === 'Home' || key === 'End') {
199
+ actions.preventDefault();
200
+ actions.edge(key === 'Home' ? 'first' : 'last');
201
+ return true;
202
+ }
203
+ if (key === 'Enter' || (spaceChooses && key === ' ')) {
204
+ actions.preventDefault();
205
+ actions.choose();
206
+ return true;
207
+ }
208
+ return false;
209
+ }
210
+ //# sourceMappingURL=select-popover.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared sandbox script generation. The builders have established byte-level
3
+ * contracts: srcdoc uses modern link-handler syntax and a different focus
4
+ * marker/listener spelling. Keep those serialization differences here; each
5
+ * builder still owns its injection positions, defaults, CSP and bridges.
6
+ */
7
+ type ScriptFormat = 'sandbox' | 'srcdoc';
8
+ /**
9
+ * Script that shims `localStorage`/`sessionStorage` with an in-memory store
10
+ * when the real Storage API throws, and intercepts `<a href>` clicks so
11
+ * hash-only links scroll within the document (instead of attempting a
12
+ * sandboxed top-level navigation) and `target="_blank"` links open via
13
+ * `window.open` behind an `http:`/`https:`/`mailto:` scheme allow-list
14
+ * (instead of being silently blocked by the iframe sandbox).
15
+ */
16
+ export declare function buildStorageScript({ format }: {
17
+ format: ScriptFormat;
18
+ }, _optional?: Record<string, never>): string;
19
+ /**
20
+ * Script that suppresses `focus()` calls (on `window` or any element) that
21
+ * aren't the direct result of a real pointer/keyboard event within the
22
+ * last second, so embedded content can't steal keyboard focus away from
23
+ * the host page around it.
24
+ */
25
+ export declare function buildFocusScript({ format }: {
26
+ format: ScriptFormat;
27
+ }, _optional?: Record<string, never>): string;
28
+ export {};
29
+ //# sourceMappingURL=sandbox-scripts.d.ts.map