@jini-ai/ui 0.6.12 → 0.6.15-rc.2

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 (54) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +14 -0
  3. package/dist/features/admin-widgets/components/InfoTip.d.ts +3 -2
  4. package/dist/features/admin-widgets/components/InfoTip.hooks.d.ts +17 -2
  5. package/dist/features/admin-widgets/components/InfoTip.hooks.js +64 -14
  6. package/dist/features/admin-widgets/components/InfoTip.js +2 -2
  7. package/dist/features/admin-widgets/index.d.ts +1 -1
  8. package/dist/features/admin-widgets/index.js +1 -1
  9. package/dist/features/execution/react/components/AgentCliEnvFields.js +27 -8
  10. package/dist/features/execution/react/components/ByokProviderForm.js +16 -19
  11. package/dist/features/interactive-ui/interactive-ui.css +1 -1
  12. package/dist/features/interactive-ui/providers/shadcn/button.d.ts +8 -6
  13. package/dist/features/interactive-ui/providers/shadcn/button.js +5 -6
  14. package/dist/features/interactive-ui/providers/shadcn/card.d.ts +3 -3
  15. package/dist/features/interactive-ui/providers/shadcn/card.js +3 -4
  16. package/dist/features/interactive-ui/providers/shadcn/select.d.ts +3 -5
  17. package/dist/features/interactive-ui/providers/shadcn/select.js +3 -7
  18. package/dist/features/interactive-ui/providers/shadcn/table.d.ts +4 -5
  19. package/dist/features/interactive-ui/providers/shadcn/table.js +4 -9
  20. package/dist/features/mcp-ui/surfaces/document.d.ts +1 -1
  21. package/dist/features/mcp-ui/surfaces/document.js +28 -0
  22. package/dist/features/mcp-ui/surfaces/fields.d.ts +28 -1
  23. package/dist/features/mcp-ui/surfaces/fields.js +5 -1
  24. package/dist/features/mcp-ui/surfaces/form.js +86 -16
  25. package/dist/features/mcp-ui/surfaces/text-input.d.ts +3 -3
  26. package/dist/features/mcp-ui/surfaces/text-input.js +26 -11
  27. package/dist/features/media-providers/catalog.d.ts +3 -0
  28. package/dist/features/media-providers/catalog.js +3 -0
  29. package/dist/features/media-providers/react/components/MediaProviderCard.js +5 -4
  30. package/dist/features/media-providers/react/components/media-providers-tab-labels.js +2 -1
  31. package/dist/features/panel-kit/fetch-query/adapter.react.d.ts +10 -5
  32. package/dist/features/panel-kit/fetch-query/adapter.react.js +9 -3
  33. package/dist/features/panel-kit/fetch-query/binding.react.d.ts +4 -0
  34. package/dist/features/panel-kit/fetch-query/binding.react.js +29 -10
  35. package/dist/features/panel-kit/fetch-query/index.d.ts +1 -1
  36. package/dist/features/panel-kit/fetch-query/index.js +1 -1
  37. package/dist/features/panel-kit/fetch-query/tanstack.d.ts +1 -1
  38. package/dist/features/panel-kit/fetch-query/tanstack.js +1 -1
  39. package/dist/features/privacy/react/components/PrivacyTab.d.ts +1 -3
  40. package/dist/features/privacy/react/components/PrivacyTab.js +5 -8
  41. package/dist/features/resource-dashboard/react/components/ResourceBoardView.js +3 -2
  42. package/dist/features/resource-dashboard/react/components/ResourceBoardViewParts.d.ts +0 -2
  43. package/dist/features/resource-dashboard/react/components/ResourceBoardViewParts.js +0 -6
  44. package/dist/features/settings/dialog/styles/settings-dialog.css +5 -70
  45. package/dist/features/source-config-list/react/components/SourceConfigField.js +27 -26
  46. package/dist/index.d.ts +1 -0
  47. package/dist/index.js +1 -0
  48. package/dist/react/components/SecretInput.d.ts +47 -0
  49. package/dist/react/components/SecretInput.hooks.d.ts +26 -0
  50. package/dist/react/components/SecretInput.hooks.js +25 -0
  51. package/dist/react/components/SecretInput.js +30 -0
  52. package/dist/styles/lightbox.css +1 -1
  53. package/dist/styles/variables.css +1 -1
  54. package/package.json +9 -9
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Ported verbatim from shadcn's `new-york` style registry (`ui.shadcn.com/r/styles/new-york/card.json`,
2
+ * Ported from shadcn's `new-york` style registry (`ui.shadcn.com/r/styles/new-york/card.json`,
3
3
  * 2026-08-18). Only the `cn` import path changed, same as `table.tsx`.
4
+ * The unused private footer primitive is omitted.
4
5
  */
5
6
  import * as React from 'react';
6
7
  declare const Card: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
@@ -8,6 +9,5 @@ declare const CardHeader: React.ForwardRefExoticComponent<React.HTMLAttributes<H
8
9
  declare const CardTitle: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
9
10
  declare const CardDescription: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
10
11
  declare const CardContent: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
11
- declare const CardFooter: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & React.RefAttributes<HTMLDivElement>>;
12
- export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
12
+ export { Card, CardHeader, CardTitle, CardDescription, CardContent };
13
13
  //# sourceMappingURL=card.d.ts.map
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /**
3
- * Ported verbatim from shadcn's `new-york` style registry (`ui.shadcn.com/r/styles/new-york/card.json`,
3
+ * Ported from shadcn's `new-york` style registry (`ui.shadcn.com/r/styles/new-york/card.json`,
4
4
  * 2026-08-18). Only the `cn` import path changed, same as `table.tsx`.
5
+ * The unused private footer primitive is omitted.
5
6
  */
6
7
  import * as React from 'react';
7
8
  import { cn } from './lib/utils.js';
@@ -15,7 +16,5 @@ const CardDescription = React.forwardRef(({ className, ...props }, ref) => (_jsx
15
16
  CardDescription.displayName = 'CardDescription';
16
17
  const CardContent = React.forwardRef(({ className, ...props }, ref) => (_jsx("div", { ref: ref, className: cn('p-6 pt-0', className), ...props })));
17
18
  CardContent.displayName = 'CardContent';
18
- const CardFooter = React.forwardRef(({ className, ...props }, ref) => (_jsx("div", { ref: ref, className: cn('flex items-center p-6 pt-0', className), ...props })));
19
- CardFooter.displayName = 'CardFooter';
20
- export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
19
+ export { Card, CardHeader, CardTitle, CardDescription, CardContent };
21
20
  //# sourceMappingURL=card.js.map
@@ -1,19 +1,17 @@
1
1
  /**
2
- * Ported verbatim from shadcn's `new-york` style registry (`ui.shadcn.com/r/styles/new-york/select.json`,
2
+ * Ported from shadcn's `new-york` style registry (`ui.shadcn.com/r/styles/new-york/select.json`,
3
3
  * 2026-08-18). Only the `cn` import path changed, same as `table.tsx`; every class name and
4
4
  * `data-*` attribute below is unmodified CLI output.
5
+ * Unused private group, label, and separator primitives are omitted.
5
6
  */
6
7
  import * as React from 'react';
7
8
  import * as SelectPrimitive from '@radix-ui/react-select';
8
9
  declare const Select: React.FC<SelectPrimitive.SelectProps>;
9
- declare const SelectGroup: React.ForwardRefExoticComponent<SelectPrimitive.SelectGroupProps & React.RefAttributes<HTMLDivElement>>;
10
10
  declare const SelectValue: React.ForwardRefExoticComponent<SelectPrimitive.SelectValueProps & React.RefAttributes<HTMLSpanElement>>;
11
11
  declare const SelectTrigger: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectTriggerProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
12
12
  declare const SelectScrollUpButton: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectScrollUpButtonProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
13
13
  declare const SelectScrollDownButton: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectScrollDownButtonProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
14
14
  declare const SelectContent: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectContentProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
15
- declare const SelectLabel: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectLabelProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
16
15
  declare const SelectItem: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectItemProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
17
- declare const SelectSeparator: React.ForwardRefExoticComponent<Omit<SelectPrimitive.SelectSeparatorProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
18
- export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, SelectScrollUpButton, SelectScrollDownButton, };
16
+ export { Select, SelectValue, SelectTrigger, SelectContent, SelectItem, SelectScrollUpButton, SelectScrollDownButton, };
19
17
  //# sourceMappingURL=select.d.ts.map
@@ -1,15 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
- * Ported verbatim from shadcn's `new-york` style registry (`ui.shadcn.com/r/styles/new-york/select.json`,
3
+ * Ported from shadcn's `new-york` style registry (`ui.shadcn.com/r/styles/new-york/select.json`,
4
4
  * 2026-08-18). Only the `cn` import path changed, same as `table.tsx`; every class name and
