@jini-ai/ui 0.4.7 → 0.4.9

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 (128) hide show
  1. package/README.md +2 -0
  2. package/dist/features/admin-widgets/components/InfoTip.d.ts +23 -2
  3. package/dist/features/admin-widgets/components/InfoTip.hooks.d.ts +22 -2
  4. package/dist/features/admin-widgets/components/InfoTip.hooks.js +75 -28
  5. package/dist/features/admin-widgets/components/InfoTip.js +12 -4
  6. package/dist/features/admin-widgets/components/InfoTip.rules.d.ts +72 -0
  7. package/dist/features/admin-widgets/components/InfoTip.rules.js +68 -0
  8. package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.d.ts +36 -0
  9. package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.hooks.d.ts +58 -0
  10. package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.hooks.js +66 -0
  11. package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.js +24 -0
  12. package/dist/features/admin-widgets/components/useDebouncedHtmlPreview.hooks.d.ts +23 -0
  13. package/dist/features/admin-widgets/components/useDebouncedHtmlPreview.hooks.js +28 -0
  14. package/dist/features/admin-widgets/index.d.ts +9 -0
  15. package/dist/features/admin-widgets/index.js +5 -0
  16. package/dist/features/execution/constants.js +11 -10
  17. package/dist/features/execution/http-adapter.d.ts +3 -1
  18. package/dist/features/execution/http-adapter.js +9 -4
  19. package/dist/features/execution/index.d.ts +1 -1
  20. package/dist/features/execution/index.js +1 -1
  21. package/dist/features/execution/react/components/ByokProviderForm.d.ts +18 -1
  22. package/dist/features/execution/react/components/ByokProviderForm.js +14 -16
  23. package/dist/features/execution/react/components/LocalCliAgentCard.d.ts +5 -1
  24. package/dist/features/execution/react/components/LocalCliAgentCard.js +12 -87
  25. package/dist/features/execution/react/components/LocalCliAgentList.d.ts +4 -1
  26. package/dist/features/execution/react/components/LocalCliAgentList.js +13 -4
  27. package/dist/features/execution/react/components/LocalCliAgentSettings.d.ts +20 -0
  28. package/dist/features/execution/react/components/LocalCliAgentSettings.js +20 -0
  29. package/dist/features/execution/react/hooks/use-byok-field-validation.hooks.d.ts +24 -0
  30. package/dist/features/execution/react/hooks/use-byok-field-validation.hooks.js +27 -0
  31. package/dist/features/execution/react/hooks/useExecutionTab.js +10 -1
  32. package/dist/features/execution/react/hooks/useLocalCliAgentCard.d.ts +48 -0
  33. package/dist/features/execution/react/hooks/useLocalCliAgentCard.js +110 -0
  34. package/dist/features/execution/react/hooks/useLocalCliAgentList.d.ts +19 -0
  35. package/dist/features/execution/react/hooks/useLocalCliAgentList.js +13 -0
  36. package/dist/features/execution/rules.d.ts +13 -0
  37. package/dist/features/execution/rules.js +14 -0
  38. package/dist/features/execution/types.d.ts +2 -0
  39. package/dist/features/html-editor/canvas-embed-placeholders.d.ts +13 -1
  40. package/dist/features/html-editor/canvas-embed-placeholders.js +61 -12
  41. package/dist/features/html-editor/dom.d.ts +5 -0
  42. package/dist/features/html-editor/dom.js +10 -0
  43. package/dist/features/html-editor/editor-blocks.d.ts +71 -0
  44. package/dist/features/html-editor/editor-blocks.js +185 -0
  45. package/dist/features/html-editor/editor-serialization.d.ts +24 -0
  46. package/dist/features/html-editor/editor-serialization.js +110 -0
  47. package/dist/features/html-editor/index.d.ts +5 -0
  48. package/dist/features/html-editor/index.js +3 -0
  49. package/dist/features/html-editor/protected-elements.d.ts +64 -0
  50. package/dist/features/html-editor/protected-elements.js +189 -0
  51. package/dist/features/html-editor/react/components/InteractiveHtmlEditor.d.ts +6 -4
  52. package/dist/features/html-editor/react/components/InteractiveHtmlEditor.js +3 -3
  53. package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.d.ts +9 -1
  54. package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.js +107 -196
  55. package/dist/features/html-editor/source-splice.d.ts +23 -0
  56. package/dist/features/html-editor/source-splice.js +45 -0
  57. package/dist/features/i18n/dictionaries/settings-dialog.ar.js +4 -0
  58. package/dist/features/i18n/dictionaries/settings-dialog.bn.js +4 -0
  59. package/dist/features/i18n/dictionaries/settings-dialog.de.js +4 -0
  60. package/dist/features/i18n/dictionaries/settings-dialog.en.js +4 -0
  61. package/dist/features/i18n/dictionaries/settings-dialog.es.js +4 -0
  62. package/dist/features/i18n/dictionaries/settings-dialog.fa.js +4 -0
  63. package/dist/features/i18n/dictionaries/settings-dialog.fr.js +4 -0
  64. package/dist/features/i18n/dictionaries/settings-dialog.hi.js +4 -0
  65. package/dist/features/i18n/dictionaries/settings-dialog.hu.js +4 -0
  66. package/dist/features/i18n/dictionaries/settings-dialog.id.js +4 -0
  67. package/dist/features/i18n/dictionaries/settings-dialog.it.js +4 -0
  68. package/dist/features/i18n/dictionaries/settings-dialog.ja.js +4 -0
  69. package/dist/features/i18n/dictionaries/settings-dialog.ko.js +4 -0
  70. package/dist/features/i18n/dictionaries/settings-dialog.pl.js +4 -0
  71. package/dist/features/i18n/dictionaries/settings-dialog.pt-BR.js +4 -0
  72. package/dist/features/i18n/dictionaries/settings-dialog.ru.js +4 -0
  73. package/dist/features/i18n/dictionaries/settings-dialog.th.js +4 -0
  74. package/dist/features/i18n/dictionaries/settings-dialog.tr.js +4 -0
  75. package/dist/features/i18n/dictionaries/settings-dialog.uk.js +4 -0
  76. package/dist/features/i18n/dictionaries/settings-dialog.ur.js +4 -0
  77. package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +4 -0
  78. package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +4 -0
  79. package/dist/features/interactive-ui/interactive-ui.css +1 -1
  80. package/dist/features/interactive-ui/providers/recharts/bar-chart.js +1 -1
  81. package/dist/features/interactive-ui/providers/recharts/line-chart.js +1 -1
  82. package/dist/features/interactive-ui/providers/shadcn/button.d.ts +1 -1
  83. package/dist/features/mcp-ui/escape.js +2 -6
  84. package/dist/features/mcp-ui/surfaces/bridge.js +4 -2
  85. package/dist/features/mcp-ui/surfaces/document.d.ts +11 -4
  86. package/dist/features/mcp-ui/surfaces/document.js +25 -3
  87. package/dist/features/mcp-ui/surfaces/outcome.js +1 -1
  88. package/dist/features/media-providers/constants.d.ts +0 -7
  89. package/dist/features/media-providers/constants.js +7 -15
  90. package/dist/features/media-providers/react/components/MediaProvidersTab.js +3 -2
  91. package/dist/features/media-providers/react/hooks/useMediaProvidersTab.js +6 -3
  92. package/dist/features/memory/dependencies.d.ts +2 -0
  93. package/dist/features/memory/dependencies.js +9 -25
  94. package/dist/features/memory/index.d.ts +1 -0
  95. package/dist/features/memory/index.js +1 -0
  96. package/dist/features/memory/react/MemoryPorts.d.ts +17 -0
  97. package/dist/features/memory/react/MemoryPorts.js +15 -0
  98. package/dist/features/memory/react/hooks/useMemoryConfig.hooks.d.ts +2 -2
  99. package/dist/features/memory/react/hooks/useMemoryConfig.hooks.js +7 -7
  100. package/dist/features/memory/react/hooks/useMemoryConnectors.hooks.d.ts +2 -3
  101. package/dist/features/memory/react/hooks/useMemoryConnectors.hooks.js +4 -5
  102. package/dist/features/memory/react/hooks/useMemoryEntries.hooks.d.ts +2 -3
  103. package/dist/features/memory/react/hooks/useMemoryEntries.hooks.js +4 -5
  104. package/dist/features/memory/react/hooks/useMemoryExtractions.hooks.js +2 -2
  105. package/dist/features/panel-kit/fetch-query/adapter.react.d.ts +2 -2
  106. package/dist/features/panel-kit/fetch-query/adapter.react.js +2 -2
  107. package/dist/features/panel-kit/fetch-query/adapter.tanstack.d.ts +1 -1
  108. package/dist/features/panel-kit/fetch-query/adapter.tanstack.js +35 -13
  109. package/dist/features/panel-kit/fetch-query/binding.react.d.ts +1 -1
  110. package/dist/features/panel-kit/fetch-query/environment.browser.d.ts +1 -1
  111. package/dist/features/panel-kit/fetch-query/index.d.ts +4 -2
  112. package/dist/features/panel-kit/fetch-query/tanstack.d.ts +4 -2
  113. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.d.ts +3 -1
  114. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +9 -2
  115. package/dist/features/settings/dialog/styles/settings-dialog.css +169 -21
  116. package/dist/features/tabbed-dialog/styles/tabbed-dialog.css +91 -0
  117. package/dist/react/components/RemixIcon.d.ts +1 -41
  118. package/dist/react/components/RemixIcon.js +3 -176
  119. package/dist/react/components/RemixIcon.stylesheet.d.ts +84 -0
  120. package/dist/react/components/RemixIcon.stylesheet.js +208 -0
  121. package/dist/react/components/remixicon-font-recovery.d.ts +55 -0
  122. package/dist/react/components/remixicon-font-recovery.js +110 -0
  123. package/dist/styles/admin.css +8 -2
  124. package/package.json +12 -7
  125. package/dist/features/panel-kit/fetch-query/cache.d.ts +0 -125
  126. package/dist/features/panel-kit/fetch-query/cache.js +0 -232
  127. package/dist/features/panel-kit/fetch-query/types.d.ts +0 -93
  128. package/dist/features/panel-kit/fetch-query/types.js +0 -2
