@jini-ai/ui 0.6.15-rc.2 → 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.
- package/CHANGELOG.md +12 -0
- package/dist/browser/useDismissOnOutsideOrEscape.js +1 -1
- package/dist/browser/useGlobalKeydown.js +2 -2
- package/dist/browser/useModalWindowDragGuard.js +1 -1
- package/dist/core/theme/css-name.d.ts +3 -0
- package/dist/core/theme/css-name.js +5 -0
- package/dist/core/theme/stylesheet.js +2 -1
- package/dist/core/theme/validation.js +15 -4
- package/dist/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/features/about/rules.js +2 -1
- package/dist/features/admin-widgets/components/InfoTip.hooks.js +22 -12
- package/dist/features/admin-widgets/components/Lightbox/Lightbox.hooks.js +1 -1
- package/dist/features/admin-widgets/components/ScrollJump/ScrollJump.hooks.js +1 -1
- package/dist/features/admin-widgets/components/SeeMore/SeeMore.hooks.js +2 -2
- package/dist/features/admin-widgets/components/SeeMore/SeeMore.js +4 -2
- package/dist/features/admin-widgets/components/Select/Select.hooks.js +2 -1
- package/dist/features/admin-widgets/components/Select/Select.js +2 -1
- package/dist/features/admin-widgets/components/SuggestInput/SuggestInput.js +4 -2
- package/dist/features/admin-widgets/components/useDebouncedHtmlPreview.hooks.js +3 -2
- package/dist/features/appearance/constants.d.ts +28 -1
- package/dist/features/appearance/constants.js +12 -9
- package/dist/features/appearance/react/components/AppearanceTab.js +4 -4
- package/dist/features/asset-grid/react/components/AssetGrid.js +8 -3
- package/dist/features/asset-grid/react/hooks/useAssetGridKeyboardShortcuts.js +7 -6
- package/dist/features/asset-grid/react/hooks/useAssetGridLiveUpdates.js +8 -10
- package/dist/features/asset-grid/react/hooks/useRubberBandDrag.js +1 -1
- package/dist/features/asset-tree-browser/react/components/AssetTreeFileRow.js +6 -8
- package/dist/features/asset-tree-browser/react/hooks/useAssetTreeRowMenu.js +1 -1
- package/dist/features/asset-tree-browser/rules.js +1 -3
- package/dist/features/browser-chrome/rules.js +10 -8
- package/dist/features/charts/browser.js +2 -1
- package/dist/features/charts/contract.d.ts +4 -4
- package/dist/features/charts/react.js +5 -3
- package/dist/features/charts/wire.d.ts +19 -0
- package/dist/features/charts/wire.js +20 -2
- package/dist/features/connectors/ConnectorsBrowser.js +3 -1
- package/dist/features/connectors/hooks/useConnectorAuthorization.js +1 -1
- package/dist/features/connectors/hooks/useConnectorCatalog.js +2 -2
- package/dist/features/connectors/rules.js +10 -14
- package/dist/features/execution/constants.js +0 -1
- package/dist/features/execution/react/components/AgentDiagnosticRow.js +2 -0
- package/dist/features/execution/react/components/ByokProviderForm.js +1 -3
- package/dist/features/execution/react/components/LocalCliAgentSettings.js +2 -1
- package/dist/features/execution/react/hooks/use-byok-field-validation.hooks.js +6 -2
- package/dist/features/execution/react/hooks/useExecutionTab.js +9 -5
- package/dist/features/execution/react/hooks/useLocalCliAgentCard.js +3 -6
- package/dist/features/execution/react/hooks/useReasoningControl.js +3 -3
- package/dist/features/execution/rules.js +23 -18
- package/dist/features/external-mcp/react/components/ExternalMcpTab.js +10 -1
- package/dist/features/file-dropzone/rules.js +3 -2
- package/dist/features/folder-path-drop/rules.d.ts +2 -3
- package/dist/features/folder-path-drop/rules.js +3 -4
- package/dist/features/html-editor/canvas-embed-placeholders.js +2 -3
- package/dist/features/html-editor/css.js +7 -5
- package/dist/features/html-editor/editor-blocks.js +60 -6
- package/dist/features/html-editor/protected-elements.js +5 -3
- package/dist/features/html-editor/react/hooks/useInteractiveHtmlEditor.js +3 -3
- package/dist/features/html-editor/source-splice.js +6 -4
- package/dist/features/i18n/dictionaries/settings-dialog.ar.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.bn.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.de.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.en.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.es.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.fa.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.fr.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.hi.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.hu.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.id.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.it.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ja.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ko.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.pl.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.pt-BR.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ru.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.th.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.tr.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.uk.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ur.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +8 -0
- package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +8 -0
- package/dist/features/iframe-pool/react/components/IframeKeepAliveProvider.js +5 -5
- package/dist/features/iframe-pool/react/dom-sync.js +2 -2
- package/dist/features/integrations/react/components/IntegrationsTab.js +3 -1
- package/dist/features/integrations/rules.js +6 -4
- package/dist/features/interactive-ui/providers/shadcn/data-table.js +10 -5
- package/dist/features/interactive-ui/table-data.js +1 -1
- package/dist/features/lexical-rich-text-editor/deserialize.js +2 -2
- package/dist/features/lexical-rich-text-editor/mention-parser.js +4 -4
- package/dist/features/lexical-rich-text-editor/react/components/RichTextInput.js +2 -1
- package/dist/features/lexical-rich-text-editor/react/hooks/useCaretFloatingLayerPosition.js +1 -1
- package/dist/features/lexical-rich-text-editor/react/hooks/useMentionColorStamping.js +1 -1
- package/dist/features/lexical-rich-text-editor/serialize.js +13 -10
- package/dist/features/list-detail-panel/react/components/ListDetailPanel.js +1 -1
- package/dist/features/mcp-ui/confirmation-store.js +3 -6
- package/dist/features/mcp-ui/surfaces/checkbox.d.ts +2 -0
- package/dist/features/mcp-ui/surfaces/choice-group.d.ts +2 -0
- package/dist/features/mcp-ui/surfaces/choice-group.js +2 -2
- package/dist/features/mcp-ui/surfaces/confirmation.d.ts +5 -0
- package/dist/features/mcp-ui/surfaces/confirmation.js +1 -0
- package/dist/features/mcp-ui/surfaces/document.d.ts +14 -16
- package/dist/features/mcp-ui/surfaces/document.js +46 -7
- package/dist/features/mcp-ui/surfaces/fields.d.ts +7 -1
- package/dist/features/mcp-ui/surfaces/fields.js +14 -11
- package/dist/features/mcp-ui/surfaces/form.js +6 -1
- package/dist/features/mcp-ui/surfaces/host-theme.js +2 -1
- package/dist/features/mcp-ui/surfaces/index.d.ts +1 -0
- package/dist/features/mcp-ui/surfaces/index.js +1 -0
- package/dist/features/mcp-ui/surfaces/outcome.js +1 -1
- package/dist/features/mcp-ui/surfaces/select.d.ts +2 -0
- package/dist/features/mcp-ui/surfaces/select.js +2 -1
- package/dist/features/mcp-ui/surfaces/structured-details.d.ts +40 -0
- package/dist/features/mcp-ui/surfaces/structured-details.js +159 -0
- package/dist/features/mcp-ui/surfaces/text-input.d.ts +2 -0
- package/dist/features/mcp-ui/surfaces/text-input.js +1 -1
- package/dist/features/mcp-ui/surfaces/tool-approval.d.ts +21 -0
- package/dist/features/mcp-ui/surfaces/tool-approval.js +42 -0
- package/dist/features/media-providers/react/hooks/useMediaProvidersTab.js +2 -1
- package/dist/features/memory/async-commit-guard.js +4 -1
- package/dist/features/memory/formatters.js +5 -1
- package/dist/features/memory/react/components/MemoryConnectedPanel.js +3 -1
- package/dist/features/memory/react/components/MemoryEntryCard.js +1 -1
- package/dist/features/memory/react/components/MemorySettingsPanel.js +3 -1
- package/dist/features/memory/react/hooks/useMemoryConfig.hooks.js +12 -10
- package/dist/features/memory/react/hooks/useMemoryConnectors.hooks.js +2 -5
- package/dist/features/memory/react/hooks/useMemoryConnectors.rules.js +10 -9
- package/dist/features/memory/react/hooks/useMemoryEntries.hooks.js +4 -2
- package/dist/features/memory/react/hooks/useMemoryFlash.hooks.js +1 -1
- package/dist/features/memory/rules.js +2 -1
- package/dist/features/panel-kit/fetch-query/adapter.react.js +3 -2
- package/dist/features/panel-kit/fetch-query/adapter.tanstack.js +3 -2
- package/dist/features/panel-kit/fetch-query/binding.react.js +2 -1
- package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +13 -4
- package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -1
- package/dist/features/panel-kit/hooks/use-focus-trap.hooks.js +1 -1
- package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +14 -10
- package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +2 -2
- package/dist/features/progress-card/components/ProgressCard.d.ts +1 -1
- package/dist/features/progress-card/reference-adapters.js +23 -13
- package/dist/features/progress-card/rules.d.ts +0 -1
- package/dist/features/progress-card/rules.js +14 -20
- package/dist/features/progress-card/types.d.ts +1 -1
- package/dist/features/project-locations/dependencies.js +6 -1
- package/dist/features/project-locations/react/components/ProjectLocationsTab.js +2 -1
- package/dist/features/project-locations/rules.js +2 -1
- package/dist/features/resource-dashboard/react/components/ResourceBoardToolbar.js +8 -1
- package/dist/features/resource-dashboard/react/components/ResourceCardParts.js +2 -1
- package/dist/features/resource-dashboard/react/components/ResourceRowListItem.js +3 -1
- package/dist/features/resource-dashboard/react/components/ResourceRowListItemParts.js +2 -1
- package/dist/features/resource-dashboard/react/components/ResourceRowListView.js +1 -1
- package/dist/features/resource-dashboard/react/hooks/useResourceBoard.js +1 -1
- package/dist/features/schedule-picker/rules.js +13 -7
- package/dist/features/sketch-editor/constants.d.ts +0 -6
- package/dist/features/sketch-editor/constants.js +17 -15
- package/dist/features/sketch-editor/dom.js +4 -7
- package/dist/features/sketch-editor/react/components/SketchMainMenu.js +2 -1
- package/dist/features/sketch-editor/react/components/SketchSaveStateBadge.js +2 -1
- package/dist/features/sketch-editor/react/hooks/useSketchDomEnhancements.js +1 -1
- package/dist/features/sketch-editor/rules.js +13 -12
- package/dist/features/skills/dependencies.js +9 -4
- package/dist/features/skills/react/components/SkillRow.js +2 -1
- package/dist/features/skills/react/components/SkillsTab.js +2 -1
- package/dist/features/skills/rules.js +3 -3
- package/dist/features/source-config-list/agent-handles.js +4 -3
- package/dist/features/source-config-list/react/components/SourceConfigItemCard.js +3 -1
- package/dist/features/tab-strip/react/components/TabBar.hooks.js +4 -2
- package/dist/features/version-manager/react/components/VersionPreviewFrame.js +4 -1
- package/dist/features/version-manager/react/components/VersionSidebar.js +8 -6
- package/dist/features/version-manager/react/hooks/usePreviewCanvasSize.js +1 -1
- package/dist/features/version-manager/react/hooks/useVersionManager.js +5 -7
- package/dist/features/version-manager/rules.js +3 -5
- package/dist/features/viewer-shell/react/components/CommentSidePanel.js +2 -1
- package/dist/features/viewer-shell/react/components/SegmentedToggle.js +2 -1
- package/dist/features/viewer-shell/react/components/ViewerShell.js +2 -1
- package/dist/features/viewer-shell/rules.js +8 -7
- package/dist/react/components/AgentIcon.js +0 -1
- package/dist/react/components/BrandLogo.js +10 -2
- package/dist/react/components/CustomSelect.js +1 -1
- package/dist/react/components/EditorIcon.js +17 -14
- package/dist/react/components/Loading.js +2 -1
- package/dist/react/components/OnboardingChipField.js +5 -1
- package/dist/react/components/OnboardingDropdown.js +6 -4
- package/dist/react/components/PaletteTweaks.js +7 -6
- package/dist/react/components/PillButton.js +2 -1
- package/dist/react/components/PopoverMenu.js +2 -1
- package/dist/react/components/RemixIcon.js +2 -1
- package/dist/react/components/SecretInput.hooks.d.ts +2 -25
- package/dist/react/components/SecretInput.hooks.js +2 -24
- package/dist/react/components/SecretInput.js +5 -4
- package/dist/react/components/Toast.js +4 -2
- package/dist/react/components/TooltipLayer.js +4 -1
- package/dist/react/components/WorkingDirPicker.js +5 -4
- package/dist/react/components/agent-icon-data-uris.generated.js +0 -1
- package/dist/react/components/remixicon-font-recovery.js +9 -1
- package/dist/react/components/select-popover.js +2 -2
- package/dist/react/hooks/useBrandFonts.js +1 -1
- package/dist/react/hooks/useEdgeAutoScroll.js +1 -1
- package/dist/react/hooks/useInView.js +9 -9
- package/dist/react/hooks/useResizableSplitPane.js +12 -20
- package/dist/react/mcp-ui/useHostSurfaceTokens.js +1 -1
- package/dist/renderers/annotation-canvas/react/components/AnnotationCanvas.js +7 -1
- package/dist/renderers/annotation-canvas/react/hooks/useAnnotationCanvas.controller.js +4 -2
- package/dist/renderers/annotation-canvas/react/hooks/useAnnotationCanvas.js +9 -5
- package/dist/renderers/annotation-canvas/rules.js +9 -13
- package/dist/renderers/annotation-canvas/theme.js +5 -2
- package/dist/renderers/renderers/index.js +1 -1
- package/dist/renderers/renderers/markdown.js +4 -6
- package/dist/renderers/shiki.js +7 -1
- package/dist/theme/apply.js +7 -3
- package/dist/utils/class-names.d.ts +3 -0
- package/dist/utils/class-names.js +5 -0
- package/dist/utils/design-md.js +3 -3
- package/dist/utils/ime-composing.d.ts +1 -1
- package/dist/utils/ime-composing.js +1 -2
- package/dist/utils/localized-url.js +2 -11
- package/dist/utils/markdown-scroll-sync.js +1 -1
- package/dist/utils/omit-keys.d.ts +3 -0
- package/dist/utils/omit-keys.js +8 -0
- package/dist/utils/polygon-selection.js +1 -1
- package/dist/utils/sse.js +3 -0
- package/dist/utils/trim-chars.d.ts +13 -0
- package/dist/utils/trim-chars.js +30 -0
- package/dist/utils/unhandled-case.d.ts +10 -0
- package/dist/utils/unhandled-case.js +12 -0
- package/dist/utils/unit-bezier.js +1 -3
- package/dist/utils/zip.js +6 -10
- package/package.json +32 -29
|
@@ -96,7 +96,9 @@ function DeeplinkInstallRow({ deeplink, info, agentHandle }) {
|
|
|
96
96
|
/** The generated snippet, or a loading / could-not-resolve placeholder while there is none. */
|
|
97
97
|
function InstallSnippet({ resolved, loading, error }) {
|
|
98
98
|
const t = useT();
|
|
99
|
-
|
|
99
|
+
const errorPlaceholder = error ? t('Could not resolve install paths.') : '';
|
|
100
|
+
const placeholder = loading ? t('Loading install paths…') : errorPlaceholder;
|
|
101
|
+
return (_jsx(SnippetBlock, { snippet: resolved?.snippet ?? '', language: resolved?.language ?? 'json', placeholder: placeholder }));
|
|
100
102
|
}
|
|
101
103
|
/** Build/Node warning, shown only once install info has loaded and reports a missing prerequisite. */
|
|
102
104
|
function PrerequisiteWarning({ info }) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { unhandledCase } from '../../utils/unhandled-case.js';
|
|
1
2
|
/** Path hint per OS — a Windows user shouldn't see a `~/...` path their
|
|
2
3
|
* shell won't expand, and vice versa. */
|
|
3
4
|
export function homeConfigPath(platform, posix, windows) {
|
|
@@ -36,7 +37,8 @@ export function buildCodexEnvToml(serverName, info) {
|
|
|
36
37
|
const entries = Object.entries(info.env ?? {});
|
|
37
38
|
if (entries.length === 0)
|
|
38
39
|
return '';
|
|
39
|
-
|
|
40
|
+
const lines = entries.map(([key, value]) => `${key} = ${JSON.stringify(value)}`).join('\n');
|
|
41
|
+
return `\n\n[mcp_servers.${serverName}.env]\n${lines}`;
|
|
40
42
|
}
|
|
41
43
|
/** The `{"mcpServers": {"<serverName>": {...}}}` shape shared by Cursor,
|
|
42
44
|
* Antigravity, and Windsurf. */
|
|
@@ -186,8 +188,8 @@ export function snippetForClient(clientId, serverName, info) {
|
|
|
186
188
|
// refactored away rather than kept behind a coverage-suppression comment.
|
|
187
189
|
// TypeScript still catches a missing case if `McpClientId` grows a member
|
|
188
190
|
// and only `methodLabelForClient` gets updated: with every current case
|
|
189
|
-
// returning, `
|
|
190
|
-
//
|
|
191
|
-
|
|
191
|
+
// returning, `unhandledCase`'s `never` parameter makes `tsc` require this
|
|
192
|
+
// switch to be exhaustive too.
|
|
193
|
+
return unhandledCase(clientId);
|
|
192
194
|
}
|
|
193
195
|
//# sourceMappingURL=rules.js.map
|
|
@@ -2,6 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useMemo, useState } from 'react';
|
|
3
3
|
import { getNumericColumnKeys, sortTableRows } from '../../table-data.js';
|
|
4
4
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './table.js';
|
|
5
|
+
/** Header glyph per column sort state; `none` marks a sortable but unsorted column. */
|
|
6
|
+
const SORT_GLYPHS = { ascending: '↑', descending: '↓', none: '↕' };
|
|
5
7
|
function resolveTablePage({ pageSize, rowCount, requestedPage }, _options = {}) {
|
|
6
8
|
const requestedSize = pageSize !== undefined && Number.isInteger(pageSize) && pageSize > 0 ? pageSize : null;
|
|
7
9
|
const paging = requestedSize !== null;
|
|
@@ -20,11 +22,14 @@ export function DataTable({ columns, rows, onRowClick, sortable = true, pageSize
|
|
|
20
22
|
const sortedRows = useMemo(() => sortTableRows({ rows, sort: activeSort, numericKeys }), [rows, activeSort, numericKeys]);
|
|
21
23
|
const { paging, size, pageCount, page, start } = resolveTablePage({ pageSize, rowCount: rows.length, requestedPage });
|
|
22
24
|
const visibleRows = sortedRows.slice(start, start + size);
|
|
23
|
-
return (_jsxs("div", { className: "jini-data-table-frame", "data-table-provider": "shadcn", children: [_jsxs(Table, { className: "jini-data-table", children: [_jsx(TableHeader, { children: _jsx(TableRow, { children: columns.map((column) =>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
return (_jsxs("div", { className: "jini-data-table-frame", "data-table-provider": "shadcn", children: [_jsxs(Table, { className: "jini-data-table", children: [_jsx(TableHeader, { children: _jsx(TableRow, { children: columns.map((column) => {
|
|
26
|
+
const columnSort = activeSort?.key === column.key ? activeSort.direction : undefined;
|
|
27
|
+
return (_jsx(TableHead, { scope: "col", "data-numeric": numericKeys.has(column.key), "aria-sort": sortable ? (columnSort ?? 'none') : undefined, children: sortable ? (_jsxs("button", { type: "button", className: "jini-data-table-sort", onClick: () => {
|
|
28
|
+
setSort({ key: column.key, direction: activeSort?.key === column.key && activeSort.direction === 'ascending'
|
|
29
|
+
? 'descending' : 'ascending' });
|
|
30
|
+
setPage(0);
|
|
31
|
+
}, children: [_jsx("span", { children: column.label }), _jsx("span", { "aria-hidden": "true", children: SORT_GLYPHS[columnSort ?? 'none'] })] })) : column.label }, column.key));
|
|
32
|
+
}) }) }), _jsxs(TableBody, { children: [rows.length === 0 && _jsx(TableRow, { children: _jsx(TableCell, { className: "jini-data-table-empty", colSpan: columns.length, children: "No results." }) }), visibleRows.map(({ row, index }) => (_jsx(TableRow, { onClick: onRowClick ? () => onRowClick(row, index) : undefined, role: onRowClick ? 'button' : undefined, tabIndex: onRowClick ? 0 : undefined, onKeyDown: onRowClick ? (event) => {
|
|
28
33
|
if (event.target === event.currentTarget && (event.key === 'Enter' || event.key === ' ')) {
|
|
29
34
|
event.preventDefault();
|
|
30
35
|
onRowClick(row, index);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
function isNumericValue(value) {
|
|
2
2
|
if (typeof value === 'number')
|
|
3
3
|
return Number.isFinite(value);
|
|
4
|
-
return typeof value === 'string' && /^[+-]?(?:\d
|
|
4
|
+
return typeof value === 'string' && /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(value.trim())
|
|
5
5
|
&& Number.isFinite(Number(value));
|
|
6
6
|
}
|
|
7
7
|
/** Infer numeric columns once for both providers; empty cells do not turn a numeric column into text.
|
|
@@ -37,10 +37,10 @@ export function setRichTextFromPlainText(editor, text, knownMentions) {
|
|
|
37
37
|
label: part.entity.label,
|
|
38
38
|
title: part.entity.title,
|
|
39
39
|
}));
|
|
40
|
+
continue;
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
+
if (part.text)
|
|
42
43
|
p.append($createTextNode(part.text));
|
|
43
|
-
}
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
46
|
root.append(p);
|
|
@@ -232,10 +232,10 @@ export function foldPresentMentions(text, present, known) {
|
|
|
232
232
|
if (part.kind !== 'mention' || part.entity.kind === 'unknown')
|
|
233
233
|
continue;
|
|
234
234
|
const key = `${part.entity.kind}:${part.entity.id}`;
|
|
235
|
-
if (seen.has(key))
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
235
|
+
if (!seen.has(key)) {
|
|
236
|
+
seen.add(key);
|
|
237
|
+
result.push(part.entity);
|
|
238
|
+
}
|
|
239
239
|
}
|
|
240
240
|
return result;
|
|
241
241
|
}
|
|
@@ -68,6 +68,7 @@ function EditorSurface(props, ref) {
|
|
|
68
68
|
}, { discrete: true });
|
|
69
69
|
},
|
|
70
70
|
insertMention(insert) {
|
|
71
|
+
const isActiveTrigger = (trigger) => trigger.id === mentionTriggerIdRef.current;
|
|
71
72
|
editor.update(() => {
|
|
72
73
|
let sel = $getSelection();
|
|
73
74
|
if (!$isRangeSelection(sel)) {
|
|
@@ -80,7 +81,7 @@ function EditorSurface(props, ref) {
|
|
|
80
81
|
// The cast documents that instead of an unreachable
|
|
81
82
|
// early-return branch no real document state can trigger.
|
|
82
83
|
const rangeSel = sel;
|
|
83
|
-
const mentionTrigger = triggersRef.current.find(
|
|
84
|
+
const mentionTrigger = triggersRef.current.find(isActiveTrigger) ??
|
|
84
85
|
triggersRef.current[0];
|
|
85
86
|
if (mentionTrigger) {
|
|
86
87
|
deleteActiveTrigger(rangeSel, buildTriggerDeletionRegex(mentionTrigger));
|
|
@@ -23,7 +23,7 @@ export function useMentionColorStamping(resolveMentionColor) {
|
|
|
23
23
|
const [editor] = useLexicalComposerContext();
|
|
24
24
|
useEffect(() => {
|
|
25
25
|
if (!resolveMentionColor)
|
|
26
|
-
return;
|
|
26
|
+
return undefined;
|
|
27
27
|
const stampByKey = (key) => {
|
|
28
28
|
const dom = editor.getElementByKey(key);
|
|
29
29
|
if (!dom)
|
|
@@ -26,17 +26,20 @@ export function serializeRichText(state) {
|
|
|
26
26
|
function serializeParagraph(at) {
|
|
27
27
|
let line = '';
|
|
28
28
|
for (const child of at.paragraph.getChildren()) {
|
|
29
|
-
|
|
30
|
-
line += child.getToken();
|
|
31
|
-
at.mentions.push(child.getEntity());
|
|
32
|
-
}
|
|
33
|
-
else if ($isLineBreakNode(child)) {
|
|
34
|
-
line += '\n';
|
|
35
|
-
}
|
|
36
|
-
else if ($isTextNode(child) || $isElementNode(child)) {
|
|
37
|
-
line += child.getTextContent();
|
|
38
|
-
}
|
|
29
|
+
line += serializedChildText(child, at.mentions);
|
|
39
30
|
}
|
|
40
31
|
return line;
|
|
41
32
|
}
|
|
33
|
+
/** One paragraph child's plain-text contribution; a mention also records its entity. Other node kinds add nothing. */
|
|
34
|
+
function serializedChildText(child, mentions) {
|
|
35
|
+
if ($isMentionNode(child)) {
|
|
36
|
+
mentions.push(child.getEntity());
|
|
37
|
+
return child.getToken();
|
|
38
|
+
}
|
|
39
|
+
if ($isLineBreakNode(child))
|
|
40
|
+
return '\n';
|
|
41
|
+
if ($isTextNode(child) || $isElementNode(child))
|
|
42
|
+
return child.getTextContent();
|
|
43
|
+
return '';
|
|
44
|
+
}
|
|
42
45
|
//# sourceMappingURL=serialize.js.map
|
|
@@ -18,6 +18,6 @@ export function ListDetailPanel({ items, selectedId, onSelect, renderItem, rende
|
|
|
18
18
|
const state = { active };
|
|
19
19
|
const resolvedItemClassName = typeof itemClassName === 'function' ? itemClassName(item, state) : itemClassName;
|
|
20
20
|
return (_jsx("button", { type: "button", className: join('jini-list-detail-panel-item', active && 'jini-list-detail-panel-item-active', resolvedItemClassName), "aria-pressed": active, "aria-label": getItemAriaLabel?.(item), "data-active": active, "data-testid": `${testId}-item-${item.id}`, onClick: () => onSelect(item.id), children: renderItem(item, state) }, item.id));
|
|
21
|
-
}) }))] }),
|
|
21
|
+
}) }))] }), _jsxs("section", { className: join('jini-list-detail-panel-detail', detailClassName), "data-testid": `${testId}-detail`, children: [loading ? (_jsx("div", { className: "jini-list-detail-panel-loading-detail", role: "status", "aria-busy": "true", "aria-label": t('Loading'), "data-testid": `${testId}-loading-detail`, children: loadingDetailContent })) : null, !loading && selectedItem ? renderDetail(selectedItem) : null, !loading && !selectedItem ? emptyDetailContent : null] })] }));
|
|
22
22
|
}
|
|
23
23
|
//# sourceMappingURL=ListDetailPanel.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { trimTrailingChars } from '../../utils/trim-chars.js';
|
|
1
2
|
/**
|
|
2
3
|
* @module features/mcp-ui/confirmation-store
|
|
3
4
|
*
|
|
@@ -66,11 +67,7 @@ function bindingsMatch(minted, presented) {
|
|
|
66
67
|
// does not — the type check below is what keeps those apart, and it is exact on purpose: a
|
|
67
68
|
// binding that silently coerces is a binding that can be satisfied by a value the minter never
|
|
68
69
|
// agreed to.
|
|
69
|
-
if (typeof left !== typeof right)
|
|
70
|
-
matched = false;
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (!constantTimeEquals(String(left), String(right)))
|
|
70
|
+
if (typeof left !== typeof right || !constantTimeEquals(String(left), String(right)))
|
|
74
71
|
matched = false;
|
|
75
72
|
}
|
|
76
73
|
return matched;
|
|
@@ -89,7 +86,7 @@ function defaultRandomToken() {
|
|
|
89
86
|
let binary = '';
|
|
90
87
|
for (const byte of bytes)
|
|
91
88
|
binary += String.fromCharCode(byte);
|
|
92
|
-
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_')
|
|
89
|
+
return trimTrailingChars(btoa(binary).replace(/\+/g, '-').replace(/\//g, '_'), '=');
|
|
93
90
|
}
|
|
94
91
|
/**
|
|
95
92
|
* Creates an in-process confirmation store.
|
|
@@ -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
|
}
|
|
@@ -67,6 +67,11 @@ export interface ConfirmationSurfaceSpec {
|
|
|
67
67
|
readonly description?: string;
|
|
68
68
|
/** The facts being agreed about. A dialog that says "delete this?" without naming *this* is not consent. */
|
|
69
69
|
readonly details?: readonly SurfaceDetail[];
|
|
70
|
+
/** Technical identifiers and input stay available without crowding out the consent wording. */
|
|
71
|
+
readonly detailDisclosure?: {
|
|
72
|
+
readonly label: string;
|
|
73
|
+
readonly details: readonly SurfaceDetail[];
|
|
74
|
+
};
|
|
70
75
|
/**
|
|
71
76
|
* Optional per-row checkboxes, rendered unchecked between the details and the warning. Ticking one
|
|
72
77
|
* never confirms anything by itself — the same property the module doc opens with, extended to a
|
|
@@ -133,6 +133,7 @@ function confirmationBodyHtml(spec, required) {
|
|
|
133
133
|
return [
|
|
134
134
|
renderSurfaceHeader(spec.description === undefined ? { title: spec.title } : { title: spec.title, description: spec.description }),
|
|
135
135
|
renderDetailList(spec.details ?? []),
|
|
136
|
+
spec.detailDisclosure === undefined ? '' : `<details class="mcpui-detail-disclosure mcpui-technical-details"><summary aria-expanded="false">${escapeHtml(spec.detailDisclosure.label)}</summary>${renderDetailList(spec.detailDisclosure.details)}</details>`,
|
|
136
137
|
renderChoices(spec.choices ?? []),
|
|
137
138
|
warning,
|
|
138
139
|
// Confirm starts disabled; the script enables it once the dwell has run out.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type BridgeScriptSpec } from './bridge.js';
|
|
2
2
|
import { type SurfaceTokenName } from './tokens.js';
|
|
3
|
+
import { type SurfaceDetail } from './structured-details.js';
|
|
4
|
+
export type { SurfaceDetail } from './structured-details.js';
|
|
3
5
|
/**
|
|
4
6
|
* The base stylesheet, applied after the token block.
|
|
5
7
|
*
|
|
@@ -8,7 +10,7 @@ import { type SurfaceTokenName } from './tokens.js';
|
|
|
8
10
|
* be a color no caller could reskin. `var(--x, fallback)` appears only where a value is genuinely
|
|
9
11
|
* optional.
|
|
10
12
|
*/
|
|
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); }";
|
|
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); }";
|
|
12
14
|
/**
|
|
13
15
|
* The Content-Security-Policy every generated surface declares in its own `<head>`.
|
|
14
16
|
*
|
|
@@ -62,27 +64,13 @@ export declare function renderSurfaceHeader(spec: {
|
|
|
62
64
|
title: string;
|
|
63
65
|
description?: string;
|
|
64
66
|
}): string;
|
|
65
|
-
/** One row of a surface's detail list. */
|
|
66
|
-
export interface SurfaceDetail {
|
|
67
|
-
readonly label: string;
|
|
68
|
-
readonly value: string;
|
|
69
|
-
/** Optional secondary context beneath the label, using the shared muted hint style. */
|
|
70
|
-
readonly description?: string;
|
|
71
|
-
/**
|
|
72
|
-
* `code` renders the value in a monospace `<pre>` that keeps its line breaks and scrolls inside
|
|
73
|
-
* its own box past a fixed height, so a long SQL statement or JSON argument stays readable without
|
|
74
|
-
* stretching the dialog past its frame. `status` reserves one line for a short outcome value.
|
|
75
|
-
* Omitted (or `text`) keeps the plain inline value.
|
|
76
|
-
*/
|
|
77
|
-
readonly format?: 'text' | 'code' | 'status';
|
|
78
|
-
}
|
|
79
67
|
/**
|
|
80
68
|
* Renders a `label: value` description list.
|
|
81
69
|
*
|
|
82
70
|
* @param options.layout - Outcome rows size independently and stack in narrow cards.
|
|
83
71
|
* @returns The `<dl>`, or `''` for an empty list — an empty `<dl>` is announced by screen readers
|
|
84
72
|
* as a list with no items, which is worse than no list.
|
|
85
|
-
* @complexity O(n)
|
|
73
|
+
* @complexity O(n * depth) with previous snapshots, otherwise O(n) rendered bytes; no effects or input mutation.
|
|
86
74
|
*/
|
|
87
75
|
export declare function renderDetailList(details: readonly SurfaceDetail[], options?: {
|
|
88
76
|
layout?: 'outcome';
|
|
@@ -130,6 +118,14 @@ export declare const DEFAULT_SURFACE_STATUS_TEXT: SurfaceStatusText;
|
|
|
130
118
|
* the builders make no assumption about it beyond ES5.
|
|
131
119
|
*/
|
|
132
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";
|
|
133
129
|
/**
|
|
134
130
|
* Builds the DOM id for a field's control, validating the name on the way through.
|
|
135
131
|
*
|
|
@@ -148,6 +144,8 @@ export declare function renderFieldLabel(spec: {
|
|
|
148
144
|
label: string;
|
|
149
145
|
required?: boolean;
|
|
150
146
|
hint?: string;
|
|
147
|
+
/** Visually hide the label (it repeats the card title); it stays the control's accessible name. */
|
|
148
|
+
labelHidden?: boolean;
|
|
151
149
|
}): string;
|
|
152
150
|
/** The `aria-describedby` attribute pointing at a field's hint, or `''` when it has none. */
|
|
153
151
|
export declare function fieldDescribedBy(spec: {
|
|
@@ -15,6 +15,7 @@ import { AGENT_ELEMENT_ATTRIBUTE, AGENT_LABEL_ATTRIBUTE, AGENT_ROLE_ATTRIBUTE }
|
|
|
15
15
|
import { escapeHtml } from '../escape.js';
|
|
16
16
|
import { SURFACE_BRIDGE_GLOBAL, renderBridgeScript } from './bridge.js';
|
|
17
17
|
import { renderTokenBlock } from './tokens.js';
|
|
18
|
+
import { DETAIL_DISCLOSURE_SCRIPT, renderStructuredDetail } from './structured-details.js';
|
|
18
19
|
/**
|
|
19
20
|
* The base stylesheet, applied after the token block.
|
|
20
21
|
*
|
|
@@ -69,6 +70,11 @@ body {
|
|
|
69
70
|
}
|
|
70
71
|
.mcpui-details dt { color: var(--jini-mcpui-text-soft); font-weight: 500; overflow-wrap: anywhere; }
|
|
71
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; }
|
|
72
78
|
.mcpui-details-outcome { display: flex; flex-direction: column; gap: 8px; }
|
|
73
79
|
.mcpui-outcome-row { display: flex; align-items: flex-start; gap: 14px; }
|
|
74
80
|
.mcpui-outcome-row dt { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
@@ -117,6 +123,7 @@ body {
|
|
|
117
123
|
.mcpui-field { display: flex; flex-direction: column; gap: 4px; }
|
|
118
124
|
.mcpui-field-inline { flex-direction: row; align-items: flex-start; gap: 8px; }
|
|
119
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; }
|
|
120
127
|
.mcpui-required { color: var(--jini-mcpui-accent); margin-left: 2px; }
|
|
121
128
|
.mcpui-hint { color: var(--jini-mcpui-text-soft); font-size: 13px; }
|
|
122
129
|
.mcpui-input, .mcpui-select, .mcpui-textarea {
|
|
@@ -202,6 +209,34 @@ body {
|
|
|
202
209
|
fully done"); the accent token reads as "worth a second look" without asserting either. */
|
|
203
210
|
.mcpui-status[data-state="partial"] { color: var(--jini-mcpui-accent); }
|
|
204
211
|
.mcpui-code { font-family: var(--jini-mcpui-font-mono); font-size: 13px; color: var(--jini-mcpui-text-faint); }`;
|
|
212
|
+
/** Structured details extend the same token system; legacy short-string documents keep their layout. */
|
|
213
|
+
const STRUCTURED_DETAILS_CSS = `
|
|
214
|
+
.mcpui-details { gap: 8px 16px; margin-top: 16px; }
|
|
215
|
+
.mcpui-details dt { font-weight: 400; }
|
|
216
|
+
.mcpui-details dd { min-width: 0; font-weight: 400; }
|
|
217
|
+
.mcpui-detail-group { margin: 0; padding: 0; padding-inline-start: 8px; border-inline-start: 1px solid var(--jini-mcpui-border); }
|
|
218
|
+
.mcpui-detail-row { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); gap: 8px; padding: 8px 0; }
|
|
219
|
+
.mcpui-detail-row:first-child { padding-top: 0; }
|
|
220
|
+
.mcpui-detail-row:last-child { padding-bottom: 0; }
|
|
221
|
+
.mcpui-detail-row:has(> dd > .mcpui-detail-group) { grid-template-columns: minmax(0, 1fr); }
|
|
222
|
+
.mcpui-detail-array { margin: 0; padding-inline-start: 16px; }
|
|
223
|
+
.mcpui-detail-array li + li { margin-top: 8px; }
|
|
224
|
+
.mcpui-detail-disclosure { min-width: 0; margin-top: 8px; }
|
|
225
|
+
.mcpui-detail-disclosure:first-child { margin-top: 0; }
|
|
226
|
+
.mcpui-detail-disclosure > summary { color: var(--jini-mcpui-text-muted); cursor: pointer; overflow-wrap: anywhere; }
|
|
227
|
+
.mcpui-detail-disclosure > summary:focus-visible { outline: 2px solid var(--jini-mcpui-accent-ring); outline-offset: 2px; }
|
|
228
|
+
.mcpui-detail-disclosure > .mcpui-code, .mcpui-detail-disclosure > .mcpui-detail-group,
|
|
229
|
+
.mcpui-detail-full { margin-top: 8px; }
|
|
230
|
+
.mcpui-detail-preview { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; color: var(--jini-mcpui-text); }
|
|
231
|
+
.mcpui-detail-long > summary { list-style: none; }
|
|
232
|
+
.mcpui-detail-long > summary::-webkit-details-marker { display: none; }
|
|
233
|
+
.mcpui-detail-less, .mcpui-detail-long[open] .mcpui-detail-more,
|
|
234
|
+
.mcpui-detail-long[open] .mcpui-detail-preview { display: none; }
|
|
235
|
+
.mcpui-detail-long[open] .mcpui-detail-less { display: inline; }
|
|
236
|
+
.mcpui-detail-change { display: flex; flex-direction: column; gap: 8px; }
|
|
237
|
+
.mcpui-detail-change > div > .mcpui-hint { display: block; }
|
|
238
|
+
.mcpui-detail-raw { font-size: 12px; }
|
|
239
|
+
.mcpui-detail-disclosure .mcpui-code { color: var(--jini-mcpui-text); }`;
|
|
205
240
|
/**
|
|
206
241
|
* The Content-Security-Policy every generated surface declares in its own `<head>`.
|
|
207
242
|
*
|
|
@@ -229,6 +264,9 @@ export const SURFACE_CSP = "default-src 'none'; script-src 'unsafe-inline'; styl
|
|
|
229
264
|
*/
|
|
230
265
|
export function renderSurfaceDocument(spec) {
|
|
231
266
|
const extraCss = spec.extraCss === undefined ? '' : `\n${spec.extraCss}`;
|
|
267
|
+
const hasDisclosures = spec.bodyHtml.includes('<details class="mcpui-detail-disclosure');
|
|
268
|
+
const detailCss = hasDisclosures ? STRUCTURED_DETAILS_CSS : '';
|
|
269
|
+
const detailScript = hasDisclosures ? `\n${DETAIL_DISCLOSURE_SCRIPT}` : '';
|
|
232
270
|
return `<!doctype html>
|
|
233
271
|
<html lang="${escapeHtml(spec.lang ?? 'en')}">
|
|
234
272
|
<head>
|
|
@@ -238,7 +276,7 @@ export function renderSurfaceDocument(spec) {
|
|
|
238
276
|
<title>${escapeHtml(spec.title)}</title>
|
|
239
277
|
<style>
|
|
240
278
|
${renderTokenBlock(spec.tokens)}
|
|
241
|
-
${SURFACE_BASE_CSS}${extraCss}
|
|
279
|
+
${SURFACE_BASE_CSS}${detailCss}${extraCss}
|
|
242
280
|
</style>
|
|
243
281
|
</head>
|
|
244
282
|
<body>
|
|
@@ -249,7 +287,7 @@ ${spec.bodyHtml}
|
|
|
249
287
|
${renderBridgeScript(spec.app)}
|
|
250
288
|
</script>
|
|
251
289
|
<script>
|
|
252
|
-
${spec.script}
|
|
290
|
+
${spec.script}${detailScript}
|
|
253
291
|
</script>
|
|
254
292
|
</body>
|
|
255
293
|
</html>`;
|
|
@@ -271,7 +309,7 @@ export function renderSurfaceHeader(spec) {
|
|
|
271
309
|
* @param options.layout - Outcome rows size independently and stack in narrow cards.
|
|
272
310
|
* @returns The `<dl>`, or `''` for an empty list — an empty `<dl>` is announced by screen readers
|
|
273
311
|
* as a list with no items, which is worse than no list.
|
|
274
|
-
* @complexity O(n)
|
|
312
|
+
* @complexity O(n * depth) with previous snapshots, otherwise O(n) rendered bytes; no effects or input mutation.
|
|
275
313
|
*/
|
|
276
314
|
export function renderDetailList(details, options = {}) {
|
|
277
315
|
if (details.length === 0)
|
|
@@ -279,10 +317,9 @@ export function renderDetailList(details, options = {}) {
|
|
|
279
317
|
const outcome = options.layout === 'outcome';
|
|
280
318
|
const rows = details
|
|
281
319
|
.map((detail) => {
|
|
282
|
-
const
|
|
283
|
-
const body = detail.format === 'code' ? `<pre class="mcpui-code">${value}</pre>` : value;
|
|
320
|
+
const body = renderStructuredDetail({ detail });
|
|
284
321
|
const description = detail.description === undefined ? '' : `<span class="mcpui-hint">${escapeHtml(detail.description)}</span>`;
|
|
285
|
-
const valueClass = detail.format === 'status' ? ` class="mcpui-detail-status" title="${value}"` : '';
|
|
322
|
+
const valueClass = detail.format === 'status' && typeof detail.value === 'string' ? ` class="mcpui-detail-status" title="${escapeHtml(detail.value)}"` : '';
|
|
286
323
|
const pair = `<dt>${escapeHtml(detail.label)}${description}</dt><dd${valueClass}>${body}</dd>`;
|
|
287
324
|
return outcome ? ` <div class="mcpui-outcome-row">${pair}</div>` : ` ${pair}`;
|
|
288
325
|
})
|
|
@@ -351,6 +388,8 @@ function reportCallFailure(error) {
|
|
|
351
388
|
* three silently — a space in a `name` produces an `id` the `for` attribute cannot reference, so
|
|
352
389
|
* clicking the label stops focusing the control and nothing reports an error.
|
|
353
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';
|
|
354
393
|
const FIELD_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
|
|
355
394
|
/**
|
|
356
395
|
* Builds the DOM id for a field's control, validating the name on the way through.
|
|
@@ -375,7 +414,7 @@ export function renderFieldLabel(spec) {
|
|
|
375
414
|
const hint = spec.hint === undefined
|
|
376
415
|
? ''
|
|
377
416
|
: `\n<span class="mcpui-hint" id="${escapeHtml(fieldElementId(spec.name))}-hint">${escapeHtml(spec.hint)}</span>`;
|
|
378
|
-
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}`;
|
|
379
418
|
}
|
|
380
419
|
/** The `aria-describedby` attribute pointing at a field's hint, or `''` when it has none. */
|
|
381
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
|
|
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;
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* return type — a compile error at the switch itself. A `default` that threw would instead move
|
|
15
15
|
* that failure to runtime, and add a branch no test could reach.
|
|
16
16
|
*/
|
|
17
|
+
import { unhandledCase } from '../../../utils/unhandled-case.js';
|
|
17
18
|
import { renderCheckbox } from './checkbox.js';
|
|
18
19
|
import { renderChoiceGroup } from './choice-group.js';
|
|
19
20
|
import { renderSelect } from './select.js';
|
|
@@ -30,42 +31,44 @@ function present(source, keys) {
|
|
|
30
31
|
}
|
|
31
32
|
return picked;
|
|
32
33
|
}
|
|
33
|
-
function base(field) {
|
|
34
|
-
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 } : {}) };
|
|
35
36
|
}
|
|
36
37
|
/** One-of-N, drawn as radios or a select per {@link EnumField.presentation}. @complexity O(n) in the rendered length. */
|
|
37
|
-
function renderEnumField(field) {
|
|
38
|
+
function renderEnumField(field, options) {
|
|
38
39
|
if (field.presentation === 'radio') {
|
|
39
40
|
return renderChoiceGroup({
|
|
40
|
-
...base(field),
|
|
41
|
+
...base(field, options),
|
|
41
42
|
selection: 'single',
|
|
42
43
|
options: field.options,
|
|
43
44
|
...(field.value === undefined ? {} : { value: [field.value] }),
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
|
-
return renderSelect({ ...base(field), options: field.options, ...present(field, ['value', 'placeholder']) });
|
|
47
|
+
return renderSelect({ ...base(field, options), options: field.options, ...present(field, ['value', 'placeholder']) });
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
49
50
|
* Renders the control fragment for one field, dispatching on its kind.
|
|
50
51
|
*
|
|
51
52
|
* @param field - See {@link SurfaceField}.
|
|
52
53
|
* @param text - Optional host translations for secret reveal controls.
|
|
54
|
+
* @param options - See {@link FieldRenderOptions}.
|
|
53
55
|
* @returns An HTML fragment.
|
|
54
56
|
* @complexity O(n) in the rendered length.
|
|
55
57
|
*/
|
|
56
|
-
export function renderFieldControl(field, text) {
|
|
58
|
+
export function renderFieldControl(field, text, options = {}) {
|
|
57
59
|
switch (field.kind) {
|
|
58
60
|
case 'string':
|
|
59
|
-
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']) });
|
|
60
62
|
case 'number':
|
|
61
|
-
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']) });
|
|
62
64
|
case 'boolean':
|
|
63
|
-
return renderCheckbox({ ...base(field), ...present(field, ['value']) });
|
|
65
|
+
return renderCheckbox({ ...base(field, options), ...present(field, ['value']) });
|
|
64
66
|
case 'enum':
|
|
65
|
-
return renderEnumField(field);
|
|
67
|
+
return renderEnumField(field, options);
|
|
66
68
|
case 'multi-enum':
|
|
67
|
-
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']) });
|
|
68
70
|
}
|
|
71
|
+
return unhandledCase(field);
|
|
69
72
|
}
|
|
70
73
|
/** Projects fields to runtime validation data; conditional requirements also carry their guidance. */
|
|
71
74
|
export function toFieldReadSpecs(fields) {
|