@jini-ai/ui 0.6.15 → 0.6.16

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 (50) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/core.d.ts +1 -1
  3. package/dist/core.js +1 -1
  4. package/dist/features/appearance/constants.d.ts +28 -1
  5. package/dist/features/appearance/constants.js +12 -9
  6. package/dist/features/appearance/react/components/AppearanceTab.js +4 -4
  7. package/dist/features/charts/contract.d.ts +4 -4
  8. package/dist/features/charts/react.js +2 -2
  9. package/dist/features/charts/wire.d.ts +19 -0
  10. package/dist/features/charts/wire.js +20 -2
  11. package/dist/features/execution/constants.js +0 -1
  12. package/dist/features/folder-path-drop/rules.d.ts +2 -3
  13. package/dist/features/folder-path-drop/rules.js +3 -4
  14. package/dist/features/i18n/dictionaries/settings-dialog.ar.js +8 -0
  15. package/dist/features/i18n/dictionaries/settings-dialog.bn.js +8 -0
  16. package/dist/features/i18n/dictionaries/settings-dialog.de.js +8 -0
  17. package/dist/features/i18n/dictionaries/settings-dialog.en.js +8 -0
  18. package/dist/features/i18n/dictionaries/settings-dialog.es.js +8 -0
  19. package/dist/features/i18n/dictionaries/settings-dialog.fa.js +8 -0
  20. package/dist/features/i18n/dictionaries/settings-dialog.fr.js +8 -0
  21. package/dist/features/i18n/dictionaries/settings-dialog.hi.js +8 -0
  22. package/dist/features/i18n/dictionaries/settings-dialog.hu.js +8 -0
  23. package/dist/features/i18n/dictionaries/settings-dialog.id.js +8 -0
  24. package/dist/features/i18n/dictionaries/settings-dialog.it.js +8 -0
  25. package/dist/features/i18n/dictionaries/settings-dialog.ja.js +8 -0
  26. package/dist/features/i18n/dictionaries/settings-dialog.ko.js +8 -0
  27. package/dist/features/i18n/dictionaries/settings-dialog.pl.js +8 -0
  28. package/dist/features/i18n/dictionaries/settings-dialog.pt-BR.js +8 -0
  29. package/dist/features/i18n/dictionaries/settings-dialog.ru.js +8 -0
  30. package/dist/features/i18n/dictionaries/settings-dialog.th.js +8 -0
  31. package/dist/features/i18n/dictionaries/settings-dialog.tr.js +8 -0
  32. package/dist/features/i18n/dictionaries/settings-dialog.uk.js +8 -0
  33. package/dist/features/i18n/dictionaries/settings-dialog.ur.js +8 -0
  34. package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +8 -0
  35. package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +8 -0
  36. package/dist/features/mcp-ui/surfaces/checkbox.d.ts +2 -0
  37. package/dist/features/mcp-ui/surfaces/choice-group.d.ts +2 -0
  38. package/dist/features/mcp-ui/surfaces/choice-group.js +2 -2
  39. package/dist/features/mcp-ui/surfaces/document.d.ts +11 -1
  40. package/dist/features/mcp-ui/surfaces/document.js +9 -1
  41. package/dist/features/mcp-ui/surfaces/fields.d.ts +7 -1
  42. package/dist/features/mcp-ui/surfaces/fields.js +12 -11
  43. package/dist/features/mcp-ui/surfaces/form.js +6 -1
  44. package/dist/features/mcp-ui/surfaces/select.d.ts +2 -0
  45. package/dist/features/mcp-ui/surfaces/text-input.d.ts +2 -0
  46. package/dist/features/mcp-ui/surfaces/tool-approval.js +4 -3
  47. package/dist/react/components/AgentIcon.js +0 -1
  48. package/dist/react/components/agent-icon-data-uris.generated.js +0 -1
  49. package/dist/renderers/shiki.js +7 -1
  50. package/package.json +32 -30
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @jini-ai/ui
2
2
 
3
+ ## 0.6.16
4
+
5
+ ### Minor Changes
6
+
7
+ - Remove the GitHub Copilot CLI agent: its def, `createCopilotStreamHandler`/`CopilotStreamEvent`,
8
+ account model discovery and the `@github/copilot-sdk` optional peer, the MCP installer's `copilot`
9
+ slug, and its icon. A saved `copilot` agent id no longer resolves (`getAgentDef` returns `null`).
10
+ CopilotKit/AG-UI is unrelated and unchanged.
11
+
12
+ Drop peers no runtime entry loads: `express` from core and http-kit (type-only imports; type
13
+ consumers keep `@types/express`), `vue` from ui-kit, `react-dom` from vibecoding.
14
+
3
15
  ## 0.6.13
4
16
 
5
17
  ### Patch Changes