package/README.md CHANGED
@@ -467,6 +467,8 @@ extraction job at the owner's direction.
467
467
 
468
468
  ## Kernel contracts
469
469
 
470
+ Import `FetchQueryCache` and neutral query contracts from `@jini-ai/core/fetch`.
471
+ The existing UI fetch-query entries retain deprecated type re-exports for compatibility.
470
472
  Fetch-query caches accept the shared `Clock` from `@jini-ai/core/primitives` through
471
473
  `new FetchQueryCache({}, { clock })`; clocks implement `nowMs()`. Freshness remains ten seconds
472
474
  by default and idle retention five minutes. Browser HTTP defaults now use injected
@@ -1,3 +1,4 @@
1
+ import { type ReactNode } from "react";
1
2
  import { useInfoTip } from "./InfoTip.hooks.js";
2
3
  /**
3
4
  * Native title tooltips have an OS-dependent hover delay, inconsistent screen-reader exposure
@@ -5,14 +6,34 @@ import { useInfoTip } from "./InfoTip.hooks.js";
5
6
  * focus, which onBlur alone cannot do.
6
7
  * A body portal escapes overflow:hidden/scrolling ancestors: z-index cannot rescue an absolutely
7
8
  * positioned descendant from clipping. Measured viewport coordinates position it above by
8
- * preference, with the hook's headroom guard choosing below near the viewport's top.
9
+ * preference, with the hook's headroom guard choosing below near the viewport's top; near a side
10
+ * edge (or a host panel marked `data-jini-tip-bounds`) it shifts by its real width, never squeezes.
9
11
  * Keep state and DOM measurement in the injectable hook so rendering can use fixed fake state.
12
+ * Touch has no hover, so a tap (or Enter/Space) pins the tip open until a second tap, an outside
13
+ * tap, blur or Escape; a hover-opened tip still closes when the pointer leaves.
10
14
  */