5
5
  * `data-*` attribute below is unmodified CLI output.
6
+ * Unused private group, label, and separator primitives are omitted.
6
7
  */
7
8
  import * as React from 'react';
8
9
  import * as SelectPrimitive from '@radix-ui/react-select';
9
10
  import { Check, ChevronDown, ChevronUp } from './icons.js';
10
11
  import { cn } from './lib/utils.js';
11
12
  const Select = SelectPrimitive.Root;
12
- const SelectGroup = SelectPrimitive.Group;
13
13
  const SelectValue = SelectPrimitive.Value;
14
14
  const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (_jsxs(SelectPrimitive.Trigger, { ref: ref, className: cn('flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1', className), ...props, children: [children, _jsx(SelectPrimitive.Icon, { asChild: true, children: _jsx(ChevronDown, { className: "h-4 w-4 opacity-50" }) })] })));
15
15
  SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
@@ -20,11 +20,7 @@ SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayNam
20
20
  const SelectContent = React.forwardRef(({ className, children, position = 'popper', ...props }, ref) => (_jsx(SelectPrimitive.Portal, { children: _jsxs(SelectPrimitive.Content, { ref: ref, className: cn('relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]', position === 'popper' &&
21
21
  'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', className), position: position, ...props, children: [_jsx(SelectScrollUpButton, {}), _jsx(SelectPrimitive.Viewport, { className: cn('p-1', position === 'popper' && 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'), children: children }), _jsx(SelectScrollDownButton, {})] }) })));
22
22
  SelectContent.displayName = SelectPrimitive.Content.displayName;
23
- const SelectLabel = React.forwardRef(({ className, ...props }, ref) => _jsx(SelectPrimitive.Label, { ref: ref, className: cn('px-2 py-1.5 text-sm font-semibold', className), ...props }));
24
- SelectLabel.displayName = SelectPrimitive.Label.displayName;
25
23
  const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (_jsxs(SelectPrimitive.Item, { ref: ref, className: cn('relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50', className), ...props, children: [_jsx("span", { className: "absolute right-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(SelectPrimitive.ItemIndicator, { children: _jsx(Check, { className: "h-4 w-4" }) }) }), _jsx(SelectPrimitive.ItemText, { children: children })] })));
26
24
  SelectItem.displayName = SelectPrimitive.Item.displayName;
27
- const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => _jsx(SelectPrimitive.Separator, { ref: ref, className: cn('-mx-1 my-1 h-px bg-muted', className), ...props }));
28
- SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
29
- export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator, SelectScrollUpButton, SelectScrollDownButton, };
25
+ export { Select, SelectValue, SelectTrigger, SelectContent, SelectItem, SelectScrollUpButton, SelectScrollDownButton, };
30
26
  //# sourceMappingURL=select.js.map
@@ -1,17 +1,16 @@
1
1
  /**
2
- * Ported verbatim from `npx shadcn@latest add table` (real CLI run, 2026-08-08, against a
2
+ * Ported from `npx shadcn@latest add table` (real CLI run, 2026-08-08, against a
3
3
  * scratch Vite+Tailwind v4 project — see source-map.md). Only the `cn` import path changed
4
4
  * (`@/lib/utils` -> `./lib/utils.js`, this monorepo's relative-import convention); every
5
- * class name, `data-slot` attribute, and prop shape below is unmodified CLI output.
5
+ * retained class name, `data-slot` attribute, and prop shape below is unmodified CLI output.
6
+ * Unused private footer and caption primitives are omitted.
6
7
  */
7
8
  import * as React from 'react';
8
9
  declare function Table({ className, ...props }: React.ComponentProps<'table'>): React.JSX.Element;
9
10
  declare function TableHeader({ className, ...props }: React.ComponentProps<'thead'>): React.JSX.Element;
10
11
  declare function TableBody({ className, ...props }: React.ComponentProps<'tbody'>): React.JSX.Element;
11
- declare function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>): React.JSX.Element;
12
12
  declare function TableRow({ className, ...props }: React.ComponentProps<'tr'>): React.JSX.Element;
13
13
  declare function TableHead({ className, ...props }: React.ComponentProps<'th'>): React.JSX.Element;
14
14
  declare function TableCell({ className, ...props }: React.ComponentProps<'td'>): React.JSX.Element;
15
- declare function TableCaption({ className, ...props }: React.ComponentProps<'caption'>): React.JSX.Element;
16
- export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
15
+ export { Table, TableHeader, TableBody, TableHead, TableRow, TableCell };
17
16
  //# sourceMappingURL=table.d.ts.map
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  /**
3
- * Ported verbatim from `npx shadcn@latest add table` (real CLI run, 2026-08-08, against a
3
+ * Ported from `npx shadcn@latest add table` (real CLI run, 2026-08-08, against a
4
4
  * scratch Vite+Tailwind v4 project — see source-map.md). Only the `cn` import path changed
5
5
  * (`@/lib/utils` -> `./lib/utils.js`, this monorepo's relative-import convention); every
6
- * class name, `data-slot` attribute, and prop shape below is unmodified CLI output.
6
+ * retained class name, `data-slot` attribute, and prop shape below is unmodified CLI output.
7
+ * Unused private footer and caption primitives are omitted.
7
8
  */
8
9
  import * as React from 'react';
9
10
  import { cn } from './lib/utils.js';
@@ -16,9 +17,6 @@ function TableHeader({ className, ...props }) {
16
17
  function TableBody({ className, ...props }) {
17
18
  return _jsx("tbody", { "data-slot": "table-body", className: cn('[&_tr:last-child]:border-0', className), ...props });
18
19
  }
19
- function TableFooter({ className, ...props }) {
20
- return (_jsx("tfoot", { "data-slot": "table-footer", className: cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className), ...props }));
21
- }
22
20
  function TableRow({ className, ...props }) {
23
21
  return (_jsx("tr", { "data-slot": "table-row", className: cn('border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted', className), ...props }));
24
22
  }
@@ -28,8 +26,5 @@ function TableHead({ className, ...props }) {
28
26
  function TableCell({ className, ...props }) {
29
27
  return (_jsx("td", { "data-slot": "table-cell", className: cn('p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0', className), ...props }));
30
28
  }
31
- function TableCaption({ className, ...props }) {
32
- return _jsx("caption", { "data-slot": "table-caption", className: cn('mt-4 text-sm text-muted-foreground', className), ...props });
33
- }
34
- export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
29
+ export { Table, TableHeader, TableBody, TableHead, TableRow, TableCell };
35
30
  //# sourceMappingURL=table.js.map
@@ -8,7 +8,7 @@ import { type SurfaceTokenName } from './tokens.js';
8
8
  * be a color no caller could reskin. `var(--x, fallback)` appears only where a value is genuinely
9
9
  * optional.
10
10
  */
11
- export declare const SURFACE_BASE_CSS = "*, *::before, *::after { box-sizing: border-box; }\nhtml, body { margin: 0; padding: 0; }\nbody {\n /* Transparent, not var(--jini-mcpui-bg): this document has no page of its own to paint. It is an\n allow-scripts-only sandboxed iframe embedded directly in a host's chat transcript, and html's\n background is left at its (also transparent) initial value, so the host's own pane shows\n straight through to .mcpui-surface below rather than through a second, unrelated rectangle of\n fill color sitting between the two. --jini-mcpui-bg stays declared for a caller styling\n something else via extraCss -- nothing in this stylesheet reads it anymore. */\n background: transparent;\n color: var(--jini-mcpui-text);\n font-family: var(--jini-mcpui-font);\n font-size: 14px;\n line-height: 1.5;\n padding: 16px;\n -webkit-font-smoothing: antialiased;\n}\n.mcpui-surface {\n background: linear-gradient(135deg,\n color-mix(in srgb, var(--jini-mcpui-accent) 5%, var(--jini-mcpui-panel)) 0%,\n var(--jini-mcpui-panel) 60%);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 18%, var(--jini-mcpui-border));\n border-radius: var(--jini-mcpui-radius-md);\n box-shadow: var(--jini-mcpui-surface-shadow);\n padding: 18px 20px 16px;\n max-width: 640px;\n margin: 0 auto;\n}\n.mcpui-title {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: -0.01em;\n color: var(--jini-mcpui-text-strong);\n}\n.mcpui-description { margin: 6px 0 0; color: var(--jini-mcpui-text-muted); }\n/* fit-content(40%), not max-content: one long label used to take the whole row and squeeze every\n value to one character per line. Labels wrap at 40% instead. */\n.mcpui-details {\n display: grid;\n grid-template-columns: fit-content(40%) minmax(0, 1fr);\n gap: 4px 14px;\n margin: 14px 0 0;\n}\n.mcpui-details dt { color: var(--jini-mcpui-text-soft); font-weight: 500; overflow-wrap: anywhere; }\n.mcpui-details dd { margin: 0; overflow-wrap: anywhere; color: var(--jini-mcpui-text); }\n.mcpui-details-outcome { display: flex; flex-direction: column; gap: 8px; }\n.mcpui-outcome-row { display: flex; align-items: flex-start; gap: 14px; }\n.mcpui-outcome-row dt { flex: 1; min-width: 0; overflow-wrap: anywhere; }\n.mcpui-outcome-row dt .mcpui-hint { display: block; font-weight: 400; }\n.mcpui-outcome-row dd { flex: 1; min-width: 0; }\n/* Capped so a long status cannot squeeze the label back to one character per line; the full text\n stays in the cell's title. */\n.mcpui-outcome-row dd.mcpui-detail-status {\n flex: 0 1 auto; max-width: 60%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n}\n/* Frames up to 480px: stack each label above its value so the value gets the full width.\n Outcome lists keep their own row layout. */\n@media (max-width: 480px) {\n .mcpui-details:not(.mcpui-details-outcome) { grid-template-columns: minmax(0, 1fr); row-gap: 0; }\n .mcpui-details:not(.mcpui-details-outcome) dd { margin-bottom: 8px; }\n}\n/* The iframe has 16px body padding on each side: 352px viewport = 320px card. Stacked, the value\n has the full width, so it wraps between words instead of truncating. */\n@media (max-width: 352px) {\n .mcpui-outcome-row { flex-direction: column; gap: 2px; }\n .mcpui-outcome-row dd.mcpui-detail-status { max-width: 100%; white-space: normal; overflow-wrap: normal; }\n}\n.mcpui-code {\n margin: 0;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n max-height: 16em;\n overflow: auto;\n font-family: var(--jini-mcpui-font-mono);\n font-size: 12.5px;\n line-height: 1.45;\n padding: 6px 8px;\n border: 1px solid var(--jini-mcpui-border);\n border-radius: var(--jini-mcpui-radius-sm);\n background: var(--jini-mcpui-bg);\n}\n.mcpui-warning {\n margin: 14px 0 0;\n padding: 9px 11px;\n border-radius: var(--jini-mcpui-radius-md);\n border: 1px solid var(--jini-mcpui-danger);\n background: var(--jini-mcpui-danger-tint);\n color: var(--jini-mcpui-text-strong);\n}\n.mcpui-fields { display: flex; flex-direction: column; gap: 12px; margin: 16px 0 0; border: 0; padding: 0; }\n.mcpui-field { display: flex; flex-direction: column; gap: 4px; }\n.mcpui-field-inline { flex-direction: row; align-items: flex-start; gap: 8px; }\n.mcpui-label { font-weight: 500; color: var(--jini-mcpui-text-strong); }\n.mcpui-required { color: var(--jini-mcpui-accent); margin-left: 2px; }\n.mcpui-hint { color: var(--jini-mcpui-text-soft); font-size: 13px; }\n.mcpui-input, .mcpui-select, .mcpui-textarea {\n font: inherit;\n color: var(--jini-mcpui-text);\n background: var(--jini-mcpui-panel);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 14%, var(--jini-mcpui-border-strong));\n border-radius: var(--jini-mcpui-radius-sm);\n padding: 7px 9px;\n width: 100%;\n min-width: 0;\n}\n.mcpui-textarea { resize: vertical; min-height: 72px; font-family: var(--jini-mcpui-font); }\n.mcpui-checkbox { margin: 3px 0 0; accent-color: var(--jini-mcpui-accent); }\n.mcpui-choice-group { border: 0; padding: 0; margin: 0; min-width: 0; }\n.mcpui-choice { display: flex; flex-direction: row; align-items: flex-start; gap: 8px; margin: 6px 0 0; }\n.mcpui-choice-label { color: var(--jini-mcpui-text); font-weight: 400; }\n.mcpui-input:focus-visible, .mcpui-select:focus-visible, .mcpui-textarea:focus-visible,\n.mcpui-checkbox:focus-visible, .mcpui-button:focus-visible {\n outline: 2px solid var(--jini-mcpui-accent-ring);\n outline-offset: 1px;\n border-color: var(--jini-mcpui-accent);\n}\n.mcpui-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 18px 0 0; }\n.mcpui-button {\n font: inherit;\n font-weight: 500;\n padding: 7px 15px;\n border-radius: var(--jini-mcpui-radius-pill);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 14%, var(--jini-mcpui-border-strong));\n background: var(--jini-mcpui-panel);\n color: var(--jini-mcpui-text);\n cursor: pointer;\n}\n.mcpui-button:hover:not([disabled]) { background: var(--jini-mcpui-accent-tint); }\n.mcpui-button-primary {\n background: var(--jini-mcpui-accent);\n border-color: var(--jini-mcpui-accent);\n color: var(--jini-mcpui-accent-ink);\n}\n.mcpui-button-primary:hover:not([disabled]) { background: var(--jini-mcpui-accent-hover); border-color: var(--jini-mcpui-accent-hover); }\n.mcpui-button-danger {\n background: var(--jini-mcpui-danger);\n border-color: var(--jini-mcpui-danger);\n color: var(--jini-mcpui-panel);\n}\n.mcpui-button-danger:hover:not([disabled]) { background: var(--jini-mcpui-danger-hover); border-color: var(--jini-mcpui-danger-hover); }\n.mcpui-button[disabled] { opacity: 0.55; cursor: default; }\n.mcpui-status { margin: 12px 0 0; min-height: 1.4em; color: var(--jini-mcpui-text-muted); }\n.mcpui-status[data-state=\"failed\"], .mcpui-status[data-state=\"invalid\"], .mcpui-status[data-state=\"expired\"] { color: var(--jini-mcpui-danger); }\n.mcpui-status[data-state=\"done\"] { color: var(--jini-mcpui-text-strong); }\n/* Neither a plain success nor a failure \u2014 e.g. an outcome surface reporting \"uploaded, not yet\n reachable\" (outcome.ts's own state: partial). Distinct from both --jini-mcpui-danger (would\n wrongly read as \"this failed\") and --jini-mcpui-text-strong (would wrongly read as \"this is\n fully done\"); the accent token reads as \"worth a second look\" without asserting either. */\n.mcpui-status[data-state=\"partial\"] { color: var(--jini-mcpui-accent); }\n.mcpui-code { font-family: var(--jini-mcpui-font-mono); font-size: 13px; color: var(--jini-mcpui-text-faint); }";
11
+ export declare const SURFACE_BASE_CSS = "*, *::before, *::after { box-sizing: border-box; }\nhtml, body { margin: 0; padding: 0; }\nbody {\n /* Transparent, not var(--jini-mcpui-bg): this document has no page of its own to paint. It is an\n allow-scripts-only sandboxed iframe embedded directly in a host's chat transcript, and html's\n background is left at its (also transparent) initial value, so the host's own pane shows\n straight through to .mcpui-surface below rather than through a second, unrelated rectangle of\n fill color sitting between the two. --jini-mcpui-bg stays declared for a caller styling\n something else via extraCss -- nothing in this stylesheet reads it anymore. */\n background: transparent;\n color: var(--jini-mcpui-text);\n font-family: var(--jini-mcpui-font);\n font-size: 14px;\n line-height: 1.5;\n padding: 16px;\n -webkit-font-smoothing: antialiased;\n}\n.mcpui-surface {\n background: linear-gradient(135deg,\n color-mix(in srgb, var(--jini-mcpui-accent) 5%, var(--jini-mcpui-panel)) 0%,\n var(--jini-mcpui-panel) 60%);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 18%, var(--jini-mcpui-border));\n border-radius: var(--jini-mcpui-radius-md);\n box-shadow: var(--jini-mcpui-surface-shadow);\n padding: 18px 20px 16px;\n max-width: 640px;\n margin: 0 auto;\n}\n.mcpui-title {\n margin: 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: -0.01em;\n color: var(--jini-mcpui-text-strong);\n}\n.mcpui-description { margin: 6px 0 0; color: var(--jini-mcpui-text-muted); }\n/* fit-content(40%), not max-content: one long label used to take the whole row and squeeze every\n value to one character per line. Labels wrap at 40% instead. */\n.mcpui-details {\n display: grid;\n grid-template-columns: fit-content(40%) minmax(0, 1fr);\n gap: 4px 14px;\n margin: 14px 0 0;\n}\n.mcpui-details dt { color: var(--jini-mcpui-text-soft); font-weight: 500; overflow-wrap: anywhere; }\n.mcpui-details dd { margin: 0; overflow-wrap: anywhere; color: var(--jini-mcpui-text); }\n.mcpui-details-outcome { display: flex; flex-direction: column; gap: 8px; }\n.mcpui-outcome-row { display: flex; align-items: flex-start; gap: 14px; }\n.mcpui-outcome-row dt { flex: 1; min-width: 0; overflow-wrap: anywhere; }\n.mcpui-outcome-row dt .mcpui-hint { display: block; font-weight: 400; }\n.mcpui-outcome-row dd { flex: 1; min-width: 0; }\n/* Capped so a long status cannot squeeze the label back to one character per line; the full text\n stays in the cell's title. */\n.mcpui-outcome-row dd.mcpui-detail-status {\n flex: 0 1 auto; max-width: 60%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n}\n/* Frames up to 480px: stack each label above its value so the value gets the full width.\n Outcome lists keep their own row layout. */\n@media (max-width: 480px) {\n .mcpui-details:not(.mcpui-details-outcome) { grid-template-columns: minmax(0, 1fr); row-gap: 0; }\n .mcpui-details:not(.mcpui-details-outcome) dd { margin-bottom: 8px; }\n}\n/* The iframe has 16px body padding on each side: 352px viewport = 320px card. Stacked, the value\n has the full width, so it wraps between words instead of truncating. */\n@media (max-width: 352px) {\n .mcpui-outcome-row { flex-direction: column; gap: 2px; }\n .mcpui-outcome-row dd.mcpui-detail-status { max-width: 100%; white-space: normal; overflow-wrap: normal; }\n}\n.mcpui-code {\n margin: 0;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n max-height: 16em;\n overflow: auto;\n font-family: var(--jini-mcpui-font-mono);\n font-size: 12.5px;\n line-height: 1.45;\n padding: 6px 8px;\n border: 1px solid var(--jini-mcpui-border);\n border-radius: var(--jini-mcpui-radius-sm);\n background: var(--jini-mcpui-bg);\n}\n.mcpui-warning {\n margin: 14px 0 0;\n padding: 9px 11px;\n border-radius: var(--jini-mcpui-radius-md);\n border: 1px solid var(--jini-mcpui-danger);\n background: var(--jini-mcpui-danger-tint);\n color: var(--jini-mcpui-text-strong);\n}\n.mcpui-fields { display: flex; flex-direction: column; gap: 12px; margin: 16px 0 0; border: 0; padding: 0; }\n.mcpui-field { display: flex; flex-direction: column; gap: 4px; }\n.mcpui-field-inline { flex-direction: row; align-items: flex-start; gap: 8px; }\n.mcpui-label { font-weight: 500; color: var(--jini-mcpui-text-strong); }\n.mcpui-required { color: var(--jini-mcpui-accent); margin-left: 2px; }\n.mcpui-hint { color: var(--jini-mcpui-text-soft); font-size: 13px; }\n.mcpui-input, .mcpui-select, .mcpui-textarea {\n font: inherit;\n color: var(--jini-mcpui-text);\n background: var(--jini-mcpui-panel);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 14%, var(--jini-mcpui-border-strong));\n border-radius: var(--jini-mcpui-radius-sm);\n padding: 7px 9px;\n width: 100%;\n min-width: 0;\n}\n.mcpui-textarea { resize: vertical; min-height: 72px; font-family: var(--jini-mcpui-font); }\n/* A secret control's eye toggle sits inside the field's right edge. The control is padded so long\n values never run under it; the button is a 40px tap target centred on the input, transparent,\n so it overhangs the ~36px input invisibly instead of resizing the field. */\n.mcpui-secret { position: relative; display: flex; flex-direction: column; }\n.mcpui-secret > .mcpui-input, .mcpui-secret > .mcpui-textarea { padding-right: 44px; }\n.mcpui-secret-toggle {\n position: absolute;\n top: 50%;\n right: 0;\n width: 40px;\n height: 40px;\n margin-top: -20px;\n padding: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: 0;\n border-radius: var(--jini-mcpui-radius-sm);\n background: transparent;\n color: var(--jini-mcpui-text-soft);\n cursor: pointer;\n}\n.mcpui-secret > .mcpui-textarea + .mcpui-secret-toggle { top: 0; margin-top: -2px; }\n.mcpui-secret-toggle:hover:not([disabled]) { color: var(--jini-mcpui-text-strong); }\n.mcpui-secret-toggle[disabled] { opacity: 0.55; cursor: default; }\n.mcpui-secret-toggle:focus-visible { outline: 2px solid var(--jini-mcpui-accent-ring); outline-offset: -6px; }\n.mcpui-secret-toggle[aria-pressed=\"true\"] .mcpui-secret-icon-show,\n.mcpui-secret-toggle:not([aria-pressed=\"true\"]) .mcpui-secret-icon-hide { display: none; }\n.mcpui-checkbox { margin: 3px 0 0; accent-color: var(--jini-mcpui-accent); }\n.mcpui-choice-group { border: 0; padding: 0; margin: 0; min-width: 0; }\n.mcpui-choice { display: flex; flex-direction: row; align-items: flex-start; gap: 8px; margin: 6px 0 0; }\n.mcpui-choice-label { color: var(--jini-mcpui-text); font-weight: 400; }\n.mcpui-input:focus-visible, .mcpui-select:focus-visible, .mcpui-textarea:focus-visible,\n.mcpui-checkbox:focus-visible, .mcpui-button:focus-visible {\n outline: 2px solid var(--jini-mcpui-accent-ring);\n outline-offset: 1px;\n border-color: var(--jini-mcpui-accent);\n}\n.mcpui-actions { display: flex; flex-wrap: wrap; gap: 8px; margin: 18px 0 0; }\n.mcpui-button {\n font: inherit;\n font-weight: 500;\n padding: 7px 15px;\n border-radius: var(--jini-mcpui-radius-pill);\n border: 1px solid color-mix(in srgb, var(--jini-mcpui-accent) 14%, var(--jini-mcpui-border-strong));\n background: var(--jini-mcpui-panel);\n color: var(--jini-mcpui-text);\n cursor: pointer;\n}\n.mcpui-button:hover:not([disabled]) { background: var(--jini-mcpui-accent-tint); }\n.mcpui-button-primary {\n background: var(--jini-mcpui-accent);\n border-color: var(--jini-mcpui-accent);\n color: var(--jini-mcpui-accent-ink);\n}\n.mcpui-button-primary:hover:not([disabled]) { background: var(--jini-mcpui-accent-hover); border-color: var(--jini-mcpui-accent-hover); }\n.mcpui-button-danger {\n background: var(--jini-mcpui-danger);\n border-color: var(--jini-mcpui-danger);\n color: var(--jini-mcpui-panel);\n}\n.mcpui-button-danger:hover:not([disabled]) { background: var(--jini-mcpui-danger-hover); border-color: var(--jini-mcpui-danger-hover); }\n.mcpui-button[disabled] { opacity: 0.55; cursor: default; }\n.mcpui-status { margin: 12px 0 0; min-height: 1.4em; color: var(--jini-mcpui-text-muted); }\n.mcpui-status[data-state=\"failed\"], .mcpui-status[data-state=\"invalid\"], .mcpui-status[data-state=\"expired\"] { color: var(--jini-mcpui-danger); }\n.mcpui-status[data-state=\"done\"] { color: var(--jini-mcpui-text-strong); }\n/* Neither a plain success nor a failure \u2014 e.g. an outcome surface reporting \"uploaded, not yet\n reachable\" (outcome.ts's own state: partial). Distinct from both --jini-mcpui-danger (would\n wrongly read as \"this failed\") and --jini-mcpui-text-strong (would wrongly read as \"this is\n fully done\"); the accent token reads as \"worth a second look\" without asserting either. */\n.mcpui-status[data-state=\"partial\"] { color: var(--jini-mcpui-accent); }\n.mcpui-code { font-family: var(--jini-mcpui-font-mono); font-size: 13px; color: var(--jini-mcpui-text-faint); }";
12
12
  /**
13
13
  * The Content-Security-Policy every generated surface declares in its own `<head>`.
14
14
  *
@@ -130,6 +130,34 @@ body {
130
130
  min-width: 0;
131
131
  }
132
132
  .mcpui-textarea { resize: vertical; min-height: 72px; font-family: var(--jini-mcpui-font); }
133
+ /* A secret control's eye toggle sits inside the field's right edge. The control is padded so long
134
+ values never run under it; the button is a 40px tap target centred on the input, transparent,
135
+ so it overhangs the ~36px input invisibly instead of resizing the field. */
136
+ .mcpui-secret { position: relative; display: flex; flex-direction: column; }
137
+ .mcpui-secret > .mcpui-input, .mcpui-secret > .mcpui-textarea { padding-right: 44px; }
138
+ .mcpui-secret-toggle {
139
+ position: absolute;
140
+ top: 50%;
141
+ right: 0;
142
+ width: 40px;
143
+ height: 40px;
144
+ margin-top: -20px;
145
+ padding: 0;
146
+ display: inline-flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ border: 0;
150
+ border-radius: var(--jini-mcpui-radius-sm);
151
+ background: transparent;
152
+ color: var(--jini-mcpui-text-soft);
153
+ cursor: pointer;
154
+ }
155
+ .mcpui-secret > .mcpui-textarea + .mcpui-secret-toggle { top: 0; margin-top: -2px; }
156
+ .mcpui-secret-toggle:hover:not([disabled]) { color: var(--jini-mcpui-text-strong); }
157
+ .mcpui-secret-toggle[disabled] { opacity: 0.55; cursor: default; }
158
+ .mcpui-secret-toggle:focus-visible { outline: 2px solid var(--jini-mcpui-accent-ring); outline-offset: -6px; }
159
+ .mcpui-secret-toggle[aria-pressed="true"] .mcpui-secret-icon-show,
160
+ .mcpui-secret-toggle:not([aria-pressed="true"]) .mcpui-secret-icon-hide { display: none; }
133
161
  .mcpui-checkbox { margin: 3px 0 0; accent-color: var(--jini-mcpui-accent); }
134
162
  .mcpui-choice-group { border: 0; padding: 0; margin: 0; min-width: 0; }
135
163
  .mcpui-choice { display: flex; flex-direction: row; align-items: flex-start; gap: 8px; margin: 6px 0 0; }
@@ -7,10 +7,24 @@ interface SurfaceFieldBase {
7
7
  /** Help text, wired up with `aria-describedby`. */
8
8
  readonly hint?: string;
9
9
  readonly required?: boolean;
10
+ /** Waive on an exact choice or an alternative control; headerNames restricts entered headers. Kept secrets are validated by the host. */
11
+ readonly requiredUnless?: {
12
+ readonly field: string;
13
+ readonly equals: string;
14
+ readonly orPresent?: string;
15
+ readonly headerNames?: readonly string[];
16
+ };
17
+ /** Require only while the controlling public field matches one of these choices. */
18
+ readonly requiredWhen?: {
19
+ readonly field: string;
20
+ readonly in: readonly string[];
21
+ };
10
22
  readonly disabled?: boolean;
11
23
  }
12
24
  export interface StringField extends SurfaceFieldBase {
13
25
  readonly kind: 'string';
26
+ /** A sealed saved value satisfies validation while the requirement remains visible. */
27
+ readonly hasSavedValue?: boolean;
14
28
  readonly value?: string;
15
29
  readonly placeholder?: string;
16
30
  readonly multiline?: boolean;
@@ -73,8 +87,21 @@ export interface FieldReadSpec {
73
87
  readonly label: string;
74
88
  readonly kind: SurfaceField['kind'];
75
89
  readonly required: boolean;
90
+ readonly hasSavedValue?: boolean;
91
+ readonly requiredUnless?: {
92
+ readonly field: string;
93
+ readonly equals: string;
94
+ readonly orPresent?: string;
95
+ readonly headerNames?: readonly string[];
96
+ };
97
+ /** Require only while the controlling public field matches one of these choices. */
98
+ readonly requiredWhen?: {
99
+ readonly field: string;
100
+ readonly in: readonly string[];
101
+ };
102
+ readonly hint?: string;
76
103
  }
77
- /** Projects fields down to what the inline script needs, so the generated JS carries no labels, hints, or option lists it will never read. */
104
+ /** Projects fields to runtime validation data; conditional requirements also carry their guidance. */
78
105
  export declare function toFieldReadSpecs(fields: readonly SurfaceField[]): readonly FieldReadSpec[];
79
106
  export {};
80
107
  //# sourceMappingURL=fields.d.ts.map
@@ -67,13 +67,17 @@ export function renderFieldControl(field, text) {
67
67
  return renderChoiceGroup({ ...base(field), selection: 'multiple', options: field.options, ...present(field, ['value']) });
68
68
  }
69
69
  }
70
- /** Projects fields down to what the inline script needs, so the generated JS carries no labels, hints, or option lists it will never read. */
70
+ /** Projects fields to runtime validation data; conditional requirements also carry their guidance. */
71
71
  export function toFieldReadSpecs(fields) {
72
72
  return fields.map((field) => ({
73
73
  name: field.name,
74
74
  label: field.label,
75
75
  kind: field.kind,
76
76
  required: field.required === true,
77
+ ...(field.requiredUnless ? { requiredUnless: field.requiredUnless } : {}),
78
+ ...(field.requiredWhen ? { requiredWhen: field.requiredWhen } : {}),
79
+ ...(field.kind === 'string' && field.hasSavedValue ? { hasSavedValue: true } : {}),
80
+ ...((field.requiredUnless || field.requiredWhen) && field.hint ? { hint: field.hint } : {}),
77
81
  }));
78
82
  }
79
83
  //# sourceMappingURL=fields.js.map
@@ -78,6 +78,7 @@ function cancelPlanOf(spec) {
78
78
  /** The inline runtime script that reads, validates and submits the fields. @complexity O(n) in the field count plus the script length. */
79
79
  function formScript(spec, text) {
80
80
  const cancelPlan = cancelPlanOf(spec);
81
+ const hasAlternatives = spec.fields.some(field => field.requiredUnless?.orPresent);
81
82
  return `(function () {
82
83
  "use strict";
83
84
  ${SURFACE_SCRIPT_PRELUDE}
@@ -124,20 +125,29 @@ ${SURFACE_SCRIPT_PRELUDE}
124
125
  control.dispatchEvent(new Event("input", { bubbles: true }));
125
126
  });
126
127
 
127
- // A reveal button only changes presentation: replacing a textarea with a password input would
128
- // discard line breaks. Listen on the form so clicks on the button's contents work too.
128
+ // The eye toggle only changes presentation. A textarea stays a textarea (a password input would
129
+ // discard line breaks) and flips its CSS masking; a single-line input flips password <-> text.
130
+ // Listen on the form so clicks on the button's icon work too.
131
+ function setRevealed(button, reveal) {
132
+ var control = document.getElementById(button.getAttribute("data-mcpui-secret-toggle"));
133
+ if (!control) return;
134
+ if (control.tagName === "TEXTAREA") control.setAttribute("style", "-webkit-text-security: " + (reveal ? "none" : "disc") + ";");
135
+ else control.type = reveal ? "text" : "password";
136
+ button.setAttribute("aria-pressed", reveal ? "true" : "false");
137
+ button.setAttribute("aria-label", button.getAttribute(reveal ? "data-mcpui-hide-secret" : "data-mcpui-show-secret"));
138
+ }
139
+ // Re-mask every secret once a save succeeds, so nothing stays readable on screen behind the result.
140
+ function concealSecrets() {
141
+ var toggles = form.querySelectorAll("button[data-mcpui-secret-toggle]");
142
+ for (var t = 0; t < toggles.length; t++) setRevealed(toggles[t], false);
143
+ }
129
144
  form.addEventListener("click", function (event) {
130
145
  if (event.isTrusted !== true) return;
131
146
  var target = event.target;
132
147
  if (!target || typeof target.closest !== "function") return;
133
148
  var button = target.closest("button[data-mcpui-secret-toggle]");
134
149
  if (!button || button.disabled) return;
135
- var control = document.getElementById(button.getAttribute("data-mcpui-secret-toggle"));
136
- if (!control) return;
137
- var reveal = button.getAttribute("aria-pressed") !== "true";
138
- control.setAttribute("style", "-webkit-text-security: " + (reveal ? "none" : "disc") + ";");
139
- button.setAttribute("aria-pressed", reveal ? "true" : "false");
140
- button.textContent = button.getAttribute(reveal ? "data-mcpui-hide-secret" : "data-mcpui-show-secret");
150
+ setRevealed(button, button.getAttribute("aria-pressed") !== "true");
141
151
  });
142
152
 
143
153
  // form.elements[name] yields a RadioNodeList when several controls share a name and the BARE
@@ -172,6 +182,69 @@ ${SURFACE_SCRIPT_PRELUDE}
172
182
  return value === "";
173
183
  }
174
184
 
185
+ // Presentation only; the host's save rule validates the effective settings and kept secrets.
186
+ ${hasAlternatives ? ` function hasAlternative(field) {
187
+ var name = field.requiredUnless && field.requiredUnless.orPresent;
188
+ if (!name) return false;
189
+ var control = form.elements[name];
190
+ var value = control && control.value.trim();
191
+ if (!value) return FIELDS.some(function(item) { return item.name === name && item.hasSavedValue; });
192
+ var names = field.requiredUnless.headerNames;
193
+ if (!names) return true;
194
+ var pairs = value.split(",").map(function(pair) { return /^([^=\\s][^=]*?)\\s*=\\s*(.+)$/.exec(pair.trim()); });
195
+ return pairs.every(Boolean) && pairs.some(function(pair) {
196
+ return names.some(function(name) { return name.toLowerCase() === pair[1].toLowerCase(); }) && pair[2].trim() !== "";
197
+ });
198
+ }
199
+ ` : ''} function requiredNow(field) {
200
+ var condition = field.requiredUnless;
201
+ var other = condition && form.elements[condition.field];
202
+ var when = field.requiredWhen;
203
+ var controlling = when && form.elements[when.field];
204
+ var required = when ? Boolean(controlling && when.in.indexOf(controlling.value) !== -1) : field.required;
205
+ ${hasAlternatives ? ` if (hasAlternative(field)) return false;
206
+ ` : ''} return required && (!condition || !other || other.value !== condition.equals);
207
+ }
208
+ function refreshRequirements() {
209
+ for (var i = 0; i < FIELDS.length; i++) {
210
+ var field = FIELDS[i];
211
+ if (!field.requiredUnless && !field.requiredWhen && !field.hasSavedValue) continue;
212
+ var control = form.elements[field.name];
213
+ var required = requiredNow(field);
214
+ control.required = required && !field.hasSavedValue;
215
+ control.setAttribute("aria-required", String(required && !field.hasSavedValue));
216
+ var label = control.labels && control.labels[0];
217
+ var mark = label && label.querySelector(".mcpui-required");
218
+ if (label && !mark) {
219
+ mark = document.createElement("span");
220
+ mark.className = "mcpui-required";
221
+ mark.setAttribute("aria-hidden", "true");
222
+ mark.textContent = "*";
223
+ label.appendChild(mark);
224
+ }
225
+ if (mark) mark.hidden = !required;
226
+ }
227
+ }
228
+ form.addEventListener("change", refreshRequirements);
229
+ form.addEventListener("input", refreshRequirements);
230
+ refreshRequirements();
231
+
232
+ // Collect field values and validation feedback before the submit path performs any effects.
233
+ function readFields() {
234
+ var params = {};
235
+ var missing = [];
236
+ var missingMessage;
237
+ for (var i = 0; i < FIELDS.length; i++) {
238
+ var value = readField(FIELDS[i]);
239
+ if (requiredNow(FIELDS[i]) && !FIELDS[i].hasSavedValue && isBlank(FIELDS[i], value)) {
240
+ missing.push(FIELDS[i].label);
241
+ if ((FIELDS[i].requiredUnless || FIELDS[i].requiredWhen) && FIELDS[i].hint) missingMessage = FIELDS[i].label + " is required. " + FIELDS[i].hint;
242
+ }
243
+ params[FIELDS[i].name] = value;
244
+ }
245
+ return { params: params, missing: missing, missingMessage: missingMessage };
246
+ }
247
+
175
248
  // NOT wired to the form's "submit" event. Per the WHATWG form submission algorithm, the
176
249
  // sandboxed-forms browsing-context check runs BEFORE that event is ever dispatched, so in the
177
250
  // real target sandbox (MCP_UI_VIEW_SANDBOX grants "allow-scripts" and nothing else -- no
@@ -201,14 +274,10 @@ ${SURFACE_SCRIPT_PRELUDE}
201
274
  var params = {};
202
275
  var key;
203
276
  for (key in BASE_PARAMS) if (Object.prototype.hasOwnProperty.call(BASE_PARAMS, key)) params[key] = BASE_PARAMS[key];
204
- var missing = [];
205
- for (var i = 0; i < FIELDS.length; i++) {
206
- var value = readField(FIELDS[i]);
207
- if (FIELDS[i].required && isBlank(FIELDS[i], value)) missing.push(FIELDS[i].label);
208
- params[FIELDS[i].name] = value;
209
- }
210
- if (missing.length > 0) {
211
- setStatus(TEXT.missingPrefix + missing.join(", "), "invalid");
277
+ var fields = readFields();
278
+ Object.assign(params, fields.params);
279
+ if (fields.missing.length > 0) {
280
+ setStatus(fields.missingMessage || TEXT.missingPrefix + fields.missing.join(", "), "invalid");
212
281
  return;
213
282
  }
214
283
  for (key in actionParams) if (Object.prototype.hasOwnProperty.call(actionParams, key)) params[key] = actionParams[key];
@@ -216,6 +285,7 @@ ${SURFACE_SCRIPT_PRELUDE}
216
285
  setBusy(true);
217
286
  setStatus(TEXT.working, "pending");
218
287
  api.callTool(TOOL, params).then(function () {
288
+ concealSecrets();
219
289
  setStatus(TEXT.done, "done");
220
290
  api.requestTeardown();
221
291
  }, function (error) {
@@ -1,4 +1,4 @@
1
- /** Labels the host can translate for a multiline secret's reveal toggle. */
1
+ /** Verbs the host can translate for a secret's reveal toggle; the field label is appended. */
2
2
  export interface SecretInputText {
3
3
  readonly showSecret: string;
4
4
  readonly hideSecret: string;
@@ -18,8 +18,8 @@ export interface TextInputProps extends Partial<SecretInputText> {
18
18
  readonly disabled?: boolean;
19
19
  /** `'number'` also constrains the on-screen keyboard on touch devices, which `'text'` would not. */
20
20
  readonly inputType?: 'text' | 'number';
21
- /** Renders as `<input type="password">`, or a masked textarea with a show/hide toggle when
22
- * multiline. No `<textarea type="password">` exists, so multiline uses CSS text security.
21
+ /** Renders as `<input type="password">`, or a masked textarea when multiline — either way with
22
+ * an eye show/hide toggle. No `<textarea type="password">` exists, so multiline uses CSS text security.
23
23
  * Ignored for presentation when `inputType` is `'number'` (no such thing as a masked number) —
24
24
  * same precedence `multiline`'s own doc comment already gives `inputType: 'number'`.
25
25
  * A supplied value is always rejected: masking cannot protect a secret serialized into HTML. */
@@ -29,35 +29,50 @@ function resolveInputType(props) {
29
29
  return 'number';
30
30
  return props.secret === true ? 'password' : 'text';
31
31
  }
32
- /** The reveal toggle a multiline secret carries. @complexity O(n) in the label lengths. */
32
+ /** Eye / eye-off glyphs. Both ship; CSS shows the one matching `aria-pressed`, so the inline script
33
+ * never builds markup. `currentColor` keeps them on the surface's theme tokens in light and dark. */
34
+ const EYE_ICON = '<svg class="mcpui-secret-icon-show" aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/></svg>' +
35
+ '<svg class="mcpui-secret-icon-hide" aria-hidden="true" focusable="false" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9.9 4.24A9.1 9.1 0 0 1 12 4c6.5 0 10 7 10 7a17.8 17.8 0 0 1-2.16 3.19M6.6 6.6C3.9 8.4 2 12 2 12s3.5 7 10 7a9.7 9.7 0 0 0 5.4-1.6M14.12 14.12a3 3 0 1 1-4.24-4.24"/><path d="M2 2l20 20"/></svg>';
36
+ /**
37
+ * The eye toggle every secret control carries, inside the field's right edge. Its accessible name
38
+ * is the host's verb plus the field label ("Show Token"); the full show/hide names ride on data
39
+ * attributes so the form script swaps them without knowing the label.
40
+ * @complexity O(n) in the label lengths.
41
+ */
33
42
  function renderSecretToggle(props, id) {
34
- const showSecret = props.showSecret ?? DEFAULT_SECRET_INPUT_TEXT.showSecret;
35
- const hideSecret = props.hideSecret ?? DEFAULT_SECRET_INPUT_TEXT.hideSecret;
36
- return `<button type="button" class="mcpui-button" data-mcpui-secret-toggle="${escapeHtml(id)}" data-mcpui-show-secret="${escapeHtml(showSecret)}" data-mcpui-hide-secret="${escapeHtml(hideSecret)}" aria-controls="${escapeHtml(id)}" aria-pressed="false"${booleanAttribute('disabled', props.disabled)}>${escapeHtml(showSecret)}</button>`;
43
+ const show = `${props.showSecret ?? DEFAULT_SECRET_INPUT_TEXT.showSecret} ${props.label}`;
44
+ const hide = `${props.hideSecret ?? DEFAULT_SECRET_INPUT_TEXT.hideSecret} ${props.label}`;
45
+ return `<button type="button" class="mcpui-secret-toggle" data-mcpui-secret-toggle="${escapeHtml(id)}" data-mcpui-show-secret="${escapeHtml(show)}" data-mcpui-hide-secret="${escapeHtml(hide)}" aria-label="${escapeHtml(show)}" aria-controls="${escapeHtml(id)}" aria-pressed="false"${booleanAttribute('disabled', props.disabled)}>${EYE_ICON}</button>`;
46
+ }
47
+ /** Wraps a secret control so its toggle sits inside the field's right edge. @complexity O(n) in the rendered length. */
48
+ function withSecretToggle(control, props, id) {
49
+ return `<div class="mcpui-secret">${control}${renderSecretToggle(props, id)}</div>`;
37
50
  }
38
51
  /** The `<textarea>` control, masked with a reveal toggle when secret. @complexity O(n) in the rendered length. */
39
52
  function renderTextarea(props, required) {
40
53
  const secret = props.secret === true;
41
- return `<textarea class="mcpui-textarea"${required.common}${optionalAttribute('rows', props.rows)}` +
54
+ const control = `<textarea class="mcpui-textarea"${required.common}${optionalAttribute('rows', props.rows)}` +
42
55
  // Password inputs strip line breaks. CSS masking keeps the textarea's actual value intact,
43
56
  // and disables browser text assistance that could retain or alter a pasted credential.
44
57
  (secret ? ' data-mcpui-secret style="-webkit-text-security: disc;" autocomplete="off" spellcheck="false"' : '') +
45
- `>${props.value === undefined ? '' : escapeHtml(String(props.value))}</textarea>` +
46
- (secret ? renderSecretToggle(props, required.id) : '');
58
+ `>${props.value === undefined ? '' : escapeHtml(String(props.value))}</textarea>`;
59
+ return secret ? withSecretToggle(control, props, required.id) : control;
47
60
  }
48
- /** The single-line `<input>` control. @complexity O(n) in the rendered length. */
61
+ /** The single-line `<input>` control, with the eye toggle when masked. @complexity O(n) in the rendered length. */
49
62
  function renderInput(props, required) {
50
63
  const { type, common } = required;
51
- return `<input class="mcpui-input" type="${type}"${common}` +
64
+ const control = `<input class="mcpui-input" type="${type}"${common}` +
52
65
  optionalAttribute('value', props.value) +
53
66
  // A stored secret (e.g. a cloud provider's access key or a database password) must never be
54
67
  // autofilled with a saved website login. `new-password`, not `off`: Chrome ignores `off` on
55
68
  // password inputs by design and fills the saved login anyway.
56
- (type === 'password' ? optionalAttribute('autocomplete', 'new-password') : '') +
69
+ // spellcheck off: once revealed as `type="text"`, enhanced spellcheck would ship the secret out.
70
+ (type === 'password' ? optionalAttribute('autocomplete', 'new-password') + optionalAttribute('spellcheck', 'false') : '') +
57
71
  (type === 'number'
58
72
  ? optionalAttribute('min', props.min) + optionalAttribute('max', props.max) + optionalAttribute('step', props.step)
59
73
  : '') +
60
74
  '>';
75
+ return type === 'password' ? withSecretToggle(control, props, required.id) : control;
61
76
  }
62
77
  /**
63
78
  * Renders one free-text field: its label, hint, and control, wrapped in `.mcpui-field`.
@@ -81,7 +96,7 @@ export function renderTextInput(props) {
81
96
  const type = resolveInputType(props);
82
97
  const control = props.multiline === true && type !== 'number'
83
98
  ? renderTextarea(props, { id, common })
84
- : renderInput(props, { type, common });
99
+ : renderInput(props, { id, type, common });
85
100
  return `<div class="mcpui-field">\n${renderFieldLabel(props)}\n${control}\n</div>`;
86
101
  }
87
102
  //# sourceMappingURL=text-input.js.map
@@ -0,0 +1,3 @@
1
+ /** Example endpoint for the provider configuration UI; no provider is selected by default. */
2
+ export declare const MEDIA_PROVIDER_BASE_URL_PLACEHOLDER = "https://api.example.com";
3
+ //# sourceMappingURL=catalog.d.ts.map
@@ -0,0 +1,3 @@
1
+ /** Example endpoint for the provider configuration UI; no provider is selected by default. */
2
+ export const MEDIA_PROVIDER_BASE_URL_PLACEHOLDER = 'https://api.example.com';
3
+ //# sourceMappingURL=catalog.js.map
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Fragment } from 'react';
2
+ import { Fragment, useId } from 'react';
3
3
  import { agentHandleProps } from '@jini-ai/agentic';
4
- import { Icon } from '../../../../react/components/Icon.js';
4
+ import { SecretInput } from '../../../../react/components/SecretInput.js';
5
5
  import { isEntryPresent, isMarkerOnlyEntry, isProviderBaseUrlInvalid, maskedKeyLabel, resolveProviderBaseUrl, } from '../../rules.js';
6
6
  /** One provider's credential card: API key, base URL, optional model, clear, and the base-URL hint. */
7
7
  export function MediaProviderCard(props) {
@@ -23,12 +23,13 @@ function CardHead({ option, labels, t, maskedLabel, pending, }) {
23
23
  _jsx("span", { className: "jini-field-status-badge jini-field-status-badge-success", children: t(labels.savedWithMaskTemplate, { mask: maskedLabel }) })) : null, !saved && pending ? _jsx("span", { className: "jini-field-status-badge", children: labels.unsavedLabel }) : null] }));
24
24
  }
25
25
  function ApiKeyField({ option, entry, labels, providerHandle, onUpdate, maskedLabel, keyVisible, onToggleKeyVisible, }) {
26
- return (_jsxs("label", { className: "jini-media-provider-field jini-media-provider-field--secret", children: [_jsx("span", { className: "jini-field-label jini-sr-only", children: labels.apiKeyLabel }), _jsx("input", { className: "jini-input", type: keyVisible ? 'text' : 'password',
26
+ const inputId = useId();
27
+ return (_jsxs("div", { className: "jini-media-provider-field jini-media-provider-field--secret", children: [_jsx("label", { htmlFor: inputId, className: "jini-field-label jini-sr-only", children: labels.apiKeyLabel }), _jsx(SecretInput, { id: inputId, label: `${option.label} ${labels.apiKeyLabel}`, revealed: keyVisible, onRevealedChange: onToggleKeyVisible, toggleLabels: { show: `${labels.showKeyLabel} ${option.label} ${labels.apiKeyLabel}`, hide: `${labels.hideKeyLabel} ${option.label} ${labels.apiKeyLabel}` }, toggleAttrs: agentHandleProps({}, { base: providerHandle, ...({ action: 'reveal-key', role: 'button', label: `${option.label} ${labels.showKeyLabel}` }) }), className: "jini-input",
27
28
  /* `new-password`, NOT `"off"` — Chrome deliberately ignores `off` on
28
29
  credential-shaped fields, which is how a saved password gets autofilled into
29
30
  an API-key box. Same defect, same fix, same reasoning as
30
31
  `ByokProviderForm`'s own API-key input; see its comment. */
31
- autoComplete: "new-password", spellCheck: false, placeholder: maskedLabel ?? labels.apiKeyPlaceholder, "aria-label": `${option.label} ${labels.apiKeyLabel}`, value: entry.apiKey ?? '', onChange: (event) => onUpdate({ apiKey: event.target.value }), ...agentHandleProps({}, { base: providerHandle, ...({ action: 'api-key', role: 'field', label: `${option.label} ${labels.apiKeyLabel}` }) }) }), _jsx("button", { type: "button", className: "jini-input-affix-btn", "aria-pressed": keyVisible, "aria-label": `${option.label} ${keyVisible ? labels.hideKeyLabel : labels.showKeyLabel}`, onClick: onToggleKeyVisible, ...agentHandleProps({}, { base: providerHandle, ...({ action: 'reveal-key', role: 'button', label: `${option.label} ${labels.showKeyLabel}` }) }), children: _jsx(Icon, { name: keyVisible ? 'eye-off' : 'eye', size: 14 }) })] }));
32
+ autoComplete: "new-password", spellCheck: false, placeholder: maskedLabel ?? labels.apiKeyPlaceholder, "aria-label": `${option.label} ${labels.apiKeyLabel}`, value: entry.apiKey ?? '', onChange: (event) => onUpdate({ apiKey: event.target.value }), ...agentHandleProps({}, { base: providerHandle, ...({ action: 'api-key', role: 'field', label: `${option.label} ${labels.apiKeyLabel}` }) }) })] }));
32
33
  }
33
34
  function BaseUrlField({ option, entry, labels, providerHandle, onUpdate }) {
34
35
  return (_jsxs("label", { className: "jini-media-provider-field", children: [_jsx("span", { className: "jini-field-label jini-sr-only", children: labels.baseUrlLabel }), _jsx("input", { className: "jini-input", type: "url", inputMode: "url", spellCheck: false, placeholder: option.defaultBaseUrl || labels.baseUrlPlaceholder, "aria-label": `${option.label} ${labels.baseUrlLabel}`, value: entry.baseUrl ?? '', "aria-invalid": isProviderBaseUrlInvalid(entry) || undefined, onChange: (event) => onUpdate({ baseUrl: event.target.value }), ...agentHandleProps({}, { base: providerHandle, ...({ action: 'base-url', role: 'field', label: `${option.label} ${labels.baseUrlLabel}` }) }) })] }));
@@ -1,3 +1,4 @@
1
+ import { MEDIA_PROVIDER_BASE_URL_PLACEHOLDER } from '../../catalog.js';
1
2
  /** English defaults, keyed exactly like `MediaProvidersTabLabels`; each is passed through `t`
2
3
  * only when the host did not supply that label (the dictionaries are keyed by this English text). */
3
4
  const DEFAULT_LABELS = {
@@ -12,7 +13,7 @@ const DEFAULT_LABELS = {
12
13
  showKeyLabel: 'Show',
13
14
  hideKeyLabel: 'Hide',
14
15
  baseUrlLabel: 'Base URL',
15
- baseUrlPlaceholder: 'https://api.example.com',
16
+ baseUrlPlaceholder: MEDIA_PROVIDER_BASE_URL_PLACEHOLDER,
16
17
  baseUrlDefaultHintTemplate: 'Uses {url} by default.',
17
18
  baseUrlInvalidLabel: 'Enter an absolute http:// or https:// URL that is not a private or internal address.',
18
19
  modelLabel: 'Model',