package/dist/core.d.ts CHANGED
@@ -9,7 +9,7 @@ export { DEFAULT_FAILURE_SOUND_ID, DEFAULT_SUCCESS_SOUND_ID, FAILURE_SOUNDS, SUC
9
9
  export type { AboutUpdateControl, AboutUpdatePrimaryAction, AboutUpdateTone, AppVersionInfo, SilentUpdatesState, UpdateKind, UpdaterDownloadProgress, UpdaterEnvironment, UpdaterModel, UpdaterState, UpdaterStatus, } from './features/about/types.js';
10
10
  export { ABOUT_UPDATE_KEYS, beginSilentUpdatesWrite, deriveAboutUpdateControl, isAboutUpdateActionDisabled, resolveSilentUpdatesWriteFailure, resolveSilentUpdatesWriteSuccess, } from './features/about/rules.js';
11
11
  export type { SettingsThemeChoice } from './features/appearance/types.js';
12
- export { THEME_OPTIONS, DEFAULT_ACCENT_COLOR, ACCENT_SWATCHES } from './features/appearance/constants.js';
12
+ export { THEME_OPTIONS, DEFAULT_ACCENT_COLOR, ACCENT_SWATCHES, ACCENT_SWATCH_PRESETS } from './features/appearance/constants.js';
13
13
  export type { ThemeOption } from './features/appearance/constants.js';
14
14
  export { normalizeAccentColor, resolveAccentColor, accentVars } from './features/appearance/rules.js';
15
15
  export type { AccentCssVars } from './features/appearance/rules.js';
package/dist/core.js CHANGED
@@ -17,7 +17,7 @@ export { randomUUID } from './utils/uuid.js';
17
17
  export { isAllowedEndpointUrl, isBlockedEndpointHost, isLoopbackEndpointHost, } from './utils/endpoint-policy.js';
18
18
  export { DEFAULT_FAILURE_SOUND_ID, DEFAULT_SUCCESS_SOUND_ID, FAILURE_SOUNDS, SUCCESS_SOUNDS, } from './features/notifications/notifications-catalog.js';
19
19
  export { ABOUT_UPDATE_KEYS, beginSilentUpdatesWrite, deriveAboutUpdateControl, isAboutUpdateActionDisabled, resolveSilentUpdatesWriteFailure, resolveSilentUpdatesWriteSuccess, } from './features/about/rules.js';
20
- export { THEME_OPTIONS, DEFAULT_ACCENT_COLOR, ACCENT_SWATCHES } from './features/appearance/constants.js';
20
+ export { THEME_OPTIONS, DEFAULT_ACCENT_COLOR, ACCENT_SWATCHES, ACCENT_SWATCH_PRESETS } from './features/appearance/constants.js';
21
21
  export { normalizeAccentColor, resolveAccentColor, accentVars } from './features/appearance/rules.js';
22
22
  // --- connectors ---
23
23
  // Full surface of the connectors domain logic. `export *` rather than a hand-listed set,
@@ -12,5 +12,32 @@ export declare const THEME_OPTIONS: ThemeOption[];
12
12
  /** Neutral default accent — hosts are expected to override via their own
13
13
  * config/branding; this only guarantees the CSS vars are always set. */
14
14
  export declare const DEFAULT_ACCENT_COLOR: string;
15
- export declare const ACCENT_SWATCHES: readonly [string, "#c96442", "#7c3aed", "#059669", "#dc2626", "#d97706", "#0891b2", "#db2777"];
15
+ /** One owner for each preset's color and English translation key. Hosts supply translations. */
16
+ export declare const ACCENT_SWATCH_PRESETS: readonly [{
17
+ readonly color: string;
18
+ readonly nameKey: "Charcoal";
19
+ }, {
20
+ readonly color: "#c96442";
21
+ readonly nameKey: "Clay";
22
+ }, {
23
+ readonly color: "#7c3aed";
24
+ readonly nameKey: "Violet";
25
+ }, {
26
+ readonly color: "#059669";
27
+ readonly nameKey: "Green";
28
+ }, {
29
+ readonly color: "#dc2626";
30
+ readonly nameKey: "Red";
31
+ }, {
32
+ readonly color: "#d97706";
33
+ readonly nameKey: "Amber";
34
+ }, {
35
+ readonly color: "#0891b2";
36
+ readonly nameKey: "Teal";
37
+ }, {
38
+ readonly color: "#db2777";
39
+ readonly nameKey: "Pink";
40
+ }];
41
+ /** Keep the existing string palette API for hosts that compose their own swatch lists. */
42
+ export declare const ACCENT_SWATCHES: readonly string[];
16
43
  //# sourceMappingURL=constants.d.ts.map
@@ -11,14 +11,17 @@ export const THEME_OPTIONS = [
11
11
  /** Neutral default accent — hosts are expected to override via their own
12
12
  * config/branding; this only guarantees the CSS vars are always set. */
13
13
  export const DEFAULT_ACCENT_COLOR = defaultAdminTheme.light.primary;
14
- export const ACCENT_SWATCHES = [
15
- DEFAULT_ACCENT_COLOR,
16
- '#c96442',
17
- '#7c3aed',
18
- '#059669',
19
- '#dc2626',
20
- '#d97706',
21
- '#0891b2',
22
- '#db2777',
14
+ /** One owner for each preset's color and English translation key. Hosts supply translations. */
15
+ export const ACCENT_SWATCH_PRESETS = [
16
+ { color: DEFAULT_ACCENT_COLOR, nameKey: 'Charcoal' },
17
+ { color: '#c96442', nameKey: 'Clay' },
18
+ { color: '#7c3aed', nameKey: 'Violet' },
19
+ { color: '#059669', nameKey: 'Green' },
20
+ { color: '#dc2626', nameKey: 'Red' },
21
+ { color: '#d97706', nameKey: 'Amber' },
22
+ { color: '#0891b2', nameKey: 'Teal' },
23
+ { color: '#db2777', nameKey: 'Pink' },
23
24
  ];
25
+ /** Keep the existing string palette API for hosts that compose their own swatch lists. */
26
+ export const ACCENT_SWATCHES = ACCENT_SWATCH_PRESETS.map(({ color }) => color);
24
27
  //# sourceMappingURL=constants.js.map
@@ -4,7 +4,7 @@ import { agentHandleProps, agentSubHandle, buildAgentListHandles } from '@jini-a
4
4
  import { useT } from '../../../i18n/index.js';
5
5
  import { Icon } from '../../../../react/components/Icon.js';
6
6
  import { ACCENT_SWATCHES, DEFAULT_ACCENT_COLOR, applyAppearanceToDocument, normalizeAccentColor, resolveAccentColor, } from '../../../../utils/appearance.js';
7
- import { THEME_OPTIONS } from '../../constants.js';
7
+ import { ACCENT_SWATCH_PRESETS, THEME_OPTIONS } from '../../constants.js';
8
8
  /**
9
9
  * Theme (system/light/dark) segmented control + accent-color swatch grid
10
10
  * with a custom color picker. Origin: `AppearanceSection` in
@@ -17,7 +17,6 @@ export function AppearanceTab({ theme, onThemeChange, accentColor, onAccentColor
17
17
  const swatchHandles = agentHandle ? buildAgentListHandles({ prefix: agentSubHandle({ base: agentHandle, action: 'swatch' }), ids: accentSwatches }) : undefined;
18
18
  const resolvedAriaLabel = ariaLabel ?? t('Appearance');
19
19
  const resolvedAccentLabel = accentLabel ?? t('Accent color');
20
- const resolvedDefaultAccentAriaLabel = defaultAccentAriaLabel ?? t('Default accent color');
21
20
  const resolvedCustomAccentAriaLabel = customAccentAriaLabel ?? t('Custom accent color');
22
21
  const appliedTheme = theme === 'system' ? undefined : theme;
23
22
  useLayoutEffect(() => {
@@ -27,8 +26,9 @@ export function AppearanceTab({ theme, onThemeChange, accentColor, onAccentColor
27
26
  }, [livePreview, appliedTheme, resolvedAccent]);
28
27
  return (_jsxs("section", { className: "jini-settings-section jini-settings-appearance", children: [_jsx("div", { className: "jini-seg-control", role: "group", "aria-label": resolvedAriaLabel, style: { '--seg-cols': THEME_OPTIONS.length }, children: THEME_OPTIONS.map((option) => (_jsxs("button", { type: "button", className: 'jini-seg-btn' + (theme === option.value ? ' active' : ''), "aria-pressed": theme === option.value, onClick: () => onThemeChange(option.value), ...agentHandleProps({}, { base: agentHandle, ...({ action: `theme-${option.value}`, role: 'button', label: t(option.label) }) }), children: [option.icon ? _jsx(Icon, { name: option.icon, size: 14, "aria-hidden": "true" }) : null, _jsx("span", { className: "jini-seg-title", children: t(option.label) })] }, option.value))) }), _jsxs("div", { className: "jini-field", children: [_jsx("span", { className: "jini-field-label", children: resolvedAccentLabel }), _jsxs("div", { className: "jini-accent-swatches", role: "radiogroup", "aria-label": resolvedAccentLabel, children: [accentSwatches.map((color, index) => {
29
28
  const active = resolvedAccent === color;
30
- const swatchLabel = color === DEFAULT_ACCENT_COLOR ? resolvedDefaultAccentAriaLabel : color;
31
- return (_jsx("button", { type: "button", className: `jini-accent-swatch${active ? ' active' : ''}`, style: { background: color }, "aria-label": swatchLabel, "aria-checked": active, role: "radio", onClick: () => onAccentColorChange(normalizeAccentColor({ value: color }) ?? color), ...agentHandleProps({}, { base: swatchHandles?.[index], ...({ role: 'button', label: swatchLabel }) }) }, color));
29
+ const nameKey = ACCENT_SWATCH_PRESETS.find((swatch) => swatch.color === color.toLowerCase())?.nameKey ?? color;
30
+ const swatchLabel = color === DEFAULT_ACCENT_COLOR && defaultAccentAriaLabel !== undefined ? defaultAccentAriaLabel : t(nameKey);
31
+ return (_jsx("button", { type: "button", className: `jini-accent-swatch${active ? ' active' : ''}`, style: { background: color }, "aria-label": swatchLabel, title: color, "aria-checked": active, role: "radio", onClick: () => onAccentColorChange(normalizeAccentColor({ value: color }) ?? color), ...agentHandleProps({}, { base: swatchHandles?.[index], ...({ role: 'button', label: swatchLabel }) }) }, color));
32
32
  }), _jsx("input", { type: "color", "aria-label": resolvedCustomAccentAriaLabel, className: "jini-accent-swatch-picker", value: resolvedAccent, onChange: (event) => onAccentColorChange(event.target.value), ...agentHandleProps({}, { base: agentHandle, ...({ action: 'custom-accent', role: 'field', label: resolvedCustomAccentAriaLabel }) }) })] })] })] }));
33
33
  }
34
34
  //# sourceMappingURL=AppearanceTab.js.map
@@ -15,12 +15,12 @@ export declare const chartSpecSchema: z.ZodObject<{
15
15
  color: z.ZodOptional<z.ZodString>;
16
16
  }, "strict", z.ZodTypeAny, {
17
17
  key: string;
18
- label?: string | undefined;
19
18
  color?: string | undefined;
19
+ label?: string | undefined;
20
20
  }, {
21
21
  key: string;
22
- label?: string | undefined;
23
22
  color?: string | undefined;
23
+ label?: string | undefined;
24
24
  }>, "many">;
25
25
  title: z.ZodOptional<z.ZodString>;
26
26
  description: z.ZodOptional<z.ZodString>;
@@ -122,8 +122,8 @@ export declare const chartSpecSchema: z.ZodObject<{
122
122
  data: Record<string, ChartValue>[];
123
123
  series: {
124
124
  key: string;
125
- label?: string | undefined;
126
125
  color?: string | undefined;
126
+ label?: string | undefined;
127
127
  }[];
128
128
  title?: string | undefined;
129
129
  provider?: string | undefined;
@@ -154,8 +154,8 @@ export declare const chartSpecSchema: z.ZodObject<{
154
154
  data: Record<string, ChartValue>[];
155
155
  series: {
156
156
  key: string;
157
- label?: string | undefined;
158
157
  color?: string | undefined;
158
+ label?: string | undefined;
159
159
  }[];
160
160
  title?: string | undefined;
161
161
  provider?: string | undefined;
@@ -6,7 +6,7 @@ import { InteractiveUiRegistry } from '../interactive-ui/registry.js';
6
6
  import { chartSpecSchema, DEFAULT_CHART_THEME } from './contract.js';
7
7
  import { mountChart } from './browser.js';
8
8
  import { chartCapability, chartProvidersByKind } from './registry.js';
9
- import { chartWireManifests, legacyChartSpec } from './wire.js';
9
+ import { chartWireManifests, legacyChartSpec, withSeriesLabels } from './wire.js';
10
10
  /** React owns only the container; mountChart owns its descendants and lifecycle. */
11
11
  export function ChartHost({ spec, registry, theme = DEFAULT_CHART_THEME, siteProvider, onProviderError }) {
12
12
  const element = useRef(null);
@@ -39,7 +39,7 @@ const LEGACY_CHART_KINDS = ['bar', 'line', 'pie'];
39
39
  function chartComponent(kind, providers, options) {
40
40
  return function RegisteredChart(props) {
41
41
  const legacyKind = LEGACY_CHART_KINDS.find((legacy) => legacy === kind);
42
- const spec = legacyKind ? legacyChartSpec({ kind: legacyKind, props }) : chartSpecSchema.parse(props.spec);
42
+ const spec = withSeriesLabels({ spec: legacyKind ? legacyChartSpec({ kind: legacyKind, props }) : chartSpecSchema.parse(props.spec) });
43
43
  return _jsx(ChartHost, { spec: spec, registry: providers, ...options });
44
44
  };
45
45
  }
@@ -8,32 +8,44 @@ export declare const rechartsBarChartPropsSchema: z.ZodObject<{
8
8
  data: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">;
9
9
  categoryKey: z.ZodString;
10
10
  valueKey: z.ZodString;
11
+ /** The legend/tooltip name for `valueKey` ("Sign-ups"); omitted, the key is humanized. */
12
+ valueLabel: z.ZodOptional<z.ZodString>;
11
13
  color: z.ZodOptional<z.ZodString>;
12
14
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
13
15
  data: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">;
14
16
  categoryKey: z.ZodString;
15
17
  valueKey: z.ZodString;
18
+ /** The legend/tooltip name for `valueKey` ("Sign-ups"); omitted, the key is humanized. */
19
+ valueLabel: z.ZodOptional<z.ZodString>;
16
20
  color: z.ZodOptional<z.ZodString>;
17
21
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
18
22
  data: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">;
19
23
  categoryKey: z.ZodString;
20
24
  valueKey: z.ZodString;
25
+ /** The legend/tooltip name for `valueKey` ("Sign-ups"); omitted, the key is humanized. */
26
+ valueLabel: z.ZodOptional<z.ZodString>;
21
27
  color: z.ZodOptional<z.ZodString>;
22
28
  }, z.ZodTypeAny, "passthrough">>;
23
29
  export declare const rechartsLineChartPropsSchema: z.ZodObject<{
24
30
  data: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">;
25
31
  categoryKey: z.ZodString;
26
32
  valueKey: z.ZodString;
33
+ /** The legend/tooltip name for `valueKey` ("Sign-ups"); omitted, the key is humanized. */
34
+ valueLabel: z.ZodOptional<z.ZodString>;
27
35
  color: z.ZodOptional<z.ZodString>;
28
36
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
29
37
  data: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">;
30
38
  categoryKey: z.ZodString;
31
39
  valueKey: z.ZodString;
40
+ /** The legend/tooltip name for `valueKey` ("Sign-ups"); omitted, the key is humanized. */
41
+ valueLabel: z.ZodOptional<z.ZodString>;
32
42
  color: z.ZodOptional<z.ZodString>;
33
43
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
34
44
  data: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>, "many">;
35
45
  categoryKey: z.ZodString;
36
46
  valueKey: z.ZodString;
47
+ /** The legend/tooltip name for `valueKey` ("Sign-ups"); omitted, the key is humanized. */
48
+ valueLabel: z.ZodOptional<z.ZodString>;
37
49
  color: z.ZodOptional<z.ZodString>;
38
50
  }, z.ZodTypeAny, "passthrough">>;
39
51
  export declare const rechartsPieChartPropsSchema: z.ZodObject<{
@@ -85,4 +97,11 @@ export declare function legacyChartSpec({ kind, props }: {
85
97
  kind: 'bar' | 'line' | 'pie';
86
98
  props: Record<string, unknown>;
87
99
  }, _options?: Record<string, never>): ChartSpec;
100
+ /**
101
+ * Give every series a label, so a provider's legend and tooltip never show a raw data key
102
+ * (owner, 2026-10-30: a sign-ups chart's legend read "signups"). A label the caller passed wins.
103
+ */
104
+ export declare function withSeriesLabels({ spec }: {
105
+ spec: ChartSpec;
106
+ }, _options?: Record<string, never>): ChartSpec;
88
107
  //# sourceMappingURL=wire.d.ts.map
@@ -4,7 +4,9 @@ import { chartSpecSchema } from './contract.js';
4
4
  /** Same passthrough/record reasoning as native data-table: A2UI action bindings and extra row data
5
5
  * belong to the wire protocol, not a renderer. Retain their established shapes during extraction. */
6
6
  export const rechartsBarChartPropsSchema = z.object({
7
- data: z.array(z.record(z.unknown())).min(1), categoryKey: z.string(), valueKey: z.string(), color: z.string().optional(),
7
+ data: z.array(z.record(z.unknown())).min(1), categoryKey: z.string(), valueKey: z.string(),
8
+ /** The legend/tooltip name for `valueKey` ("Sign-ups"); omitted, the key is humanized. */
9
+ valueLabel: z.string().min(1).optional().describe('Human name for valueKey shown in the legend and tooltip, e.g. "Sign-ups". Defaults to the humanized key.'), color: z.string().optional(),
8
10
  }).passthrough();
9
11
  export const rechartsLineChartPropsSchema = rechartsBarChartPropsSchema;
10
12
  export const rechartsPieChartPropsSchema = z.object({
@@ -31,8 +33,24 @@ export function legacyChartSpec({ kind, props }, _options = {}) {
31
33
  const parsed = (kind === 'pie' ? rechartsPieChartPropsSchema : rechartsBarChartPropsSchema).parse(props);
32
34
  return chartSpecSchema.parse({
33
35
  kind, data: parsed.data, categoryKey: kind === 'pie' ? 'name' : parsed.categoryKey,
34
- series: [{ key: kind === 'pie' ? 'value' : parsed.valueKey, ...(parsed.color !== undefined ? { color: parsed.color } : {}) }],
36
+ series: [{
37
+ key: kind === 'pie' ? 'value' : parsed.valueKey,
38
+ ...('valueLabel' in parsed && typeof parsed.valueLabel === 'string' ? { label: parsed.valueLabel } : {}),
39
+ ...(parsed.color !== undefined ? { color: parsed.color } : {}),
40
+ }],
35
41
  ...(typeof parsed.provider === 'string' ? { provider: parsed.provider } : {}),
36
42
  });
37
43
  }
44
+ /** `signups` -> `Signups`, `sign_ups`/`signUps` -> `Sign ups`. Display only; the key still addresses the data. */
45
+ function humanizeSeriesKey(key) {
46
+ const words = key.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ').trim().toLowerCase();
47
+ return words ? words[0].toUpperCase() + words.slice(1) : key;
48
+ }
49
+ /**
50
+ * Give every series a label, so a provider's legend and tooltip never show a raw data key
51
+ * (owner, 2026-10-30: a sign-ups chart's legend read "signups"). A label the caller passed wins.
52
+ */
53
+ export function withSeriesLabels({ spec }, _options = {}) {
54
+ return { ...spec, series: spec.series.map((series) => ({ ...series, label: series.label ?? humanizeSeriesKey(series.key) })) };
55
+ }
38
56
  //# sourceMappingURL=wire.js.map
@@ -70,7 +70,6 @@ export const DEFAULT_AGENT_DESCRIPTIONS = {
70
70
  'cursor-agent': 'Cursor command line',
71
71
  opencode: 'Open-source agent CLI',
72
72
  qwen: 'Qwen coding CLI',
73
- copilot: 'GitHub coding CLI',
74
73
  devin: 'Cognition terminal CLI',
75
74
  kimi: 'Moonshot Kimi CLI',
76
75
  qoder: 'Alibaba coding CLI',
@@ -24,9 +24,8 @@ import type { CaptureFolderPathDropInput } from './types.js';
24
24
  */
25
25
  export declare function folderPathsFromDataTransfer(dataTransfer: DataTransfer, port: FolderPathDropPort): string[];
26
26
  /**
27
- * The text a folder drop inserts: every path, space-joined, in drop order. The one place the format
28
- * lives — a path that itself contains a space is ambiguous in this format, so any change to quoting
29
- * or separators belongs here and nowhere else.
27
+ * The text a folder drop inserts: every path, space-joined, in drop order. Quote whitespace-containing
28
+ * paths so text consumers can distinguish the path from adjacent prose or secrets.
30
29
  *
31
30
  * @complexity Time/space: O(total path length).
32
31
  */
@@ -51,14 +51,13 @@ export function folderPathsFromDataTransfer(dataTransfer, port) {
51
51
  return paths;
52
52
  }
53
53
  /**
54
- * The text a folder drop inserts: every path, space-joined, in drop order. The one place the format
55
- * lives — a path that itself contains a space is ambiguous in this format, so any change to quoting
56
- * or separators belongs here and nowhere else.
54
+ * The text a folder drop inserts: every path, space-joined, in drop order. Quote whitespace-containing
55
+ * paths so text consumers can distinguish the path from adjacent prose or secrets.
57
56
  *
58
57
  * @complexity Time/space: O(total path length).
59
58
  */
60
59
  export function formatDroppedFolderPaths(paths) {
61
- return paths.join(' ');
60
+ return paths.map(path => /\s/.test(path) ? `"${path}"` : path).join(' ');
62
61
  }
63
62
  /**
64
63
  * A capture-phase drop handler's whole decision. A drop carrying at least one folder with a
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_AR = {
68
68
  'Custom instructions': 'تعليمات مخصصة',
69
69
  'Custom model id': 'معرّف نموذج مخصص',
70
70
  'Custom…': 'مخصص…',
71
+ 'Charcoal': 'فحمي',
72
+ 'Clay': 'طيني',
73
+ 'Violet': 'بنفسجي',
74
+ 'Green': 'أخضر',
75
+ 'Red': 'أحمر',
76
+ 'Amber': 'كهرماني',
77
+ 'Teal': 'أزرق مخضر',
78
+ 'Pink': 'وردي',
71
79
  'Default accent color': 'لون التمييز الافتراضي',
72
80
  'Default endpoint. Usually no need to change this.': 'نقطة النهاية الافتراضية. عادةً لا حاجة لتغيير هذا.',
73
81
  'Default model': 'النموذج الافتراضي',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_BN = {
68
68
  'Custom instructions': 'কাস্টম নির্দেশনা',
69
69
  'Custom model id': 'কাস্টম মডেল আইডি',
70
70
  'Custom…': 'কাস্টম…',
71
+ 'Charcoal': 'কয়লা',
72
+ 'Clay': 'মাটি',
73
+ 'Violet': 'বেগুনি',
74
+ 'Green': 'সবুজ',
75
+ 'Red': 'লাল',
76
+ 'Amber': 'অ্যাম্বার',
77
+ 'Teal': 'নীলচে সবুজ',
78
+ 'Pink': 'গোলাপি',
71
79
  'Default accent color': 'ডিফল্ট হাইলাইট রং',
72
80
  'Default endpoint. Usually no need to change this.': 'ডিফল্ট এন্ডপয়েন্ট। সাধারণত এটি পরিবর্তনের প্রয়োজন হয় না।',
73
81
  'Default model': 'ডিফল্ট মডেল',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_DE = {
68
68
  'Custom instructions': 'Benutzerdefinierte Anweisungen',
69
69
  'Custom model id': 'Benutzerdefinierte Modell-ID',
70
70
  'Custom…': 'Benutzerdefiniert…',
71
+ 'Charcoal': 'Anthrazit',
72
+ 'Clay': 'Ton',
73
+ 'Violet': 'Violett',
74
+ 'Green': 'Grün',
75
+ 'Red': 'Rot',
76
+ 'Amber': 'Bernstein',
77
+ 'Teal': 'Petrol',
78
+ 'Pink': 'Pink',
71
79
  'Default accent color': 'Standard-Akzentfarbe',
72
80
  'Default endpoint. Usually no need to change this.': 'Standard-Endpunkt. Muss normalerweise nicht geändert werden.',
73
81
  'Default model': 'Standardmodell',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_EN = {
68
68
  'Custom instructions': 'Custom instructions',
69
69
  'Custom model id': 'Custom model id',
70
70
  'Custom…': 'Custom…',
71
+ 'Charcoal': 'Charcoal',
72
+ 'Clay': 'Clay',
73
+ 'Violet': 'Violet',
74
+ 'Green': 'Green',
75
+ 'Red': 'Red',
76
+ 'Amber': 'Amber',
77
+ 'Teal': 'Teal',
78
+ 'Pink': 'Pink',
71
79
  'Default accent color': 'Default accent color',
72
80
  'Default endpoint. Usually no need to change this.': 'Default endpoint. Usually no need to change this.',
73
81
  'Default model': 'Default model',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_ES = {
68
68
  'Custom instructions': 'Instrucciones personalizadas',
69
69
  'Custom model id': 'Id. de modelo personalizado',
70
70
  'Custom…': 'Personalizado…',
71
+ 'Charcoal': 'Carbón',
72
+ 'Clay': 'Arcilla',
73
+ 'Violet': 'Violeta',
74
+ 'Green': 'Verde',
75
+ 'Red': 'Rojo',
76
+ 'Amber': 'Ámbar',
77
+ 'Teal': 'Verde azulado',
78
+ 'Pink': 'Rosa',
71
79
  'Default accent color': 'Color de acento predeterminado',
72
80
  'Default endpoint. Usually no need to change this.': 'Endpoint predeterminado. Normalmente no hace falta cambiarlo.',
73
81
  'Default model': 'Modelo predeterminado',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_FA = {
68
68
  'Custom instructions': 'دستورالعمل‌های سفارشی',
69
69
  'Custom model id': 'شناسهٔ مدل سفارشی',
70
70
  'Custom…': 'سفارشی…',
71
+ 'Charcoal': 'زغالی',
72
+ 'Clay': 'سفالی',
73
+ 'Violet': 'بنفش',
74
+ 'Green': 'سبز',
75
+ 'Red': 'قرمز',
76
+ 'Amber': 'کهربایی',
77
+ 'Teal': 'سبزآبی',
78
+ 'Pink': 'صورتی',
71
79
  'Default accent color': 'رنگ تأکیدی پیش‌فرض',
72
80
  'Default endpoint. Usually no need to change this.': 'نقطهٔ پایانی پیش‌فرض. معمولاً نیازی به تغییر آن نیست.',
73
81
  'Default model': 'مدل پیش‌فرض',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_FR = {
68
68
  'Custom instructions': 'Instructions personnalisées',
69
69
  'Custom model id': 'Identifiant de modèle personnalisé',
70
70
  'Custom…': 'Personnalisé…',
71
+ 'Charcoal': 'Anthracite',
72
+ 'Clay': 'Argile',
73
+ 'Violet': 'Violet',
74
+ 'Green': 'Vert',
75
+ 'Red': 'Rouge',
76
+ 'Amber': 'Ambre',
77
+ 'Teal': 'Bleu canard',
78
+ 'Pink': 'Rose',
71
79
  'Default accent color': 'Couleur d\'accent par défaut',
72
80
  'Default endpoint. Usually no need to change this.': 'Point de terminaison par défaut. Il n\'est généralement pas nécessaire de le modifier.',
73
81
  'Default model': 'Modèle par défaut',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_HI = {
68
68
  'Custom instructions': 'कस्टम निर्देश',
69
69
  'Custom model id': 'कस्टम मॉडल आईडी',
70
70
  'Custom…': 'कस्टम…',
71
+ 'Charcoal': 'चारकोल',
72
+ 'Clay': 'मिट्टी',
73
+ 'Violet': 'बैंगनी',
74
+ 'Green': 'हरा',
75
+ 'Red': 'लाल',
76
+ 'Amber': 'एम्बर',
77
+ 'Teal': 'हरित-नील',
78
+ 'Pink': 'गुलाबी',
71
79
  'Default accent color': 'डिफ़ॉल्ट हाइलाइट रंग',
72
80
  'Default endpoint. Usually no need to change this.': 'डिफ़ॉल्ट एंडपॉइंट। आमतौर पर इसे बदलने की आवश्यकता नहीं होती।',
73
81
  'Default model': 'डिफ़ॉल्ट मॉडल',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_HU = {
68
68
  'Custom instructions': 'Egyéni utasítások',
69
69
  'Custom model id': 'Egyéni modellazonosító',
70
70
  'Custom…': 'Egyéni…',
71
+ 'Charcoal': 'Grafit',
72
+ 'Clay': 'Agyag',
73
+ 'Violet': 'Ibolya',
74
+ 'Green': 'Zöld',
75
+ 'Red': 'Piros',
76
+ 'Amber': 'Borostyán',
77
+ 'Teal': 'Kékeszöld',
78
+ 'Pink': 'Rózsaszín',
71
79
  'Default accent color': 'Alapértelmezett kiemelőszín',
72
80
  'Default endpoint. Usually no need to change this.': 'Alapértelmezett végpont. Általában nem szükséges módosítani.',
73
81
  'Default model': 'Alapértelmezett modell',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_ID = {
68
68
  'Custom instructions': 'Instruksi kustom',
69
69
  'Custom model id': 'ID model kustom',
70
70
  'Custom…': 'Kustom…',
71
+ 'Charcoal': 'Arang',
72
+ 'Clay': 'Tanah liat',
73
+ 'Violet': 'Ungu',
74
+ 'Green': 'Hijau',
75
+ 'Red': 'Merah',
76
+ 'Amber': 'Kuning amber',
77
+ 'Teal': 'Hijau kebiruan',
78
+ 'Pink': 'Merah muda',
71
79
  'Default accent color': 'Warna aksen default',
72
80
  'Default endpoint. Usually no need to change this.': 'Endpoint default. Biasanya tidak perlu diubah.',
73
81
  'Default model': 'Model default',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_IT = {
68
68
  'Custom instructions': 'Istruzioni personalizzate',
69
69
  'Custom model id': 'ID modello personalizzato',
70
70
  'Custom…': 'Personalizzato…',
71
+ 'Charcoal': 'Antracite',
72
+ 'Clay': 'Argilla',
73
+ 'Violet': 'Viola',
74
+ 'Green': 'Verde',
75
+ 'Red': 'Rosso',
76
+ 'Amber': 'Ambra',
77
+ 'Teal': 'Verde acqua',
78
+ 'Pink': 'Rosa',
71
79
  'Default accent color': 'Colore di accento predefinito',
72
80
  'Default endpoint. Usually no need to change this.': 'Endpoint predefinito. Di solito non è necessario modificarlo.',
73
81
  'Default model': 'Modello predefinito',
@@ -69,6 +69,14 @@ export const SETTINGS_DIALOG_JA = {
69
69
  'Custom instructions': 'カスタム指示',
70
70
  'Custom model id': 'カスタムモデルID',
71
71
  'Custom…': 'カスタム…',
72
+ 'Charcoal': 'チャコール',
73
+ 'Clay': 'クレイ',
74
+ 'Violet': 'バイオレット',
75
+ 'Green': 'グリーン',
76
+ 'Red': 'レッド',
77
+ 'Amber': 'アンバー',
78
+ 'Teal': 'ティール',
79
+ 'Pink': 'ピンク',
72
80
  'Default accent color': '既定のアクセントカラー',
73
81
  'Default endpoint. Usually no need to change this.': '既定のエンドポイントです。通常は変更する必要はありません。',
74
82
  'Default model': '既定のモデル',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_KO = {
68
68
  'Custom instructions': '사용자 지정 지침',
69
69
  'Custom model id': '사용자 지정 모델 ID',
70
70
  'Custom…': '사용자 지정…',
71
+ 'Charcoal': '차콜',
72
+ 'Clay': '클레이',
73
+ 'Violet': '바이올렛',
74
+ 'Green': '초록',
75
+ 'Red': '빨강',
76
+ 'Amber': '호박색',
77
+ 'Teal': '청록',
78
+ 'Pink': '분홍',
71
79
  'Default accent color': '기본 강조 색상',
72
80
  'Default endpoint. Usually no need to change this.': '기본 엔드포인트입니다. 보통 변경할 필요가 없습니다.',
73
81
  'Default model': '기본 모델',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_PL = {
68
68
  'Custom instructions': 'Niestandardowe instrukcje',
69
69
  'Custom model id': 'Niestandardowy identyfikator modelu',
70
70
  'Custom…': 'Niestandardowy…',
71
+ 'Charcoal': 'Grafitowy',
72
+ 'Clay': 'Gliniany',
73
+ 'Violet': 'Fioletowy',
74
+ 'Green': 'Zielony',
75
+ 'Red': 'Czerwony',
76
+ 'Amber': 'Bursztynowy',
77
+ 'Teal': 'Morski',
78
+ 'Pink': 'Różowy',
71
79
  'Default accent color': 'Domyślny kolor akcentu',
72
80
  'Default endpoint. Usually no need to change this.': 'Domyślny punkt końcowy. Zwykle nie trzeba go zmieniać.',
73
81
  'Default model': 'Model domyślny',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_PT_BR = {
68
68
  'Custom instructions': 'Instruções personalizadas',
69
69
  'Custom model id': 'ID de modelo personalizado',
70
70
  'Custom…': 'Personalizado…',
71
+ 'Charcoal': 'Carvão',
72
+ 'Clay': 'Argila',
73
+ 'Violet': 'Violeta',
74
+ 'Green': 'Verde',
75
+ 'Red': 'Vermelho',
76
+ 'Amber': 'Âmbar',
77
+ 'Teal': 'Azul-petróleo',
78
+ 'Pink': 'Rosa',
71
79
  'Default accent color': 'Cor de destaque padrão',
72
80
  'Default endpoint. Usually no need to change this.': 'Endpoint padrão. Geralmente não é necessário alterar isso.',
73
81
  'Default model': 'Modelo padrão',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_RU = {
68
68
  'Custom instructions': 'Пользовательские инструкции',
69
69
  'Custom model id': 'Пользовательский id. модели',
70
70
  'Custom…': 'Другое…',
71
+ 'Charcoal': 'Угольный',
72
+ 'Clay': 'Глиняный',
73
+ 'Violet': 'Фиолетовый',
74
+ 'Green': 'Зелёный',
75
+ 'Red': 'Красный',
76
+ 'Amber': 'Янтарный',
77
+ 'Teal': 'Бирюзовый',
78
+ 'Pink': 'Розовый',
71
79
  'Default accent color': 'Акцентный цвет по умолчанию',
72
80
  'Default endpoint. Usually no need to change this.': 'Конечная точка по умолчанию. Обычно менять её не требуется.',
73
81
  'Default model': 'Модель по умолчанию',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_TH = {
68
68
  'Custom instructions': 'คำสั่งที่กำหนดเอง',
69
69
  'Custom model id': 'รหัสโมเดลที่กำหนดเอง',
70
70
  'Custom…': 'กำหนดเอง…',
71
+ 'Charcoal': 'ถ่าน',
72
+ 'Clay': 'ดินเผา',
73
+ 'Violet': 'ม่วง',
74
+ 'Green': 'เขียว',
75
+ 'Red': 'แดง',
76
+ 'Amber': 'อำพัน',
77
+ 'Teal': 'เขียวน้ำทะเล',
78
+ 'Pink': 'ชมพู',
71
79
  'Default accent color': 'สีเน้นเริ่มต้น',
72
80
  'Default endpoint. Usually no need to change this.': 'เอนด์พอยต์เริ่มต้น โดยปกติไม่จำเป็นต้องเปลี่ยนค่านี้',
73
81
  'Default model': 'โมเดลเริ่มต้น',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_TR = {
68
68
  'Custom instructions': 'Özel talimatlar',
69
69
  'Custom model id': 'Özel model kimliği',
70
70
  'Custom…': 'Özel…',
71
+ 'Charcoal': 'Kömür',
72
+ 'Clay': 'Kil',
73
+ 'Violet': 'Mor',
74
+ 'Green': 'Yeşil',
75
+ 'Red': 'Kırmızı',
76
+ 'Amber': 'Kehribar',
77
+ 'Teal': 'Camgöbeği',
78
+ 'Pink': 'Pembe',
71
79
  'Default accent color': 'Varsayılan vurgu rengi',
72
80
  'Default endpoint. Usually no need to change this.': 'Varsayılan uç nokta. Genellikle bunu değiştirmeye gerek yoktur.',
73
81
  'Default model': 'Varsayılan model',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_UK = {
68
68
  'Custom instructions': 'Власні інструкції',
69
69
  'Custom model id': 'Власний ідентифікатор моделі',
70
70
  'Custom…': 'Власний…',
71
+ 'Charcoal': 'Вугільний',
72
+ 'Clay': 'Глиняний',
73
+ 'Violet': 'Фіолетовий',
74
+ 'Green': 'Зелений',
75
+ 'Red': 'Червоний',
76
+ 'Amber': 'Бурштиновий',
77
+ 'Teal': 'Бірюзовий',
78
+ 'Pink': 'Рожевий',
71
79
  'Default accent color': 'Акцентний колір за замовчуванням',
72
80
  'Default endpoint. Usually no need to change this.': 'Кінцева точка за замовчуванням. Зазвичай змінювати її не потрібно.',
73
81
  'Default model': 'Модель за замовчуванням',
@@ -68,6 +68,14 @@ export const SETTINGS_DIALOG_UR = {
68
68
  'Custom instructions': 'کسٹم ہدایات',
69
69
  'Custom model id': 'کسٹم ماڈل آئی ڈی',
70
70
  'Custom…': 'کسٹم…',
71
+ 'Charcoal': 'کوئلہ',
72
+ 'Clay': 'مٹی',
73
+ 'Violet': 'بنفشی',
74
+ 'Green': 'سبز',
75
+ 'Red': 'سرخ',
76
+ 'Amber': 'کہربائی',
77
+ 'Teal': 'نیلگوں سبز',
78
+ 'Pink': 'گلابی',
71
79
  'Default accent color': 'ڈیفالٹ نمایاں رنگ',
72
80
  'Default endpoint. Usually no need to change this.': 'ڈیفالٹ اینڈ پوائنٹ۔ عام طور پر اسے تبدیل کرنے کی ضرورت نہیں ہوتی۔',
73
81
  'Default model': 'ڈیفالٹ ماڈل',
@@ -69,6 +69,14 @@ export const SETTINGS_DIALOG_ZH_CN = {
69
69
  'Custom instructions': '自定义指令',
70
70
  'Custom model id': '自定义模型 ID',
71
71
  'Custom…': '自定义…',
72
+ 'Charcoal': '炭灰',
73
+ 'Clay': '陶土',
74
+ 'Violet': '紫罗兰',
75
+ 'Green': '绿色',
76
+ 'Red': '红色',
77
+ 'Amber': '琥珀色',
78
+ 'Teal': '青色',
79
+ 'Pink': '粉色',
72
80
  'Default accent color': '默认强调色',
73
81
  'Default endpoint. Usually no need to change this.': '默认终结点,通常无需更改。',
74
82
  'Default model': '默认模型',
@@ -69,6 +69,14 @@ export const SETTINGS_DIALOG_ZH_TW = {
69
69
  'Custom instructions': '自訂指令',
70
70
  'Custom model id': '自訂模型 ID',
71
71
  'Custom…': '自訂…',
72
+ 'Charcoal': '炭灰',
73
+ 'Clay': '陶土',
74
+ 'Violet': '紫羅蘭',
75
+ 'Green': '綠色',
76
+ 'Red': '紅色',
77
+ 'Amber': '琥珀色',
78
+ 'Teal': '青色',
79
+ 'Pink': '粉紅色',
72
80
  'Default accent color': '預設強調色',
73
81
  'Default endpoint. Usually no need to change this.': '預設端點,通常不需要變更。',
74
82
  'Default model': '預設模型',
@@ -13,6 +13,8 @@ export interface CheckboxProps {
13
13
  */
14
14
  readonly required?: boolean;
15
15
  readonly disabled?: boolean;
16
+ /** Visually hide the label (it repeats the card title); it stays the accessible name. */
17
+ readonly labelHidden?: boolean;
16
18
  /**
17
19
  * One extra `data-*` attribute on the `<input>`, escaped like every other value. For a caller
18
20
  * that needs to look the box up by something other than its DOM `name` — e.g. the confirmation
@@ -22,6 +22,8 @@ export interface ChoiceGroupProps {
22
22
  */
23
23
  readonly required?: boolean;
24
24
  readonly disabled?: boolean;
25
+ /** Visually hide the legend (it repeats the card title); it stays the group's accessible name. */
26
+ readonly labelHidden?: boolean;
25
27
  }
26
28
  /**
27
29
  * Renders one grouped-choice field.
@@ -30,7 +30,7 @@
30
30
  * value unless normalised — see `form.ts`'s `readChecked`, which is written against this fact.
31
31
  */
32
32
  import { escapeHtml } from '../escape.js';
33
- import { fieldElementId } from './document.js';
33
+ import { fieldElementId, VISUALLY_HIDDEN_CLASS } from './document.js';
34
34
  /**
35
35
  * Renders one grouped-choice field.
36
36
  *
@@ -63,7 +63,7 @@ export function renderChoiceGroup(props) {
63
63
  })
64
64
  .join('\n');
65
65
  return (`<fieldset class="mcpui-field mcpui-choice-group"${describedBy}>\n` +
66
- `<legend class="mcpui-label">${escapeHtml(props.label)}${required}</legend>${hint}\n` +
66
+ `<legend class="mcpui-label${props.labelHidden === true ? ` ${VISUALLY_HIDDEN_CLASS}` : ''}">${escapeHtml(props.label)}${required}</legend>${hint}\n` +
67
67
  `${options}\n` +
68
68
  `</fieldset>`);
69
69
  }
@@ -10,7 +10,7 @@ export type { SurfaceDetail } from './structured-details.js';
10
10
  * be a color no caller could reskin. `var(--x, fallback)` appears only where a value is genuinely
11
11
  * optional.
12
12
  */
13
- 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); }";
13
+ 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 dt .mcpui-hint { display: block; font-weight: 400; }\n/* A nested group (a tool's input fields) gets the card's full width, not the narrow value column. */\n.mcpui-details > dt:has(+ dd > .mcpui-detail-group),\n.mcpui-details > dd:has(> .mcpui-detail-group) { grid-column: 1 / -1; }\n.mcpui-details > dt:has(+ dd > .mcpui-detail-group) { margin-bottom: 6px; }\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-visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }\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); }";
14
14
  /**
15
15
  * The Content-Security-Policy every generated surface declares in its own `<head>`.
16
16
  *
@@ -118,6 +118,14 @@ export declare const DEFAULT_SURFACE_STATUS_TEXT: SurfaceStatusText;
118
118
  * the builders make no assumption about it beyond ES5.
119
119
  */
120
120
  export declare const SURFACE_SCRIPT_PRELUDE: string;
121
+ /**
122
+ * A field name that is simultaneously an HTML `name`, part of a DOM `id`, and a key in the JSON
123
+ * params object the surface posts back. Anything outside this set breaks at least one of those
124
+ * three silently — a space in a `name` produces an `id` the `for` attribute cannot reference, so
125
+ * clicking the label stops focusing the control and nothing reports an error.
126
+ */
127
+ /** Hides an element visually while keeping it in the accessibility tree (see `.mcpui-visually-hidden` in {@link SURFACE_BASE_CSS}). */
128
+ export declare const VISUALLY_HIDDEN_CLASS = "mcpui-visually-hidden";
121
129
  /**
122
130
  * Builds the DOM id for a field's control, validating the name on the way through.
123
131
  *
@@ -136,6 +144,8 @@ export declare function renderFieldLabel(spec: {
136
144
  label: string;
137
145
  required?: boolean;
138
146
  hint?: string;
147
+ /** Visually hide the label (it repeats the card title); it stays the control's accessible name. */
148
+ labelHidden?: boolean;
139
149
  }): string;