11
15
  export interface InfoTipProps {
16
+ /** The explanation the bubble shows. */
12
17
  label: string;
18
+ /**
19
+ * Short accessible name for the icon ("About subagents"). Omitted, the icon is named by `label`
20
+ * itself, the original contract. Given, `label` becomes the icon's description instead, so a
21
+ * screen reader hears a short name plus the full text rather than one long name.
22
+ */
23
+ triggerLabel?: string | undefined;
24
+ /**
25
+ * Id for an always-present hidden copy of `label`, so the control the tip explains (a checkbox,
26
+ * a field) can point its own `aria-describedby` at it; the icon references it too.
27
+ */
28
+ descriptionId?: string | undefined;
29
+ /** Preferred side, or inline help that reserves space instead of covering nearby controls. */
30
+ placement?: "top" | "bottom" | "inline" | undefined;
13
31
  useTip?: typeof useInfoTip | undefined;
14
32
  agentHandle?: string | undefined;
33
+ /** What the trigger shows instead of the default ⓘ, e.g. a status icon whose meaning the bubble
34
+ * explains. Decorative: the trigger is still named by `triggerLabel`/`label`. */
35
+ icon?: ReactNode | undefined;
15
36
  }
16
37
  /** Focus/hover tooltip that portals outside clipping ancestors; Escape dismisses in place. */
17
- export declare function InfoTip({ label, useTip, agentHandle: handle }: InfoTipProps): import("react").JSX.Element;
38
+ export declare function InfoTip({ label, triggerLabel, descriptionId, placement: preferredPlacement, useTip, agentHandle: handle, icon }: InfoTipProps): import("react").JSX.Element;
18
39
  //# sourceMappingURL=InfoTip.d.ts.map
@@ -1,16 +1,36 @@
1
1
  import { type KeyboardEvent } from "react";
2
+ /** Separate the hook for injectable rendering state. Its only outside effect is synchronous DOM
3
+ * measurement, so another async dependency/port layer would provide no rejection seam to test. */
4
+ /** Hosts mark a panel (e.g. an assistant dock) with this attribute to keep tips inside it. */
5
+ export declare const TIP_BOUNDS_ATTRIBUTE = "data-jini-tip-bounds";
2
6
  export interface InfoTipController {
3
7
  open: boolean;
4
8
  placement: "above" | "below";
9
+ /** `arrowLeft` (px inside the bubble) keeps the arrow on the icon after an edge shift; optional
10
+ * for injected controllers, whose arrow stays centred. */
5
11
  coords: {
6
12
  top: number;
7
13
  left: number;
14
+ arrowLeft?: number | undefined;
8
15
  };
9
16
  iconRef: React.RefObject<HTMLSpanElement | null>;
17
+ /** Portal measurement for a requested placement; optional for injected controllers. */
18
+ bubbleRef?: React.RefObject<HTMLSpanElement | null>;
10
19
  show: () => void;
20
+ /** Closes and unpins: blur, Escape, an outside tap. */
11
21
  hide: () => void;
12
- /** Escape closes in place; blur only covers leaving the trigger with Tab/Shift+Tab. */
22
+ /** Pointer left the icon: closes only a hover-opened tip, never one the operator tapped open. */
23
+ leave: () => void;
24
+ /** Tap/click/Enter/Space: pins an open tip so it survives the pointer leaving (touch has no
25
+ * hover to keep it up), or closes a pinned one. Hover alone never pins. */
26
+ toggle: () => void;
27
+ /** Escape closes in place; Enter/Space toggle like a button; blur covers Tab/Shift+Tab. */
13
28
  handleIconKeyDown: (e: KeyboardEvent<HTMLSpanElement>) => void;
14
29
  }
15
- export declare function useInfoTip(): InfoTipController;
30
+ /** Prefer a host's placement when the measured bubble fits; default preserves automatic flipping.
31
+ * @complexity Time/space: O(1), with one icon and one bubble measurement.
32
+ */
33
+ export declare function useInfoTip(_input?: Record<string, never>, { placement: preferredPlacement }?: {
34
+ placement?: "top" | "bottom" | undefined;
35
+ }): InfoTipController;
16
36
  //# sourceMappingURL=InfoTip.hooks.d.ts.map
@@ -1,45 +1,92 @@
1
- import { useRef, useState } from "react";
1
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
2
+ import { useDismissOnOutsideOrEscape } from "../../../browser/index.js";
3
+ import { placeInfoTipBubble } from "./InfoTip.rules.js";
2
4
  /** Separate the hook for injectable rendering state. Its only outside effect is synchronous DOM
3
5
  * measurement, so another async dependency/port layer would provide no rejection seam to test. */
