@script-it/json-views-react 2026.10.29 → 2026.11.44
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/README.md +3 -1
- package/dist/json/edit-interaction.d.ts +1 -0
- package/dist/json/edit-interaction.js +2 -0
- package/dist/json/json-content.d.ts +2 -0
- package/dist/json/json-content.js +2 -2
- package/dist/json/json-view.d.ts +1 -1
- package/dist/json/json-view.js +30 -29
- package/dist/json/schema-value.js +5 -2
- package/dist/json/table-view.js +6 -4
- package/dist/json/view-types.d.ts +2 -0
- package/dist/json/viewers/general/general-json-view.js +4 -4
- package/dist/structured-data/atomic-value-editor.d.ts +2 -1
- package/dist/structured-data/atomic-value-editor.js +3 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -319,4 +319,6 @@ The frame inherits `--jv-background`, `--jv-foreground`, `--jv-muted`, `--jv-bor
|
|
|
319
319
|
|
|
320
320
|
Limits: HTML and CSS 256 KiB each, template 5,000 nodes / depth 64, expansion 10,000 nodes. Large arbitrary layouts are not virtualized. HTML views do not use built-in columns/filter/sort/group configuration; use another ordinary view to browse the same collection as a table or board.
|
|
321
321
|
|
|
322
|
-
|
|
322
|
+
App-specific HTML views are bundled in the website’s `App Views` folder.
|
|
323
|
+
|
|
324
|
+
Set `buttonToEdit={true}` on `JSONContent` or `JsonViewer` to edit Markdown/body fields only through the pencil button at the top right (visible on hover, keyboard focus, and touch devices). The default is `false` for click-to-edit compatibility. The website always enables this behavior.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const ButtonToEditContext: import("react").Context<boolean>;
|
|
@@ -49,6 +49,8 @@ export interface StructuredDataContentProps {
|
|
|
49
49
|
clearBehavior?: JsonViewClearBehavior;
|
|
50
50
|
/** Clamps tall table cells and reveals one on selection (default), or lets rows grow to fit. */
|
|
51
51
|
tableCells?: JsonViewsTableCellOptions;
|
|
52
|
+
/** Require the hover/focus edit button for Markdown fields instead of clicking their content. Defaults to false. */
|
|
53
|
+
buttonToEdit?: boolean;
|
|
52
54
|
onDraftRowsChange?: (paths: readonly ValuePath[]) => void;
|
|
53
55
|
renderMarkdown?: (content: string) => ReactNode;
|
|
54
56
|
renderExternalWidget?: JsonViewSchemaExternalWidgetRenderer;
|
|
@@ -64,7 +64,7 @@ export function JSONContent(props) {
|
|
|
64
64
|
export function CSVContent(props) {
|
|
65
65
|
return _jsx(StructuredDataContent, { ...props, adapter: csvFormat });
|
|
66
66
|
}
|
|
67
|
-
function StructuredDataContentSession({ content, adapter, documentId, revision, metadata, metadataPersistence, presentationState, onMetadataChange, onRequestMetadataPersistence, metadataConversionConfirmation = 'viewer', allowRootArrayWrapping = true, path, uiSize, fillHeight = false, edit, onSave, isSaving, isDirty, saveError, sourceVisible, onSourceVisibleChange, clearBehavior = 'null', tableCells, onDraftRowsChange, renderExternalWidget, renderMarkdown, }) {
|
|
67
|
+
function StructuredDataContentSession({ content, adapter, documentId, revision, metadata, metadataPersistence, presentationState, onMetadataChange, onRequestMetadataPersistence, metadataConversionConfirmation = 'viewer', allowRootArrayWrapping = true, path, uiSize, fillHeight = false, edit, onSave, isSaving, isDirty, saveError, sourceVisible, onSourceVisibleChange, clearBehavior = 'null', buttonToEdit, tableCells, onDraftRowsChange, renderExternalWidget, renderMarkdown, }) {
|
|
68
68
|
const { types } = useJsonViewsRegistries();
|
|
69
69
|
const [editorGeneration, setEditorGeneration] = useState(0);
|
|
70
70
|
const [draftRows, setDraftRows] = useState([]);
|
|
@@ -436,5 +436,5 @@ function StructuredDataContentSession({ content, adapter, documentId, revision,
|
|
|
436
436
|
replace: replaceValue, replaceMany: replaceValues, ...(metadataWritable ? { setOptionColor } : {}), replaceViews,
|
|
437
437
|
removeMany: removeValues, saving: document.saving,
|
|
438
438
|
} : undefined;
|
|
439
|
-
return (_jsxs(TooltipProvider, { children: [document.error && _jsxs("div", { className: "grid gap-2 px-4 py-2 text-sm", children: [_jsx("p", { role: "alert", className: "text-destructive", children: document.error }), document.dirty && _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx("button", { type: "button", disabled: document.saving, className: "rounded-md border border-border px-2 py-1 disabled:opacity-50", onClick: () => { void document.commit((source) => source).catch(() => { }); }, children: "Retry save" }), _jsx("button", { type: "button", disabled: document.saving, className: "rounded-md border border-border px-2 py-1 disabled:opacity-50", onClick: () => { document.reset(); setEditorGeneration((generation) => generation + 1); }, children: "Discard unsaved changes" }), _jsxs("details", { className: "basis-full", children: [_jsx("summary", { children: "Unsaved source" }), _jsx("pre", { className: "max-h-64 overflow-auto whitespace-pre-wrap font-mono text-xs", children: document.content })] })] })] }), formatDiagnostics.length > 0 && _jsxs("details", { className: "px-4 py-2 text-sm", children: [_jsx("summary", { children: formatDiagnostics.some((item) => item.severity === 'error') ? 'This document needs repair before structured editing.' : 'Some values require source editing to preserve their exact representation.' }), _jsx(JsonSourceRecovery, { source: document.content, error: formatDiagnostics.map((item) => item.message).join(' '), onCommit: recoveryCommit })] }), _jsx(JsonRendererBoundary, { source: document.content, onCommit: recoveryCommit, children: _jsx(JsonSourceLiteralsProvider, { diagnostics: sourceDiagnostics, children: _jsx(EditBaseContext.Provider, { value: document.acknowledgedContent, children: _jsx(JsonViewer, { compiled: compiled, defaultKey: path?.split('/').pop() || 'root', filePath: documentId ?? path, sourceContent: sourceEditor, sourceVisible: showingSource, onSourceVisibleChange: setShowingSource, tabularRoot: adapter.format === 'csv', editing: editing, onCurrentPathChange: rememberSourcePaths, renderExternalWidget: renderExternalWidget, renderMarkdown: renderMarkdown, sourceControl: sourceControl, tableCells: tableCells, uiSize: uiSize, fillHeight: fillHeight }) }) }) }, editorGeneration)] }));
|
|
439
|
+
return (_jsxs(TooltipProvider, { children: [document.error && _jsxs("div", { className: "grid gap-2 px-4 py-2 text-sm", children: [_jsx("p", { role: "alert", className: "text-destructive", children: document.error }), document.dirty && _jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx("button", { type: "button", disabled: document.saving, className: "rounded-md border border-border px-2 py-1 disabled:opacity-50", onClick: () => { void document.commit((source) => source).catch(() => { }); }, children: "Retry save" }), _jsx("button", { type: "button", disabled: document.saving, className: "rounded-md border border-border px-2 py-1 disabled:opacity-50", onClick: () => { document.reset(); setEditorGeneration((generation) => generation + 1); }, children: "Discard unsaved changes" }), _jsxs("details", { className: "basis-full", children: [_jsx("summary", { children: "Unsaved source" }), _jsx("pre", { className: "max-h-64 overflow-auto whitespace-pre-wrap font-mono text-xs", children: document.content })] })] })] }), formatDiagnostics.length > 0 && _jsxs("details", { className: "px-4 py-2 text-sm", children: [_jsx("summary", { children: formatDiagnostics.some((item) => item.severity === 'error') ? 'This document needs repair before structured editing.' : 'Some values require source editing to preserve their exact representation.' }), _jsx(JsonSourceRecovery, { source: document.content, error: formatDiagnostics.map((item) => item.message).join(' '), onCommit: recoveryCommit })] }), _jsx(JsonRendererBoundary, { source: document.content, onCommit: recoveryCommit, children: _jsx(JsonSourceLiteralsProvider, { diagnostics: sourceDiagnostics, children: _jsx(EditBaseContext.Provider, { value: document.acknowledgedContent, children: _jsx(JsonViewer, { compiled: compiled, defaultKey: path?.split('/').pop() || 'root', filePath: documentId ?? path, sourceContent: sourceEditor, sourceVisible: showingSource, onSourceVisibleChange: setShowingSource, tabularRoot: adapter.format === 'csv', editing: editing, onCurrentPathChange: rememberSourcePaths, renderExternalWidget: renderExternalWidget, renderMarkdown: renderMarkdown, sourceControl: sourceControl, buttonToEdit: buttonToEdit, tableCells: tableCells, uiSize: uiSize, fillHeight: fillHeight }) }) }) }, editorGeneration)] }));
|
|
440
440
|
}
|
package/dist/json/json-view.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { type JsonViewerProps } from './view-types.js';
|
|
2
|
-
export declare function JsonViewer({ compiled, defaultKey, editing, filePath, fillHeight, onCurrentPathChange, renderExternalWidget, renderMarkdown, sourceControl, sourceContent, sourceVisible, onSourceVisibleChange, tableCells, uiSize, viewsEnabled, tabularRoot, }: JsonViewerProps): import("react").JSX.Element;
|
|
2
|
+
export declare function JsonViewer({ compiled, defaultKey, editing, filePath, fillHeight, onCurrentPathChange, renderExternalWidget, renderMarkdown, sourceControl, sourceContent, sourceVisible, onSourceVisibleChange, buttonToEdit, tableCells, uiSize, viewsEnabled, tabularRoot, }: JsonViewerProps): import("react").JSX.Element;
|
|
3
3
|
export type { JsonViewJsonEditing } from './view-types.js';
|
package/dist/json/json-view.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { HtmlView } from './html-view.js';
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { Braces, LayoutGrid, Columns3, FileText, Search, Table2 } from 'lucide-react';
|
|
5
|
+
import { ButtonToEditContext } from './edit-interaction.js';
|
|
5
6
|
import { useJsonViewsRegistries } from '../widget-registry.js';
|
|
6
7
|
import { cn } from '../lib/cn.js';
|
|
7
8
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../primitives/tabs.js';
|
|
@@ -99,7 +100,7 @@ function ConfiguredView(props) {
|
|
|
99
100
|
}
|
|
100
101
|
return _jsx(AdaptiveView, { ...props });
|
|
101
102
|
}
|
|
102
|
-
export function JsonViewer({ compiled, defaultKey, editing, filePath, fillHeight = false, onCurrentPathChange, renderExternalWidget, renderMarkdown, sourceControl, sourceContent, sourceVisible, onSourceVisibleChange, tableCells, uiSize, viewsEnabled = true, tabularRoot = false, }) {
|
|
103
|
+
export function JsonViewer({ compiled, defaultKey, editing, filePath, fillHeight = false, onCurrentPathChange, renderExternalWidget, renderMarkdown, sourceControl, sourceContent, sourceVisible, onSourceVisibleChange, buttonToEdit = false, tableCells, uiSize, viewsEnabled = true, tabularRoot = false, }) {
|
|
103
104
|
const { types } = useJsonViewsRegistries();
|
|
104
105
|
const viewerState = useViewerState();
|
|
105
106
|
const firstViewTab = viewsEnabled && compiled.views[0] ? `view:${compiled.views[0].id}` : 'root';
|
|
@@ -293,32 +294,32 @@ export function JsonViewer({ compiled, defaultKey, editing, filePath, fillHeight
|
|
|
293
294
|
if (!query)
|
|
294
295
|
setSearchOpenTab(undefined);
|
|
295
296
|
};
|
|
296
|
-
return (_jsx(TableCellOptionsContext.Provider, { value: tableCells, children: _jsx(Tabs, { value: selectedTab, onValueChange: (value) => { if (value !== 'source')
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
297
|
+
return (_jsx(ButtonToEditContext.Provider, { value: buttonToEdit, children: _jsx(TableCellOptionsContext.Provider, { value: tableCells, children: _jsx(Tabs, { value: selectedTab, onValueChange: (value) => { if (value !== 'source')
|
|
298
|
+
setActiveTab(value); onSourceVisibleChange?.(value === 'source'); }, className: cn('flex min-h-0 flex-1 flex-col bg-background text-foreground', fillHeight && 'h-full'), children: _jsxs("div", { "data-id": "json-viewer-content", className: cn('flex min-h-0 w-full max-w-none flex-1 flex-col px-3 py-4 sm:px-6 sm:py-5', fillHeight && 'h-full'), children: [hasToolbar && (_jsxs("div", { "data-id": "json-viewer-toolbar", className: "relative flex min-h-8 shrink-0 flex-nowrap items-center gap-x-2", children: [_jsxs("div", { "data-id": "jsonView-toolbar-actions", className: "ml-auto flex h-8 shrink-0 items-center gap-1", children: [selectedTab !== 'source' && searchable && (searchOpen ? (_jsxs("label", { "data-id": "jsonView-search-control", className: "flex h-7 w-44 items-center overflow-hidden rounded-md border border-transparent bg-transparent transition-[width,background-color] focus-within:border-border focus-within:bg-card sm:w-52", children: [_jsx(Search, { className: "mx-1.5 h-3.5 w-3.5 shrink-0 text-muted-foreground" }), _jsx("input", { ref: searchInput, "aria-label": "Search rows", className: "h-full min-w-0 flex-1 bg-transparent pr-2 text-xs outline-none placeholder:text-muted-foreground", value: query, onBlur: closeSearch, onChange: (event) => setQuery(event.target.value), onKeyDown: (event) => {
|
|
299
|
+
if (event.key === 'Escape') {
|
|
300
|
+
setQuery('');
|
|
301
|
+
setSearchOpenTab(undefined);
|
|
302
|
+
}
|
|
303
|
+
}, placeholder: "Type to search\u2026" })] })) : (_jsx("button", { "data-id": "jsonView-search-button", type: "button", "aria-label": "Search rows", title: "Search", className: "grid h-7 w-7 shrink-0 place-items-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring", onClick: () => setSearchOpenTab(selectedTab), children: _jsx(Search, { className: "h-3.5 w-3.5" }) }))), editing && settingsRawView && (_jsx(JsonViewViewSettings, { hasUnsavedChanges: previewRawView !== undefined, view: previewView ?? settingsView ?? activeView, rawView: activeView.display === 'html' ? activeRawView ?? settingsDraftRawView : settingsDraftRawView, schema: compiled.schema, disabled: editing.saving, saveMode: viewSaveMode, saveUnavailableReason: canPersistViews ? undefined : editing.viewCreationUnavailableReason, unavailableReason: settingsUnavailableReason, onPreview: previewSettings, onSave: saveSettingsView, onDuplicate: declaredView && activeRawView && canPersistViews ? async () => {
|
|
304
|
+
const name = uniqueViewName(`${declaredView.name} copy`, rawViews);
|
|
305
|
+
const id = uniqueViewId(name, rawViews);
|
|
306
|
+
const duplicate = { ...activeRawView, id, name };
|
|
307
|
+
const nextViews = [...rawViews];
|
|
308
|
+
const duplicateIndex = declaredView.declarationIndex + 1;
|
|
309
|
+
nextViews.splice(duplicateIndex, 0, duplicate);
|
|
310
|
+
await persistViews(nextViews, duplicateIndex);
|
|
311
|
+
setActiveTab(`view:${id}`);
|
|
312
|
+
} : undefined, onDelete: declaredView && canPersistViews ? async () => {
|
|
313
|
+
const nextViews = [...rawViews];
|
|
314
|
+
nextViews.splice(declaredView.declarationIndex, 1);
|
|
315
|
+
await persistViews(nextViews);
|
|
316
|
+
setActiveTab('root');
|
|
317
|
+
} : undefined }, typeof settingsDraftRawView.id === 'string' ? settingsDraftRawView.id : '__view_settings__')), sourceControl, _jsx(Diagnostics, { compiled: compiled })] }), (hasTabs || canAddViews || sourceContent) && (_jsxs("div", { "data-id": "jsonView-view-controls", className: "flex min-w-0 flex-1 items-center", children: [canAddViews && _jsx(JsonViewAddView, { align: "start", root: compiled.root, schema: compiled.schema, currentPath: jsonPathForValuePath(currentPath), disabled: editing.saving, onAdd: addView, unavailableReason: addViewUnavailableReason }, "add-view"), (hasTabs || sourceContent) && (_jsxs(TabsList, { "data-id": "jsonView-json-tabs", className: "h-8 min-w-0 flex-initial justify-start gap-0.5 overflow-x-auto overscroll-x-contain rounded-none bg-transparent p-0", children: [compiled.views.map((view) => {
|
|
318
|
+
const Icon = view.display === 'html' ? LayoutGrid : view.display === 'kanban'
|
|
319
|
+
? Columns3
|
|
320
|
+
: isInferredSingleRecord(view) || (view.value !== null && typeof view.value === 'object' && !Array.isArray(view.value) && !isRecordCollection(view.value))
|
|
321
|
+
? FileText
|
|
322
|
+
: Table2;
|
|
323
|
+
return _jsxs(ViewTab, { id: `jsonView-json-tab-${view.id}`, value: `view:${view.id}`, path: slashPath(view), children: [_jsx(Icon, { className: "h-3.5 w-3.5" }), view.name] }, view.id);
|
|
324
|
+
}), _jsxs(ViewTab, { id: "jsonView-json-tab-root", value: "root", path: isInferredSingleRecord(rootView) && !forceTable ? 'Root record' : 'Root table', children: [isInferredSingleRecord(rootView) && !forceTable ? _jsx(FileText, { className: "h-3.5 w-3.5" }) : _jsx(Table2, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "sr-only", children: isInferredSingleRecord(rootView) && !forceTable ? 'Record' : 'Table' })] }), sourceContent && _jsxs(ViewTab, { id: "jsonView-json-tab-source", value: "source", path: "Source", children: [_jsx(Braces, { className: "h-3.5 w-3.5" }), _jsx("span", { className: "sr-only", children: "Source" })] })] }, "view-tabs"))] }))] })), _jsx(TabsContent, { value: selectedTab, className: cn('mt-2 min-w-0', fillHeight && 'flex min-h-0 flex-1 flex-col'), children: selectedTab === 'source' ? sourceContent : _jsx(ConfiguredView, { forceTable: forceTable, compiled: compiled, copyFileName: defaultKey === 'root' ? 'document.json' : defaultKey, editing: editing, filePath: filePath, fillHeight: fillHeight, onCurrentPathChange: rememberCurrentPath, onSaveView: saveColumnView, query: query, rawView: columnRawView, renderExternalWidget: renderExternalWidget, renderMarkdown: renderMarkdown, uiSize: uiSize, view: renderedView }, selectedTab) })] }) }) }) }));
|
|
324
325
|
}
|
|
@@ -11,6 +11,7 @@ import { JsonSourceNumber, useJsonSourceLiterals, useJsonSourceSubtreeRisk } fro
|
|
|
11
11
|
import { validateJsonViewSchemaValue } from '@script-it/json-views-core';
|
|
12
12
|
import { useJsonViewsRegistries } from '../widget-registry.js';
|
|
13
13
|
import { useEditBaseChanged } from '../structured-data/edit-base.js';
|
|
14
|
+
import { ButtonToEditContext } from './edit-interaction.js';
|
|
14
15
|
import { HtmlPreview } from './html-preview.js';
|
|
15
16
|
export function withColumnOptionColors(descriptor, values, offset) {
|
|
16
17
|
if (descriptor.type !== 'select' && descriptor.type !== 'multi-select')
|
|
@@ -227,6 +228,8 @@ export function SchemaEditor({ descriptor, disabled, label, minHeight, onCancel,
|
|
|
227
228
|
return fillCell ? _jsx("div", { className: "px-1.5 py-1.5", children: editor }) : editor;
|
|
228
229
|
}
|
|
229
230
|
export function JsonViewSchemaValueCell({ compact = false, descriptor, initiallyEditing = false, label, onClear, onCommit, onOptionColorChange, onOpen, renderExternalWidget, renderMarkdown, saving = false, suppressRequiredValidation = false, variant = 'default', value, sourcePath, }) {
|
|
231
|
+
const buttonToEdit = useContext(ButtonToEditContext);
|
|
232
|
+
const markdownButton = buttonToEdit && (descriptor.type === 'markdown' || descriptor.type === 'body');
|
|
230
233
|
const [editing, setEditing] = useState(initiallyEditing);
|
|
231
234
|
const displayRef = useRef(null);
|
|
232
235
|
const [editorMinHeight, setEditorMinHeight] = useState();
|
|
@@ -292,11 +295,11 @@ export function JsonViewSchemaValueCell({ compact = false, descriptor, initially
|
|
|
292
295
|
const linkedValue = typeof value === 'string'
|
|
293
296
|
&& (descriptor.type === 'url' || descriptor.type === 'email');
|
|
294
297
|
const interactiveText = ((descriptor.type === 'body' || descriptor.type === 'markdown') && renderMarkdown !== undefined || descriptor.type === 'html') && !compact && typeof value === 'string';
|
|
295
|
-
const explicitEditAction = linkedValue || descriptor.type === 'html';
|
|
298
|
+
const explicitEditAction = linkedValue || descriptor.type === 'html' || markdownButton;
|
|
296
299
|
const startEditing = () => {
|
|
297
300
|
preserveLayoutHeight();
|
|
298
301
|
setEditing(true);
|
|
299
302
|
};
|
|
300
303
|
const actions = (_jsxs(_Fragment, { children: [explicitEditAction && editable && _jsx(EditValueAction, { disabled: saving, label: label, onClick: startEditing }), (container || textPreview) && _jsx(ExpandValueButton, { onClick: onOpen })] }));
|
|
301
|
-
return (_jsx("div", { ref: displayRef, "data-id": "jsonView-schema-value", className: cn('h-full min-w-0', invalid && 'ring-1 ring-inset ring-destructive', !invalid && warning && 'ring-1 ring-inset ring-amber-500'), style: stableLayout && editorMinHeight ? { minHeight: editorMinHeight } : undefined, title: invalid ?? warning, children: _jsx(StructuredValueCellFrame, { actions: explicitEditAction || container || textPreview ? actions : undefined, overlayActions: descriptor.type === 'html' && !compact, activationDisabled: !editable || saving || explicitEditAction, activationLabel: editable && !explicitEditAction ? `Edit ${label}` : undefined, activationRole: interactiveText ? 'group' : 'button', className: variant === 'record-title' ? 'min-h-10 w-fit max-w-full px-1.5 py-1' : stableText ? 'px-0' : undefined, onActivate: editable && !explicitEditAction ? startEditing : undefined, children: stableText ? (_jsxs("div", { className: "flex min-w-0 flex-1 items-start gap-1.5", children: [_jsx(JsonViewSchemaValueDisplay, { sourcePath: sourcePath, compact: compact, descriptor: descriptor, value: value, onOpen: onOpen, renderMarkdown: renderMarkdown }), (invalid || warning) && _jsx(AlertTriangle, { "aria-label": invalid ?? warning, className: cn('h-3.5 w-3.5 shrink-0', invalid ? 'text-destructive' : 'text-amber-600 dark:text-amber-300') })] })) : (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx(JsonViewSchemaValueDisplay, { sourcePath: sourcePath, compact: compact, descriptor: descriptor, value: value, onOpen: onOpen, renderMarkdown: renderMarkdown }), (invalid || warning) && _jsx(AlertTriangle, { "aria-label": invalid ?? warning, className: cn('h-3.5 w-3.5 shrink-0', invalid ? 'text-destructive' : 'text-amber-600 dark:text-amber-300') })] })) }) }));
|
|
304
|
+
return (_jsx("div", { ref: displayRef, "data-id": "jsonView-schema-value", className: cn('h-full min-w-0', invalid && 'ring-1 ring-inset ring-destructive', !invalid && warning && 'ring-1 ring-inset ring-amber-500'), style: stableLayout && editorMinHeight ? { minHeight: editorMinHeight } : undefined, title: invalid ?? warning, children: _jsx(StructuredValueCellFrame, { highlightOnHover: descriptor.type !== 'markdown' && descriptor.type !== 'body', actions: explicitEditAction || container || textPreview ? actions : undefined, overlayActions: (descriptor.type === 'html' || markdownButton) && !compact, activationDisabled: !editable || saving || explicitEditAction, activationLabel: editable && !explicitEditAction ? `Edit ${label}` : undefined, activationRole: interactiveText ? 'group' : 'button', className: variant === 'record-title' ? 'min-h-10 w-fit max-w-full px-1.5 py-1' : stableText ? 'px-0' : undefined, onActivate: editable && !explicitEditAction ? startEditing : undefined, children: stableText ? (_jsxs("div", { className: "flex min-w-0 flex-1 items-start gap-1.5", children: [_jsx(JsonViewSchemaValueDisplay, { sourcePath: sourcePath, compact: compact, descriptor: descriptor, value: value, onOpen: onOpen, renderMarkdown: renderMarkdown }), (invalid || warning) && _jsx(AlertTriangle, { "aria-label": invalid ?? warning, className: cn('h-3.5 w-3.5 shrink-0', invalid ? 'text-destructive' : 'text-amber-600 dark:text-amber-300') })] })) : (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx(JsonViewSchemaValueDisplay, { sourcePath: sourcePath, compact: compact, descriptor: descriptor, value: value, onOpen: onOpen, renderMarkdown: renderMarkdown }), (invalid || warning) && _jsx(AlertTriangle, { "aria-label": invalid ?? warning, className: cn('h-3.5 w-3.5 shrink-0', invalid ? 'text-destructive' : 'text-amber-600 dark:text-amber-300') })] })) }) }));
|
|
302
305
|
}
|
package/dist/json/table-view.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ButtonToEditContext } from './edit-interaction.js';
|
|
2
3
|
import { isLongText } from './long-text.js';
|
|
3
4
|
import { useJsonSourceLiterals } from './source-literals.js';
|
|
4
|
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
5
|
+
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
5
6
|
import { useJsonViewsRegistries } from '../widget-registry.js';
|
|
6
7
|
import { AtomicValueEditor, EditValueAction, StructuredValueCellFrame } from '../structured-data/atomic-value-editor.js';
|
|
7
8
|
import { isEditableAtomicValue } from '../structured-data/atomic-value.js';
|
|
@@ -41,6 +42,7 @@ const DATE_FILTER_OPERATORS = [
|
|
|
41
42
|
{ value: 'isNotEmpty', label: 'is not empty' },
|
|
42
43
|
];
|
|
43
44
|
export function ProjectedTableView({ compiled, editing, filePath, copyFileName, fillHeight, onSaveView, onCurrentPathChange, query, rawView, renderExternalWidget, renderMarkdown, uiSize, view, }) {
|
|
45
|
+
const buttonToEdit = useContext(ButtonToEditContext);
|
|
44
46
|
const { types } = useJsonViewsRegistries();
|
|
45
47
|
const sourceLiteral = useJsonSourceLiterals();
|
|
46
48
|
const [editPath, setEditPath] = useState();
|
|
@@ -290,9 +292,9 @@ export function ProjectedTableView({ compiled, editing, filePath, copyFileName,
|
|
|
290
292
|
const openRecordEditor = () => { setEditPath(resolved.sourcePath); setOpenRowPath(row.sourcePath); };
|
|
291
293
|
// Render before applying the plain-text shortcut. A group preserves
|
|
292
294
|
// native link interactions without nesting links inside a button.
|
|
293
|
-
if (renderMarkdown && (descriptor?.type === 'markdown' || descriptor?.type === 'body'))
|
|
294
|
-
return (_jsx(StructuredValueCellFrame, { activationLabel: `Open ${displayColumn.label} in record`, activationRole: "group", onActivate: openRecordEditor, actions: editing && editing.canReplace?.(resolved.sourcePath) !== false
|
|
295
|
-
? _jsx(EditValueAction, { label: displayColumn.label, onClick: openRecordEditor })
|
|
295
|
+
if ((renderMarkdown || buttonToEdit) && (descriptor?.type === 'markdown' || descriptor?.type === 'body'))
|
|
296
|
+
return (_jsx(StructuredValueCellFrame, { activationLabel: `Open ${displayColumn.label} in record`, activationRole: "group", onActivate: buttonToEdit ? undefined : openRecordEditor, overlayActions: buttonToEdit, highlightOnHover: false, actions: editing && editing.canReplace?.(resolved.sourcePath) !== false
|
|
297
|
+
? _jsx(EditValueAction, { disabled: editing.saving, label: displayColumn.label, onClick: openRecordEditor })
|
|
296
298
|
: undefined, children: _jsx("div", { className: "min-w-0 flex-1 break-words pr-2", children: _jsx(JsonViewSchemaValueDisplay, { sourcePath: resolved.sourcePath, descriptor: descriptor, value: value, onOpen: open, renderMarkdown: renderMarkdown }) }) }));
|
|
297
299
|
return _jsx("button", { type: "button", className: "block h-full w-full min-w-0 truncate px-1.5 py-1 text-left hover:bg-accent focus-visible:ring-1 focus-visible:ring-ring", "aria-label": `Open ${displayColumn.label} in record`, onClick: openRecordEditor, children: value || '—' });
|
|
298
300
|
}
|
|
@@ -41,6 +41,8 @@ export interface JsonViewerProps {
|
|
|
41
41
|
sourceVisible?: boolean;
|
|
42
42
|
onSourceVisibleChange?: (visible: boolean) => void;
|
|
43
43
|
sourceControl?: ReactNode;
|
|
44
|
+
/** Require the hover/focus edit button for Markdown fields instead of clicking their content. Defaults to false. */
|
|
45
|
+
buttonToEdit?: boolean;
|
|
44
46
|
/** Clamps tall table cells and reveals one on selection (default), or lets rows grow to fit. */
|
|
45
47
|
tableCells?: JsonViewsTableCellOptions;
|
|
46
48
|
uiSize?: UiSize;
|
|
@@ -156,8 +156,8 @@ export function GeneralJsonView({ breadcrumb, root, defaultKey, initialPath = []
|
|
|
156
156
|
}
|
|
157
157
|
return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col", children: [_jsx(NavigationBreadcrumb, { labels: [...(breadcrumb?.ancestors ?? []), ...effectiveHistory.map(({ key }) => key)], onBack: effectiveHistory.length > 1 ? goBack : breadcrumb?.onBack, backLabel: effectiveHistory.length > 1 ? 'Back' : breadcrumb?.backLabel }), _jsx("div", { ref: scrollContainerRef, className: fillHeight
|
|
158
158
|
? 'flex min-h-0 flex-1 flex-col overflow-auto overscroll-x-none'
|
|
159
|
-
: 'flex-1 overflow-auto overscroll-x-none', onScroll: captureScrollPosition, children: _jsx(GeneralJsonNodeView, { node: currentNode, navigateTo: navigateTo, filePath: filePath, currentPathKey: currentPathKey, currentJsonPath: effectiveCurrent.path, currentLabel: effectiveCurrent.key, renderAsMarkdown: typeof currentNode === 'string'
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
: 'flex-1 overflow-auto overscroll-x-none', onScroll: captureScrollPosition, children: _jsx("div", { className: currentNode === null || typeof currentNode !== 'object' ? 'mx-auto w-full max-w-2xl px-3 pb-6 sm:px-6' : 'contents', children: _jsx(GeneralJsonNodeView, { node: currentNode, navigateTo: navigateTo, filePath: filePath, currentPathKey: currentPathKey, currentJsonPath: effectiveCurrent.path, currentLabel: effectiveCurrent.key, renderAsMarkdown: typeof currentNode === 'string'
|
|
160
|
+
&& schemaForPath?.(effectiveCurrent.path)?.type === 'markdown', getTableCellPosition: (path) => tableCellPositionsRef.current[path], setTableCellPosition: (path, cell) => {
|
|
161
|
+
tableCellPositionsRef.current[path] = cell;
|
|
162
|
+
}, renderMarkdown: renderMarkdown, uiSize: uiSize, fillHeight: fillHeight, atomicEditing: atomicEditing, schemaForPath: schemaForPath, renderExternalWidget: renderExternalWidget, onVisibleSourcePathsChange: reportVisibleSourcePaths }) }) })] }));
|
|
163
163
|
}
|
|
@@ -21,7 +21,7 @@ export declare function EditValueAction({ disabled, label, onClick, }: {
|
|
|
21
21
|
onClick: () => void;
|
|
22
22
|
}): import("react").JSX.Element;
|
|
23
23
|
export declare function AtomicValueEditor({ actions, children, className, editorClassName, enterKey, label, multiline, onCommit, preserveLayout, saving, value, }: AtomicValueEditorProps): import("react").JSX.Element;
|
|
24
|
-
export declare function StructuredValueCellFrame({ activationDisabled, activationLabel, activationRole, actions, children, leading, className, onActivate, overlayActions, }: {
|
|
24
|
+
export declare function StructuredValueCellFrame({ activationDisabled, activationLabel, activationRole, actions, children, leading, className, onActivate, overlayActions, highlightOnHover, }: {
|
|
25
25
|
activationDisabled?: boolean;
|
|
26
26
|
activationLabel?: string;
|
|
27
27
|
/** Use a group when rendered content contains its own interactive descendants. */
|
|
@@ -32,5 +32,6 @@ export declare function StructuredValueCellFrame({ activationDisabled, activatio
|
|
|
32
32
|
className?: string;
|
|
33
33
|
onActivate?: () => void;
|
|
34
34
|
overlayActions?: boolean;
|
|
35
|
+
highlightOnHover?: boolean;
|
|
35
36
|
}): import("react").JSX.Element;
|
|
36
37
|
export {};
|
|
@@ -52,7 +52,7 @@ export function AtomicValueEditor({ actions, children, className, editorClassNam
|
|
|
52
52
|
? _jsx("div", { ref: frameRef, "data-id": "atomic-value-editor-frame", className: "h-full min-w-0", style: editorMinHeight ? { minHeight: editorMinHeight } : undefined, children: display })
|
|
53
53
|
: display;
|
|
54
54
|
}
|
|
55
|
-
export function StructuredValueCellFrame({ activationDisabled = false, activationLabel, activationRole = 'button', actions, children, leading, className, onActivate, overlayActions = false, }) {
|
|
55
|
+
export function StructuredValueCellFrame({ activationDisabled = false, activationLabel, activationRole = 'button', actions, children, leading, className, onActivate, overlayActions = false, highlightOnHover = true, }) {
|
|
56
56
|
const fillCell = useContext(StructuredCellFillContext);
|
|
57
57
|
// An editable cell advertises itself with a primary-tinted hover — the
|
|
58
58
|
// neutral accent tone sits on white/card surfaces where it is barely
|
|
@@ -64,13 +64,13 @@ export function StructuredValueCellFrame({ activationDisabled = false, activatio
|
|
|
64
64
|
// Table values use the entire width up to the column divider. The
|
|
65
65
|
// right edge is deliberately flush so clipped content makes the
|
|
66
66
|
// boundary of its column unmistakable.
|
|
67
|
-
fillCell ? 'h-full items-center rounded-none py-2 pl-3 pr-0' : 'rounded-md px-1.5', onActivate && !activationDisabled
|
|
67
|
+
fillCell ? 'h-full items-center rounded-none py-2 pl-3 pr-0' : 'rounded-md px-1.5', highlightOnHover && (onActivate && !activationDisabled
|
|
68
68
|
? fillCell
|
|
69
69
|
? 'before:pointer-events-none before:absolute before:inset-y-0 before:-left-5 before:right-0 hover:before:bg-primary/10 dark:hover:before:bg-primary/25'
|
|
70
70
|
: 'hover:bg-primary/10 dark:hover:bg-primary/25'
|
|
71
71
|
: fillCell
|
|
72
72
|
? 'before:pointer-events-none before:absolute before:inset-y-0 before:-left-5 before:right-0 hover:before:bg-accent/70'
|
|
73
|
-
: 'hover:bg-accent/70', fillCell && onActivate && !activationDisabled && 'cursor-text', className), onClick: (event) => {
|
|
73
|
+
: 'hover:bg-accent/70'), fillCell && onActivate && !activationDisabled && 'cursor-text', className), onClick: (event) => {
|
|
74
74
|
// The table cell's padding belongs to the same action as its value.
|
|
75
75
|
// Child actions keep their own handlers and must not activate twice.
|
|
76
76
|
if (fillCell && event.target === event.currentTarget && !activationDisabled)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@script-it/json-views-react",
|
|
3
|
-
"version": "2026.
|
|
3
|
+
"version": "2026.11.44",
|
|
4
4
|
"description": "Extensible React viewer and editor for JSON Views",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"@radix-ui/react-slot": "^1.2.4",
|
|
39
39
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
40
40
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
41
|
-
"@script-it/json-views-core": "2026.
|
|
41
|
+
"@script-it/json-views-core": "2026.11.44",
|
|
42
42
|
"@tanstack/react-virtual": "3.14.10",
|
|
43
43
|
"class-variance-authority": "^0.7.1",
|
|
44
44
|
"clsx": "^2.1.1",
|