@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,79 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useRef, useState } from 'react';
|
|
3
|
+
/** Bounded, instance-owned presentation state. Never shared across host viewers. */
|
|
4
|
+
export class ViewStateCache {
|
|
5
|
+
onChange;
|
|
6
|
+
entries;
|
|
7
|
+
constructor(initial, onChange) {
|
|
8
|
+
this.onChange = onChange;
|
|
9
|
+
this.entries = new Map(Object.entries(initial ?? {}).slice(-30));
|
|
10
|
+
}
|
|
11
|
+
get(key) { return this.entries.get(key); }
|
|
12
|
+
set(key, value) {
|
|
13
|
+
this.entries.delete(key);
|
|
14
|
+
this.entries.set(key, value);
|
|
15
|
+
if (this.entries.size > 30)
|
|
16
|
+
this.entries.delete(this.entries.keys().next().value);
|
|
17
|
+
this.onChange?.();
|
|
18
|
+
}
|
|
19
|
+
delete(key) {
|
|
20
|
+
if (!this.entries.delete(key))
|
|
21
|
+
return;
|
|
22
|
+
this.onChange?.();
|
|
23
|
+
}
|
|
24
|
+
toRecord() { return Object.fromEntries(this.entries); }
|
|
25
|
+
}
|
|
26
|
+
function createViewerState(initial, onChange) {
|
|
27
|
+
let activeView = initial?.version === 1 ? initial.activeView : undefined;
|
|
28
|
+
const changed = () => onChange();
|
|
29
|
+
const state = {
|
|
30
|
+
get activeView() { return activeView; },
|
|
31
|
+
setActiveView(value) {
|
|
32
|
+
if (activeView === value)
|
|
33
|
+
return;
|
|
34
|
+
activeView = value;
|
|
35
|
+
changed();
|
|
36
|
+
},
|
|
37
|
+
queries: new ViewStateCache(initial?.version === 1 ? initial.queries : undefined, changed),
|
|
38
|
+
viewDrafts: new ViewStateCache(initial?.version === 1 ? initial.viewDrafts : undefined, changed),
|
|
39
|
+
navigation: new ViewStateCache(initial?.version === 1 ? initial.navigation : undefined, changed),
|
|
40
|
+
tables: new ViewStateCache(initial?.version === 1 ? initial.tables : undefined, changed),
|
|
41
|
+
general: new ViewStateCache(initial?.version === 1 ? initial.general : undefined, changed),
|
|
42
|
+
snapshot() {
|
|
43
|
+
const queries = state.queries.toRecord();
|
|
44
|
+
const viewDrafts = state.viewDrafts.toRecord();
|
|
45
|
+
const navigation = state.navigation.toRecord();
|
|
46
|
+
const tables = state.tables.toRecord();
|
|
47
|
+
const general = state.general.toRecord();
|
|
48
|
+
return {
|
|
49
|
+
version: 1,
|
|
50
|
+
...(activeView ? { activeView } : {}),
|
|
51
|
+
...(Object.keys(queries).length > 0 ? { queries } : {}),
|
|
52
|
+
...(Object.keys(viewDrafts).length > 0 ? { viewDrafts } : {}),
|
|
53
|
+
...(Object.keys(navigation).length > 0 ? { navigation } : {}),
|
|
54
|
+
...(Object.keys(tables).length > 0 ? { tables } : {}),
|
|
55
|
+
...(Object.keys(general).length > 0 ? { general } : {}),
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
return state;
|
|
60
|
+
}
|
|
61
|
+
const ViewerStateContext = createContext(undefined);
|
|
62
|
+
export function ViewerStateProvider({ children, initialState, onChange, }) {
|
|
63
|
+
const changeHandler = useRef(onChange);
|
|
64
|
+
changeHandler.current = onChange;
|
|
65
|
+
const stateRef = useRef(undefined);
|
|
66
|
+
if (!stateRef.current) {
|
|
67
|
+
stateRef.current = createViewerState(initialState, () => {
|
|
68
|
+
const state = stateRef.current;
|
|
69
|
+
if (state)
|
|
70
|
+
changeHandler.current?.(state.snapshot());
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return _jsx(ViewerStateContext.Provider, { value: stateRef.current, children: children });
|
|
74
|
+
}
|
|
75
|
+
export function useViewerState() {
|
|
76
|
+
const provided = useContext(ViewerStateContext);
|
|
77
|
+
const [local] = useState(() => createViewerState(undefined, () => undefined));
|
|
78
|
+
return provided ?? local;
|
|
79
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type ComponentType, type ReactNode } from 'react';
|
|
2
|
+
import { type JsonViewSchemaDescriptor, type JsonViewTypeRegistry } from '@script-it/json-views-core';
|
|
3
|
+
export interface JsonViewEditWidgetProps {
|
|
4
|
+
descriptor: JsonViewSchemaDescriptor;
|
|
5
|
+
disabled: boolean;
|
|
6
|
+
error?: string;
|
|
7
|
+
label: string;
|
|
8
|
+
value: unknown;
|
|
9
|
+
stringValue: string;
|
|
10
|
+
onChange: (value: unknown) => void;
|
|
11
|
+
onClear?: () => void;
|
|
12
|
+
onCommit: (value?: unknown) => void;
|
|
13
|
+
onCancel: () => void;
|
|
14
|
+
/** Keeps a multiline editor at least as tall as the value it replaced. */
|
|
15
|
+
minHeight?: number;
|
|
16
|
+
/** Persists a built-in choice color in the matching annotation descriptor. */
|
|
17
|
+
onOptionColorChange?: (optionValue: string, color: string) => void | Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export type JsonViewEditWidget = ComponentType<JsonViewEditWidgetProps>;
|
|
20
|
+
export interface JsonViewDisplayWidgetProps {
|
|
21
|
+
compact: boolean;
|
|
22
|
+
descriptor: JsonViewSchemaDescriptor;
|
|
23
|
+
onOpen: () => void;
|
|
24
|
+
renderMarkdown?: (content: string) => ReactNode;
|
|
25
|
+
value: unknown;
|
|
26
|
+
}
|
|
27
|
+
export type JsonViewDisplayWidget = ComponentType<JsonViewDisplayWidgetProps>;
|
|
28
|
+
export interface JsonViewWidgetDefinition {
|
|
29
|
+
editor: JsonViewEditWidget;
|
|
30
|
+
display?: JsonViewDisplayWidget;
|
|
31
|
+
/** Optional single-action editor; returns an editor-model candidate. */
|
|
32
|
+
quickEdit?: (value: unknown) => unknown;
|
|
33
|
+
}
|
|
34
|
+
export type JsonViewWidgetRegistration = JsonViewEditWidget | JsonViewWidgetDefinition;
|
|
35
|
+
export declare class JsonViewWidgetRegistry {
|
|
36
|
+
private readonly widgets;
|
|
37
|
+
private readonly listeners;
|
|
38
|
+
private revision;
|
|
39
|
+
get version(): number;
|
|
40
|
+
readonly subscribe: (listener: () => void) => (() => void);
|
|
41
|
+
private changed;
|
|
42
|
+
constructor(widgets?: readonly [string, JsonViewWidgetRegistration][]);
|
|
43
|
+
register(type: string, widget: JsonViewWidgetRegistration): this;
|
|
44
|
+
unregister(type: string): this;
|
|
45
|
+
get(type: string): JsonViewEditWidget | undefined;
|
|
46
|
+
getDefinition(type: string): JsonViewWidgetDefinition | undefined;
|
|
47
|
+
getDisplay(type: string): JsonViewDisplayWidget | undefined;
|
|
48
|
+
clone(): JsonViewWidgetRegistry;
|
|
49
|
+
}
|
|
50
|
+
export declare function createDefaultWidgetRegistry(): JsonViewWidgetRegistry;
|
|
51
|
+
export interface JsonViewsRegistryValue {
|
|
52
|
+
types: JsonViewTypeRegistry;
|
|
53
|
+
widgets: JsonViewWidgetRegistry;
|
|
54
|
+
}
|
|
55
|
+
export declare function JsonViewsProvider({ children, types, widgets, }: {
|
|
56
|
+
children: ReactNode;
|
|
57
|
+
types?: JsonViewTypeRegistry;
|
|
58
|
+
widgets?: JsonViewWidgetRegistry;
|
|
59
|
+
}): import("react").JSX.Element;
|
|
60
|
+
export declare function useJsonViewsRegistries(): JsonViewsRegistryValue;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useState, useSyncExternalStore } from 'react';
|
|
3
|
+
import { createDefaultTypeRegistry, } from '@script-it/json-views-core';
|
|
4
|
+
import { BooleanWidget } from './widgets/boolean-widget.js';
|
|
5
|
+
import { DateValueDisplay, DateValueEditor } from './widgets/date-picker-widget.js';
|
|
6
|
+
import { DropdownWidget } from './widgets/dropdown-widget.js';
|
|
7
|
+
import { MultiSelectWidget } from './widgets/multi-select-widget.js';
|
|
8
|
+
import { NumberWidget } from './widgets/number-widget.js';
|
|
9
|
+
import { TextareaWidget } from './widgets/textarea-widget.js';
|
|
10
|
+
import { TextWidget } from './widgets/text-widget.js';
|
|
11
|
+
function common(props) {
|
|
12
|
+
return {
|
|
13
|
+
value: props.stringValue,
|
|
14
|
+
onChange: props.onChange,
|
|
15
|
+
onBlur: () => props.onCommit(),
|
|
16
|
+
disabled: props.disabled,
|
|
17
|
+
ariaLabel: `Edit ${props.label}`,
|
|
18
|
+
invalid: Boolean(props.error),
|
|
19
|
+
minHeight: props.minHeight,
|
|
20
|
+
surface: 'inline',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const TextEditWidget = (props) => {
|
|
24
|
+
const Control = props.descriptor.multiline === true ? TextareaWidget : TextWidget;
|
|
25
|
+
return (_jsx(Control, { ...common(props), uiHint: {
|
|
26
|
+
type: 'text',
|
|
27
|
+
placeholder: typeof props.descriptor.placeholder === 'string' ? props.descriptor.placeholder : undefined,
|
|
28
|
+
multiline: props.descriptor.multiline === true,
|
|
29
|
+
} }));
|
|
30
|
+
};
|
|
31
|
+
const NumberEditWidget = (props) => (_jsx(NumberWidget, { ...common(props), metadata: { type: 'float' }, uiHint: {
|
|
32
|
+
type: 'number',
|
|
33
|
+
min: typeof props.descriptor.minimum === 'number' ? props.descriptor.minimum : undefined,
|
|
34
|
+
max: typeof props.descriptor.maximum === 'number' ? props.descriptor.maximum : undefined,
|
|
35
|
+
step: props.descriptor.step,
|
|
36
|
+
} }));
|
|
37
|
+
const CheckboxEditWidget = (props) => (_jsx(BooleanWidget, { ...common(props), onDirectSave: (value) => {
|
|
38
|
+
props.onChange(value);
|
|
39
|
+
props.onCommit(value);
|
|
40
|
+
} }));
|
|
41
|
+
const SelectEditWidget = (props) => (_jsx(DropdownWidget, { ...common(props), onChange: (value) => {
|
|
42
|
+
props.onChange(value);
|
|
43
|
+
props.onCommit(value);
|
|
44
|
+
}, onBlur: () => undefined, onDismiss: props.onCancel, options: (props.descriptor.options ?? []).map((value) => ({ value, label: value, color: props.descriptor.optionColors?.[value] })), profile: "pill-cell", creatable: true, openOnMount: true, emptyOptionLabel: props.descriptor.required ? undefined : 'None', onOptionColorChange: props.onOptionColorChange }));
|
|
45
|
+
const MultiSelectEditWidget = (props) => (_jsx(MultiSelectWidget, { ...common(props), value: JSON.stringify(Array.isArray(props.value) ? props.value : []), options: (props.descriptor.options ?? []).map((value) => ({ value, label: value, color: props.descriptor.optionColors?.[value] })), profile: "pill-cell", creatable: true, openOnMount: true, onDirectSave: (value) => {
|
|
46
|
+
props.onChange(value);
|
|
47
|
+
props.onCommit(value);
|
|
48
|
+
}, onBlur: () => undefined, onDismiss: props.onCancel, onOptionColorChange: props.onOptionColorChange }));
|
|
49
|
+
const DateEditWidget = (props) => (_jsx(DateValueEditor, { descriptor: props.descriptor, disabled: props.disabled, error: props.error, label: props.label, value: props.value, onCancel: props.onCancel, onClear: props.onClear, onCommit: props.onCommit }));
|
|
50
|
+
const DateDisplayWidget = ({ compact, value }) => (_jsx(DateValueDisplay, { compact: compact, value: value }));
|
|
51
|
+
const LinkedTextEditWidget = (props) => (_jsx("input", { autoFocus: true, "aria-label": `Edit ${props.label}`, "aria-invalid": Boolean(props.error) || undefined, type: props.descriptor.type === 'email' ? 'email' : 'url', value: props.stringValue, disabled: props.disabled, className: "h-7 w-full min-w-0 rounded-sm border border-transparent bg-transparent px-1 py-0 text-xs text-foreground outline-none hover:bg-accent/40 focus:ring-1 focus:ring-inset focus:ring-ring disabled:opacity-60", onChange: (event) => props.onChange(event.target.value), onBlur: () => props.onCommit() }));
|
|
52
|
+
const MarkdownEditWidget = (props) => (_jsx(TextareaWidget, { ...common(props), autoSize: true, metadata: { type: 'string' }, uiHint: { type: 'text', multiline: true, placeholder: typeof props.descriptor.placeholder === 'string' ? props.descriptor.placeholder : undefined } }));
|
|
53
|
+
export class JsonViewWidgetRegistry {
|
|
54
|
+
widgets = new Map();
|
|
55
|
+
listeners = new Set();
|
|
56
|
+
revision = 0;
|
|
57
|
+
get version() { return this.revision; }
|
|
58
|
+
subscribe = (listener) => {
|
|
59
|
+
this.listeners.add(listener);
|
|
60
|
+
return () => { this.listeners.delete(listener); };
|
|
61
|
+
};
|
|
62
|
+
changed() {
|
|
63
|
+
this.revision += 1;
|
|
64
|
+
this.listeners.forEach((listener) => listener());
|
|
65
|
+
}
|
|
66
|
+
constructor(widgets = []) {
|
|
67
|
+
widgets.forEach(([type, widget]) => this.register(type, widget));
|
|
68
|
+
}
|
|
69
|
+
register(type, widget) {
|
|
70
|
+
if (!type.trim())
|
|
71
|
+
throw new TypeError('A widget type must have a name');
|
|
72
|
+
// memo/forwardRef components are React component objects at runtime.
|
|
73
|
+
const registration = typeof widget === 'object' && widget !== null && 'editor' in widget
|
|
74
|
+
? widget : { editor: widget };
|
|
75
|
+
this.widgets.set(type, Object.freeze({ display: this.widgets.get(type)?.display, ...registration }));
|
|
76
|
+
this.changed();
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
unregister(type) {
|
|
80
|
+
if (this.widgets.delete(type))
|
|
81
|
+
this.changed();
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
get(type) {
|
|
85
|
+
return this.widgets.get(type)?.editor;
|
|
86
|
+
}
|
|
87
|
+
getDefinition(type) {
|
|
88
|
+
return this.widgets.get(type);
|
|
89
|
+
}
|
|
90
|
+
getDisplay(type) {
|
|
91
|
+
return this.widgets.get(type)?.display;
|
|
92
|
+
}
|
|
93
|
+
clone() {
|
|
94
|
+
return new JsonViewWidgetRegistry([...this.widgets.entries()]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export function createDefaultWidgetRegistry() {
|
|
98
|
+
return new JsonViewWidgetRegistry([
|
|
99
|
+
['text', TextEditWidget],
|
|
100
|
+
['number', NumberEditWidget],
|
|
101
|
+
['checkbox', { editor: CheckboxEditWidget, quickEdit: (value) => value !== true }],
|
|
102
|
+
['select', SelectEditWidget],
|
|
103
|
+
['multi-select', MultiSelectEditWidget],
|
|
104
|
+
['date', { editor: DateEditWidget, display: DateDisplayWidget }],
|
|
105
|
+
['url', LinkedTextEditWidget],
|
|
106
|
+
['email', LinkedTextEditWidget],
|
|
107
|
+
['markdown', MarkdownEditWidget],
|
|
108
|
+
['html', MarkdownEditWidget],
|
|
109
|
+
['body', MarkdownEditWidget],
|
|
110
|
+
]);
|
|
111
|
+
}
|
|
112
|
+
const defaultRegistries = {
|
|
113
|
+
types: createDefaultTypeRegistry(),
|
|
114
|
+
widgets: createDefaultWidgetRegistry(),
|
|
115
|
+
};
|
|
116
|
+
const JsonViewsRegistryContext = createContext(defaultRegistries);
|
|
117
|
+
export function JsonViewsProvider({ children, types, widgets, }) {
|
|
118
|
+
const [defaults] = useState(() => ({ types: createDefaultTypeRegistry(), widgets: createDefaultWidgetRegistry() }));
|
|
119
|
+
return (_jsx(JsonViewsRegistryContext.Provider, { value: { types: types ?? defaults.types, widgets: widgets ?? defaults.widgets }, children: children }));
|
|
120
|
+
}
|
|
121
|
+
export function useJsonViewsRegistries() {
|
|
122
|
+
const registries = useContext(JsonViewsRegistryContext);
|
|
123
|
+
useSyncExternalStore(registries.types.subscribe, () => registries.types.version, () => registries.types.version);
|
|
124
|
+
useSyncExternalStore(registries.widgets.subscribe, () => registries.widgets.version, () => registries.widgets.version);
|
|
125
|
+
return registries;
|
|
126
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { BooleanToggle } from '../primitives/boolean-toggle.js';
|
|
3
|
+
import { ReadOnlyChip } from './read-only-field.js';
|
|
4
|
+
import { cn } from '../lib/cn.js';
|
|
5
|
+
export function BooleanWidget({ value, onChange, onDirectSave, ariaLabel, ariaDescribedBy, invalid = false, readOnly = false, disabled = false, }) {
|
|
6
|
+
const boolValue = value.toLowerCase() === 'true';
|
|
7
|
+
if (readOnly) {
|
|
8
|
+
return (_jsxs("div", { className: "flex items-center gap-2", "aria-readonly": "true", children: [_jsx(ReadOnlyChip, { emphasis: boolValue, children: "True" }), _jsx(ReadOnlyChip, { emphasis: !boolValue, children: "False" })] }));
|
|
9
|
+
}
|
|
10
|
+
const handleChange = (next) => {
|
|
11
|
+
onChange(String(next));
|
|
12
|
+
onDirectSave(next);
|
|
13
|
+
};
|
|
14
|
+
return _jsx(BooleanToggle, { "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, "aria-invalid": invalid || undefined, value: boolValue, onChange: handleChange, disabled: disabled, className: cn(invalid && '[&_button]:border-destructive') });
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type DateTypeDescriptor } from '@script-it/json-views-core';
|
|
2
|
+
interface DateValueDisplayProps {
|
|
3
|
+
compact?: boolean;
|
|
4
|
+
value: unknown;
|
|
5
|
+
}
|
|
6
|
+
interface DateValueEditorProps {
|
|
7
|
+
descriptor: DateTypeDescriptor;
|
|
8
|
+
disabled: boolean;
|
|
9
|
+
error?: string;
|
|
10
|
+
label: string;
|
|
11
|
+
value: unknown;
|
|
12
|
+
onCancel: () => void;
|
|
13
|
+
onClear?: () => void;
|
|
14
|
+
onCommit: (value?: unknown) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function DateValueDisplay({ compact, value }: DateValueDisplayProps): import("react").JSX.Element;
|
|
17
|
+
export declare function DateValueEditor({ descriptor, disabled, error, label, value, onCancel, onClear, onCommit, }: DateValueEditorProps): import("react").JSX.Element;
|
|
18
|
+
export type { DateValueDisplayProps, DateValueEditorProps };
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { AlertTriangle, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
4
|
+
import { convertDateTimeToOffset, formatFixedOffset, localOffsetForDate, parseDateValue, serializeDateEditorValue, validateDateValue, } from '@script-it/json-views-core';
|
|
5
|
+
import { cn } from '../lib/cn.js';
|
|
6
|
+
import { MenuSelect } from '../primitives/menu-select.js';
|
|
7
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, } from '../primitives/dropdown-menu.js';
|
|
8
|
+
function pad(value, size = 2) {
|
|
9
|
+
return String(value).padStart(size, '0');
|
|
10
|
+
}
|
|
11
|
+
function calendarDate(year, month, day) {
|
|
12
|
+
return `${pad(year, 4)}-${pad(month)}-${pad(day)}`;
|
|
13
|
+
}
|
|
14
|
+
function localToday() {
|
|
15
|
+
const today = new Date();
|
|
16
|
+
return calendarDate(today.getFullYear(), today.getMonth() + 1, today.getDate());
|
|
17
|
+
}
|
|
18
|
+
function calendarDays(year, month) {
|
|
19
|
+
const first = new Date(Date.UTC(year, month - 1, 1));
|
|
20
|
+
const mondayOffset = (first.getUTCDay() + 6) % 7;
|
|
21
|
+
return Array.from({ length: 42 }, (_, index) => {
|
|
22
|
+
const current = new Date(Date.UTC(year, month - 1, index - mondayOffset + 1));
|
|
23
|
+
return {
|
|
24
|
+
date: calendarDate(current.getUTCFullYear(), current.getUTCMonth() + 1, current.getUTCDate()),
|
|
25
|
+
day: current.getUTCDate(),
|
|
26
|
+
inMonth: current.getUTCMonth() === month - 1,
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function dateParts(value) {
|
|
31
|
+
const [year, month, day] = value.split('-').map(Number);
|
|
32
|
+
return { year, month, day };
|
|
33
|
+
}
|
|
34
|
+
function shiftDate(value, days) {
|
|
35
|
+
const parts = dateParts(value);
|
|
36
|
+
const shifted = new Date(Date.UTC(parts.year, parts.month - 1, parts.day + days));
|
|
37
|
+
return calendarDate(shifted.getUTCFullYear(), shifted.getUTCMonth() + 1, shifted.getUTCDate());
|
|
38
|
+
}
|
|
39
|
+
function shiftMonth(value, months) {
|
|
40
|
+
const parts = dateParts(value);
|
|
41
|
+
const shifted = new Date(Date.UTC(parts.year, parts.month - 1 + months, 1));
|
|
42
|
+
return calendarDate(shifted.getUTCFullYear(), shifted.getUTCMonth() + 1, 1);
|
|
43
|
+
}
|
|
44
|
+
function offsetLabel(timezone, padded = false) {
|
|
45
|
+
if (timezone === 'Z')
|
|
46
|
+
return 'UTC';
|
|
47
|
+
if (!padded) {
|
|
48
|
+
const match = /^([+-])0?(\d{1,2}):(\d{2})$/.exec(timezone);
|
|
49
|
+
if (match)
|
|
50
|
+
return `GMT${match[1]}${Number(match[2])}${match[3] === '00' ? '' : `:${match[3]}`}`;
|
|
51
|
+
}
|
|
52
|
+
return `GMT${timezone}`;
|
|
53
|
+
}
|
|
54
|
+
function wallClockDate(parsed) {
|
|
55
|
+
if (parsed.year === undefined || parsed.month === undefined || parsed.day === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
return new Date(Date.UTC(parsed.year, parsed.month - 1, parsed.day, parsed.hour ?? 12, parsed.minute ?? 0, parsed.second ?? 0));
|
|
58
|
+
}
|
|
59
|
+
export function DateValueDisplay({ compact = false, value }) {
|
|
60
|
+
const parsed = parseDateValue(value);
|
|
61
|
+
if (parsed.kind === 'empty')
|
|
62
|
+
return _jsx("span", { className: "italic text-muted-foreground", children: "\u2014" });
|
|
63
|
+
if (parsed.kind === 'invalid')
|
|
64
|
+
return _jsx("span", { className: "break-words", children: String(value) });
|
|
65
|
+
const wall = wallClockDate(parsed);
|
|
66
|
+
if (!wall)
|
|
67
|
+
return _jsx("span", { children: String(value) });
|
|
68
|
+
const date = new Intl.DateTimeFormat(undefined, {
|
|
69
|
+
day: 'numeric',
|
|
70
|
+
month: 'short',
|
|
71
|
+
...(compact && parsed.year === new Date().getFullYear() ? {} : { year: 'numeric' }),
|
|
72
|
+
timeZone: 'UTC',
|
|
73
|
+
}).format(wall);
|
|
74
|
+
if (parsed.kind === 'date')
|
|
75
|
+
return _jsx("span", { title: String(value), children: date });
|
|
76
|
+
const time = new Intl.DateTimeFormat(undefined, {
|
|
77
|
+
hour: 'numeric',
|
|
78
|
+
minute: '2-digit',
|
|
79
|
+
timeZone: 'UTC',
|
|
80
|
+
}).format(wall);
|
|
81
|
+
const timezone = parsed.kind === 'datetime' ? ` ${offsetLabel(parsed.timezone ?? 'Z')}` : '';
|
|
82
|
+
return _jsxs("span", { title: String(value), children: [date, " \u00B7 ", time, timezone] });
|
|
83
|
+
}
|
|
84
|
+
function timezoneChoices(original, local) {
|
|
85
|
+
const choices = new Map();
|
|
86
|
+
if (original)
|
|
87
|
+
choices.set(original, `Original · ${offsetLabel(original, true)}`);
|
|
88
|
+
if (!choices.has(local))
|
|
89
|
+
choices.set(local, `Local · ${offsetLabel(local, true)}`);
|
|
90
|
+
choices.set('Z', 'UTC');
|
|
91
|
+
for (let offset = -14 * 60; offset <= 14 * 60; offset += 15) {
|
|
92
|
+
const value = formatFixedOffset(offset);
|
|
93
|
+
if (!choices.has(value))
|
|
94
|
+
choices.set(value, offsetLabel(value, true));
|
|
95
|
+
}
|
|
96
|
+
return [...choices].map(([value, label]) => ({ value, label }));
|
|
97
|
+
}
|
|
98
|
+
export function DateValueEditor({ descriptor, disabled, error, label, value, onCancel, onClear, onCommit, }) {
|
|
99
|
+
const editorId = useId();
|
|
100
|
+
const popoverRef = useRef(null);
|
|
101
|
+
const parsed = useMemo(() => parseDateValue(value), [value]);
|
|
102
|
+
const today = useMemo(localToday, []);
|
|
103
|
+
const initialDate = parsed.calendarDate ?? '';
|
|
104
|
+
const initialMonth = dateParts(initialDate || today);
|
|
105
|
+
const [open, setOpen] = useState(true);
|
|
106
|
+
const [selectedDate, setSelectedDate] = useState(initialDate);
|
|
107
|
+
const [focusDate, setFocusDate] = useState(initialDate || today);
|
|
108
|
+
const [month, setMonth] = useState({ year: initialMonth.year, month: initialMonth.month });
|
|
109
|
+
const existingHasTime = parsed.kind === 'datetime' || parsed.kind === 'floating-datetime';
|
|
110
|
+
const [includeTime, setIncludeTime] = useState(existingHasTime || (parsed.kind === 'empty' && descriptor.defaultIncludeTime === true));
|
|
111
|
+
const [time, setTime] = useState(existingHasTime ? `${pad(parsed.hour ?? 0)}:${pad(parsed.minute ?? 0)}` : '');
|
|
112
|
+
const [seconds, setSeconds] = useState(existingHasTime ? parsed.second ?? 0 : 0);
|
|
113
|
+
const [fractionalSeconds, setFractionalSeconds] = useState(existingHasTime ? parsed.fractionalSeconds ?? '' : '');
|
|
114
|
+
const defaultTimezone = localOffsetForDate(initialDate || today, time || '12:00');
|
|
115
|
+
const [timezone, setTimezone] = useState(parsed.kind === 'datetime' ? parsed.timezone ?? defaultTimezone : '');
|
|
116
|
+
const [attemptedSave, setAttemptedSave] = useState(false);
|
|
117
|
+
const completed = useRef(false);
|
|
118
|
+
const days = useMemo(() => calendarDays(month.year, month.month), [month]);
|
|
119
|
+
const localTimezone = localOffsetForDate(selectedDate || today, time || '12:00');
|
|
120
|
+
const timezoneOptions = useMemo(() => timezoneChoices(parsed.kind === 'datetime' ? parsed.timezone : undefined, localTimezone), [localTimezone, parsed.kind, parsed.timezone]);
|
|
121
|
+
const serialized = useMemo(() => {
|
|
122
|
+
if (!selectedDate)
|
|
123
|
+
return { value: undefined, error: 'Select a date' };
|
|
124
|
+
try {
|
|
125
|
+
const next = serializeDateEditorValue({
|
|
126
|
+
date: selectedDate,
|
|
127
|
+
includeTime,
|
|
128
|
+
time,
|
|
129
|
+
seconds,
|
|
130
|
+
fractionalSeconds,
|
|
131
|
+
timezone,
|
|
132
|
+
});
|
|
133
|
+
return { value: next, error: validateDateValue(next, descriptor) };
|
|
134
|
+
}
|
|
135
|
+
catch (serializeError) {
|
|
136
|
+
return { value: undefined, error: serializeError instanceof Error ? serializeError.message : 'Enter a valid date' };
|
|
137
|
+
}
|
|
138
|
+
}, [descriptor, fractionalSeconds, includeTime, seconds, selectedDate, time, timezone]);
|
|
139
|
+
const monthLabel = new Intl.DateTimeFormat(undefined, { month: 'long', year: 'numeric', timeZone: 'UTC' })
|
|
140
|
+
.format(new Date(Date.UTC(month.year, month.month - 1, 1)));
|
|
141
|
+
const weekdays = useMemo(() => Array.from({ length: 7 }, (_, index) => (new Intl.DateTimeFormat(undefined, { weekday: 'short', timeZone: 'UTC' })
|
|
142
|
+
.format(new Date(Date.UTC(2024, 0, index + 1))))), []);
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (!open)
|
|
145
|
+
return;
|
|
146
|
+
const timer = window.setTimeout(() => {
|
|
147
|
+
popoverRef.current?.querySelector(`[data-date="${focusDate}"]`)?.focus();
|
|
148
|
+
});
|
|
149
|
+
return () => window.clearTimeout(timer);
|
|
150
|
+
}, [focusDate, open]);
|
|
151
|
+
const cancel = () => {
|
|
152
|
+
completed.current = true;
|
|
153
|
+
setOpen(false);
|
|
154
|
+
onCancel();
|
|
155
|
+
};
|
|
156
|
+
const save = () => {
|
|
157
|
+
setAttemptedSave(true);
|
|
158
|
+
if (!serialized.value || serialized.error || disabled)
|
|
159
|
+
return;
|
|
160
|
+
completed.current = true;
|
|
161
|
+
onCommit(serialized.value);
|
|
162
|
+
};
|
|
163
|
+
const clear = () => {
|
|
164
|
+
if (descriptor.required || disabled)
|
|
165
|
+
return;
|
|
166
|
+
completed.current = true;
|
|
167
|
+
if (onClear)
|
|
168
|
+
onClear();
|
|
169
|
+
else
|
|
170
|
+
onCommit(null);
|
|
171
|
+
};
|
|
172
|
+
const chooseDate = (date) => {
|
|
173
|
+
const parts = dateParts(date);
|
|
174
|
+
setSelectedDate(date);
|
|
175
|
+
setFocusDate(date);
|
|
176
|
+
setMonth({ year: parts.year, month: parts.month });
|
|
177
|
+
};
|
|
178
|
+
const moveFocus = (next) => {
|
|
179
|
+
const parts = dateParts(next);
|
|
180
|
+
setFocusDate(next);
|
|
181
|
+
setMonth({ year: parts.year, month: parts.month });
|
|
182
|
+
};
|
|
183
|
+
const onCalendarKeyDown = (event) => {
|
|
184
|
+
const target = event.target instanceof HTMLElement ? event.target.closest('[data-date]') : null;
|
|
185
|
+
const current = target?.dataset.date;
|
|
186
|
+
if (!current)
|
|
187
|
+
return;
|
|
188
|
+
const weekday = (new Date(`${current}T00:00:00Z`).getUTCDay() + 6) % 7;
|
|
189
|
+
let next;
|
|
190
|
+
if (event.key === 'ArrowLeft')
|
|
191
|
+
next = shiftDate(current, -1);
|
|
192
|
+
else if (event.key === 'ArrowRight')
|
|
193
|
+
next = shiftDate(current, 1);
|
|
194
|
+
else if (event.key === 'ArrowUp')
|
|
195
|
+
next = shiftDate(current, -7);
|
|
196
|
+
else if (event.key === 'ArrowDown')
|
|
197
|
+
next = shiftDate(current, 7);
|
|
198
|
+
else if (event.key === 'Home')
|
|
199
|
+
next = shiftDate(current, -weekday);
|
|
200
|
+
else if (event.key === 'End')
|
|
201
|
+
next = shiftDate(current, 6 - weekday);
|
|
202
|
+
else if (event.key === 'PageUp')
|
|
203
|
+
next = shiftMonth(current, -1);
|
|
204
|
+
else if (event.key === 'PageDown')
|
|
205
|
+
next = shiftMonth(current, 1);
|
|
206
|
+
if (!next)
|
|
207
|
+
return;
|
|
208
|
+
event.preventDefault();
|
|
209
|
+
event.stopPropagation();
|
|
210
|
+
moveFocus(next);
|
|
211
|
+
};
|
|
212
|
+
const changeTimezone = (nextTimezone) => {
|
|
213
|
+
if (includeTime && selectedDate && time && timezone && nextTimezone) {
|
|
214
|
+
try {
|
|
215
|
+
const current = serializeDateEditorValue({ date: selectedDate, includeTime: true, time, seconds, fractionalSeconds, timezone });
|
|
216
|
+
const converted = parseDateValue(convertDateTimeToOffset(current, nextTimezone));
|
|
217
|
+
if (converted.kind === 'datetime') {
|
|
218
|
+
setSelectedDate(converted.calendarDate ?? selectedDate);
|
|
219
|
+
setTime(`${pad(converted.hour ?? 0)}:${pad(converted.minute ?? 0)}`);
|
|
220
|
+
setSeconds(converted.second ?? 0);
|
|
221
|
+
setFractionalSeconds(converted.fractionalSeconds ?? '');
|
|
222
|
+
if (converted.year && converted.month)
|
|
223
|
+
setMonth({ year: converted.year, month: converted.month });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
// An incomplete draft has no instant yet; choosing a timezone simply
|
|
228
|
+
// establishes the offset that will be used when it becomes complete.
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
setTimezone(nextTimezone);
|
|
232
|
+
};
|
|
233
|
+
const visibleError = error ?? (attemptedSave || (selectedDate && serialized.error?.startsWith('Date must')) ? serialized.error : undefined);
|
|
234
|
+
return (_jsxs(DropdownMenu, { open: open, onOpenChange: (nextOpen) => {
|
|
235
|
+
setOpen(nextOpen);
|
|
236
|
+
if (!nextOpen && !completed.current)
|
|
237
|
+
onCancel();
|
|
238
|
+
}, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { type: "button", "data-id": "date-editor-trigger", "aria-label": `Edit ${label}`, className: "flex min-h-7 w-full items-center rounded-sm px-1 text-left text-xs outline-none ring-1 ring-inset ring-ring", children: selectedDate || descriptor.placeholder || 'Select a date' }) }), _jsxs(DropdownMenuContent, { ref: popoverRef, "data-id": "date-editor-popover", align: "start", sideOffset: 6, onOpenAutoFocus: (event) => event.preventDefault(), className: "w-[min(21rem,calc(100vw-1.5rem))] p-3 text-foreground", onKeyDown: (event) => {
|
|
239
|
+
if (event.key === 'Escape') {
|
|
240
|
+
event.preventDefault();
|
|
241
|
+
event.stopPropagation();
|
|
242
|
+
cancel();
|
|
243
|
+
}
|
|
244
|
+
}, children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("strong", { className: "text-sm font-medium", children: monthLabel }), _jsxs("div", { className: "flex gap-1", children: [_jsx("button", { type: "button", "aria-label": "Previous month", className: "grid h-8 w-8 place-items-center rounded-md hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", onClick: () => {
|
|
245
|
+
const next = shiftMonth(calendarDate(month.year, month.month, 1), -1);
|
|
246
|
+
const parts = dateParts(next);
|
|
247
|
+
setMonth({ year: parts.year, month: parts.month });
|
|
248
|
+
setFocusDate(next);
|
|
249
|
+
}, children: _jsx(ChevronLeft, { className: "h-4 w-4" }) }), _jsx("button", { type: "button", "aria-label": "Next month", className: "grid h-8 w-8 place-items-center rounded-md hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", onClick: () => {
|
|
250
|
+
const next = shiftMonth(calendarDate(month.year, month.month, 1), 1);
|
|
251
|
+
const parts = dateParts(next);
|
|
252
|
+
setMonth({ year: parts.year, month: parts.month });
|
|
253
|
+
setFocusDate(next);
|
|
254
|
+
}, children: _jsx(ChevronRight, { className: "h-4 w-4" }) })] })] }), _jsxs("div", { role: "grid", "aria-label": `${monthLabel} calendar`, className: "mt-2 grid grid-cols-7 gap-0.5", onKeyDown: onCalendarKeyDown, children: [_jsx("div", { role: "row", className: "contents", children: weekdays.map((weekday) => _jsx("div", { role: "columnheader", className: "py-1 text-center text-[0.6875rem] font-medium text-muted-foreground", children: weekday }, weekday)) }), Array.from({ length: 6 }, (_, row) => (_jsx("div", { role: "row", className: "contents", children: days.slice(row * 7, row * 7 + 7).map((item) => {
|
|
255
|
+
const selected = item.date === selectedDate;
|
|
256
|
+
const current = item.date === today;
|
|
257
|
+
const readable = new Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeZone: 'UTC' })
|
|
258
|
+
.format(new Date(`${item.date}T00:00:00Z`));
|
|
259
|
+
return (_jsx("div", { role: "gridcell", "aria-selected": selected, className: "grid place-items-center", children: _jsx("button", { type: "button", "data-date": item.date, "aria-label": `Choose ${readable}`, "aria-current": current ? 'date' : undefined, tabIndex: item.date === focusDate ? 0 : -1, className: cn('grid h-8 w-8 place-items-center rounded-md text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring', item.inMonth ? 'text-foreground' : 'text-muted-foreground/55', selected ? 'bg-primary font-semibold text-primary-foreground' : 'hover:bg-accent', current && !selected && 'ring-1 ring-inset ring-ring'), onClick: () => chooseDate(item.date), children: item.day }) }, item.date));
|
|
260
|
+
}) }, row)))] }), _jsxs("div", { className: "mt-3 border-t border-border pt-3", children: [_jsxs("label", { className: "flex min-h-8 items-center gap-2 text-xs font-medium", children: [_jsx("input", { type: "checkbox", "aria-label": "Include time", checked: includeTime, disabled: disabled, className: "h-4 w-4 accent-primary", onChange: (event) => setIncludeTime(event.target.checked) }), "Include time"] }), includeTime && (_jsxs("div", { className: "mt-2 grid grid-cols-[5rem_minmax(0,1fr)] items-center gap-2 text-xs", children: [_jsx("label", { htmlFor: `${editorId}-time`, className: "text-muted-foreground", children: "Time" }), _jsx("input", { id: `${editorId}-time`, type: "time", "aria-label": "Time", value: time, disabled: disabled, className: "h-8 rounded-md border border-border bg-background px-2 outline-none focus-visible:ring-2 focus-visible:ring-ring", onInput: (event) => {
|
|
261
|
+
setTime(event.currentTarget.value);
|
|
262
|
+
setSeconds(0);
|
|
263
|
+
setFractionalSeconds('');
|
|
264
|
+
} }), _jsx("span", { className: "text-muted-foreground", children: "Timezone" }), _jsx(MenuSelect, { ariaLabel: "Timezone", disabled: disabled, className: "min-w-0", emptyOptionLabel: "No timezone \u00B7 floating time", options: timezoneOptions, value: timezone, onChange: changeTimezone })] })), !includeTime && existingHasTime && (_jsxs("p", { className: "mt-2 flex items-start gap-1.5 text-[0.6875rem] text-amber-700 dark:text-amber-300", children: [_jsx(AlertTriangle, { className: "mt-0.5 h-3 w-3 shrink-0" }), " Saving will remove the stored time."] })), visibleError && _jsx("p", { role: "alert", className: "mt-2 text-[0.6875rem] text-destructive", children: visibleError })] }), _jsxs("footer", { className: "mt-3 flex items-center gap-1 border-t border-border pt-2", children: [_jsx("button", { type: "button", "data-id": "date-editor-clear", disabled: descriptor.required || disabled, className: "h-8 rounded-md px-2 text-xs text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-40", onClick: clear, children: "Clear" }), _jsx("span", { className: "flex-1" }), _jsx("button", { type: "button", "data-id": "date-editor-cancel", className: "h-8 rounded-md px-3 text-xs text-muted-foreground hover:bg-accent hover:text-foreground", onClick: cancel, children: "Cancel" }), _jsx("button", { type: "button", "data-id": "date-editor-save", disabled: disabled || !serialized.value || Boolean(serialized.error), className: "h-8 rounded-md bg-primary px-3 text-xs font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-40", onClick: save, children: "Save" })] })] })] }));
|
|
265
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ChoiceControlProfile } from '../primitives/choice-control.js';
|
|
2
|
+
import type { WidgetProps } from './types.js';
|
|
3
|
+
export interface DropdownWidgetProps extends WidgetProps {
|
|
4
|
+
profile?: Extract<ChoiceControlProfile, 'pill' | 'pill-cell'>;
|
|
5
|
+
creatable?: boolean;
|
|
6
|
+
openOnMount?: boolean;
|
|
7
|
+
onDismiss?: () => void;
|
|
8
|
+
onOptionColorChange?: (optionValue: string, color: string) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function DropdownWidget({ value, onChange, onBlur, ariaLabel, ariaDescribedBy, invalid, options: optionOverrides, emptyOptionLabel, profile, creatable, openOnMount, onDismiss, onOptionColorChange, readOnly, disabled, uiHint, }: DropdownWidgetProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDown } from 'lucide-react';
|
|
3
|
+
import { ChoiceControl } from '../primitives/choice-control.js';
|
|
4
|
+
import { cn } from '../lib/cn.js';
|
|
5
|
+
import { READONLY_FIELD_CLASSES, ReadOnlyEmpty } from './read-only-field.js';
|
|
6
|
+
export function DropdownWidget({ value, onChange, onBlur, ariaLabel, ariaDescribedBy, invalid = false, options: optionOverrides, emptyOptionLabel, profile, creatable = false, openOnMount, onDismiss, onOptionColorChange, readOnly = false, disabled = false, uiHint, }) {
|
|
7
|
+
const options = optionOverrides ?? (uiHint && 'options' in uiHint && Array.isArray(uiHint.options)
|
|
8
|
+
? uiHint.options
|
|
9
|
+
: []);
|
|
10
|
+
const normalizedOptions = options.map((option) => typeof option === 'object'
|
|
11
|
+
? option
|
|
12
|
+
: { value: String(option), label: String(option) });
|
|
13
|
+
if (readOnly) {
|
|
14
|
+
const match = options.find((option) => (typeof option === 'object' ? option.value : String(option)) === value);
|
|
15
|
+
const label = typeof match === 'object' ? match.label : match ?? value;
|
|
16
|
+
return (_jsxs("div", { className: "relative w-full", "aria-readonly": "true", children: [_jsx("div", { className: cn(READONLY_FIELD_CLASSES, 'truncate pr-9'), children: label ? label : _jsx(ReadOnlyEmpty, {}) }), _jsx(ChevronDown, { "aria-hidden": true, className: "pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground opacity-50" })] }));
|
|
17
|
+
}
|
|
18
|
+
return (_jsx(ChoiceControl, { mode: "single", profile: profile ?? 'menu', "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, invalid: invalid, creatable: creatable, openOnMount: openOnMount, onDismiss: onDismiss, value: value ? [value] : [], onValueChange: (next) => onChange(next[0] ?? ''), onSelectionBlur: onBlur, disabled: disabled, emptyOptionLabel: emptyOptionLabel, options: normalizedOptions, onOptionColorChange: onOptionColorChange }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ChoiceControlProfile } from '../primitives/choice-control.js';
|
|
2
|
+
import type { WidgetProps } from './types.js';
|
|
3
|
+
export interface MultiSelectWidgetProps extends WidgetProps {
|
|
4
|
+
onDirectSave?: (value: unknown) => void;
|
|
5
|
+
profile?: ChoiceControlProfile;
|
|
6
|
+
creatable?: boolean;
|
|
7
|
+
openOnMount?: boolean;
|
|
8
|
+
onDismiss?: () => void;
|
|
9
|
+
onOptionColorChange?: (optionValue: string, color: string) => void;
|
|
10
|
+
}
|
|
11
|
+
export declare function MultiSelectWidget({ value, onChange, onDirectSave, onBlur, ariaLabel, ariaDescribedBy, invalid, options: optionOverrides, profile, creatable, openOnMount, onDismiss, onOptionColorChange, readOnly, disabled, uiHint, }: MultiSelectWidgetProps): import("react").JSX.Element;
|