4
- /** Above is a preference: require this much headroom before allowing an above-opening bubble. */
5
- const ABOVE_HEADROOM_PX = 176;
6
- /** Half the bubble's 18rem CSS max-width at a 16px root font. This worst-case width keeps narrow
7
- * copy safe too. Pure icon-centering previously placed half a tip offscreen near a viewport edge. */
8
- const HALF_MAX_BUBBLE_WIDTH_PX = 144;
9
- /** Keep breathing room between the clamped bubble and either viewport edge. */
10
- const EDGE_MARGIN_PX = 8;
11
- /** Below 304px the clamp's minimum exceeds its maximum; center instead of using an inverted range. */
12
- function clampBubbleLeft(idealLeft) {
13
- const minLeft = EDGE_MARGIN_PX + HALF_MAX_BUBBLE_WIDTH_PX;
14
- const maxLeft = window.innerWidth - EDGE_MARGIN_PX - HALF_MAX_BUBBLE_WIDTH_PX;
15
- if (maxLeft < minLeft)
16
- return window.innerWidth / 2;
17
- return Math.min(Math.max(idealLeft, minLeft), maxLeft);
18
- }
19
- export function useInfoTip() {
6
+ /** Hosts mark a panel (e.g. an assistant dock) with this attribute to keep tips inside it. */
7
+ export const TIP_BOUNDS_ATTRIBUTE = "data-jini-tip-bounds";
8
+ /** Prefer a host's placement when the measured bubble fits; default preserves automatic flipping.
9
+ * @complexity Time/space: O(1), with one icon and one bubble measurement.
10
+ */
11
+ export function useInfoTip(_input = {}, { placement: preferredPlacement } = {}) {
20
12
  const [open, setOpen] = useState(false);
21
13
  const [placement, setPlacement] = useState("above");
22
14
  const [coords, setCoords] = useState({ top: 0, left: 0 });
15
+ const [pinned, setPinned] = useState(false);
23
16
  const iconRef = useRef(null);
17
+ const bubbleRef = useRef(null);
18
+ // Placement math lives in `placeInfoTipBubble`; this only gathers the measurements. Before the
19
+ // portal exists the bubble is unmeasured and the rules assume the worst-case width.
20
+ const place = () => {
21
+ const icon = iconRef.current;
22
+ const rect = icon?.getBoundingClientRect();
23
+ if (!icon || !rect)
24
+ return false;
25
+ const measured = bubbleRef.current?.getBoundingClientRect();
26
+ const boundsRect = icon.closest(`[${TIP_BOUNDS_ATTRIBUTE}]`)?.getBoundingClientRect();
27
+ const next = placeInfoTipBubble({
28
+ anchor: { left: rect.left, top: rect.top, width: rect.width, height: rect.bottom - rect.top },
29
+ bubble: measured ? { width: measured.width, height: measured.height } : null,
30
+ viewport: { width: window.innerWidth, height: window.innerHeight },
31
+ }, { bounds: boundsRect ? { left: boundsRect.left, right: boundsRect.right } : undefined, preferred: preferredPlacement });
32
+ setPlacement(next.placement);
33
+ setCoords(measured && measured.width > 0 ? { top: next.top, left: next.left, arrowLeft: next.arrowLeft } : { top: next.top, left: next.left });
34
+ return true;
35
+ };
24
36
  const show = () => {
25
- const rect = iconRef.current?.getBoundingClientRect();
26
- if (!rect)
37
+ if (place())
38
+ setOpen(true);
39
+ };
40
+ // The portal exists only after opening. Re-place before paint with its real size, so a short tip
41
+ // near an edge shifts only as far as it must and a short tip fits above its label.
42
+ useLayoutEffect(() => {
43
+ if (open)
44
+ place();
45
+ }, [open, preferredPlacement]);
46
+ const hide = () => {
47
+ setOpen(false);
48
+ setPinned(false);
49
+ };
50
+ const leave = () => {
51
+ if (!pinned)
52
+ setOpen(false);
53
+ };
54
+ const toggle = () => {
55
+ if (open && pinned) {
56
+ hide();
27
57
  return;
28
- const nextPlacement = rect.top < ABOVE_HEADROOM_PX ? "below" : "above";
29
- setPlacement(nextPlacement);
30
- setCoords({
31
- top: nextPlacement === "above" ? rect.top : rect.bottom,
32
- left: clampBubbleLeft(rect.left + rect.width / 2),
33
- });
34
- setOpen(true);
58
+ }
59
+ show();
60
+ setPinned(true);
35
61
  };
36
- const hide = () => setOpen(false);
62
+ // A tapped-open tip stays up until the operator taps elsewhere; the bubble itself is
63
+ // pointer-events:none, so "outside the icon" is every other target.
64
+ useDismissOnOutsideOrEscape({ onDismiss: hide }, { enabled: pinned, containerRef: iconRef });
65
+ // The bubble is position:fixed at measured viewport coordinates, so a scroll while it is open
66
+ // would leave it floating over the wrong content. Re-measure instead of closing: a pinned tip
67
+ // the operator is reading should follow its icon.
68
+ useEffect(() => {
69
+ if (!open)
70
+ return;
71
+ const follow = () => show();
72
+ window.addEventListener("scroll", follow, true);
73
+ window.addEventListener("resize", follow);
74
+ return () => {
75
+ window.removeEventListener("scroll", follow, true);
76
+ window.removeEventListener("resize", follow);
77
+ };
78
+ }, [open]);
37
79
  function handleIconKeyDown(e) {
38
80
  if (e.key === "Escape" && open) {
39
81
  e.stopPropagation();
40
82
  hide();
83
+ return;
84
+ }
85
+ if (e.key === "Enter" || e.key === " ") {
86
+ e.preventDefault();
87
+ toggle();
41
88
  }
42
89
  }
43
- return { open, placement, coords, iconRef, show, hide, handleIconKeyDown };
90
+ return { open, placement, coords, iconRef, bubbleRef, show, hide, leave, toggle, handleIconKeyDown };
44
91
  }
45
92
  //# sourceMappingURL=InfoTip.hooks.js.map
@@ -1,12 +1,20 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from "react";
2
3
  import { createPortal } from "react-dom";
3
4
  import { agentHandle } from "@jini-ai/agentic";
4
5
  import { useInfoTip } from "./InfoTip.hooks.js";
6
+ import { infoTipBubbleStyle } from "./InfoTip.rules.js";
5
7
  /** Focus/hover tooltip that portals outside clipping ancestors; Escape dismisses in place. */
6
- export function InfoTip({ label, useTip = useInfoTip, agentHandle: handle }) {
7
- const { open, placement, coords, iconRef, show, hide, handleIconKeyDown } = useTip();
8
- return (_jsxs("span", { className: "info-tip", children: [_jsx("span", { ref: iconRef, className: "info-tip-icon", tabIndex: 0, "aria-label": label, onMouseEnter: show, onMouseLeave: hide, onFocus: show, onBlur: hide, onKeyDown: handleIconKeyDown, ...(handle ? agentHandle({ handle: handle }, { role: "button", label }) : {}), children: "\u24D8" }), open
9
- ? createPortal(_jsx("span", { className: placement === "below" ? "info-tip-bubble info-tip-bubble-below" : "info-tip-bubble", role: "presentation", "aria-hidden": "true", style: { top: coords.top, left: coords.left }, children: label }), document.body)
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 });
10
+ const autoId = useId();
11
+ const name = triggerLabel ?? label;
12
+ // With no short name the icon is already named by the full text; describing it again would
13
+ // make a screen reader read it twice.
14
+ 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)
10
18
  : null] }));
