@jini-ai/ui 0.6.15-rc.2 → 0.6.15
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/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/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/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/react.js +3 -1
- 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/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/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/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/confirmation.d.ts +5 -0
- package/dist/features/mcp-ui/surfaces/confirmation.js +1 -0
- package/dist/features/mcp-ui/surfaces/document.d.ts +3 -15
- package/dist/features/mcp-ui/surfaces/document.js +37 -6
- package/dist/features/mcp-ui/surfaces/fields.js +2 -0
- 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.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.js +1 -1
- package/dist/features/mcp-ui/surfaces/tool-approval.d.ts +21 -0
- package/dist/features/mcp-ui/surfaces/tool-approval.js +41 -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/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/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/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 +10 -9
|
@@ -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.
|
|
@@ -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
|
*
|
|
@@ -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';
|
|
@@ -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
|
*
|
|
@@ -202,6 +203,34 @@ body {
|
|
|
202
203
|
fully done"); the accent token reads as "worth a second look" without asserting either. */
|
|
203
204
|
.mcpui-status[data-state="partial"] { color: var(--jini-mcpui-accent); }
|
|
204
205
|
.mcpui-code { font-family: var(--jini-mcpui-font-mono); font-size: 13px; color: var(--jini-mcpui-text-faint); }`;
|
|
206
|
+
/** Structured details extend the same token system; legacy short-string documents keep their layout. */
|
|
207
|
+
const STRUCTURED_DETAILS_CSS = `
|
|
208
|
+
.mcpui-details { gap: 8px 16px; margin-top: 16px; }
|
|
209
|
+
.mcpui-details dt { font-weight: 400; }
|
|
210
|
+
.mcpui-details dd { min-width: 0; font-weight: 400; }
|
|
211
|
+
.mcpui-detail-group { margin: 0; padding: 0; padding-inline-start: 8px; border-inline-start: 1px solid var(--jini-mcpui-border); }
|
|
212
|
+
.mcpui-detail-row { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); gap: 8px; padding: 8px 0; }
|
|
213
|
+
.mcpui-detail-row:first-child { padding-top: 0; }
|
|
214
|
+
.mcpui-detail-row:last-child { padding-bottom: 0; }
|
|
215
|
+
.mcpui-detail-row:has(> dd > .mcpui-detail-group) { grid-template-columns: minmax(0, 1fr); }
|
|
216
|
+
.mcpui-detail-array { margin: 0; padding-inline-start: 16px; }
|
|
217
|
+
.mcpui-detail-array li + li { margin-top: 8px; }
|
|
218
|
+
.mcpui-detail-disclosure { min-width: 0; margin-top: 8px; }
|
|
219
|
+
.mcpui-detail-disclosure:first-child { margin-top: 0; }
|
|
220
|
+
.mcpui-detail-disclosure > summary { color: var(--jini-mcpui-text-muted); cursor: pointer; overflow-wrap: anywhere; }
|
|
221
|
+
.mcpui-detail-disclosure > summary:focus-visible { outline: 2px solid var(--jini-mcpui-accent-ring); outline-offset: 2px; }
|
|
222
|
+
.mcpui-detail-disclosure > .mcpui-code, .mcpui-detail-disclosure > .mcpui-detail-group,
|
|
223
|
+
.mcpui-detail-full { margin-top: 8px; }
|
|
224
|
+
.mcpui-detail-preview { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; color: var(--jini-mcpui-text); }
|
|
225
|
+
.mcpui-detail-long > summary { list-style: none; }
|
|
226
|
+
.mcpui-detail-long > summary::-webkit-details-marker { display: none; }
|
|
227
|
+
.mcpui-detail-less, .mcpui-detail-long[open] .mcpui-detail-more,
|
|
228
|
+
.mcpui-detail-long[open] .mcpui-detail-preview { display: none; }
|
|
229
|
+
.mcpui-detail-long[open] .mcpui-detail-less { display: inline; }
|
|
230
|
+
.mcpui-detail-change { display: flex; flex-direction: column; gap: 8px; }
|
|
231
|
+
.mcpui-detail-change > div > .mcpui-hint { display: block; }
|
|
232
|
+
.mcpui-detail-raw { font-size: 12px; }
|
|
233
|
+
.mcpui-detail-disclosure .mcpui-code { color: var(--jini-mcpui-text); }`;
|
|
205
234
|
/**
|
|
206
235
|
* The Content-Security-Policy every generated surface declares in its own `<head>`.
|
|
207
236
|
*
|
|
@@ -229,6 +258,9 @@ export const SURFACE_CSP = "default-src 'none'; script-src 'unsafe-inline'; styl
|
|
|
229
258
|
*/
|
|
230
259
|
export function renderSurfaceDocument(spec) {
|
|
231
260
|
const extraCss = spec.extraCss === undefined ? '' : `\n${spec.extraCss}`;
|
|
261
|
+
const hasDisclosures = spec.bodyHtml.includes('<details class="mcpui-detail-disclosure');
|
|
262
|
+
const detailCss = hasDisclosures ? STRUCTURED_DETAILS_CSS : '';
|
|
263
|
+
const detailScript = hasDisclosures ? `\n${DETAIL_DISCLOSURE_SCRIPT}` : '';
|
|
232
264
|
return `<!doctype html>
|
|
233
265
|
<html lang="${escapeHtml(spec.lang ?? 'en')}">
|
|
234
266
|
<head>
|
|
@@ -238,7 +270,7 @@ export function renderSurfaceDocument(spec) {
|
|
|
238
270
|
<title>${escapeHtml(spec.title)}</title>
|
|
239
271
|
<style>
|
|
240
272
|
${renderTokenBlock(spec.tokens)}
|
|
241
|
-
${SURFACE_BASE_CSS}${extraCss}
|
|
273
|
+
${SURFACE_BASE_CSS}${detailCss}${extraCss}
|
|
242
274
|
</style>
|
|
243
275
|
</head>
|
|
244
276
|
<body>
|
|
@@ -249,7 +281,7 @@ ${spec.bodyHtml}
|
|
|
249
281
|
${renderBridgeScript(spec.app)}
|
|
250
282
|
</script>
|
|
251
283
|
<script>
|
|
252
|
-
${spec.script}
|
|
284
|
+
${spec.script}${detailScript}
|
|
253
285
|
</script>
|
|
254
286
|
</body>
|
|
255
287
|
</html>`;
|
|
@@ -271,7 +303,7 @@ export function renderSurfaceHeader(spec) {
|
|
|
271
303
|
* @param options.layout - Outcome rows size independently and stack in narrow cards.
|
|
272
304
|
* @returns The `<dl>`, or `''` for an empty list — an empty `<dl>` is announced by screen readers
|
|
273
305
|
* as a list with no items, which is worse than no list.
|
|
274
|
-
* @complexity O(n)
|
|
306
|
+
* @complexity O(n * depth) with previous snapshots, otherwise O(n) rendered bytes; no effects or input mutation.
|
|
275
307
|
*/
|
|
276
308
|
export function renderDetailList(details, options = {}) {
|
|
277
309
|
if (details.length === 0)
|
|
@@ -279,10 +311,9 @@ export function renderDetailList(details, options = {}) {
|
|
|
279
311
|
const outcome = options.layout === 'outcome';
|
|
280
312
|
const rows = details
|
|
281
313
|
.map((detail) => {
|
|
282
|
-
const
|
|
283
|
-
const body = detail.format === 'code' ? `<pre class="mcpui-code">${value}</pre>` : value;
|
|
314
|
+
const body = renderStructuredDetail({ detail });
|
|
284
315
|
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}"` : '';
|
|
316
|
+
const valueClass = detail.format === 'status' && typeof detail.value === 'string' ? ` class="mcpui-detail-status" title="${escapeHtml(detail.value)}"` : '';
|
|
286
317
|
const pair = `<dt>${escapeHtml(detail.label)}${description}</dt><dd${valueClass}>${body}</dd>`;
|
|
287
318
|
return outcome ? ` <div class="mcpui-outcome-row">${pair}</div>` : ` ${pair}`;
|
|
288
319
|
})
|
|
@@ -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';
|
|
@@ -66,6 +67,7 @@ export function renderFieldControl(field, text) {
|
|
|
66
67
|
case 'multi-enum':
|
|
67
68
|
return renderChoiceGroup({ ...base(field), selection: 'multiple', options: field.options, ...present(field, ['value']) });
|
|
68
69
|
}
|
|
70
|
+
return unhandledCase(field);
|
|
69
71
|
}
|
|
70
72
|
/** Projects fields to runtime validation data; conditional requirements also carry their guidance. */
|
|
71
73
|
export function toFieldReadSpecs(fields) {
|
|
@@ -79,7 +79,8 @@ export function applyHostSurfaceTokens(html, tokens) {
|
|
|
79
79
|
if (close === -1)
|
|
80
80
|
return html;
|
|
81
81
|
const at = close + '</style>'.length;
|
|
82
|
-
const
|
|
82
|
+
const declarations = entries.map(([name, value]) => ` ${name}: ${value};`).join('\n');
|
|
83
|
+
const rule = `\n<style ${HOST_THEME_STYLE_ATTRIBUTE}>\n:root {\n${declarations}\n}\n</style>`;
|
|
83
84
|
return `${html.slice(0, at)}${rule}${html.slice(at)}`;
|
|
84
85
|
}
|
|
85
86
|
//# sourceMappingURL=host-theme.js.map
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
*/
|
|
45
45
|
import { createUIResource } from '../resource.js';
|
|
46
46
|
import { escapeHtml, escapeJsValue } from '../escape.js';
|
|
47
|
-
import { renderActions, renderDetailList,
|
|
47
|
+
import { renderActions, renderDetailList, renderSurfaceDocument, renderSurfaceHeader, SURFACE_SCRIPT_PRELUDE, SURFACE_STATUS_ELEMENT_ID, } from './document.js';
|
|
48
48
|
const DEFAULT_APP = { appName: 'jini-mcp-ui-outcome', appVersion: '1' };
|
|
49
49
|
const DEFAULT_OPEN_LINK_LABEL = 'Open site';
|
|
50
50
|
const OPEN_LINK_ACTION_ID = 'open-link';
|
|
@@ -17,8 +17,9 @@ import { fieldDescribedBy, fieldElementId, renderFieldLabel } from './document.j
|
|
|
17
17
|
*/
|
|
18
18
|
export function renderSelect(props) {
|
|
19
19
|
const id = fieldElementId(props.name);
|
|
20
|
+
const placeholderSelected = props.value === undefined ? 'selected ' : '';
|
|
20
21
|
const placeholderOption = props.required === true
|
|
21
|
-
? `\n <option value="" ${
|
|
22
|
+
? `\n <option value="" ${placeholderSelected}disabled>${escapeHtml(props.placeholder ?? '—')}</option>`
|
|
22
23
|
: '';
|
|
23
24
|
const options = props.options
|
|
24
25
|
.map((option) => {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** One fact. Values and previous snapshots must be JSON-serializable; strings remain supported. */
|
|
2
|
+
export interface SurfaceDetail {
|
|
3
|
+
readonly label: string;
|
|
4
|
+
readonly value: unknown;
|
|
5
|
+
/** Presence enables a diff, including an explicitly absent previous override. */
|
|
6
|
+
readonly previous?: unknown;
|
|
7
|
+
/** Explicit, optionally translated labels keyed by dotted object path. */
|
|
8
|
+
readonly labels?: Readonly<Record<string, string>>;
|
|
9
|
+
/** Optional secondary context beneath the label, using the shared muted hint style. */
|
|
10
|
+
readonly description?: string;
|
|
11
|
+
/**
|
|
12
|
+
* `code` discloses a monospace `<pre>` that keeps line breaks and scrolls inside its own box
|
|
13
|
+
* past a fixed height, so long SQL or JSON remains readable without stretching the dialog past
|
|
14
|
+
* its frame. `status` reserves one line for a short outcome value. Omitted (or `text`) permits
|
|
15
|
+
* value-sensitive presentation, preserving plain inline strings for short prose.
|
|
16
|
+
*/
|
|
17
|
+
readonly format?: 'text' | 'code' | 'status';
|
|
18
|
+
}
|
|
19
|
+
type ValueContext = {
|
|
20
|
+
value: unknown;
|
|
21
|
+
path: string;
|
|
22
|
+
labels: Readonly<Record<string, string>>;
|
|
23
|
+
format?: SurfaceDetail['format'];
|
|
24
|
+
};
|
|
25
|
+
/** Native details owns keyboard interaction; mirror its state for explicit ARIA consumers. */
|
|
26
|
+
export declare const DETAIL_DISCLOSURE_SCRIPT = "document.addEventListener(\"toggle\", function (event) {\n var disclosure = event.target;\n if (!disclosure.matches || !disclosure.matches(\"details.mcpui-detail-disclosure\")) return;\n disclosure.querySelector(\"summary\").setAttribute(\"aria-expanded\", disclosure.open ? \"true\" : \"false\");\n}, true);";
|
|
27
|
+
/** Humanized English fallback; caller labels remain verbatim. @complexity O(k) key length. */
|
|
28
|
+
export declare function surfaceDetailLabel({ key, path, labels }: {
|
|
29
|
+
key: string;
|
|
30
|
+
path: string;
|
|
31
|
+
labels: ValueContext['labels'];
|
|
32
|
+
}, _options?: {}): string;
|
|
33
|
+
/** One reusable detail body. Raw JSON is escaped text, never a script or HTML fragment.
|
|
34
|
+
* @returns HTML for JSON-compatible caller data; JSON.stringify errors propagate for invalid data.
|
|
35
|
+
* @complexity O(n * depth) with a previous snapshot, otherwise O(n), in serialized bytes. */
|
|
36
|
+
export declare function renderStructuredDetail({ detail }: {
|
|
37
|
+
detail: SurfaceDetail;
|
|
38
|
+
}, _options?: {}): string;
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=structured-details.d.ts.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/** Shared value presentation for confirmation, form and outcome details. Never interprets HTML. */
|
|
2
|
+
import { escapeHtml } from '../escape.js';
|
|
3
|
+
/** Native details owns keyboard interaction; mirror its state for explicit ARIA consumers. */
|
|
4
|
+
export const DETAIL_DISCLOSURE_SCRIPT = `document.addEventListener("toggle", function (event) {
|
|
5
|
+
var disclosure = event.target;
|
|
6
|
+
if (!disclosure.matches || !disclosure.matches("details.mcpui-detail-disclosure")) return;
|
|
7
|
+
disclosure.querySelector("summary").setAttribute("aria-expanded", disclosure.open ? "true" : "false");
|
|
8
|
+
}, true);`;
|
|
9
|
+
/** Humanized English fallback; caller labels remain verbatim. @complexity O(k) key length. */
|
|
10
|
+
export function surfaceDetailLabel({ key, path, labels }, _options = {}) {
|
|
11
|
+
if (Object.hasOwn(labels, path))
|
|
12
|
+
return labels[path];
|
|
13
|
+
const words = key.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/([A-Z])([A-Z][a-z])/g, '$1 $2').replace(/[_-]+/g, ' ').toLowerCase();
|
|
14
|
+
const acronyms = words.replace(/\b(url|css|html|json|api|id)\b/g, word => word.toUpperCase());
|
|
15
|
+
return acronyms.charAt(0).toUpperCase() + acronyms.slice(1);
|
|
16
|
+
}
|
|
17
|
+
function isRecord(value) {
|
|
18
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
function disclosure({ summary, body, kind = '' }, _options = {}) {
|
|
21
|
+
return `<details class="mcpui-detail-disclosure ${kind}"><summary aria-expanded="false">${escapeHtml(summary)}</summary>${body}</details>`;
|
|
22
|
+
}
|
|
23
|
+
/** JSON scalar display; missing override values use the same empty marker as null. */
|
|
24
|
+
function scalarText({ value }, _options = {}) {
|
|
25
|
+
if (value === undefined || value === null || value === '')
|
|
26
|
+
return '—';
|
|
27
|
+
if (typeof value === 'boolean')
|
|
28
|
+
return value ? 'On' : 'Off';
|
|
29
|
+
return String(value);
|
|
30
|
+
}
|
|
31
|
+
/** Classifies content for a disclosure, preserving the original text bytes in the body. */
|
|
32
|
+
function codeKind({ value, path, format }, _options = {}) {
|
|
33
|
+
const hasDeclaration = value.split('}').slice(0, -1).some(part => {
|
|
34
|
+
const open = part.indexOf('{');
|
|
35
|
+
return open >= 0 && part.indexOf(':', open + 1) >= 0;
|
|
36
|
+
});
|
|
37
|
+
if (/css$/i.test(path) || (hasDeclaration && !/^\s*[\[{]/.test(value)))
|
|
38
|
+
return 'CSS';
|
|
39
|
+
if (/^\s*</.test(value))
|
|
40
|
+
return 'HTML';
|
|
41
|
+
if (/^\s*[\[{]/.test(value))
|
|
42
|
+
return 'JSON';
|
|
43
|
+
if (format === 'code')
|
|
44
|
+
return 'Code';
|
|
45
|
+
return /[\r\n]/.test(value) ? 'Text' : undefined;
|
|
46
|
+
}
|
|
47
|
+
/** Escapes all text once at the HTML sink. Long prose uses a three-line summary. */
|
|
48
|
+
function renderText({ value, path, format }, _options = {}) {
|
|
49
|
+
const text = escapeHtml(value);
|
|
50
|
+
const kind = codeKind({ value, path, format });
|
|
51
|
+
if (kind) {
|
|
52
|
+
const lines = value.split(/\r\n|\r|\n/).length;
|
|
53
|
+
return disclosure({ summary: `${kind} · ${lines} ${lines === 1 ? 'line' : 'lines'}`, body: `<pre class="mcpui-code">${text}</pre>`, kind: 'mcpui-detail-code' });
|
|
54
|
+
}
|
|
55
|
+
if (value.length <= 80)
|
|
56
|
+
return text;
|
|
57
|
+
return `<details class="mcpui-detail-disclosure mcpui-detail-long"><summary aria-expanded="false"><span class="mcpui-detail-preview">${text}</span><span class="mcpui-detail-more">Show more</span><span class="mcpui-detail-less">Show less</span></summary><div class="mcpui-detail-full">${text}</div></details>`;
|
|
58
|
+
}
|
|
59
|
+
/** Arrays keep scalar items compact; nested or long items use readable bullet rows. */
|
|
60
|
+
function renderArray({ value, ...context }, _options = {}) {
|
|
61
|
+
if (value.length === 0)
|
|
62
|
+
return '<span class="mcpui-detail-value">—</span>';
|
|
63
|
+
const compact = value.every(item => item === null || ['boolean', 'number', 'string'].includes(typeof item))
|
|
64
|
+
&& value.every(item => String(item).length <= 80 && !/[\r\n<{[]/.test(String(item)));
|
|
65
|
+
if (compact)
|
|
66
|
+
return renderText({ ...context, value: value.map(item => scalarText({ value: item })).join(', ') });
|
|
67
|
+
const items = value.map((item, index) => {
|
|
68
|
+
const path = context.path ? `${context.path}.${index}` : String(index);
|
|
69
|
+
return `<li>${renderValue({ ...context, value: item, path })}</li>`;
|
|
70
|
+
});
|
|
71
|
+
const rest = items.length <= 5 ? '' : disclosure({ summary: `Show more (${items.length - 5} items)`, body: `<ul class="mcpui-detail-array">${items.slice(5).join('')}</ul>` });
|
|
72
|
+
return `<ul class="mcpui-detail-array">${items.slice(0, 5).join('')}</ul>${rest}`;
|
|
73
|
+
}
|
|
74
|
+
function objectRow({ key, context, body }, _options = {}) {
|
|
75
|
+
return `<div class="mcpui-detail-row"><dt>${escapeHtml(surfaceDetailLabel({ key, path: context.path, labels: context.labels }))}</dt><dd>${body}</dd></div>`;
|
|
76
|
+
}
|
|
77
|
+
/** Nested objects stay definition lists, indented once per actual group. @complexity O(n) rendered bytes. */
|
|
78
|
+
function renderObject({ value, ...context }, _options = {}) {
|
|
79
|
+
const entries = Object.entries(value);
|
|
80
|
+
if (entries.length === 0)
|
|
81
|
+
return '<span class="mcpui-detail-value">—</span>';
|
|
82
|
+
const rows = entries.map(([key, item]) => {
|
|
83
|
+
const child = { ...context, value: item, path: context.path ? `${context.path}.${key}` : key };
|
|
84
|
+
return objectRow({ key, context: child, body: renderValue(child) });
|
|
85
|
+
}).join('');
|
|
86
|
+
return `<dl class="mcpui-detail-group">${rows}</dl>`;
|
|
87
|
+
}
|
|
88
|
+
/** Pure recursive rendering, O(n) in output bytes and O(depth) traversal stack; no external I/O. */
|
|
89
|
+
function renderValue(context, _options = {}) {
|
|
90
|
+
const { value } = context;
|
|
91
|
+
if (Array.isArray(value))
|
|
92
|
+
return renderArray({ ...context, value });
|
|
93
|
+
if (isRecord(value))
|
|
94
|
+
return renderObject({ ...context, value });
|
|
95
|
+
if (typeof value === 'string' && value !== '')
|
|
96
|
+
return renderText({ ...context, value });
|
|
97
|
+
return escapeHtml(scalarText({ value }));
|
|
98
|
+
}
|
|
99
|
+
/** Object order is immaterial; array order and missing keys matter to an approval diff. */
|
|
100
|
+
function equalValue({ before, after }, _options = {}) {
|
|
101
|
+
if (Object.is(before, after))
|
|
102
|
+
return true;
|
|
103
|
+
if (Array.isArray(before) && Array.isArray(after)) {
|
|
104
|
+
return before.length === after.length && before.every((item, index) => equalValue({ before: item, after: after[index] }));
|
|
105
|
+
}
|
|
106
|
+
if (!isRecord(before) || !isRecord(after))
|
|
107
|
+
return false;
|
|
108
|
+
const keys = Object.keys(before);
|
|
109
|
+
return keys.length === Object.keys(after).length && keys.every(key => Object.hasOwn(after, key) && equalValue({ before: before[key], after: after[key] }));
|
|
110
|
+
}
|
|
111
|
+
/** A missing JSON key must never read an inherited property such as __proto__ or constructor. */
|
|
112
|
+
function ownValue({ record, key }, _options = {}) {
|
|
113
|
+
return Object.hasOwn(record, key) ? record[key] : undefined;
|
|
114
|
+
}
|
|
115
|
+
/** Presence needs its own label: null and missing otherwise both display an empty marker. */
|
|
116
|
+
function renderFieldDiff({ child, previous, value, key }, _options = {}) {
|
|
117
|
+
const body = renderDiff(child);
|
|
118
|
+
if (Object.hasOwn(previous, key) && Object.hasOwn(value, key))
|
|
119
|
+
return body;
|
|
120
|
+
const label = Object.hasOwn(value, key) ? 'Added' : 'Removed';
|
|
121
|
+
return `<span class="mcpui-hint">${label}</span>${body}`;
|
|
122
|
+
}
|
|
123
|
+
/** Partition by equality before rendering so unchanged fields never crowd out changes.
|
|
124
|
+
* @complexity O(n * depth) worst-case nested comparison work; O(n) markup space. No reads or mutation. */
|
|
125
|
+
function renderObjectDiff(context, _options = {}) {
|
|
126
|
+
const { previous, value } = context;
|
|
127
|
+
const keys = [...new Set([...Object.keys(value), ...Object.keys(previous)])];
|
|
128
|
+
const changed = [];
|
|
129
|
+
const unchanged = [];
|
|
130
|
+
for (const key of keys) {
|
|
131
|
+
const child = { ...context, previous: ownValue({ record: previous, key }), value: ownValue({ record: value, key }), path: context.path ? `${context.path}.${key}` : key };
|
|
132
|
+
const same = Object.hasOwn(previous, key) === Object.hasOwn(value, key) && equalValue({ before: child.previous, after: child.value });
|
|
133
|
+
const body = same ? renderValue(child) : renderFieldDiff({ child, previous, value, key });
|
|
134
|
+
(same ? unchanged : changed).push(objectRow({ key, context: child, body }));
|
|
135
|
+
}
|
|
136
|
+
const rest = unchanged.length === 0 ? '' : disclosure({ summary: `Unchanged fields (${unchanged.length})`, body: `<dl class="mcpui-detail-group">${unchanged.join('')}</dl>`, kind: 'mcpui-detail-unchanged' });
|
|
137
|
+
const changes = changed.length === 0 ? '' : `<dl class="mcpui-detail-group">${changed.join('')}</dl>`;
|
|
138
|
+
return changes + rest || '<span class="mcpui-detail-value">—</span>';
|
|
139
|
+
}
|
|
140
|
+
/** Recurse only through object pairs; atomic and array updates show explicit before/after values. */
|
|
141
|
+
function renderDiff(context, _options = {}) {
|
|
142
|
+
if (isRecord(context.previous) && isRecord(context.value))
|
|
143
|
+
return renderObjectDiff({ ...context, previous: context.previous, value: context.value });
|
|
144
|
+
return `<div class="mcpui-detail-change"><div><span class="mcpui-hint">Before</span>${renderValue({ ...context, value: context.previous })}</div><div><span class="mcpui-hint">After</span>${renderValue(context)}</div></div>`;
|
|
145
|
+
}
|
|
146
|
+
/** One reusable detail body. Raw JSON is escaped text, never a script or HTML fragment.
|
|
147
|
+
* @returns HTML for JSON-compatible caller data; JSON.stringify errors propagate for invalid data.
|
|
148
|
+
* @complexity O(n * depth) with a previous snapshot, otherwise O(n), in serialized bytes. */
|
|
149
|
+
export function renderStructuredDetail({ detail }, _options = {}) {
|
|
150
|
+
const context = { value: detail.value, path: '', labels: detail.labels ?? {}, format: detail.format };
|
|
151
|
+
const hasPrevious = Object.hasOwn(detail, 'previous');
|
|
152
|
+
const body = hasPrevious ? renderDiff({ ...context, previous: detail.previous }) : renderValue(context);
|
|
153
|
+
const raw = hasPrevious ? { previous: detail.previous, value: detail.value } : detail.value;
|
|
154
|
+
const showRaw = hasPrevious || (typeof detail.value === 'object' && detail.value !== null) || body.startsWith('<details ');
|
|
155
|
+
if (!showRaw)
|
|
156
|
+
return body;
|
|
157
|
+
return body + disclosure({ summary: 'View raw', body: `<pre class="mcpui-code">${escapeHtml(JSON.stringify(raw, null, 2))}</pre>`, kind: 'mcpui-detail-raw' });
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=structured-details.js.map
|