@script-it/json-views-react 0.1.0-beta.1
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/LICENSE +21 -0
- package/README.md +301 -0
- package/THIRD_PARTY_NOTICES.txt +4917 -0
- package/dist/browser-device.d.ts +21 -0
- package/dist/browser-device.js +22 -0
- package/dist/file-content-edit-state.d.ts +10 -0
- package/dist/file-content-edit-state.js +1 -0
- package/dist/file-content-view.d.ts +8 -0
- package/dist/file-content-view.js +7 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +8 -0
- package/dist/json/copy-json.d.ts +15 -0
- package/dist/json/copy-json.js +52 -0
- package/dist/json/html-preview.d.ts +5 -0
- package/dist/json/html-preview.js +12 -0
- package/dist/json/html-view/native-control.d.ts +44 -0
- package/dist/json/html-view/native-control.js +96 -0
- package/dist/json/html-view/settings.d.ts +2 -0
- package/dist/json/html-view/settings.js +58 -0
- package/dist/json/html-view/template.d.ts +12 -0
- package/dist/json/html-view/template.js +55 -0
- package/dist/json/html-view.d.ts +9 -0
- package/dist/json/html-view.js +163 -0
- package/dist/json/json-content.d.ts +61 -0
- package/dist/json/json-content.js +433 -0
- package/dist/json/json-source-editor.d.ts +36 -0
- package/dist/json/json-source-editor.js +203 -0
- package/dist/json/json-view.d.ts +3 -0
- package/dist/json/json-view.js +322 -0
- package/dist/json/kanban-view.d.ts +20 -0
- package/dist/json/kanban-view.js +328 -0
- package/dist/json/long-text.d.ts +3 -0
- package/dist/json/long-text.js +6 -0
- package/dist/json/record-view.d.ts +38 -0
- package/dist/json/record-view.js +85 -0
- package/dist/json/row-mutations.d.ts +7 -0
- package/dist/json/row-mutations.js +93 -0
- package/dist/json/schema-value.d.ts +57 -0
- package/dist/json/schema-value.js +294 -0
- package/dist/json/source-default.d.ts +1 -0
- package/dist/json/source-default.js +1 -0
- package/dist/json/source-edit-capabilities.d.ts +8 -0
- package/dist/json/source-edit-capabilities.js +22 -0
- package/dist/json/source-literals.d.ts +15 -0
- package/dist/json/source-literals.js +24 -0
- package/dist/json/source-recovery.d.ts +32 -0
- package/dist/json/source-recovery.js +38 -0
- package/dist/json/source-selection.d.ts +3 -0
- package/dist/json/source-selection.js +16 -0
- package/dist/json/table-view.d.ts +20 -0
- package/dist/json/table-view.js +300 -0
- package/dist/json/use-kanban-card-drag.d.ts +13 -0
- package/dist/json/use-kanban-card-drag.js +255 -0
- package/dist/json/use-kanban-column-drag.d.ts +14 -0
- package/dist/json/use-kanban-column-drag.js +179 -0
- package/dist/json/view-diagnostics.d.ts +4 -0
- package/dist/json/view-diagnostics.js +26 -0
- package/dist/json/view-model.d.ts +27 -0
- package/dist/json/view-model.js +180 -0
- package/dist/json/view-save-status.d.ts +12 -0
- package/dist/json/view-save-status.js +38 -0
- package/dist/json/view-settings.d.ts +36 -0
- package/dist/json/view-settings.js +661 -0
- package/dist/json/view-types.d.ts +47 -0
- package/dist/json/view-types.js +4 -0
- package/dist/json/viewers/general/general-json-view.d.ts +46 -0
- package/dist/json/viewers/general/general-json-view.js +163 -0
- package/dist/json/viewers/general/json-flatten.d.ts +33 -0
- package/dist/json/viewers/general/json-flatten.js +51 -0
- package/dist/json/viewers/general/json-node-view.d.ts +43 -0
- package/dist/json/viewers/general/json-node-view.js +215 -0
- package/dist/lib/clipboard.d.ts +1 -0
- package/dist/lib/clipboard.js +24 -0
- package/dist/lib/cn.d.ts +2 -0
- package/dist/lib/cn.js +5 -0
- package/dist/lib/file-data.d.ts +7 -0
- package/dist/lib/file-data.js +5 -0
- package/dist/lib/floating-portal-selectors.d.ts +4 -0
- package/dist/lib/floating-portal-selectors.js +4 -0
- package/dist/lib/time.d.ts +24 -0
- package/dist/lib/time.js +60 -0
- package/dist/lib/ui-size.d.ts +28 -0
- package/dist/lib/ui-size.js +39 -0
- package/dist/lib/use-anchored-position.d.ts +5 -0
- package/dist/lib/use-anchored-position.js +65 -0
- package/dist/lib/use-dismiss.d.ts +31 -0
- package/dist/lib/use-dismiss.js +65 -0
- package/dist/primitives/boolean-toggle.d.ts +10 -0
- package/dist/primitives/boolean-toggle.js +11 -0
- package/dist/primitives/button.d.ts +11 -0
- package/dist/primitives/button.js +34 -0
- package/dist/primitives/checkbox.d.ts +4 -0
- package/dist/primitives/checkbox.js +8 -0
- package/dist/primitives/choice-control.d.ts +29 -0
- package/dist/primitives/choice-control.js +131 -0
- package/dist/primitives/dialog.d.ts +29 -0
- package/dist/primitives/dialog.js +25 -0
- package/dist/primitives/dropdown-menu.d.ts +37 -0
- package/dist/primitives/dropdown-menu.js +39 -0
- package/dist/primitives/inline-feedback-action.d.ts +13 -0
- package/dist/primitives/inline-feedback-action.js +6 -0
- package/dist/primitives/menu-select.d.ts +12 -0
- package/dist/primitives/menu-select.js +7 -0
- package/dist/primitives/multi-select.d.ts +18 -0
- package/dist/primitives/multi-select.js +7 -0
- package/dist/primitives/number-input.d.ts +10 -0
- package/dist/primitives/number-input.js +8 -0
- package/dist/primitives/option-pill.d.ts +13 -0
- package/dist/primitives/option-pill.js +50 -0
- package/dist/primitives/tabs.d.ts +7 -0
- package/dist/primitives/tabs.js +12 -0
- package/dist/primitives/textarea.d.ts +4 -0
- package/dist/primitives/textarea.js +8 -0
- package/dist/primitives/tooltip.d.ts +31 -0
- package/dist/primitives/tooltip.js +37 -0
- package/dist/standalone.css +113 -0
- package/dist/structured-data/atomic-value-editor.d.ts +33 -0
- package/dist/structured-data/atomic-value-editor.js +195 -0
- package/dist/structured-data/atomic-value.d.ts +3 -0
- package/dist/structured-data/atomic-value.js +6 -0
- package/dist/structured-data/column-controls.d.ts +21 -0
- package/dist/structured-data/column-controls.js +180 -0
- package/dist/structured-data/copy-json-action.d.ts +10 -0
- package/dist/structured-data/copy-json-action.js +87 -0
- package/dist/structured-data/delete-row-action.d.ts +10 -0
- package/dist/structured-data/delete-row-action.js +37 -0
- package/dist/structured-data/edit-base.d.ts +3 -0
- package/dist/structured-data/edit-base.js +8 -0
- package/dist/structured-data/filter-value.d.ts +2 -0
- package/dist/structured-data/filter-value.js +28 -0
- package/dist/structured-data/row-selection-icon.d.ts +9 -0
- package/dist/structured-data/row-selection-icon.js +6 -0
- package/dist/structured-data/structured-cell-fill-context.d.ts +6 -0
- package/dist/structured-data/structured-cell-fill-context.js +7 -0
- package/dist/structured-data/structured-data-content.d.ts +1 -0
- package/dist/structured-data/structured-data-content.js +1 -0
- package/dist/structured-data/table-types.d.ts +108 -0
- package/dist/structured-data/table-types.js +1 -0
- package/dist/structured-data/use-metadata-persistence.d.ts +15 -0
- package/dist/structured-data/use-metadata-persistence.js +18 -0
- package/dist/structured-data/use-optimistic-text-document.d.ts +22 -0
- package/dist/structured-data/use-optimistic-text-document.js +30 -0
- package/dist/structured-data/value-cell.d.ts +15 -0
- package/dist/structured-data/value-cell.js +81 -0
- package/dist/styles.css +2 -0
- package/dist/surface.d.ts +14 -0
- package/dist/surface.js +73 -0
- package/dist/tabular-data-view.d.ts +5 -0
- package/dist/tabular-data-view.js +493 -0
- package/dist/viewer-state.d.ts +72 -0
- package/dist/viewer-state.js +79 -0
- package/dist/widget-registry.d.ts +60 -0
- package/dist/widget-registry.js +126 -0
- package/dist/widgets/boolean-widget.d.ts +2 -0
- package/dist/widgets/boolean-widget.js +15 -0
- package/dist/widgets/date-picker-widget.d.ts +18 -0
- package/dist/widgets/date-picker-widget.js +265 -0
- package/dist/widgets/dropdown-widget.d.ts +10 -0
- package/dist/widgets/dropdown-widget.js +19 -0
- package/dist/widgets/multi-select-widget.d.ts +11 -0
- package/dist/widgets/multi-select-widget.js +46 -0
- package/dist/widgets/number-widget.d.ts +2 -0
- package/dist/widgets/number-widget.js +19 -0
- package/dist/widgets/read-only-field.d.ts +36 -0
- package/dist/widgets/read-only-field.js +39 -0
- package/dist/widgets/text-widget.d.ts +2 -0
- package/dist/widgets/text-widget.js +19 -0
- package/dist/widgets/textarea-widget.d.ts +2 -0
- package/dist/widgets/textarea-widget.js +34 -0
- package/dist/widgets/types.d.ts +53 -0
- package/dist/widgets/types.js +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { Copy } from 'lucide-react';
|
|
4
|
+
import { copyTextToClipboard } from '../lib/clipboard.js';
|
|
5
|
+
import { InlineFeedbackAction } from '../primitives/inline-feedback-action.js';
|
|
6
|
+
import { cn } from '../lib/cn.js';
|
|
7
|
+
export function CopyJsonAction({ className, dataId, getPathText, getRecordText, label, pathLabel, recordLabel, tone = 'default', }) {
|
|
8
|
+
const [status, setStatus] = useState('idle');
|
|
9
|
+
const [copying, setCopying] = useState(false);
|
|
10
|
+
const [format, setFormat] = useState('path');
|
|
11
|
+
const resetTimer = useRef(undefined);
|
|
12
|
+
const rootRef = useRef(null);
|
|
13
|
+
const selectedLabel = useRef(null);
|
|
14
|
+
const alternateLabel = useRef(null);
|
|
15
|
+
const previousLabelRects = useRef(undefined);
|
|
16
|
+
useEffect(() => () => {
|
|
17
|
+
if (resetTimer.current)
|
|
18
|
+
clearTimeout(resetTimer.current);
|
|
19
|
+
}, []);
|
|
20
|
+
useLayoutEffect(() => {
|
|
21
|
+
const previous = previousLabelRects.current;
|
|
22
|
+
previousLabelRects.current = undefined;
|
|
23
|
+
if (!previous || (typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches))
|
|
24
|
+
return;
|
|
25
|
+
for (const copyFormat of ['path', 'record']) {
|
|
26
|
+
const choice = copyFormat === format ? selectedLabel.current : alternateLabel.current;
|
|
27
|
+
const prior = previous[copyFormat];
|
|
28
|
+
if (!choice || !prior || typeof choice.animate !== 'function')
|
|
29
|
+
continue;
|
|
30
|
+
const next = choice.getBoundingClientRect();
|
|
31
|
+
const x = prior.left - next.left;
|
|
32
|
+
if (Math.abs(x) < 1)
|
|
33
|
+
continue;
|
|
34
|
+
choice.animate([{ transform: `translateX(${x}px)` }, { transform: 'translateX(0)' }], { duration: 180, easing: 'cubic-bezier(.2, .8, .2, 1)' });
|
|
35
|
+
}
|
|
36
|
+
}, [format]);
|
|
37
|
+
const scheduleReset = () => {
|
|
38
|
+
if (resetTimer.current)
|
|
39
|
+
clearTimeout(resetTimer.current);
|
|
40
|
+
resetTimer.current = setTimeout(() => {
|
|
41
|
+
const restoreFocus = rootRef.current?.contains(document.activeElement) === true;
|
|
42
|
+
setStatus('idle');
|
|
43
|
+
setFormat('path');
|
|
44
|
+
if (restoreFocus)
|
|
45
|
+
setTimeout(() => rootRef.current?.querySelector('button')?.focus(), 0);
|
|
46
|
+
}, 2000);
|
|
47
|
+
};
|
|
48
|
+
const copy = async (nextFormat) => {
|
|
49
|
+
if (copying)
|
|
50
|
+
return;
|
|
51
|
+
if (resetTimer.current)
|
|
52
|
+
clearTimeout(resetTimer.current);
|
|
53
|
+
if (status === 'copied' && nextFormat !== format) {
|
|
54
|
+
const alternateFormat = format === 'path' ? 'record' : 'path';
|
|
55
|
+
previousLabelRects.current = {
|
|
56
|
+
[format]: selectedLabel.current?.getBoundingClientRect(),
|
|
57
|
+
[alternateFormat]: alternateLabel.current?.getBoundingClientRect(),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
setCopying(true);
|
|
61
|
+
try {
|
|
62
|
+
await copyTextToClipboard(nextFormat === 'path' ? getPathText() : getRecordText());
|
|
63
|
+
setFormat(nextFormat);
|
|
64
|
+
setStatus('copied');
|
|
65
|
+
scheduleReset();
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
previousLabelRects.current = undefined;
|
|
69
|
+
setStatus('error');
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
setCopying(false);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const names = { path: pathLabel, record: recordLabel };
|
|
76
|
+
const alternateFormat = format === 'path' ? 'record' : 'path';
|
|
77
|
+
return (_jsx("div", { ref: rootRef, className: cn('inline-flex shrink-0 items-center', className), children: status === 'copied' ? (_jsxs("div", { "data-id": dataId, role: "group", "aria-label": "Copy format", "aria-live": "polite", className: "inline-flex h-6 items-center gap-0.5 rounded-lg bg-muted p-0.5", children: [_jsxs("button", { type: "button", "data-copy-format": format, "aria-label": `Copied ${names[format]}`, "aria-pressed": "true", disabled: copying, className: "inline-flex h-5 items-center gap-1 whitespace-nowrap rounded-md bg-card px-1.5 text-xs font-medium text-foreground shadow-sm transition-[background-color,color,box-shadow] disabled:opacity-50", onClick: (event) => {
|
|
78
|
+
event.stopPropagation();
|
|
79
|
+
void copy(format);
|
|
80
|
+
}, children: [_jsx("span", { children: "Copied" }), _jsx("span", { ref: selectedLabel, children: names[format] })] }), _jsx("button", { type: "button", "data-copy-format": alternateFormat, "aria-label": `Copy ${names[alternateFormat]}`, "aria-pressed": "false", disabled: copying, className: "inline-flex h-5 items-center whitespace-nowrap rounded-md px-1.5 text-xs font-normal text-muted-foreground/65 transition-colors hover:text-foreground disabled:opacity-50", onClick: (event) => {
|
|
81
|
+
event.stopPropagation();
|
|
82
|
+
void copy(alternateFormat);
|
|
83
|
+
}, children: _jsx("span", { ref: alternateLabel, children: `${names[alternateFormat].charAt(0).toUpperCase()}${names[alternateFormat].slice(1)}` }) })] })) : (_jsx(InlineFeedbackAction, { dataId: dataId, disabled: copying, feedback: status === 'error' ? 'Could not copy' : undefined, icon: _jsx(Copy, { className: "h-3.5 w-3.5" }), label: label, onClick: (event) => {
|
|
84
|
+
event.stopPropagation();
|
|
85
|
+
void copy('path');
|
|
86
|
+
}, tone: status === 'error' ? 'destructive' : tone })) }));
|
|
87
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
interface DeleteSelectedRowsActionProps {
|
|
2
|
+
count: number;
|
|
3
|
+
disabled?: boolean;
|
|
4
|
+
onDelete: () => Promise<void>;
|
|
5
|
+
portalContainer?: HTMLElement | null;
|
|
6
|
+
}
|
|
7
|
+
/** Confirmed bulk deletion for the rows a table has selected. One dialog
|
|
8
|
+
* covers the whole batch, which lands as a single file write. */
|
|
9
|
+
export declare function DeleteSelectedRowsAction({ count, disabled, onDelete, portalContainer, }: DeleteSelectedRowsActionProps): import("react").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Trash2 } from 'lucide-react';
|
|
4
|
+
import { Button } from '../primitives/button.js';
|
|
5
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '../primitives/dialog.js';
|
|
6
|
+
/** Confirmed bulk deletion for the rows a table has selected. One dialog
|
|
7
|
+
* covers the whole batch, which lands as a single file write. */
|
|
8
|
+
export function DeleteSelectedRowsAction({ count, disabled = false, onDelete, portalContainer, }) {
|
|
9
|
+
const [open, setOpen] = useState(false);
|
|
10
|
+
const [deleting, setDeleting] = useState(false);
|
|
11
|
+
const [error, setError] = useState(null);
|
|
12
|
+
const rowsLabel = count === 1 ? '1 row' : `${count.toLocaleString()} rows`;
|
|
13
|
+
const confirmDelete = async () => {
|
|
14
|
+
setDeleting(true);
|
|
15
|
+
setError(null);
|
|
16
|
+
try {
|
|
17
|
+
await onDelete();
|
|
18
|
+
setOpen(false);
|
|
19
|
+
}
|
|
20
|
+
catch (caught) {
|
|
21
|
+
setError(caught instanceof Error ? caught.message : 'Could not delete the rows');
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
setDeleting(false);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
return (_jsxs(Dialog, { modal: portalContainer == null, open: open, onOpenChange: (next) => {
|
|
28
|
+
if (deleting)
|
|
29
|
+
return;
|
|
30
|
+
setOpen(next);
|
|
31
|
+
if (!next)
|
|
32
|
+
setError(null);
|
|
33
|
+
}, children: [_jsx("button", { type: "button", "data-id": "structured-data-delete-selected-rows", "aria-label": `Delete ${rowsLabel}`, title: `Delete ${rowsLabel}`, disabled: disabled, className: "grid h-6 w-6 shrink-0 place-items-center rounded-md text-destructive transition-colors hover:text-destructive/80 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", onClick: (event) => {
|
|
34
|
+
event.stopPropagation();
|
|
35
|
+
setOpen(true);
|
|
36
|
+
}, children: _jsx(Trash2, { className: "h-3.5 w-3.5" }) }), _jsxs(DialogContent, { className: "max-w-sm", contained: portalContainer != null, portalContainer: portalContainer, children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { children: ["Delete ", rowsLabel, "?"] }), _jsxs(DialogDescription, { children: ["This removes the selected ", count === 1 ? 'row' : 'rows', " from the file. This action cannot be undone."] })] }), error && _jsx("p", { role: "alert", className: "rounded-md border border-destructive-border bg-destructive-surface px-3 py-2 text-xs text-destructive-surface-foreground", children: error }), _jsxs(DialogFooter, { children: [_jsx(Button, { type: "button", variant: "outline", disabled: deleting, onClick: () => setOpen(false), children: "Cancel" }), _jsx(Button, { type: "button", variant: "destructive", disabled: deleting, "data-id": "structured-data-confirm-delete-selected-rows", onClick: () => { void confirmDelete(); }, children: deleting ? 'Deleting…' : `Delete ${rowsLabel}` })] })] })] }));
|
|
37
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createContext, useContext, useRef } from 'react';
|
|
2
|
+
/** Compare the saved source, so a failed optimistic save can still be retried. */
|
|
3
|
+
export const EditBaseContext = createContext(undefined);
|
|
4
|
+
export function useEditBaseChanged() {
|
|
5
|
+
const source = useContext(EditBaseContext);
|
|
6
|
+
const initial = useRef(source);
|
|
7
|
+
return source !== initial.current;
|
|
8
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { compareJsonNumberTokens, inspectJsonSource } from '@script-it/json-views-core';
|
|
2
|
+
const PRECISION_ERROR = 'This filter value needs exact numeric source text. Define the filter in JSON source to preserve its precision.';
|
|
3
|
+
/** Never silently round a value typed into a filter or saved-view editor. */
|
|
4
|
+
export function parseFilterValue(value, kind) {
|
|
5
|
+
if (kind === 'string')
|
|
6
|
+
return value;
|
|
7
|
+
if (kind === 'boolean')
|
|
8
|
+
return value === 'true';
|
|
9
|
+
if (kind === 'number') {
|
|
10
|
+
const token = value.trim();
|
|
11
|
+
if (!/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/.test(token))
|
|
12
|
+
return value;
|
|
13
|
+
const number = Number(token);
|
|
14
|
+
if (!Number.isFinite(number) || compareJsonNumberTokens(token, String(number)) !== 0)
|
|
15
|
+
throw new Error(PRECISION_ERROR);
|
|
16
|
+
return number;
|
|
17
|
+
}
|
|
18
|
+
let inspected;
|
|
19
|
+
try {
|
|
20
|
+
inspected = inspectJsonSource(value);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
if (inspected.diagnostics.some((issue) => issue.code === 'unsafe-number'))
|
|
26
|
+
throw new Error(PRECISION_ERROR);
|
|
27
|
+
return inspected.value;
|
|
28
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/** Both faces share a fixed slot; an invisible checkbox stays in tab order. */
|
|
3
|
+
export declare function RowSelectionIcon({ icon, checked, disabled, label, onToggle }: {
|
|
4
|
+
icon: ReactNode;
|
|
5
|
+
checked: boolean;
|
|
6
|
+
disabled: boolean;
|
|
7
|
+
label: string;
|
|
8
|
+
onToggle?: () => void;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Checkbox } from '../primitives/checkbox.js';
|
|
3
|
+
/** Both faces share a fixed slot; an invisible checkbox stays in tab order. */
|
|
4
|
+
export function RowSelectionIcon({ icon, checked, disabled, label, onToggle }) {
|
|
5
|
+
return _jsxs("span", { className: "tabular-row-icon relative grid h-3.5 w-3.5 shrink-0 place-items-center", "data-id": onToggle ? 'tabular-row-select' : undefined, children: [_jsx("span", { "aria-hidden": true, className: onToggle ? 'tabular-selection-icon flex' : 'flex', children: icon }), onToggle && _jsx(Checkbox, { "aria-label": label, checked: checked, disabled: disabled, className: "tabular-selection-checkbox absolute inset-0 h-3.5 w-3.5 border-muted-foreground", onCheckedChange: onToggle, onClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation() })] });
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True inside a data cell of the tabular view, whose `<td>` carries no
|
|
3
|
+
* padding: the value frame supplies the cell padding itself, so its hover
|
|
4
|
+
* wash covers the full cell face instead of an inset box around the text.
|
|
5
|
+
*/
|
|
6
|
+
export declare const StructuredCellFillContext: import("react").Context<boolean>;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* True inside a data cell of the tabular view, whose `<td>` carries no
|
|
4
|
+
* padding: the value frame supplies the cell padding itself, so its hover
|
|
5
|
+
* wash covers the full cell face instead of an inset box around the text.
|
|
6
|
+
*/
|
|
7
|
+
export const StructuredCellFillContext = createContext(false);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { StructuredDataContent, type StructuredDataContentProps, type MetadataPersistence, type MetadataPersistenceRequest, type ObjectRootConversionRequest, } from '../json/json-content.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { StructuredDataContent, } from '../json/json-content.js';
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { UiSize } from '../lib/ui-size.js';
|
|
3
|
+
import type { JsonViewFilterOperator, ValuePath } from '@script-it/json-views-core';
|
|
4
|
+
export type SortDirection = 'asc' | 'desc';
|
|
5
|
+
export type SortConfig = {
|
|
6
|
+
column: string;
|
|
7
|
+
direction: SortDirection;
|
|
8
|
+
} | null;
|
|
9
|
+
export interface ColumnFilterRule {
|
|
10
|
+
operator: JsonViewFilterOperator;
|
|
11
|
+
value?: unknown;
|
|
12
|
+
}
|
|
13
|
+
export interface ColumnFilterConfig extends ColumnFilterRule {
|
|
14
|
+
column: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ColumnFilterOperatorOption {
|
|
17
|
+
label: string;
|
|
18
|
+
value: JsonViewFilterOperator;
|
|
19
|
+
}
|
|
20
|
+
export interface TabularObjectArrayViewProps {
|
|
21
|
+
arr: Record<string, unknown>[];
|
|
22
|
+
/** Stable source identities when the host reorders projected records. */
|
|
23
|
+
rowKeys?: readonly string[];
|
|
24
|
+
/** Exact source identity for comparison of numeric literals, including nested values. */
|
|
25
|
+
getCellSourcePath?: (rowIndex: number, column: string) => ValuePath | undefined;
|
|
26
|
+
columns: string[];
|
|
27
|
+
/** Optional display labels let projected views keep stable internal column
|
|
28
|
+
* keys even when two declared labels are identical. */
|
|
29
|
+
columnLabels?: Readonly<Record<string, string>>;
|
|
30
|
+
/** Stable field identities for cached column state when projected columns move. */
|
|
31
|
+
columnStateKeys?: Readonly<Record<string, string>>;
|
|
32
|
+
filePath?: string;
|
|
33
|
+
viewStateKey: string;
|
|
34
|
+
onOpenCell: (cell: {
|
|
35
|
+
value: unknown;
|
|
36
|
+
rowIndex: number;
|
|
37
|
+
column: string;
|
|
38
|
+
}) => void;
|
|
39
|
+
/** All rows after filtering/sorting, in display order (independent of virtualization). */
|
|
40
|
+
onVisibleRowsChange?: (rowIndices: readonly number[], filtered: boolean) => void;
|
|
41
|
+
/** Lets a host add behavior around a cell without coupling this table to a schema. */
|
|
42
|
+
renderCell?: (cell: {
|
|
43
|
+
value: unknown;
|
|
44
|
+
rowIndex: number;
|
|
45
|
+
column: string;
|
|
46
|
+
open: () => void;
|
|
47
|
+
}) => ReactNode;
|
|
48
|
+
/** Declares the one cell that represents and opens the whole record. */
|
|
49
|
+
recordNavigation?: {
|
|
50
|
+
titleColumn: string;
|
|
51
|
+
getLabel: (rowIndex: number) => string;
|
|
52
|
+
onOpen: (rowIndex: number) => void;
|
|
53
|
+
renderTitle: (cell: {
|
|
54
|
+
value: unknown;
|
|
55
|
+
rowIndex: number;
|
|
56
|
+
column: string;
|
|
57
|
+
}) => ReactNode;
|
|
58
|
+
};
|
|
59
|
+
/** Appends a blank record to an array-backed table. */
|
|
60
|
+
onAddRow?: () => Promise<void>;
|
|
61
|
+
rowAdditionDisabled?: boolean;
|
|
62
|
+
/** Adds row deletion via checkbox selection plus the footer's delete
|
|
63
|
+
* action (single rows included). Receives unsorted row indices in
|
|
64
|
+
* descending order so positional patchers can apply them in one pass. */
|
|
65
|
+
onDeleteRows?: (rowIndices: number[]) => Promise<void>;
|
|
66
|
+
/** Enables selection for copying and returns agent-ready JSON for the
|
|
67
|
+
* selected unsorted row indices in their current visible table order. */
|
|
68
|
+
getRowsCopyText?: (rowIndices: readonly number[]) => string;
|
|
69
|
+
/** Returns filename-qualified JSON Pointer references for selected rows. */
|
|
70
|
+
getRowsReferenceText?: (rowIndices: readonly number[]) => string;
|
|
71
|
+
rowDeletionDisabled?: boolean;
|
|
72
|
+
initialCell?: {
|
|
73
|
+
row: number;
|
|
74
|
+
col: string;
|
|
75
|
+
};
|
|
76
|
+
/** Scales row height to match the host's UI size. */
|
|
77
|
+
uiSize?: UiSize;
|
|
78
|
+
/**
|
|
79
|
+
* When true, the table stays content-sized until it reaches the parent's
|
|
80
|
+
* height, then its rows scroll internally while the footer remains visible.
|
|
81
|
+
* When false (default), the scroll area is capped at min(600px, 70vh) for
|
|
82
|
+
* unbounded layouts (inline outputs, embeds).
|
|
83
|
+
*/
|
|
84
|
+
fillHeight?: boolean;
|
|
85
|
+
/** Enables transient sort/hide state for tables without a saved view. */
|
|
86
|
+
presentationControls?: boolean;
|
|
87
|
+
/** Persisted views can expose the same actions directly from the column
|
|
88
|
+
* heading while the host owns the metadata update. */
|
|
89
|
+
activeColumnSort?: SortConfig;
|
|
90
|
+
onColumnSort?: (column: string, direction: SortDirection | null) => void | Promise<void>;
|
|
91
|
+
/** Externally managed filters, used by declared JSON Views whose metadata
|
|
92
|
+
* owns filtering. Omit to use transient table presentation state. */
|
|
93
|
+
activeColumnFilters?: readonly ColumnFilterConfig[];
|
|
94
|
+
/** Optional field-specific wording or operator restrictions. */
|
|
95
|
+
columnFilterOperators?: Readonly<Record<string, readonly ColumnFilterOperatorOption[]>>;
|
|
96
|
+
/** Values shown as input suggestions while still allowing free text. */
|
|
97
|
+
columnFilterValues?: Readonly<Record<string, readonly unknown[]>>;
|
|
98
|
+
onColumnFilter?: (column: string, filter: ColumnFilterRule | null) => void | Promise<void>;
|
|
99
|
+
onColumnRename?: (column: string, label: string) => void | Promise<void>;
|
|
100
|
+
onColumnHide?: (column: string) => void | Promise<void>;
|
|
101
|
+
/** Reorders a declared column while preserving its data path. */
|
|
102
|
+
onColumnMove?: (column: string, target: string, position: 'before' | 'after') => void | Promise<void>;
|
|
103
|
+
onColumnDelete?: (column: string) => void | Promise<void>;
|
|
104
|
+
onAddColumn?: (column: {
|
|
105
|
+
label: string;
|
|
106
|
+
type: 'text' | 'markdown' | 'html' | 'number' | 'checkbox' | 'date' | 'select' | 'multi-select';
|
|
107
|
+
}) => void | Promise<void>;
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StructuredDocumentFormat } from '@script-it/json-views-core';
|
|
2
|
+
export type MetadataPersistence = 'inferred' | 'session' | 'embedded';
|
|
3
|
+
/** Annotation storage is independent of source format conversion.
|
|
4
|
+
* Controlled metadata is always authoritative, including its removal.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useMetadataPersistence({ metadata, mode, format, onMetadataChange }: {
|
|
7
|
+
metadata?: unknown;
|
|
8
|
+
mode?: MetadataPersistence;
|
|
9
|
+
format: StructuredDocumentFormat;
|
|
10
|
+
onMetadataChange?: (metadata: Record<string, unknown>) => Promise<void>;
|
|
11
|
+
}): {
|
|
12
|
+
persistence: MetadataPersistence;
|
|
13
|
+
effectiveMetadata: {} | null | undefined;
|
|
14
|
+
saveSessionMetadata: (next: Record<string, unknown>) => Promise<void>;
|
|
15
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
/** Annotation storage is independent of source format conversion.
|
|
3
|
+
* Controlled metadata is always authoritative, including its removal.
|
|
4
|
+
*/
|
|
5
|
+
export function useMetadataPersistence({ metadata, mode, format, onMetadataChange }) {
|
|
6
|
+
const [sessionMetadata, setSessionMetadata] = useState();
|
|
7
|
+
const persistence = metadata !== undefined ? 'session' : mode ?? (format === 'json-object' ? 'embedded' : 'inferred');
|
|
8
|
+
const effectiveMetadata = metadata !== undefined ? metadata : persistence === 'session' ? sessionMetadata : undefined;
|
|
9
|
+
const saveSessionMetadata = useCallback(async (next) => {
|
|
10
|
+
if (metadata !== undefined && !onMetadataChange)
|
|
11
|
+
throw new Error('These annotations are read-only');
|
|
12
|
+
await onMetadataChange?.(next);
|
|
13
|
+
// Do not cache controlled props: removing them must restore inference/embedded metadata.
|
|
14
|
+
if (metadata === undefined)
|
|
15
|
+
setSessionMetadata(next);
|
|
16
|
+
}, [metadata, onMetadataChange]);
|
|
17
|
+
return { persistence, effectiveMetadata, saveSessionMetadata };
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type DocumentContentValidator, type JsonDocumentSave } from '@script-it/json-views-core';
|
|
2
|
+
interface OptimisticTextDocumentOptions {
|
|
3
|
+
content: string;
|
|
4
|
+
documentKey?: string;
|
|
5
|
+
revision?: string;
|
|
6
|
+
saveContent?: JsonDocumentSave;
|
|
7
|
+
saving?: boolean;
|
|
8
|
+
validateContent?: DocumentContentValidator;
|
|
9
|
+
}
|
|
10
|
+
/** React subscription to a document session; the host owns durable persistence. */
|
|
11
|
+
export declare function useOptimisticTextDocument({ content, documentKey, revision, saveContent, saving, validateContent, }: OptimisticTextDocumentOptions): {
|
|
12
|
+
canCommit: boolean;
|
|
13
|
+
commit: (buildNext: (source: string) => string) => Promise<void>;
|
|
14
|
+
content: string;
|
|
15
|
+
acknowledgedContent: string;
|
|
16
|
+
edit: (nextContent: string) => void;
|
|
17
|
+
saving: boolean;
|
|
18
|
+
error: string | undefined;
|
|
19
|
+
dirty: boolean;
|
|
20
|
+
reset: () => void;
|
|
21
|
+
};
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useSyncExternalStore } from 'react';
|
|
2
|
+
import { JsonDocumentSession } from '@script-it/json-views-core';
|
|
3
|
+
const useBrowserLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
4
|
+
/** React subscription to a document session; the host owns durable persistence. */
|
|
5
|
+
export function useOptimisticTextDocument({ content, documentKey, revision, saveContent, saving = false, validateContent, }) {
|
|
6
|
+
const session = useMemo(() => new JsonDocumentSession({
|
|
7
|
+
id: documentKey ?? 'document', content, revision, validateContent,
|
|
8
|
+
}), [documentKey, validateContent]);
|
|
9
|
+
const snapshot = useSyncExternalStore(session.subscribe, session.getSnapshot, session.getSnapshot);
|
|
10
|
+
useBrowserLayoutEffect(() => { session.receive({ content, revision }); }, [session, content, revision]);
|
|
11
|
+
const commit = useCallback((buildNext) => {
|
|
12
|
+
if (!saveContent)
|
|
13
|
+
return Promise.reject(new Error('This file is read-only'));
|
|
14
|
+
if (saving)
|
|
15
|
+
return Promise.reject(new Error('A save is already in progress'));
|
|
16
|
+
return session.commit(buildNext, saveContent);
|
|
17
|
+
}, [session, saveContent, saving]);
|
|
18
|
+
const edit = useCallback((nextContent) => { session.edit(nextContent); }, [session]);
|
|
19
|
+
return {
|
|
20
|
+
canCommit: saveContent !== undefined,
|
|
21
|
+
commit,
|
|
22
|
+
content: snapshot.content,
|
|
23
|
+
acknowledgedContent: snapshot.acknowledgedContent,
|
|
24
|
+
edit,
|
|
25
|
+
saving: snapshot.saving || saving,
|
|
26
|
+
error: snapshot.error?.message,
|
|
27
|
+
dirty: snapshot.dirty,
|
|
28
|
+
reset: () => session.reset(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ValuePath } from '@script-it/json-views-core';
|
|
2
|
+
export declare function ExpandValueButton({ onClick }: {
|
|
3
|
+
onClick: () => void;
|
|
4
|
+
}): import("react").JSX.Element;
|
|
5
|
+
export declare function ValueCell({ value, sourcePath, onOpen, }: {
|
|
6
|
+
value: unknown;
|
|
7
|
+
sourcePath?: ValuePath;
|
|
8
|
+
onOpen: () => void;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
10
|
+
export declare function ValueCellContent({ containerInteractive, value, sourcePath, onOpen, }: {
|
|
11
|
+
containerInteractive?: boolean;
|
|
12
|
+
value: unknown;
|
|
13
|
+
sourcePath?: ValuePath;
|
|
14
|
+
onOpen: () => void;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Maximize2, Table2 } from 'lucide-react';
|
|
3
|
+
import { JsonSourceNumber, useJsonSourceLiterals } from '../json/source-literals.js';
|
|
4
|
+
import { formatAbsoluteTimestamp, formatRelativeTimeShortIfPast, parseIsoDateString } from '../lib/time.js';
|
|
5
|
+
import { StructuredValueCellFrame } from './atomic-value-editor.js';
|
|
6
|
+
import { OptionPill } from '../primitives/option-pill.js';
|
|
7
|
+
function isSimpleList(value) {
|
|
8
|
+
return Array.isArray(value) && value.every((item) => item === null || ['string', 'number', 'boolean'].includes(typeof item));
|
|
9
|
+
}
|
|
10
|
+
function isWebUrl(value) {
|
|
11
|
+
if (typeof value !== 'string')
|
|
12
|
+
return false;
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(value);
|
|
15
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function getValuePreview(value, sourcePath, sourceLiteral, depth = 0) {
|
|
22
|
+
const entries = Array.isArray(value)
|
|
23
|
+
? value.map((item, index) => [index, item])
|
|
24
|
+
: Object.entries(value);
|
|
25
|
+
if (entries.length === 0)
|
|
26
|
+
return '(empty)';
|
|
27
|
+
const limit = depth === 0 ? 8 : 2;
|
|
28
|
+
const parts = entries.slice(0, limit).map(([key, item]) => {
|
|
29
|
+
const path = sourcePath ? [...sourcePath, key] : undefined;
|
|
30
|
+
const literal = sourceLiteral(path);
|
|
31
|
+
const text = item === null ? 'null' : typeof item === 'object'
|
|
32
|
+
? depth < 1 ? getValuePreview(item, path, sourceLiteral, depth + 1) : '…'
|
|
33
|
+
: literal ?? String(item);
|
|
34
|
+
return Array.isArray(value) ? text : `${key}: ${text}`;
|
|
35
|
+
});
|
|
36
|
+
return parts.join(', ') + (entries.length > limit ? ', …' : '');
|
|
37
|
+
}
|
|
38
|
+
export function ExpandValueButton({ onClick }) {
|
|
39
|
+
return (_jsx("button", { type: "button", className: "grid h-6 w-6 shrink-0 place-items-center rounded-md bg-transparent text-muted-foreground opacity-100 hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover/structured-cell:opacity-100 [@media(hover:hover)]:group-focus-within/structured-cell:opacity-100", onClick: (event) => {
|
|
40
|
+
event.stopPropagation();
|
|
41
|
+
onClick();
|
|
42
|
+
}, title: "Expand value", children: _jsx(Maximize2, { className: "h-3.5 w-3.5" }) }));
|
|
43
|
+
}
|
|
44
|
+
export function ValueCell({ value, sourcePath, onOpen, }) {
|
|
45
|
+
const container = value !== null && typeof value === 'object' && !isSimpleList(value);
|
|
46
|
+
return (_jsx(StructuredValueCellFrame, { actions: container ? undefined : _jsx(ExpandValueButton, { onClick: onOpen }), activationLabel: container ? 'Open nested value' : undefined, onActivate: container ? onOpen : undefined, children: _jsx(ValueCellContent, { containerInteractive: !container, sourcePath: sourcePath, value: value, onOpen: onOpen }) }));
|
|
47
|
+
}
|
|
48
|
+
export function ValueCellContent({ containerInteractive = true, value, sourcePath, onOpen, }) {
|
|
49
|
+
const sourceLiteral = useJsonSourceLiterals();
|
|
50
|
+
const literal = sourceLiteral(sourcePath);
|
|
51
|
+
if (literal !== undefined)
|
|
52
|
+
return _jsx(JsonSourceNumber, { literal: literal });
|
|
53
|
+
if (isSimpleList(value)) {
|
|
54
|
+
if (value.every((item) => typeof item === 'string' && item.length <= 60 && !isWebUrl(item))) {
|
|
55
|
+
const visible = value.slice(0, 8);
|
|
56
|
+
return _jsxs("span", { className: "flex min-w-0 flex-1 flex-wrap gap-1 py-0.5", children: [visible.length === 0 ? _jsx("span", { className: "text-muted-foreground", children: "(empty)" }) : visible.map((item, index) => _jsx(OptionPill, { label: item, className: "max-w-full", labelClassName: "truncate" }, index)), value.length > visible.length && _jsxs("button", { type: "button", className: "rounded-md bg-muted px-2 py-0.5 text-xs text-muted-foreground hover:bg-accent hover:text-foreground", onClick: (event) => { event.stopPropagation(); onOpen(); }, children: ["+", value.length - visible.length, " more"] })] });
|
|
57
|
+
}
|
|
58
|
+
return _jsx("span", { className: "min-w-0 flex-1 whitespace-normal break-words leading-5", children: value.length === 0 ? '(empty)' : value.map((item, index) => {
|
|
59
|
+
const itemLiteral = sourceLiteral(sourcePath ? [...sourcePath, index] : undefined);
|
|
60
|
+
return _jsxs("span", { children: [index > 0 && ', ', itemLiteral !== undefined ? _jsx(JsonSourceNumber, { literal: itemLiteral }) : isWebUrl(item)
|
|
61
|
+
? _jsx("a", { href: item, target: "_blank", rel: "noopener noreferrer", className: "text-link hover:underline", onClick: (event) => event.stopPropagation(), children: item })
|
|
62
|
+
: String(item)] }, index);
|
|
63
|
+
}) });
|
|
64
|
+
}
|
|
65
|
+
const isObject = value !== null && typeof value === 'object';
|
|
66
|
+
if (!isObject) {
|
|
67
|
+
const scalarValue = String(value ?? '');
|
|
68
|
+
// An ISO datetime string reads as a local date-time ("Jul 5, 2026,
|
|
69
|
+
// 1:05 PM · 2h ago"); the raw value stays on the tooltip and in the
|
|
70
|
+
// expanded view, so nothing is lost for copying. The relative suffix
|
|
71
|
+
// only appears for past times — "ago" phrasing makes no sense for a
|
|
72
|
+
// future timestamp.
|
|
73
|
+
const isoMs = parseIsoDateString(value);
|
|
74
|
+
const relative = isoMs !== null ? formatRelativeTimeShortIfPast(isoMs) : null;
|
|
75
|
+
return (_jsx("div", { className: "min-w-0 flex-1 overflow-hidden whitespace-nowrap leading-[max(1.25rem,1.4em)] text-foreground", title: scalarValue, children: isoMs !== null ? (_jsxs(_Fragment, { children: [formatAbsoluteTimestamp(isoMs), relative !== null && (_jsxs("span", { className: "text-muted-foreground", children: [" \u00B7 ", relative] }))] })) : (scalarValue) }));
|
|
76
|
+
}
|
|
77
|
+
const preview = getValuePreview(value, sourcePath, sourceLiteral);
|
|
78
|
+
const displayText = preview;
|
|
79
|
+
const content = (_jsxs(_Fragment, { children: [_jsx(Table2, { "aria-hidden": true, className: "h-4 w-4 shrink-0 text-source-number" }), _jsx("span", { className: "min-w-0 truncate underline", children: displayText })] }));
|
|
80
|
+
return containerInteractive ? (_jsx("button", { type: "button", className: "inline-flex min-h-7 max-w-full min-w-0 flex-1 cursor-pointer items-center gap-2 rounded-md px-1 text-left leading-5 transition-colors hover:bg-accent", onClick: onOpen, title: preview, children: content })) : (_jsx("span", { className: "inline-flex min-h-7 max-w-full min-w-0 flex-1 items-center gap-2 px-1 leading-5", title: preview, children: content }));
|
|
81
|
+
}
|