11
19
  }
12
20
  //# sourceMappingURL=InfoTip.js.map
@@ -0,0 +1,72 @@
1
+ import type { CSSProperties } from "react";
2
+ /** Above is a preference: require this much headroom before allowing an above-opening bubble
3
+ * whose height is not measured yet. */
4
+ export declare const ABOVE_HEADROOM_PX = 176;
5
+ /** The bubble's 18rem CSS max-width at a 16px root font. Before the portal renders there is no
6
+ * real width to measure, so the first estimate assumes the worst case. */
7
+ export declare const MAX_BUBBLE_WIDTH_PX = 288;
8
+ /** Keep breathing room between the bubble and either edge of its bounds. */
9
+ export declare const EDGE_MARGIN_PX = 8;
10
+ export interface InfoTipPlacement {
11
+ placement: "above" | "below";
12
+ /** Viewport y the CSS translates from: the icon's top (above) or bottom (below). */
13
+ top: number;
14
+ /** The bubble's centre x in viewport pixels (its CSS is `translate(-50%, …)`). */
15
+ left: number;
16
+ /** The arrow's x inside the bubble, so it still points at the icon after a shift. */
17
+ arrowLeft: number;
18
+ }
19
+ /**
20
+ * Where an `InfoTip`-style bubble goes: centred under/over its icon, then shifted (never squeezed)
21
+ * just far enough to stay inside `bounds` (e.g. the assistant dock) and the viewport.
22
+ *
23
+ * Why the real measured width: the old clamp assumed every bubble was 18rem wide, so every short tip
24
+ * within 152px of the right edge got the same left — "Remove" sat ~86px away from its ✕ in the
25
+ * dock (Chrome re-check, 2026-10-11). Shared by `useInfoTip` and Tovu's dock-header tips so both
26
+ * place identically.
27
+ * @param input.anchor The icon's `getBoundingClientRect()`.
28
+ * @param input.bubble The rendered bubble's measured size; `null` before the portal exists.
29
+ * @param input.viewport `window.innerWidth` / `innerHeight`.
30
+ * @param options.bounds Horizontal limits of a containing panel; ignored when the bubble cannot fit
31
+ * in them (it then only has to fit the viewport).
32
+ * @param options.preferred A host's preferred side. Bottom flips above only when it would overflow
33
+ * the viewport's bottom and fits above; top falls back below without the measured headroom.
34
+ * @returns Coordinates for the portal plus the arrow offset. A bubble wider than the viewport's
35
+ * usable span centres in the viewport rather than using an inverted clamp range.
36
+ * @complexity O(1).
37
+ */
38
+ export declare function placeInfoTipBubble(input: {
39
+ anchor: {
40
+ left: number;
41
+ top: number;
42
+ width: number;
43
+ height: number;
44
+ };
45
+ bubble: {
46
+ width: number;
47
+ height: number;
48
+ } | null;
49
+ viewport: {
50
+ width: number;
51
+ height: number;
52
+ };
53
+ }, { bounds, preferred }?: {
54
+ bounds?: {
55
+ left: number;
56
+ right: number;
57
+ } | undefined;
58
+ preferred?: "top" | "bottom" | undefined;
59
+ }): InfoTipPlacement;
60
+ /**
61
+ * The portal's inline style: position plus, once measured, the arrow offset as a CSS variable
62
+ * (`.info-tip-bubble::after` reads `--info-tip-arrow-left`, defaulting to centred).
63
+ * @param coords The controller's coordinates.
64
+ * @returns Inline style for the bubble.
65
+ * @complexity O(1).
66
+ */
67
+ export declare function infoTipBubbleStyle(coords: {
68
+ top: number;
69
+ left: number;
70
+ arrowLeft?: number | undefined;
71
+ }): CSSProperties;
72
+ //# sourceMappingURL=InfoTip.rules.d.ts.map
@@ -0,0 +1,68 @@
1
+ /** Above is a preference: require this much headroom before allowing an above-opening bubble
2
+ * whose height is not measured yet. */
3
+ export const ABOVE_HEADROOM_PX = 176;
4
+ /** The bubble's 18rem CSS max-width at a 16px root font. Before the portal renders there is no
5
+ * real width to measure, so the first estimate assumes the worst case. */
6
+ export const MAX_BUBBLE_WIDTH_PX = 288;
7
+ /** Keep breathing room between the bubble and either edge of its bounds. */
8
+ export const EDGE_MARGIN_PX = 8;
9
+ /** The CSS gap between icon and bubble (`translate(…, 8px)`), plus the arrow. */
10
+ const GAP_PX = 16;
11
+ /** The arrow is 10px wide; keep its centre clear of the bubble's rounded corners. */
12
+ const ARROW_INSET_PX = 10;
13
+ /**
14
+ * Where an `InfoTip`-style bubble goes: centred under/over its icon, then shifted (never squeezed)
15
+ * just far enough to stay inside `bounds` (e.g. the assistant dock) and the viewport.
16
+ *
17
+ * Why the real measured width: the old clamp assumed every bubble was 18rem wide, so every short tip
18
+ * within 152px of the right edge got the same left — "Remove" sat ~86px away from its ✕ in the
19
+ * dock (Chrome re-check, 2026-10-11). Shared by `useInfoTip` and Tovu's dock-header tips so both
20
+ * place identically.
21
+ * @param input.anchor The icon's `getBoundingClientRect()`.
22
+ * @param input.bubble The rendered bubble's measured size; `null` before the portal exists.
23
+ * @param input.viewport `window.innerWidth` / `innerHeight`.
24
+ * @param options.bounds Horizontal limits of a containing panel; ignored when the bubble cannot fit
25
+ * in them (it then only has to fit the viewport).
26
+ * @param options.preferred A host's preferred side. Bottom flips above only when it would overflow
27
+ * the viewport's bottom and fits above; top falls back below without the measured headroom.
28
+ * @returns Coordinates for the portal plus the arrow offset. A bubble wider than the viewport's
29
+ * usable span centres in the viewport rather than using an inverted clamp range.
30
+ * @complexity O(1).
31
+ */
32
+ export function placeInfoTipBubble(input, { bounds, preferred } = {}) {
33
+ const { anchor, bubble, viewport } = input;
34
+ const width = bubble && bubble.width > 0 ? bubble.width : MAX_BUBBLE_WIDTH_PX;
35
+ const anchorCentre = anchor.left + anchor.width / 2;
36
+ const viewportSpan = { lo: EDGE_MARGIN_PX, hi: viewport.width - EDGE_MARGIN_PX };
37
+ const boundsSpan = bounds
38
+ ? { lo: Math.max(bounds.left, 0) + EDGE_MARGIN_PX, hi: Math.min(bounds.right, viewport.width) - EDGE_MARGIN_PX }
39
+ : null;
40
+ const span = boundsSpan && boundsSpan.hi - boundsSpan.lo >= width ? boundsSpan : viewportSpan;
41
+ const left = span.hi - span.lo < width
42
+ ? viewport.width / 2
43
+ : Math.min(Math.max(anchorCentre, span.lo + width / 2), span.hi - width / 2);
44
+ const bubbleLeftEdge = left - width / 2;
45
+ 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;
48
+ 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 };
54
+ }
55
+ /**
56
+ * The portal's inline style: position plus, once measured, the arrow offset as a CSS variable
57
+ * (`.info-tip-bubble::after` reads `--info-tip-arrow-left`, defaulting to centred).
58
+ * @param coords The controller's coordinates.
59
+ * @returns Inline style for the bubble.
60
+ * @complexity O(1).
61
+ */
62
+ export function infoTipBubbleStyle(coords) {
63
+ const style = { top: coords.top, left: coords.left };
64
+ if (coords.arrowLeft !== undefined)
65
+ style["--info-tip-arrow-left"] = `${coords.arrowLeft}px`;
66
+ return style;
67
+ }
68
+ //# sourceMappingURL=InfoTip.rules.js.map
@@ -0,0 +1,36 @@
1
+ import { type KeyboardEvent } from 'react';
2
+ import { useSuggestInput } from './SuggestInput.hooks.js';
3
+ /**
4
+ * A free-text input with a styled suggestion list under it: the browser's native `<datalist>` cannot
5
+ * be styled (Chrome on macOS draws it as a dark bubble with a pointer and adds a large ▼ to the box),
6
+ * so hosts get plain `jini-suggest-input*` classes to style instead. Unlike `Select`, the typed text
7
+ * is the value: picking a suggestion is a shortcut, not the only way to fill it.
8
+ *
9
+ * Keys: ArrowDown/ArrowUp move through matches, Enter picks the highlighted one, Escape closes the
10
+ * list. Any key the list does not consume (Enter with nothing highlighted, a comma, Escape with the
11
+ * list closed) reaches the caller's `onKeyDown`, so "Enter adds what was typed" keeps working.
12
+ * Options are picked on mousedown so the input's blur doesn't close the list before the click lands.
13
+ * Every match is listed unless `limit` is set; the host's max-height makes the list scroll, and an
14
+ * arrow-key move scrolls the highlighted row into view.
15
+ */
16
+ export interface SuggestInputProps {
17
+ value: string;
18
+ onValueChange: (value: string) => void;
19
+ suggestions: readonly string[];
20
+ /** A suggestion was chosen (click or Enter on a highlighted row). */
21
+ onPick: (value: string) => void;
22
+ onKeyDown?: ((event: KeyboardEvent<HTMLInputElement>) => void) | undefined;
23
+ placeholder?: string | undefined;
24
+ 'aria-label'?: string | undefined;
25
+ disabled?: boolean | undefined;
26
+ className?: string | undefined;
27
+ /** Cap on listed matches; omit to list them all. */
28
+ limit?: number | undefined;
29
+ agentHandle?: string | undefined;
30
+ agentLabel?: string | undefined;
31
+ useSuggest?: typeof useSuggestInput | undefined;
32
+ /** Injectable for tests; defaults to `scrollIntoView({ block: 'nearest' })`. */
33
+ scrollOptionIntoView?: ((option: Element) => void) | undefined;
34
+ }
35
+ export declare function SuggestInput({ value, onValueChange, suggestions, onPick, onKeyDown, placeholder, disabled, className, limit, agentHandle: handle, agentLabel, useSuggest, scrollOptionIntoView, ...rest }: SuggestInputProps): import("react").JSX.Element;
36
+ //# sourceMappingURL=SuggestInput.d.ts.map
@@ -0,0 +1,58 @@
1
+ import { type RefObject } from 'react';
2
+ /**
3
+ * State for {@link SuggestInput}: which suggestions match what is typed, whether the list is open, and
4
+ * which row the arrow keys have highlighted. Pure state, no DOM, so the component renders against it.
5
+ */
6
+ export interface SuggestInputController {
7
+ open: boolean;
8
+ /** Suggestions containing the typed text (case-insensitive): all of them unless the caller passed
9
+ * `limit`. The host styles the list with a max-height so a long list scrolls inside it. */
10
+ matches: readonly string[];
11
+ /** Index into `matches`, or -1 when nothing is highlighted. */
12
+ highlighted: number;
13
+ setHighlighted: (index: number) => void;
14
+ /** The row an arrow key just moved to, as a fresh object per key press so the component's effect
15
+ * re-runs even when the index repeats. Null until a key moves; mouse hover never sets it, so
16
+ * hovering a half-visible row while wheel-scrolling doesn't yank the list. */
17
+ scrollTarget: {
18
+ index: number;
19
+ } | null;
20
+ openList: () => void;
21
+ closeList: () => void;
22
+ /** Handles the list's own keys. Returns true when it consumed the key, so the caller's own
23
+ * handler (e.g. "Enter adds what was typed") must not also run. */
24
+ handleKey: (key: string) => {
25
+ consumed: boolean;
26
+ picked?: string | undefined;
27
+ };
28
+ }
29
+ /** Suggestions containing `typed` (case-insensitive), in their given order; capped at `limit` only when one is given. */
30
+ export declare function matchSuggestions({ typed, suggestions }: {
31
+ typed: string;
32
+ suggestions: readonly string[];
33
+ }, { limit }?: {
34
+ limit?: number | undefined;
35
+ }): string[];
36
+ /** @complexity O(n) over `suggestions` per render. */
37
+ export declare function useSuggestInput({ value, suggestions }: {
38
+ value: string;
39
+ suggestions: readonly string[];
40
+ }, { limit }?: {
41
+ limit?: number | undefined;
42
+ }): SuggestInputController;
43
+ /** Default scroller: `block: 'nearest'` moves the list only as far as needed. Optional call because
44
+ * non-browser DOMs (jsdom in host test suites) don't implement `scrollIntoView`. */
45
+ export declare function scrollOptionIntoView(option: Element): void;
46
+ /**
47
+ * Keeps the arrow-key row visible in a scrolling list: when `scrollTarget` changes, scrolls the
48
+ * list's child at that index into view. `scrollIntoView` is injectable for tests.
49
+ */
50
+ export declare function useScrollHighlightedIntoView({ listRef, scrollTarget }: {
51
+ listRef: RefObject<HTMLElement | null>;
52
+ scrollTarget: {
53
+ index: number;
54
+ } | null;
55
+ }, { scrollIntoView }?: {
56
+ scrollIntoView?: ((option: Element) => void) | undefined;
57
+ }): void;
58
+ //# sourceMappingURL=SuggestInput.hooks.d.ts.map
@@ -0,0 +1,66 @@
1
+ import { useEffect, useState } from 'react';
2
+ /** Suggestions containing `typed` (case-insensitive), in their given order; capped at `limit` only when one is given. */
3
+ export function matchSuggestions({ typed, suggestions }, { limit } = {}) {
4
+ const needle = typed.trim().toLowerCase();
5
+ const hits = needle ? suggestions.filter((s) => s.toLowerCase().includes(needle)) : [...suggestions];
6
+ return limit === undefined ? hits : hits.slice(0, limit);
7
+ }
8
+ /** @complexity O(n) over `suggestions` per render. */
9
+ export function useSuggestInput({ value, suggestions }, { limit } = {}) {
10
+ const [open, setOpen] = useState(false);
11
+ const [highlighted, setHighlighted] = useState(-1);
12
+ const [scrollTarget, setScrollTarget] = useState(null);
13
+ const matches = matchSuggestions({ typed: value, suggestions }, { limit });
14
+ const visible = open && matches.length > 0;
15
+ const current = highlighted < matches.length ? highlighted : -1;
16
+ return {
17
+ open: visible,
18
+ matches,
19
+ highlighted: current,
20
+ setHighlighted,
21
+ scrollTarget,
22
+ openList: () => setOpen(true),
23
+ closeList: () => { setOpen(false); setHighlighted(-1); },
24
+ handleKey: (key) => {
25
+ if (key === 'ArrowDown' || key === 'ArrowUp') {
26
+ if (matches.length === 0)
27
+ return { consumed: false };
28
+ setOpen(true);
29
+ const step = key === 'ArrowDown' ? 1 : -1;
30
+ const next = (current + step + matches.length + (current === -1 && step === -1 ? 1 : 0)) % matches.length;
31
+ setHighlighted(next);
32
+ setScrollTarget({ index: next });
33
+ return { consumed: true };
34
+ }
35
+ if (key === 'Escape' && visible) {
36
+ setOpen(false);
37
+ setHighlighted(-1);
38
+ return { consumed: true };
39
+ }
40
+ const picked = current >= 0 ? matches[current] : undefined;
41
+ if (key === 'Enter' && visible && picked !== undefined) {
42
+ setOpen(false);
43
+ setHighlighted(-1);
44
+ return { consumed: true, picked };
45
+ }
46
+ return { consumed: false };
47
+ },
48
+ };
49
+ }
50
+ /** Default scroller: `block: 'nearest'` moves the list only as far as needed. Optional call because
51
+ * non-browser DOMs (jsdom in host test suites) don't implement `scrollIntoView`. */
52
+ export function scrollOptionIntoView(option) {
53
+ option.scrollIntoView?.({ block: 'nearest' });
54
+ }
55
+ /**
56
+ * Keeps the arrow-key row visible in a scrolling list: when `scrollTarget` changes, scrolls the
57
+ * list's child at that index into view. `scrollIntoView` is injectable for tests.
58
+ */
59
+ export function useScrollHighlightedIntoView({ listRef, scrollTarget }, { scrollIntoView = scrollOptionIntoView } = {}) {
60
+ useEffect(() => {
61
+ const option = scrollTarget ? listRef.current?.children.item(scrollTarget.index) : null;
62
+ if (option)
63
+ scrollIntoView(option);
64
+ }, [scrollTarget, listRef, scrollIntoView]);
65
+ }
66
+ //# sourceMappingURL=SuggestInput.hooks.js.map
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId, useRef } from 'react';
3
+ import { agentHandle } from '@jini-ai/agentic';
4
+ import { useScrollHighlightedIntoView, useSuggestInput } from './SuggestInput.hooks.js';
5
+ export function SuggestInput({ value, onValueChange, suggestions, onPick, onKeyDown, placeholder, disabled, className, limit, agentHandle: handle, agentLabel, useSuggest = useSuggestInput, scrollOptionIntoView, ...rest }) {
6
+ const vm = useSuggest({ value, suggestions }, { limit });
7
+ const listRef = useRef(null);
8
+ useScrollHighlightedIntoView({ listRef, scrollTarget: vm.scrollTarget }, { scrollIntoView: scrollOptionIntoView });
9
+ const listId = useId();
10
+ const optionId = (index) => `${listId}-opt-${index}`;
11
+ return (_jsxs("span", { className: className ? `jini-suggest-input ${className}` : 'jini-suggest-input', children: [_jsx("input", { type: "text", role: "combobox", "aria-autocomplete": "list", "aria-expanded": vm.open, "aria-controls": listId, "aria-activedescendant": vm.open && vm.highlighted >= 0 ? optionId(vm.highlighted) : undefined, "aria-label": rest['aria-label'], placeholder: placeholder, disabled: disabled, value: value, autoComplete: "off", onChange: (event) => { onValueChange(event.target.value); vm.openList(); }, onFocus: vm.openList, onBlur: vm.closeList, onKeyDown: (event) => {
12
+ if (event.nativeEvent.isComposing)
13
+ return onKeyDown?.(event);
14
+ const result = vm.handleKey(event.key);
15
+ if (result.consumed) {
16
+ event.preventDefault();
17
+ if (result.picked !== undefined)
18
+ onPick(result.picked);
19
+ return;
20
+ }
21
+ onKeyDown?.(event);
22
+ }, ...(handle ? agentHandle({ handle }, { role: 'field', label: agentLabel ?? rest['aria-label'] ?? 'Text with suggestions' }) : {}) }), vm.open ? (_jsx("ul", { ref: listRef, id: listId, role: "listbox", className: "jini-suggest-input__list", children: vm.matches.map((match, index) => (_jsx("li", { id: optionId(index), role: "option", "aria-selected": index === vm.highlighted, className: index === vm.highlighted ? 'jini-suggest-input__option is-active' : 'jini-suggest-input__option', onMouseEnter: () => vm.setHighlighted(index), onMouseDown: (event) => { event.preventDefault(); vm.closeList(); onPick(match); }, children: match }, match))) })) : (_jsx("ul", { id: listId, role: "listbox", hidden: true }))] }));
23
+ }
24
+ //# sourceMappingURL=SuggestInput.js.map
@@ -0,0 +1,23 @@
1
+ export type HtmlPreviewPort = (input: {
2
+ source: string;
3
+ }, options: {
4
+ signal: AbortSignal;
5
+ }) => Promise<string>;
6
+ export interface HtmlPreviewState {
7
+ document: string;
8
+ loading: boolean;
9
+ error: string | null;
10
+ }
11
+ /** Debounces an unsaved source through the host's renderer; cleanup cancels timers and ignores
12
+ * responses even when the transport cannot abort. No rendering policy belongs to this hook.
13
+ * @returns The last successful document and one-line request state.
14
+ * @complexity O(1) per source change, excluding the injected renderer's cost.
15
+ */
16
+ export declare function useDebouncedHtmlPreview({ source, render }: {
17
+ source: string;
18
+ render: HtmlPreviewPort;
19
+ }, { revision, delayMs }?: {
20
+ revision?: string;
21
+ delayMs?: number;
22
+ }): HtmlPreviewState;
23
+ //# sourceMappingURL=useDebouncedHtmlPreview.hooks.d.ts.map
@@ -0,0 +1,28 @@
1
+ import { useEffect, useState } from 'react';
2
+ /** Debounces an unsaved source through the host's renderer; cleanup cancels timers and ignores
3
+ * responses even when the transport cannot abort. No rendering policy belongs to this hook.
4
+ * @returns The last successful document and one-line request state.
5
+ * @complexity O(1) per source change, excluding the injected renderer's cost.
6
+ */
7
+ export function useDebouncedHtmlPreview({ source, render }, { revision = '', delayMs = 400 } = {}) {
8
+ // `settled` names the source+revision the stored result belongs to, so `loading` and a stale
9
+ // error are derived during render. Setting state from the effect on every keystroke left a
10
+ // pending update after each commit, and ~50 fast keystrokes then tripped React's nested-update
11
+ // limit, which dropped that keystroke from the host's controlled input.
12
+ const key = JSON.stringify([source, revision]);
13
+ const [state, setState] = useState({ document: '', error: null, settled: null });
14
+ useEffect(() => {
15
+ let current = true;
16
+ const controller = new AbortController();
17
+ const timer = setTimeout(() => {
18
+ // Promise.resolve also maps a synchronously throwing host adapter to the error state.
19
+ void Promise.resolve().then(() => render({ source }, { signal: controller.signal })).then(document => { if (current)
20
+ setState({ document, error: null, settled: key }); }, () => { if (current)
21
+ setState(previous => ({ ...previous, error: 'Preview unavailable.', settled: key })); });
22
+ }, delayMs);
23
+ return () => { current = false; clearTimeout(timer); controller.abort(); };
24
+ }, [key, source, render, delayMs]);
25
+ const settled = state.settled === key;
26
+ return { document: state.document, loading: !settled, error: settled ? state.error : null };
27
+ }
28
+ //# sourceMappingURL=useDebouncedHtmlPreview.hooks.js.map