140
150
  /** The `aria-describedby` attribute pointing at a field's hint, or `''` when it has none. */
141
151
  export declare function fieldDescribedBy(spec: {
@@ -70,6 +70,11 @@ body {
70
70
  }
71
71
  .mcpui-details dt { color: var(--jini-mcpui-text-soft); font-weight: 500; overflow-wrap: anywhere; }
72
72
  .mcpui-details dd { margin: 0; overflow-wrap: anywhere; color: var(--jini-mcpui-text); }
73
+ .mcpui-details dt .mcpui-hint { display: block; font-weight: 400; }
74
+ /* A nested group (a tool's input fields) gets the card's full width, not the narrow value column. */
75
+ .mcpui-details > dt:has(+ dd > .mcpui-detail-group),
76
+ .mcpui-details > dd:has(> .mcpui-detail-group) { grid-column: 1 / -1; }
77
+ .mcpui-details > dt:has(+ dd > .mcpui-detail-group) { margin-bottom: 6px; }
73
78
  .mcpui-details-outcome { display: flex; flex-direction: column; gap: 8px; }
74
79
  .mcpui-outcome-row { display: flex; align-items: flex-start; gap: 14px; }
75
80
  .mcpui-outcome-row dt { flex: 1; min-width: 0; overflow-wrap: anywhere; }
@@ -118,6 +123,7 @@ body {
118
123
  .mcpui-field { display: flex; flex-direction: column; gap: 4px; }
119
124
  .mcpui-field-inline { flex-direction: row; align-items: flex-start; gap: 8px; }
120
125
  .mcpui-label { font-weight: 500; color: var(--jini-mcpui-text-strong); }
126
+ .mcpui-visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
121
127
  .mcpui-required { color: var(--jini-mcpui-accent); margin-left: 2px; }
122
128
  .mcpui-hint { color: var(--jini-mcpui-text-soft); font-size: 13px; }
123
129
  .mcpui-input, .mcpui-select, .mcpui-textarea {
@@ -382,6 +388,8 @@ function reportCallFailure(error) {
382
388
  * three silently — a space in a `name` produces an `id` the `for` attribute cannot reference, so
383
389
  * clicking the label stops focusing the control and nothing reports an error.
384
390
  */
391
+ /** Hides an element visually while keeping it in the accessibility tree (see `.mcpui-visually-hidden` in {@link SURFACE_BASE_CSS}). */
392
+ export const VISUALLY_HIDDEN_CLASS = 'mcpui-visually-hidden';
385
393
  const FIELD_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
386
394
  /**
387
395
  * Builds the DOM id for a field's control, validating the name on the way through.
@@ -406,7 +414,7 @@ export function renderFieldLabel(spec) {
406
414
  const hint = spec.hint === undefined
407
415
  ? ''
408
416
  : `\n<span class="mcpui-hint" id="${escapeHtml(fieldElementId(spec.name))}-hint">${escapeHtml(spec.hint)}</span>`;
409
- return `<label class="mcpui-label" for="${escapeHtml(fieldElementId(spec.name))}">${escapeHtml(spec.label)}${required}</label>${hint}`;
417
+ return `<label class="mcpui-label${spec.labelHidden === true ? ` ${VISUALLY_HIDDEN_CLASS}` : ''}" for="${escapeHtml(fieldElementId(spec.name))}">${escapeHtml(spec.label)}${required}</label>${hint}`;
410
418
  }
411
419
  /** The `aria-describedby` attribute pointing at a field's hint, or `''` when it has none. */
412
420
  export function fieldDescribedBy(spec) {
@@ -72,15 +72,21 @@ export interface MultiEnumField extends SurfaceFieldBase {
72
72
  readonly options: readonly SelectOption[];
73
73
  }
74
74
  export type SurfaceField = StringField | NumberField | BooleanField | EnumField | MultiEnumField;
75
+ /** Presentation the form decides for a field, not part of the field's own data. */
76
+ export interface FieldRenderOptions {
77
+ /** The label repeats the card title: keep it as the accessible name, but don't show it twice. */
78
+ readonly labelHidden?: boolean;
79
+ }
75
80
  /**
76
81
  * Renders the control fragment for one field, dispatching on its kind.
77
82
  *
78
83
  * @param field - See {@link SurfaceField}.
79
84
  * @param text - Optional host translations for secret reveal controls.
85
+ * @param options - See {@link FieldRenderOptions}.
80
86
  * @returns An HTML fragment.
81
87
  * @complexity O(n) in the rendered length.
82
88
  */
83
- export declare function renderFieldControl(field: SurfaceField, text?: Partial<SecretInputText>): string;
89
+ export declare function renderFieldControl(field: SurfaceField, text?: Partial<SecretInputText>, options?: FieldRenderOptions): string;
84
90
  /** What the surface's inline script needs to read a field's value back and coerce it — the runtime half of {@link SurfaceField}, with the presentation stripped out. */
85
91
  export interface FieldReadSpec {
86
92
  readonly name: string;
@@ -31,41 +31,42 @@ function present(source, keys) {
31
31
  }
32
32
  return picked;
33
33
  }
34
- function base(field) {
35
- return { name: field.name, label: field.label, ...present(field, ['hint', 'required', 'disabled']) };
34
+ function base(field, options = {}) {
35
+ return { name: field.name, label: field.label, ...present(field, ['hint', 'required', 'disabled']), ...(options.labelHidden === true ? { labelHidden: true } : {}) };
36
36
  }
37
37
  /** One-of-N, drawn as radios or a select per {@link EnumField.presentation}. @complexity O(n) in the rendered length. */
38
- function renderEnumField(field) {
38
+ function renderEnumField(field, options) {
39
39
  if (field.presentation === 'radio') {
40
40
  return renderChoiceGroup({
41
- ...base(field),
41
+ ...base(field, options),
42
42
  selection: 'single',
43
43
  options: field.options,
44
44
  ...(field.value === undefined ? {} : { value: [field.value] }),
45
45
  });
46
46
  }
47
- return renderSelect({ ...base(field), options: field.options, ...present(field, ['value', 'placeholder']) });
47
+ return renderSelect({ ...base(field, options), options: field.options, ...present(field, ['value', 'placeholder']) });
48
48
  }
49
49
  /**
50
50
  * Renders the control fragment for one field, dispatching on its kind.
51
51
  *
52
52
  * @param field - See {@link SurfaceField}.
53
53
  * @param text - Optional host translations for secret reveal controls.
54
+ * @param options - See {@link FieldRenderOptions}.
54
55
  * @returns An HTML fragment.
55
56
  * @complexity O(n) in the rendered length.
56
57
  */
57
- export function renderFieldControl(field, text) {
58
+ export function renderFieldControl(field, text, options = {}) {
58
59
  switch (field.kind) {
59
60
  case 'string':
60
- return renderTextInput({ ...base(field), ...text, ...present(field, ['value', 'placeholder', 'multiline', 'rows', 'secret']) });
61
+ return renderTextInput({ ...base(field, options), ...text, ...present(field, ['value', 'placeholder', 'multiline', 'rows', 'secret']) });
61
62
  case 'number':
62
- return renderTextInput({ ...base(field), inputType: 'number', ...present(field, ['value', 'placeholder', 'min', 'max', 'step']) });
63
+ return renderTextInput({ ...base(field, options), inputType: 'number', ...present(field, ['value', 'placeholder', 'min', 'max', 'step']) });
63
64
  case 'boolean':
64
- return renderCheckbox({ ...base(field), ...present(field, ['value']) });
65
+ return renderCheckbox({ ...base(field, options), ...present(field, ['value']) });
65
66
  case 'enum':
66
- return renderEnumField(field);
67
+ return renderEnumField(field, options);
67
68
  case 'multi-enum':
68
- return renderChoiceGroup({ ...base(field), selection: 'multiple', options: field.options, ...present(field, ['value']) });
69
+ return renderChoiceGroup({ ...base(field, options), selection: 'multiple', options: field.options, ...present(field, ['value']) });
69
70
  }
70
71
  return unhandledCase(field);
71
72
  }
@@ -54,10 +54,15 @@ function formActions(spec) {
54
54
  actions.push({ id: 'cancel', label: spec.cancel.label, variant: 'neutral' });
55
55
  return actions;
56
56
  }
57
+ /** Case- and whitespace-insensitive equality: a field label that only repeats the card title is the same question. */
58
+ function sameText(a, b) {
59
+ const norm = (s) => s.trim().replace(/\s+/g, ' ').toLowerCase();
60
+ return norm(a) === norm(b);
61
+ }
57
62
  /** Header, context, the `<form>` itself and the status region. @complexity O(n) in the rendered length. */
58
63
  function formBodyHtml(spec, text) {
59
64
  const controls = spec.fields
60
- .map((field) => renderFieldControl(field, { showSecret: text.showSecret, hideSecret: text.hideSecret }))
65
+ .map((field) => renderFieldControl(field, { showSecret: text.showSecret, hideSecret: text.hideSecret }, { labelHidden: sameText(field.label, spec.title) }))
61
66
  .join('\n');
62
67
  const warning = spec.warning === undefined ? '' : `<p class="mcpui-warning">${escapeHtml(spec.warning)}</p>`;
63
68
  return [
@@ -23,6 +23,8 @@ export interface SelectProps {
23
23
  /** Label for that leading empty option. Defaults to an em-dash. */
24
24
  readonly placeholder?: string;
25
25
  readonly disabled?: boolean;
26
+ /** Visually hide the label (it repeats the card title); it stays the accessible name. */
27
+ readonly labelHidden?: boolean;
26
28
  }
27
29
  /**
28
30
  * Renders one enumerated field.
@@ -16,6 +16,8 @@ export interface TextInputProps extends Partial<SecretInputText> {
16
16
  readonly hint?: string;
17
17
  readonly required?: boolean;
18
18
  readonly disabled?: boolean;
19
+ /** Visually hide the label (it repeats the card title); it stays the accessible name. */
20
+ readonly labelHidden?: boolean;
19
21
  /** `'number'` also constrains the on-screen keyboard on touch devices, which `'text'` would not. */
20
22
  readonly inputType?: 'text' | 'number';
21
23
  /** Renders as `<input type="password">`, or a masked textarea when multiline — either way with
@@ -7,15 +7,16 @@ function record({ value }, _options = {}) {
7
7
  const candidate = { value };
8
8
  return isNonArrayRecord(candidate) ? candidate.value : {};
9
9
  }
10
- /** Project inline JSON-schema property copy into the existing dotted-path label contract.
11
- * No reference resolution or schema validation; unknown fields keep the renderer's fallback.
10
+ /** Project inline JSON-schema property titles into the existing dotted-path label contract.
11
+ * Only `title`: `description` is guidance written for the model (often a paragraph), not a label;
12
+ * untitled fields keep the renderer's humanized-key fallback. No reference resolution or validation.
12
13
  * @complexity O(n * depth) time and space in property-path bytes; no effects or input mutation. */
13
14
  function schemaLabels({ schema, path = '' }, { text }) {
14
15
  const properties = record({ value: record({ value: schema }).properties });
15
16
  return Object.fromEntries(Object.entries(properties).flatMap(([key, property]) => {
16
17
  const field = record({ value: property });
17
18
  const fieldPath = path ? `${path}.${key}` : key;
18
- const label = nonBlank({ value: field.title }) ?? nonBlank({ value: field.description });
19
+ const label = nonBlank({ value: field.title });
19
20
  return [
20
21
  ...(label ? [[fieldPath, text({ key: label })]] : []),
21
22
  ...Object.entries(schemaLabels({ schema: field, path: fieldPath }, { text })),
@@ -12,7 +12,6 @@ const ICON_EXT = {
12
12
  gemini: 'svg',
13
13
  opencode: 'svg',
14
14
  'cursor-agent': 'svg',
15
- copilot: 'svg',
16
15
  qwen: 'svg',
17
16
  qoder: 'svg',
18
17
  deepseek: 'svg',
@@ -7,7 +7,6 @@ export const AGENT_ICON_DATA_URIS = {
7
7
  "antigravity": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0yIC0xIDI4IDI4Ij48cGF0aCBkPSJNMjEuNzUxIDIyLjYwN2MxLjM0IDEuMDA1IDMuMzUuMzM1IDEuNTA4LTEuNTA4QzE3LjczIDE1Ljc0IDE4LjkwNCAxIDEyLjAzNyAxIDUuMTcgMSA2LjM0MiAxNS43NC44MTUgMjEuMWMtMi4wMSAyLjAwOS4xNjcgMi41MTEgMS41MDcgMS41MDYgNS4xOTItMy41MTcgNC44NTctOS43MTQgOS43MTUtOS43MTQgNC44NTcgMCA0LjUyMiA2LjE5NyA5LjcxNCA5LjcxNXoiIGZpbGw9InVybCgjYWcpIi8+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJhZyIgeDE9IjIiIHkxPSIxMiIgeDI9IjIyIiB5Mj0iMTIiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjNDI4NUY0Ii8+PHN0b3Agb2Zmc2V0PSIuMzMiIHN0b3AtY29sb3I9IiNFQTQzMzUiLz48c3RvcCBvZmZzZXQ9Ii42NiIgc3RvcC1jb2xvcj0iI0ZCQkMwNCIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzM0QTg1MyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjwvc3ZnPgo=",
8
8
  "claude": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iI2Q5Nzc1NyIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMjAuOTk4IDEwLjk0OUgyNHYzLjEwMmgtM3YzLjAyOGgtMS40ODdWMjBIMTh2LTIuOTIxaC0xLjQ4N1YyMEgxNXYtMi45MjFIOVYyMEg3LjQ4OHYtMi45MjFINlYyMEg0LjQ4N3YtMi45MjFIM1YxNC4wNUgwdi0zLjFoM1Y1aDE3Ljk5OHpNNiAxMC45NDloMS40ODhWOC4xMDJINnptMTAuNTEgMEgxOFY4LjEwMmgtMS40OXoiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg==",
9
9
  "codex": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE5LjUwMyAwSDQuNDk2QTQuNDk2IDQuNDk2IDAgMCAwIDAgNC40OTZ2MTUuMDA3QTQuNDk2IDQuNDk2IDAgMCAwIDQuNDk2IDI0aDE1LjAwN0E0LjQ5NiA0LjQ5NiAwIDAgMCAyNCAxOS41MDNWNC40OTZBNC40OTYgNC40OTYgMCAwIDAgMTkuNTAzIDAiLz48cGF0aCBmaWxsPSJ1cmwoI2EpIiBkPSJNOS4wNjQgMy4zNDRhNC42IDQuNiAwIDAgMSAyLjI4NS0uMzEycTEuNS4xNzMgMi42NzMgMS4yNzUuMDE2LjAxNS4wMzcuMDIxYS4xLjEgMCAwIDAgLjA0MyAwIDQuNTUgNC41NSAwIDAgMSAzLjA0Ni4yNzVsLjA0Ny4wMjIuMTE2LjA1N2E0LjU4IDQuNTggMCAwIDEgMi4xODggMi4zOTlxLjMxMy43NjUuMzE1IDEuNTk1YTQuMiA0LjIgMCAwIDEtLjEzNCAxLjIyMy4xMi4xMiAwIDAgMCAuMDMuMTE1cS44OS45MSAxLjE4MyAyLjE3LjQzMyAyLjEzOC0uODg3IDMuODU0bC0uMTM2LjE2NmE0LjU1IDQuNTUgMCAwIDEtMi4yMDEgMS4zODguMTIuMTIgMCAwIDAtLjA4MS4wNzZjLS4xOTEuNTUxLS4zODMgMS4wMjMtLjc0IDEuNDk0LS45IDEuMTg3LTIuMjIyIDEuODQ2LTMuNzExIDEuODM4cS0xLjc4LS4wMDktMy4xNTctMS4zMDJhLjExLjExIDAgMCAwLS4xMDUtLjAyNGMtLjM4OC4xMjUtLjc4LjE0My0xLjIwNC4xMzhhNC40NCA0LjQ0IDAgMCAxLTEuOTQ1LS40NjYgNC41NCA0LjU0IDAgMCAxLTEuNjEtMS4zMzVjLS4xNTItLjIwMi0uMzAzLS4zOTItLjQxNC0uNjE3YTYgNiAwIDAgMS0uMzctLjk2MSA0LjYgNC42IDAgMCAxLS4wMTQtMi4yOTguMS4xIDAgMCAwIC4wMDYtLjA1Ni4xLjEgMCAwIDAtLjAyNy0uMDQ4IDQuNSA0LjUgMCAwIDEtMS4wMzQtMS42NTEgMy45IDMuOSAwIDAgMS0uMjUxLTEuMTkyIDUuMiA1LjIgMCAwIDEgLjE0MS0xLjZRMy42NTkgNy45MiA1LjA4NiA2Ljk3cS4zMTgtLjIxMi42MDEtLjMzYTYgNiAwIDAgMSAuNjQ2LS4yMjcuMS4xIDAgMCAwIC4wNjUtLjA2NiA0LjUgNC41IDAgMCAxIC44MjktMS42MTUgNC41NCA0LjU0IDAgMCAxIDEuODM3LTEuMzg4bTMuNDgyIDEwLjU2NWEuNjM3LjYzNyAwIDAgMCAwIDEuMjcyaDMuNjM2YS42MzcuNjM3IDAgMSAwIDAtMS4yNzJ6TTguNDYyIDkuMjNhLjYzNy42MzcgMCAwIDAtMS4xMDYuNjMxbDEuMjcyIDIuMjI0LTEuMjY2IDIuMTM2YS42MzYuNjM2IDAgMSAwIDEuMDk1LjY0OWwxLjQ1NC0yLjQ1NWEuNjQuNjQgMCAwIDAgLjAwNS0uNjR6Ii8+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJhIiB4MT0iMTIiIHgyPSIxMiIgeTE9IjMiIHkyPSIyMSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiNiMWE3ZmYiLz48c3RvcCBvZmZzZXQ9Ii41IiBzdG9wLWNvbG9yPSIjN2E5ZGZmIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMzk0MWZmIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PC9zdmc+",
10
- "copilot": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iIzg1MzRGMyIgZD0iTTIzLjkyMiAxNi45OTJjLS44NjEgMS40OTUtNS44NTkgNS4wMjMtMTEuOTIyIDUuMDIzLTYuMDYzIDAtMTEuMDYxLTMuNTI4LTExLjkyMi01LjAyM0EuNjQxLjY0MSAwIDAgMSAwIDE2LjczNnYtMi44NjlhLjg0MS44NDEgMCAwIDEgLjA1My0uMjJjLjM3Mi0uOTM1IDEuMzQ3LTIuMjkyIDIuNjA1LTIuNjU2LjE2Ny0uNDI5LjQxNC0xLjA1NS42NDQtMS41MTdhMTAuMTk1IDEwLjE5NSAwIDAgMS0uMDUyLTEuMDg2YzAtMS4zMzEuMjgyLTIuNDk5IDEuMTMyLTMuMzY4LjM5Ny0uNDA2Ljg5LS43MTcgMS40NzQtLjk1MiAxLjM5OS0xLjEzNiAzLjM5Mi0yLjA5MyA2LjEyMi0yLjA5MyAyLjczMSAwIDQuNzY3Ljk1NyA2LjE2NiAyLjA5My41ODQuMjM1IDEuMDc3LjU0NiAxLjQ3NC45NTIuODUuODY5IDEuMTMyIDIuMDM3IDEuMTMyIDMuMzY4IDAgLjM2OC0uMDE0LjczMy0uMDUyIDEuMDg2LjIzLjQ2Mi40NzcgMS4wODguNjQ0IDEuNTE3IDEuMjU4LjM2NCAyLjIzMyAxLjcyMSAyLjYwNSAyLjY1NmEuODMyLjgzMiAwIDAgMSAuMDUzLjIydjIuODY5YS42NDEuNjQxIDAgMCAxLS4wNzguMjU2Wk0xMi4xNzIgMTFoLS4zNDRhNC4zMjMgNC4zMjMgMCAwIDEtLjM1NS41MDhDMTAuNzAzIDEyLjQ1NSA5LjU1NSAxMyA3Ljk2NSAxM2MtMS43MjUgMC0yLjk4OS0uMzU5LTMuNzgyLTEuMjU5YTIuMDA1IDIuMDA1IDAgMCAxLS4wODUtLjEwNEw0IDExLjc0MXY2LjU4NWMxLjQzNS43NzkgNC41MTQgMi4xNzkgOCAyLjE3OSAzLjQ4NiAwIDYuNTY1LTEuNCA4LTIuMTc5di02LjU4NWwtLjA5OC0uMTA0cy0uMDMzLjA0NS0uMDg1LjEwNGMtLjc5My45LTIuMDU3IDEuMjU5LTMuNzgyIDEuMjU5LTEuNTkgMC0yLjczOC0uNTQ1LTMuNTA4LTEuNDkyYTQuMzIzIDQuMzIzIDAgMCAxLS4zNTUtLjUwOGgtLjAxNi4wMTZabS42NDEtMi45MzVjLjEzNiAxLjA1Ny40MDMgMS45MTMuODc4IDIuNDk3LjQ0Mi41NDQgMS4xMzQuOTM4IDIuMzQ0LjkzOCAxLjU3MyAwIDIuMjkyLS4zMzcgMi42NTctLjc1MS4zODQtLjQzNS41NTgtMS4xNS41NTgtMi4zNjEgMC0xLjE0LS4yNDMtMS44NDctLjcwNS0yLjMxOS0uNDc3LS40ODgtMS4zMTktLjg2Mi0yLjgyNC0xLjAyNS0xLjQ4Ny0uMTYxLTIuMTkyLjEzOC0yLjUzMy41MjktLjI2OS4zMDctLjQzNy44MDgtLjQzOCAxLjU3OHYuMDIxYzAgLjI2NS4wMjEuNTYyLjA2My44OTNabS0xLjYyNiAwYy4wNDItLjMzMS4wNjMtLjYyOC4wNjMtLjg5NHYtLjAyYy0uMDAxLS43Ny0uMTY5LTEuMjcxLS40MzgtMS41NzgtLjM0MS0uMzkxLTEuMDQ2LS42OS0yLjUzMy0uNTI5LTEuNTA1LjE2My0yLjM0Ny41MzctMi44MjQgMS4wMjUtLjQ2Mi40NzItLjcwNSAxLjE3OS0uNzA1IDIuMzE5IDAgMS4yMTEuMTc1IDEuOTI2LjU1OCAyLjM2MS4zNjUuNDE0IDEuMDg0Ljc1MSAyLjY1Ny43NTEgMS4yMSAwIDEuOTAyLS4zOTQgMi4zNDQtLjkzOC40NzUtLjU4NC43NDItMS40NC44NzgtMi40OTdaIi8+PHBhdGggZmlsbD0iIzg1MzRGMyIgZD0iTTE0LjUgMTQuMjVhMSAxIDAgMCAxIDEgMXYyYTEgMSAwIDAgMS0yIDB2LTJhMSAxIDAgMCAxIDEtMVptLTUgMGExIDEgMCAwIDEgMSAxdjJhMSAxIDAgMCAxLTIgMHYtMmExIDEgMCAwIDEgMS0xWiIvPjwvc3ZnPg==",
11
10
  "cursor-agent": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9IiMxYzFiMWEiIGZpbGwtcnVsZT0iZXZlbm9kZCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMjIuMTA2IDUuNjggMTIuNS4xMzVhMSAxIDAgMCAwLS45OTggMEwxLjg5MyA1LjY4YS44NC44NCAwIDAgMC0uNDE5LjcyNnYxMS4xODZjMCAuMy4xNi41NzcuNDIuNzI3bDkuNjA3IDUuNTQ3YTEgMSAwIDAgMCAuOTk4IDBsOS42MDgtNS41NDdhLjg0Ljg0IDAgMCAwIC40Mi0uNzI3VjYuNDA3YS44NC44NCAwIDAgMC0uNDItLjcyNnptLS42MDMgMS4xNzZMMTIuMjI4IDIyLjkyYy0uMDYzLjEwOC0uMjI4LjA2NC0uMjI4LS4wNjFWMTIuMzRhLjU5LjU5IDAgMCAwLS4yOTUtLjUxbC05LjExLTUuMjZjLS4xMDctLjA2Mi0uMDYzLS4yMjguMDYyLS4yMjhoMTguNTVjLjI2NCAwIC40MjguMjg2LjI5Ni41MTQiLz48L3N2Zz4=",
12
11
  "deepseek": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0iIzRkNmJmZSIgZD0iTTIzLjc0OCA0LjQ4MmMtLjI1NC0uMTI0LS4zNjQuMTEzLS41MTIuMjM0LS4wNTEuMDM5LS4wOTQuMDktLjEzNy4xMzYtLjM3Mi4zOTctLjgwNi42NTctMS4zNzMuNjI2LS44MjktLjA0Ni0xLjUzNy4yMTQtMi4xNjMuODQ4LS4xMzMtLjc4Mi0uNTc1LTEuMjQ4LTEuMjQ3LTEuNTQ4LS4zNTItLjE1Ni0uNzA4LS4zMTEtLjk1NS0uNjUtLjE3Mi0uMjQxLS4yMTktLjUxLS4zMDUtLjc3NC0uMDU1LS4xNi0uMTEtLjMyMy0uMjkzLS4zNS0uMi0uMDMxLS4yNzguMTM2LS4zNTYuMjc2LS4zMTMuNTcyLS40MzQgMS4yMDItLjQyMiAxLjg0LjAyNyAxLjQzNi42MzMgMi41OCAxLjgzOCAzLjM5My4xMzcuMDkzLjE3Mi4xODcuMTI5LjMyMy0uMDgyLjI4LS4xOC41NTItLjI2Ni44MzMtLjA1NS4xNzktLjEzNy4yMTctLjMyOS4xNGE1LjUgNS41IDAgMCAxLTEuNzM2LTEuMThjLS44NTctLjgyOC0xLjYzMS0xLjc0Mi0yLjU5Ny0yLjQ1OGExMSAxMSAwIDAgMC0uNjg5LS40NzFjLS45ODUtLjk1Ny4xMy0xLjc0My4zODgtMS44MzYuMjctLjA5OC4wOTMtLjQzMi0uNzc5LS40MjhzLTEuNjcuMjk1LTIuNjg3LjY4NGEzIDMgMCAwIDEtLjQ2NS4xMzcgOS42IDkuNiAwIDAgMC0yLjg4My0uMTAyYy0xLjg4NS4yMS0zLjM5IDEuMTAyLTQuNDk3IDIuNjIzQy4wODIgOC42MDYtLjIzMSAxMC42ODQuMTUyIDEyLjg1Yy40MDMgMi4yODQgMS41NjkgNC4xNzUgMy4zNiA1LjY1MyAxLjg1OCAxLjUzMyAzLjk5NyAyLjI4NCA2LjQzOCAyLjE0IDEuNDgyLS4wODUgMy4xMzMtLjI4NCA0Ljk5NC0xLjg2LjQ3LjIzNC45NjIuMzI3IDEuNzguMzk3LjYzLjA1OSAxLjIzNi0uMDMgMS43MDUtLjEyOC43MzUtLjE1Ni42ODQtLjgzNy40MTktLjk2MS0yLjE1NS0xLjAwNC0xLjY4Mi0uNTk1LTIuMTEzLS45MjYgMS4wOTYtMS4yOTYgMi43NDYtMi42NDIgMy4zOTItNy4wMDMuMDUtLjM0Ny4wMDctLjU2NSAwLS44NDUtLjAwNC0uMTcuMDM1LS4yMzcuMjMtLjI1NmE0LjIgNC4yIDAgMCAwIDEuNTQ1LS40NzVjMS4zOTYtLjc2MyAxLjk2LTIuMDE1IDIuMDkzLTMuNTE3LjAyLS4yMy0uMDA0LS40NjctLjI0Ny0uNTg4ek0xMS41ODEgMThjLTIuMDg5LTEuNjQyLTMuMTAyLTIuMTgzLTMuNTItMi4xNi0uMzkyLjAyNC0uMzIxLjQ3MS0uMjM1Ljc2My4wOS4yODguMjA3LjQ4Ni4zNzEuNzM5LjExNC4xNjcuMTkyLjQxNi0uMTEzLjYwMy0uNjczLjQxNi0xLjg0Mi0uMTQtMS44OTctLjE2Ny0xLjM2MS0uODAyLTIuNS0xLjg2LTMuMzAxLTMuMzA3LS43NzQtMS4zOTMtMS4yMjQtMi44ODctMS4yOTgtNC40ODItLjAyLS4zODYuMDkzLS41MjIuNDc3LS41OTJhNC43IDQuNyAwIDAgMSAxLjUyOS0uMDM5YzIuMTMyLjMxMiAzLjk0NiAxLjI2NSA1LjQ2OCAyLjc3NC44NjguODYgMS41MjUgMS44ODcgMi4yMDIgMi44OTEuNzIgMS4wNjYgMS40OTQgMi4wODIgMi40OCAyLjkxNC4zNDguMjkyLjYyNS41MTQuODkxLjY3Ny0uODAyLjA5LTIuMTQuMTEtMy4wNTQtLjYxNG0xLTYuNDRhLjMwNi4zMDYgMCAwIDEgLjQxNS0uMjg3LjMuMyAwIDAgMSAuMi4yODguMzA2LjMwNiAwIDAgMS0uMzEuMzA3LjMwMy4zMDMgMCAwIDEtLjMwNC0uMzA4em0zLjExIDEuNTk2Yy0uMi4wODEtLjM5OS4xNTEtLjU5LjE2YTEuMjUgMS4yNSAwIDAgMS0uNzk4LS4yNTRjLS4yNzQtLjIzLS40Ny0uMzU4LS41NTItLjc1OGExLjcgMS43IDAgMCAxIC4wMTYtLjU4OGMuMDctLjMyNy0uMDA4LS41MzctLjIzOS0uNzI3LS4xODctLjE1Ni0uNDI2LS4xOTktLjY4OC0uMTk5YS41Ni41NiAwIDAgMS0uMjU0LS4wNzguMjUzLjI1MyAwIDAgMS0uMTE0LS4zNThjLjAyOC0uMDU0LjE2LS4xODYuMTkyLS4yMS4zNTYtLjIwMi43NjctLjEzNiAxLjE0Ni4wMTYuMzUyLjE0NC42MTguNDA4IDEuMDAxLjc4Mi4zOTEuNDUxLjQ2Mi41NzYuNjg1LjkxNC4xNzYuMjY1LjMzNi41MzcuNDQ1Ljg0OC4wNjcuMTk1LS4wMTkuMzU0LS4yNS40NTIiLz48L3N2Zz4=",
13
12
  "gemini": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZmlsbD0idXJsKCNhKSIgZD0iTTAgNC4zOTFBNC4zOSA0LjM5IDAgMCAxIDQuMzkxIDBoMTUuMjE3QTQuMzkgNC4zOSAwIDAgMSAyNCA0LjM5MXYxNS4yMTdBNC4zOSA0LjM5IDAgMCAxIDE5LjYwOCAyNEg0LjM5MUE0LjM5IDQuMzkgMCAwIDEgMCAxOS42MDh6Ii8+PHBhdGggZmlsbD0iIzFlMWUyZSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMTkuNzQgMS40NDRhMi44MTYgMi44MTYgMCAwIDEgMi44MTYgMi44MTZ2MTUuNDhhMi44MTYgMi44MTYgMCAwIDEtMi44MTYgMi44MTZINC4yNmEyLjgxNiAyLjgxNiAwIDAgMS0yLjgxNi0yLjgxNlY0LjI2QTIuODE2IDIuODE2IDAgMCAxIDQuMjYgMS40NDR6TTcuMjM2IDguNTY0bDcuNzUyIDMuNzI4LTcuNzUyIDMuNzI3djIuODAybDkuNTU3LTQuNTk2di0zLjg2Nkw3LjIzNiA1Ljc2M3oiIGNsaXAtcnVsZT0iZXZlbm9kZCIvPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iYSIgeDE9IjI0IiB4Mj0iMCIgeTE9IjYuNTg3IiB5Mj0iMTYuNDk0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agc3RvcC1jb2xvcj0iI2VlNGQ1ZCIvPjxzdG9wIG9mZnNldD0iLjMyOCIgc3RvcC1jb2xvcj0iI2IzODFkZCIvPjxzdG9wIG9mZnNldD0iLjQ3NiIgc3RvcC1jb2xvcj0iIzIwN2NmZSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjwvc3ZnPg==",
@@ -1,3 +1,9 @@
1
+ /**
2
+ * Syntax-highlighted code fragments for artifact rendering. Verbatim port of
3
+ * `apps/web/src/runtime/shiki.ts` in the origin project — no product-specific
4
+ * logic in the original. See `archived provenance ledger`.
5
+ */
6
+ import { loadOptionalPeer } from '@jini-ai/core/optional-peer';
1
7
  let highlighterPromise = null;
2
8
  const cache = new Map();
3
9
  const CACHE_MAX = 128;
@@ -8,7 +14,7 @@ const CACHE_MAX = 128;
8
14
  // around with a cast that would fail at runtime. See `archived provenance ledger`.
9
15
  function getHighlighter() {
10
16
  if (!highlighterPromise) {
11
- highlighterPromise = import('shiki/bundle/web').then(({ createHighlighter }) => createHighlighter({
17
+ highlighterPromise = loadOptionalPeer({ feature: 'Syntax highlighting', dependency: 'shiki', load: () => import('shiki/bundle/web') }).then(({ createHighlighter }) => createHighlighter({
12
18
  themes: ['github-light-default', 'github-dark-default'],
13
19
  langs: [
14
20
  'javascript', 'typescript', 'tsx', 'jsx', 'html', 'css', 'json',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/ui",
3
- "version": "0.6.15",
3
+ "version": "0.6.16",
4
4
  "description": "Generic, product-neutral React UI primitives and feature domains. Framework-free types, decision rules, catalogs, and host port contracts live behind ./core (no React, no DOM). The Excalidraw and Lexical editors live behind ./sketch-editor and ./lexical-rich-text-editor; the GrapesJS-backed inline HTML editor lives behind ./html-editor.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -58,6 +58,26 @@
58
58
  "src/theme/validation.ts",
59
59
  "src/theme/types.ts"
60
60
  ]
61
+ },
62
+ "optionalPeers": {
63
+ "@tanstack/react-query": "opt-in: cached react queries; import @jini-ai/ui/fetch-query/tanstack — Cached React queries could not load @tanstack/react-query. Install @tanstack/react-query to enable this feature, or check its installed version and native binaries.",
64
+ "@excalidraw/excalidraw": "opt-in: sketch editor; import @jini-ai/ui/sketch-editor — Sketch editor could not load @excalidraw/excalidraw. Install @excalidraw/excalidraw to enable this feature, or check its installed version and native binaries.",
65
+ "@lexical/react": "opt-in: rich text editor react bindings; import @jini-ai/ui/lexical-rich-text-editor — Rich text editor React bindings could not load @lexical/react. Install @lexical/react to enable this feature, or check its installed version and native binaries.",
66
+ "@lexical/utils": "opt-in: rich text editor utilities; import @jini-ai/ui/lexical-rich-text-editor — Rich text editor utilities could not load @lexical/utils. Install @lexical/utils to enable this feature, or check its installed version and native binaries.",
67
+ "@mcp-ui/client": "opt-in: mcp ui renderer; import @jini-ai/ui/mcp-ui — MCP UI renderer could not load @mcp-ui/client. Install @mcp-ui/client to enable this feature, or check its installed version and native binaries.",
68
+ "@radix-ui/react-checkbox": "opt-in: interactive ui controls; import @jini-ai/ui/interactive-ui — Interactive UI controls could not load @radix-ui/react-checkbox. Install @radix-ui/react-checkbox to enable this feature, or check its installed version and native binaries.",
69
+ "@radix-ui/react-label": "opt-in: interactive ui controls; import @jini-ai/ui/interactive-ui — Interactive UI controls could not load @radix-ui/react-label. Install @radix-ui/react-label to enable this feature, or check its installed version and native binaries.",
70
+ "@radix-ui/react-radio-group": "opt-in: interactive ui controls; import @jini-ai/ui/interactive-ui — Interactive UI controls could not load @radix-ui/react-radio-group. Install @radix-ui/react-radio-group to enable this feature, or check its installed version and native binaries.",
71
+ "@radix-ui/react-select": "opt-in: interactive ui controls; import @jini-ai/ui/interactive-ui — Interactive UI controls could not load @radix-ui/react-select. Install @radix-ui/react-select to enable this feature, or check its installed version and native binaries.",
72
+ "@radix-ui/react-slot": "opt-in: interactive ui controls — Interactive UI controls could not load @radix-ui/react-slot. Install @radix-ui/react-slot to enable this feature, or check its installed version and native binaries.",
73
+ "grapesjs": "opt-in: visual html editor; import @jini-ai/ui/html-editor — Visual HTML editor could not load grapesjs. Install grapesjs to enable this feature, or check its installed version and native binaries.",
74
+ "lexical": "opt-in: rich text editor; import @jini-ai/ui/lexical-rich-text-editor — Rich text editor could not load lexical. Install lexical to enable this feature, or check its installed version and native binaries.",
75
+ "class-variance-authority": "opt-in: styled interactive ui components; import @jini-ai/ui/interactive-ui — Styled interactive UI components could not load class-variance-authority. Install class-variance-authority to enable this feature, or check its installed version and native binaries.",
76
+ "clsx": "opt-in: styled interactive ui components; import @jini-ai/ui/interactive-ui — Styled interactive UI components could not load clsx. Install clsx to enable this feature, or check its installed version and native binaries.",
77
+ "parse5": "opt-in: html parsing; import @jini-ai/ui/html-editor — HTML parsing could not load parse5. Install parse5 to enable this feature, or check its installed version and native binaries.",
78
+ "shiki": "opt-in: syntax highlighting; import @jini-ai/ui/renderers — Syntax highlighting could not load shiki. Install shiki to enable this feature, or check its installed version and native binaries.",
79
+ "tailwind-merge": "opt-in: styled interactive ui components; import @jini-ai/ui/interactive-ui — Styled interactive UI components could not load tailwind-merge. Install tailwind-merge to enable this feature, or check its installed version and native binaries.",
80
+ "zod": "opt-in: schema-based features; import @jini-ai/ui/interactive-ui — Schema-based features could not load zod. Install zod to enable this feature, or check its installed version and native binaries."
61
81
  }
62
82
  },
63
83
  "type": "module",
@@ -243,15 +263,19 @@
243
263
  "shiki": "^1.24.4",
244
264
  "tailwind-merge": "^2.6.0",
245
265
  "zod": "^3.25.76",
246
- "@jini-ai/agent-runtime": "^0.6.15",
247
- "@jini-ai/integrations": "^0.6.15",
248
- "@jini-ai/agentic": "^0.6.15",
249
- "@jini-ai/core": "^0.6.15",
250
- "@jini-ai/platform": "^0.6.15"
266
+ "@jini-ai/agentic": "^0.6.16",
267
+ "@jini-ai/platform": "^0.6.16",
268
+ "@jini-ai/core": "^0.6.16",
269
+ "@jini-ai/integrations": "^0.6.16",
270
+ "@jini-ai/agent-runtime": "^0.6.16"
251
271
  },
252
272
  "dependencies": {
253
- "@jini-ai/ui-kit": "^0.6.15",
254
- "@jini-ai/core": "^0.6.15"
273
+ "micromark": "^4.0.0",
274
+ "micromark-extension-gfm": "^3.0.0",
275
+ "@jini-ai/ui-kit": "^0.6.16",
276
+ "@jini-ai/core": "^0.6.16",
277
+ "@jini-ai/agentic": "^0.6.16",
278
+ "@jini-ai/platform": "^0.6.16"
255
279
  },
256
280
  "peerDependencies": {
257
281
  "@tanstack/react-query": "^5.0.0",
@@ -268,12 +292,8 @@
268
292
  "lexical": "0.36.2",
269
293
  "react": "^18.3.0 || ^19.0.0",
270
294
  "react-dom": "^18.3.0 || ^19.0.0",
271
- "@jini-ai/platform": "^0.6.10",
272
- "@jini-ai/agentic": "^0.6.10",
273
295
  "class-variance-authority": "^0.7.1",
274
296
  "clsx": "^2.1.1",
275
- "micromark": "^4.0.0",
276
- "micromark-extension-gfm": "^3.0.0",
277
297
  "parse5": "^7.3.0 || ^8.0.1",
278
298
  "shiki": "^1.24.4",
279
299
  "tailwind-merge": "^2.6.0",
@@ -316,30 +336,12 @@
316
336
  "lexical": {
317
337
  "optional": true
318
338
  },
319
- "react": {
320
- "optional": true
321
- },
322
- "react-dom": {
323
- "optional": true
324
- },
325
- "@jini-ai/platform": {
326
- "optional": true
327
- },
328
- "@jini-ai/agentic": {
329
- "optional": true
330
- },
331
339
  "class-variance-authority": {
332
340
  "optional": true
333
341
  },
334
342
  "clsx": {
335
343
  "optional": true
336
344
  },
337
- "micromark": {
338
- "optional": true
339
- },
340
- "micromark-extension-gfm": {
341
- "optional": true
342
- },
343
345
  "parse5": {
344
346
  "optional": true
345
347
  },