@script-it/json-views-react 2026.10.27 → 2026.11.42

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 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
- Examples are bundled as `Examples/HTML Launch.json`, `Examples/HTML Room Plan.json`, and `Examples/HTML Product Sheet.json`.
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.
@@ -3996,7 +3996,7 @@ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
3996
3996
  TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
3997
3997
  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
3998
3998
 
3999
- re2js@2.8.6
3999
+ re2js@2.9.1
4000
4000
  ===========
4001
4001
  MIT License
4002
4002
 
@@ -0,0 +1 @@
1
+ export declare const ButtonToEditContext: import("react").Context<boolean>;
@@ -0,0 +1,2 @@
1
+ import { createContext } from 'react';
2
+ export const ButtonToEditContext = createContext(false);
@@ -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
  }
@@ -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';
@@ -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
- 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) => {
298
- if (event.key === 'Escape') {
299
- setQuery('');
300
- setSearchOpenTab(undefined);
301
- }
302
- }, 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 () => {
303
- const name = uniqueViewName(`${declaredView.name} copy`, rawViews);
304
- const id = uniqueViewId(name, rawViews);
305
- const duplicate = { ...activeRawView, id, name };
306
- const nextViews = [...rawViews];
307
- const duplicateIndex = declaredView.declarationIndex + 1;
308
- nextViews.splice(duplicateIndex, 0, duplicate);
309
- await persistViews(nextViews, duplicateIndex);
310
- setActiveTab(`view:${id}`);
311
- } : undefined, onDelete: declaredView && canPersistViews ? async () => {
312
- const nextViews = [...rawViews];
313
- nextViews.splice(declaredView.declarationIndex, 1);
314
- await persistViews(nextViews);
315
- setActiveTab('root');
316
- } : 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) => {
317
- const Icon = view.display === 'html' ? LayoutGrid : view.display === 'kanban'
318
- ? Columns3
319
- : isInferredSingleRecord(view) || (view.value !== null && typeof view.value === 'object' && !Array.isArray(view.value) && !isRecordCollection(view.value))
320
- ? FileText
321
- : Table2;
322
- 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);
323
- }), _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) })] }) }) }));
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
  }
@@ -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
- && 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 }) })] }));
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.10.27",
3
+ "version": "2026.11.42",
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.10.27",
41
+ "@script-it/json-views-core": "2026.11.42",
42
42
  "@tanstack/react-virtual": "3.14.10",
43
43
  "class-variance-authority": "^0.7.1",
44
44
  "clsx": "^2.1.1",
@@ -60,7 +60,7 @@
60
60
  "react": "^19.1.1",
61
61
  "react-dom": "^19.1.1",
62
62
  "typescript": "^5.9.3",
63
- "vitest": "4.1.11"
63
+ "vitest": "4.1.12"
64
64
  },
65
65
  "repository": {
66
66
  "type": "git",