@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,24 @@
|
|
|
1
|
+
export async function copyTextToClipboard(value) {
|
|
2
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
|
|
3
|
+
try {
|
|
4
|
+
await navigator.clipboard.writeText(value);
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
catch { /* Fall back when the browser exposes but denies the Clipboard API. */ }
|
|
8
|
+
}
|
|
9
|
+
if (typeof document === 'undefined')
|
|
10
|
+
throw new Error('Clipboard access is unavailable');
|
|
11
|
+
const textarea = document.createElement('textarea');
|
|
12
|
+
textarea.value = value;
|
|
13
|
+
textarea.style.position = 'fixed';
|
|
14
|
+
textarea.style.opacity = '0';
|
|
15
|
+
document.body.appendChild(textarea);
|
|
16
|
+
textarea.select();
|
|
17
|
+
try {
|
|
18
|
+
if (!document.execCommand('copy'))
|
|
19
|
+
throw new Error('Clipboard access was denied');
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
textarea.remove();
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/lib/cn.d.ts
ADDED
package/dist/lib/cn.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a value as an ISO-8601 datetime string. Returns the epoch millis, or
|
|
3
|
+
* null when the value isn't a string in ISO datetime shape.
|
|
4
|
+
*/
|
|
5
|
+
export declare function parseIsoDateString(value: unknown): number | null;
|
|
6
|
+
/**
|
|
7
|
+
* Format an epoch-millis timestamp as a readable local date-time, e.g.
|
|
8
|
+
* "Jul 5, 2026, 1:05 PM" (locale-dependent).
|
|
9
|
+
*/
|
|
10
|
+
export declare function formatAbsoluteTimestamp(timestamp: number): string;
|
|
11
|
+
/** Compact relative form: "just now", "12s ago", "2m ago", "3h ago", "5d ago",
|
|
12
|
+
* else a local date. Sub-minute rendering is per-surface: the default
|
|
13
|
+
* `'seconds'` shows "12s ago" (with "just now" under 5s) for surfaces where
|
|
14
|
+
* events land moments apart (e.g. a trigger's run history);
|
|
15
|
+
* `'just-now'` collapses everything under a minute for surfaces where
|
|
16
|
+
* second-level churn is noise (e.g. file modification times). */
|
|
17
|
+
export declare function formatRelativeTimeShort(timestamp: number, { subMinute }?: {
|
|
18
|
+
subMinute?: 'seconds' | 'just-now';
|
|
19
|
+
}): string;
|
|
20
|
+
/**
|
|
21
|
+
* Short relative form ("2m ago") for past timestamps, null for future ones —
|
|
22
|
+
* "ago" phrasing makes no sense for a time that hasn't happened yet.
|
|
23
|
+
*/
|
|
24
|
+
export declare function formatRelativeTimeShortIfPast(timestamp: number): string | null;
|
package/dist/lib/time.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ISO-8601 datetime shape: `2026-07-05T13:05:56Z`, with optional fractional
|
|
3
|
+
* seconds and either a `Z` or `±HH[:]MM` offset (a timezone-less string parses
|
|
4
|
+
* as local time). Date-only strings and epoch numbers are deliberately NOT
|
|
5
|
+
* matched — bare `2026-07-05` or a 13-digit integer could just as well be an
|
|
6
|
+
* identifier, and misformatting data is worse than leaving it raw.
|
|
7
|
+
*/
|
|
8
|
+
const ISO_DATETIME_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2}(\.\d{1,9})?)?(Z|[+-]\d{2}:?\d{2})?$/;
|
|
9
|
+
/**
|
|
10
|
+
* Parse a value as an ISO-8601 datetime string. Returns the epoch millis, or
|
|
11
|
+
* null when the value isn't a string in ISO datetime shape.
|
|
12
|
+
*/
|
|
13
|
+
export function parseIsoDateString(value) {
|
|
14
|
+
if (typeof value !== 'string' || !ISO_DATETIME_RE.test(value))
|
|
15
|
+
return null;
|
|
16
|
+
const ms = Date.parse(value);
|
|
17
|
+
return Number.isNaN(ms) ? null : ms;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Format an epoch-millis timestamp as a readable local date-time, e.g.
|
|
21
|
+
* "Jul 5, 2026, 1:05 PM" (locale-dependent).
|
|
22
|
+
*/
|
|
23
|
+
export function formatAbsoluteTimestamp(timestamp) {
|
|
24
|
+
return new Date(timestamp).toLocaleString(undefined, {
|
|
25
|
+
dateStyle: 'medium',
|
|
26
|
+
timeStyle: 'short',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Compact relative form: "just now", "12s ago", "2m ago", "3h ago", "5d ago",
|
|
30
|
+
* else a local date. Sub-minute rendering is per-surface: the default
|
|
31
|
+
* `'seconds'` shows "12s ago" (with "just now" under 5s) for surfaces where
|
|
32
|
+
* events land moments apart (e.g. a trigger's run history);
|
|
33
|
+
* `'just-now'` collapses everything under a minute for surfaces where
|
|
34
|
+
* second-level churn is noise (e.g. file modification times). */
|
|
35
|
+
export function formatRelativeTimeShort(timestamp, { subMinute = 'seconds' } = {}) {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
const diffSeconds = Math.floor((now - timestamp) / 1000);
|
|
38
|
+
const diffMinutes = Math.floor(diffSeconds / 60);
|
|
39
|
+
const diffHours = Math.floor(diffMinutes / 60);
|
|
40
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
41
|
+
if (diffSeconds < 60) {
|
|
42
|
+
if (subMinute === 'just-now' || diffSeconds < 5)
|
|
43
|
+
return 'just now';
|
|
44
|
+
return `${diffSeconds}s ago`;
|
|
45
|
+
}
|
|
46
|
+
if (diffMinutes < 60)
|
|
47
|
+
return `${diffMinutes}m ago`;
|
|
48
|
+
if (diffHours < 24)
|
|
49
|
+
return `${diffHours}h ago`;
|
|
50
|
+
if (diffDays < 7)
|
|
51
|
+
return `${diffDays}d ago`;
|
|
52
|
+
return new Date(timestamp).toLocaleDateString();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Short relative form ("2m ago") for past timestamps, null for future ones —
|
|
56
|
+
* "ago" phrasing makes no sense for a time that hasn't happened yet.
|
|
57
|
+
*/
|
|
58
|
+
export function formatRelativeTimeShortIfPast(timestamp) {
|
|
59
|
+
return timestamp <= Date.now() ? formatRelativeTimeShort(timestamp) : null;
|
|
60
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-wide UI size: the three stops a user can pick in Settings → Appearance.
|
|
3
|
+
*
|
|
4
|
+
* The CSS side lives in `src/tokens/ui-size.css`, which keys the root
|
|
5
|
+
* font-size off a `data-ui-size` attribute on <html> — that cascade is what
|
|
6
|
+
* scales the whole rem-based UI. This module is the JS mirror: the stop names,
|
|
7
|
+
* the attribute contract, and the numeric multiplier for consumers that need
|
|
8
|
+
* the scale outside CSS (e.g. virtualized row heights). Keep in sync with the
|
|
9
|
+
* token file.
|
|
10
|
+
*/
|
|
11
|
+
export declare const UI_SIZES: readonly ["small", "medium", "large"];
|
|
12
|
+
export type UiSize = (typeof UI_SIZES)[number];
|
|
13
|
+
/** Attribute on <html> that `tokens/ui-size.css` keys the root font-size off. */
|
|
14
|
+
export declare const UI_SIZE_ATTRIBUTE = "data-ui-size";
|
|
15
|
+
/** Root font-size multiplier per stop — mirrors `tokens/ui-size.css`. */
|
|
16
|
+
export declare const UI_SIZE_SCALE: Record<UiSize, number>;
|
|
17
|
+
/**
|
|
18
|
+
* Explorer floor width per stop, in px — the JS mirror of the
|
|
19
|
+
* `--explorer-sidebar-min-width` custom property in `tokens/ui-size.css`
|
|
20
|
+
* (which carries the same numbers for CSS-side pins). Kept as its own stop
|
|
21
|
+
* table rather than `UI_SIZE_SCALE × a base`, because the explorer's tab row
|
|
22
|
+
* grows more slowly than the root font-size: each value is that row's
|
|
23
|
+
* measured content plus a few px, so its trailing controls stay snug against
|
|
24
|
+
* the tabs at every stop. Consumed by hosts whose sizing API takes numbers
|
|
25
|
+
* (react-resizable-panels' Panel props). Keep in sync with the token file.
|
|
26
|
+
*/
|
|
27
|
+
export declare const EXPLORER_SIDEBAR_MIN_WIDTH_PX: Record<UiSize, number>;
|
|
28
|
+
export declare function isUiSize(value: unknown): value is UiSize;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App-wide UI size: the three stops a user can pick in Settings → Appearance.
|
|
3
|
+
*
|
|
4
|
+
* The CSS side lives in `src/tokens/ui-size.css`, which keys the root
|
|
5
|
+
* font-size off a `data-ui-size` attribute on <html> — that cascade is what
|
|
6
|
+
* scales the whole rem-based UI. This module is the JS mirror: the stop names,
|
|
7
|
+
* the attribute contract, and the numeric multiplier for consumers that need
|
|
8
|
+
* the scale outside CSS (e.g. virtualized row heights). Keep in sync with the
|
|
9
|
+
* token file.
|
|
10
|
+
*/
|
|
11
|
+
export const UI_SIZES = ['small', 'medium', 'large'];
|
|
12
|
+
/** Attribute on <html> that `tokens/ui-size.css` keys the root font-size off. */
|
|
13
|
+
export const UI_SIZE_ATTRIBUTE = 'data-ui-size';
|
|
14
|
+
/** Root font-size multiplier per stop — mirrors `tokens/ui-size.css`. */
|
|
15
|
+
export const UI_SIZE_SCALE = {
|
|
16
|
+
small: 1,
|
|
17
|
+
medium: 1.125,
|
|
18
|
+
large: 1.25,
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Explorer floor width per stop, in px — the JS mirror of the
|
|
22
|
+
* `--explorer-sidebar-min-width` custom property in `tokens/ui-size.css`
|
|
23
|
+
* (which carries the same numbers for CSS-side pins). Kept as its own stop
|
|
24
|
+
* table rather than `UI_SIZE_SCALE × a base`, because the explorer's tab row
|
|
25
|
+
* grows more slowly than the root font-size: each value is that row's
|
|
26
|
+
* measured content plus a few px, so its trailing controls stay snug against
|
|
27
|
+
* the tabs at every stop. Consumed by hosts whose sizing API takes numbers
|
|
28
|
+
* (react-resizable-panels' Panel props). Keep in sync with the token file.
|
|
29
|
+
*/
|
|
30
|
+
export const EXPLORER_SIDEBAR_MIN_WIDTH_PX = {
|
|
31
|
+
small: 270,
|
|
32
|
+
medium: 294,
|
|
33
|
+
// 318 + 0.5rem (10px at this stop's 20px root), matching the token file's
|
|
34
|
+
// calc(): `large` needs the extra breathing room the others don't.
|
|
35
|
+
large: 328,
|
|
36
|
+
};
|
|
37
|
+
export function isUiSize(value) {
|
|
38
|
+
return typeof value === 'string' && UI_SIZES.includes(value);
|
|
39
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type CSSProperties, type RefObject } from 'react';
|
|
2
|
+
/** Fixed popovers use viewport coordinates and stay within its visible edges. */
|
|
3
|
+
export declare function useAnchoredPosition(enabled: boolean, anchorRef: RefObject<HTMLElement | null>, contentRef: RefObject<HTMLElement | null>, side?: 'bottom' | 'right', align?: 'start' | 'end',
|
|
4
|
+
/** Floors the content at the anchor's width, for an editor that replaces it. */
|
|
5
|
+
matchAnchorWidth?: boolean): CSSProperties;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useState } from 'react';
|
|
2
|
+
const useBrowserLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
3
|
+
const PADDING = 8;
|
|
4
|
+
const GAP = 4;
|
|
5
|
+
/** Fixed popovers use viewport coordinates and stay within its visible edges. */
|
|
6
|
+
export function useAnchoredPosition(enabled, anchorRef, contentRef, side = 'bottom', align = 'start',
|
|
7
|
+
/** Floors the content at the anchor's width, for an editor that replaces it. */
|
|
8
|
+
matchAnchorWidth = false) {
|
|
9
|
+
const [position, setPosition] = useState();
|
|
10
|
+
const update = () => {
|
|
11
|
+
const anchor = anchorRef.current;
|
|
12
|
+
const content = contentRef.current;
|
|
13
|
+
if (!enabled || !anchor || !content)
|
|
14
|
+
return;
|
|
15
|
+
const local = anchor.getBoundingClientRect();
|
|
16
|
+
const frame = anchor.ownerDocument.defaultView?.frameElement;
|
|
17
|
+
const offset = frame?.getBoundingClientRect();
|
|
18
|
+
const bounds = { left: local.left + (offset?.left ?? 0), right: local.right + (offset?.left ?? 0), top: local.top + (offset?.top ?? 0), bottom: local.bottom + (offset?.top ?? 0) };
|
|
19
|
+
const size = content.getBoundingClientRect();
|
|
20
|
+
const width = Math.min(size.width, window.innerWidth - PADDING * 2);
|
|
21
|
+
const height = Math.min(size.height, window.innerHeight - PADDING * 2);
|
|
22
|
+
const fitsBelow = bounds.bottom + GAP + height <= window.innerHeight - PADDING;
|
|
23
|
+
const fitsRight = bounds.right + GAP + width <= window.innerWidth - PADDING;
|
|
24
|
+
const left = side === 'right'
|
|
25
|
+
? fitsRight ? bounds.right + GAP : bounds.left - width - GAP
|
|
26
|
+
: align === 'end' ? bounds.right - width : bounds.left;
|
|
27
|
+
const top = side === 'bottom'
|
|
28
|
+
? fitsBelow ? bounds.bottom + GAP : bounds.top - height - GAP
|
|
29
|
+
: bounds.top;
|
|
30
|
+
const next = {
|
|
31
|
+
left: Math.max(PADDING, Math.min(left, window.innerWidth - width - PADDING)),
|
|
32
|
+
top: Math.max(PADDING, Math.min(top, window.innerHeight - height - PADDING)),
|
|
33
|
+
minWidth: matchAnchorWidth ? Math.min(bounds.right - bounds.left, window.innerWidth - PADDING * 2) : undefined,
|
|
34
|
+
};
|
|
35
|
+
setPosition((current) => current?.left === next.left && current.top === next.top && current.minWidth === next.minWidth ? current : next);
|
|
36
|
+
};
|
|
37
|
+
useBrowserLayoutEffect(update);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!enabled)
|
|
40
|
+
return;
|
|
41
|
+
const observer = typeof ResizeObserver === 'undefined' ? undefined : new ResizeObserver(update);
|
|
42
|
+
if (anchorRef.current)
|
|
43
|
+
observer?.observe(anchorRef.current);
|
|
44
|
+
if (contentRef.current)
|
|
45
|
+
observer?.observe(contentRef.current);
|
|
46
|
+
anchorRef.current?.ownerDocument.addEventListener('scroll', update, true);
|
|
47
|
+
window.addEventListener('resize', update);
|
|
48
|
+
window.addEventListener('scroll', update, true);
|
|
49
|
+
return () => {
|
|
50
|
+
anchorRef.current?.ownerDocument.removeEventListener('scroll', update, true);
|
|
51
|
+
observer?.disconnect();
|
|
52
|
+
window.removeEventListener('resize', update);
|
|
53
|
+
window.removeEventListener('scroll', update, true);
|
|
54
|
+
};
|
|
55
|
+
}, [enabled, anchorRef, contentRef, side, align, matchAnchorWidth]);
|
|
56
|
+
return {
|
|
57
|
+
position: 'fixed',
|
|
58
|
+
left: position?.left ?? 0,
|
|
59
|
+
top: position?.top ?? 0,
|
|
60
|
+
minWidth: position?.minWidth,
|
|
61
|
+
maxWidth: 'calc(100vw - 16px)',
|
|
62
|
+
maxHeight: 'calc(100vh - 16px)',
|
|
63
|
+
overflowY: 'auto',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
export interface UseDismissOptions {
|
|
3
|
+
/** When false the listeners are detached (the popup is closed). */
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
/** Called on Escape or a click outside every `insideRefs` subtree. */
|
|
6
|
+
onDismiss: () => void;
|
|
7
|
+
/** Elements whose subtree counts as "inside" — a click within any of them
|
|
8
|
+
* (the popup itself, its anchor row, etc.) does not dismiss. Read live at
|
|
9
|
+
* event time, so the array identity may change between renders without
|
|
10
|
+
* re-subscribing the listeners. */
|
|
11
|
+
insideRefs: Array<RefObject<HTMLElement | null>>;
|
|
12
|
+
/** Delay (ms) before arming click-outside, so the click that opened the popup
|
|
13
|
+
* doesn't immediately dismiss it. Only needed when the opening click lands
|
|
14
|
+
* outside every `insideRefs` subtree. @default 0 */
|
|
15
|
+
armDelayMs?: number;
|
|
16
|
+
/** Also treat clicks inside any portalled `[data-anchored-popup]` layer as
|
|
17
|
+
* inside — for popups that themselves contain nested `AnchoredPopup`s (e.g.
|
|
18
|
+
* a widget dropdown), which render at `document.body` and so escape the
|
|
19
|
+
* `insideRefs` subtree checks. @default false */
|
|
20
|
+
ignoreAnchoredPopups?: boolean;
|
|
21
|
+
/** Host-owned layers that should not dismiss this popup. */
|
|
22
|
+
ignoreSelectors?: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Click-outside + Escape dismissal for portalled popups. Treats Radix popper
|
|
26
|
+
* content (menus, tooltips) — which renders at `document.body`, outside any
|
|
27
|
+
* anchor subtree — as inside, so interacting with a popup's own menu never
|
|
28
|
+
* dismisses it. Shared by `AnchoredPopup` and the inputs-table inline editor so
|
|
29
|
+
* dismissal behaves identically across portalled surfaces.
|
|
30
|
+
*/
|
|
31
|
+
export declare function useDismiss({ enabled, onDismiss, insideRefs, armDelayMs, ignoreAnchoredPopups, ignoreSelectors, }: UseDismissOptions): void;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect, useRef } from 'react';
|
|
2
|
+
import { FLOATING_PORTAL_SELECTORS } from './floating-portal-selectors.js';
|
|
3
|
+
const useBrowserLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
4
|
+
/**
|
|
5
|
+
* Click-outside + Escape dismissal for portalled popups. Treats Radix popper
|
|
6
|
+
* content (menus, tooltips) — which renders at `document.body`, outside any
|
|
7
|
+
* anchor subtree — as inside, so interacting with a popup's own menu never
|
|
8
|
+
* dismisses it. Shared by `AnchoredPopup` and the inputs-table inline editor so
|
|
9
|
+
* dismissal behaves identically across portalled surfaces.
|
|
10
|
+
*/
|
|
11
|
+
export function useDismiss({ enabled, onDismiss, insideRefs, armDelayMs = 0, ignoreAnchoredPopups = false, ignoreSelectors = [], }) {
|
|
12
|
+
// Read `insideRefs` and `onDismiss` live inside the handler so unstable
|
|
13
|
+
// identities each render (a fresh refs array, an inline `onDismiss` closure)
|
|
14
|
+
// don't force the listeners to re-subscribe. Re-subscribing would also clear
|
|
15
|
+
// and restart the arming timer every render, so with `armDelayMs > 0` rapid
|
|
16
|
+
// renders (typing, hovering) could keep `armed` false forever and break
|
|
17
|
+
// click-outside dismissal. Synced in a layout effect — before the browser
|
|
18
|
+
// dispatches the next mousedown, so a click landing between commit and this
|
|
19
|
+
// sync reads the current refs rather than the previous render's. Never
|
|
20
|
+
// written during render, and read only inside the event handlers below.
|
|
21
|
+
const insideRefsRef = useRef(insideRefs);
|
|
22
|
+
const onDismissRef = useRef(onDismiss);
|
|
23
|
+
useBrowserLayoutEffect(() => {
|
|
24
|
+
insideRefsRef.current = insideRefs;
|
|
25
|
+
onDismissRef.current = onDismiss;
|
|
26
|
+
});
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!enabled)
|
|
29
|
+
return;
|
|
30
|
+
let armed = armDelayMs === 0;
|
|
31
|
+
const handlePointerDown = (event) => {
|
|
32
|
+
if (!armed)
|
|
33
|
+
return;
|
|
34
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
35
|
+
if (!target)
|
|
36
|
+
return;
|
|
37
|
+
if (insideRefsRef.current.some((ref) => ref.current?.contains(target)))
|
|
38
|
+
return;
|
|
39
|
+
if (target.closest(FLOATING_PORTAL_SELECTORS.radixPopper))
|
|
40
|
+
return;
|
|
41
|
+
if (ignoreAnchoredPopups && target.closest(FLOATING_PORTAL_SELECTORS.anchoredPopup))
|
|
42
|
+
return;
|
|
43
|
+
if (ignoreSelectors.some((selector) => target.closest(selector)))
|
|
44
|
+
return;
|
|
45
|
+
onDismissRef.current();
|
|
46
|
+
};
|
|
47
|
+
const handleKeyDown = (event) => {
|
|
48
|
+
if (event.key === 'Escape')
|
|
49
|
+
onDismissRef.current();
|
|
50
|
+
};
|
|
51
|
+
const timerId = armDelayMs > 0
|
|
52
|
+
? window.setTimeout(() => {
|
|
53
|
+
armed = true;
|
|
54
|
+
}, armDelayMs)
|
|
55
|
+
: undefined;
|
|
56
|
+
document.addEventListener('mousedown', handlePointerDown);
|
|
57
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
58
|
+
return () => {
|
|
59
|
+
if (timerId !== undefined)
|
|
60
|
+
window.clearTimeout(timerId);
|
|
61
|
+
document.removeEventListener('mousedown', handlePointerDown);
|
|
62
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
63
|
+
};
|
|
64
|
+
}, [enabled, armDelayMs, ignoreAnchoredPopups, ignoreSelectors.join('\n')]);
|
|
65
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface BooleanToggleProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
3
|
+
value: boolean;
|
|
4
|
+
onChange: (next: boolean) => void;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
trueLabel?: string;
|
|
7
|
+
falseLabel?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const BooleanToggle: React.ForwardRefExoticComponent<BooleanToggleProps & React.RefAttributes<HTMLDivElement>>;
|
|
10
|
+
export { BooleanToggle };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { cn } from '../lib/cn.js';
|
|
4
|
+
const BooleanToggle = React.forwardRef(({ className, value, onChange, disabled = false, trueLabel = 'True', falseLabel = 'False', ...props }, ref) => {
|
|
5
|
+
const buttonClass = (selected) => cn('rounded-md border px-2.5 py-1 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-50', selected
|
|
6
|
+
? 'border-primary bg-primary text-primary-foreground'
|
|
7
|
+
: 'border-border bg-card text-foreground hover:bg-accent');
|
|
8
|
+
return (_jsxs("div", { ref: ref, className: cn('flex items-center gap-2', className), ...props, children: [_jsx("button", { type: "button", onClick: () => onChange(true), disabled: disabled, "aria-pressed": value, className: buttonClass(value), children: trueLabel }), _jsx("button", { type: "button", onClick: () => onChange(false), disabled: disabled, "aria-pressed": !value, className: buttonClass(!value), children: falseLabel })] }));
|
|
9
|
+
});
|
|
10
|
+
BooleanToggle.displayName = 'BooleanToggle';
|
|
11
|
+
export { BooleanToggle };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { type VariantProps } from 'class-variance-authority';
|
|
3
|
+
declare const buttonVariants: (props?: ({
|
|
4
|
+
variant?: "link" | "destructive" | "default" | "outline" | "secondary" | "ghost" | null | undefined;
|
|
5
|
+
size?: "default" | "icon" | "sm" | "lg" | null | undefined;
|
|
6
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
|
8
|
+
asChild?: boolean;
|
|
9
|
+
}
|
|
10
|
+
declare const Button: React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement>>;
|
|
11
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
4
|
+
import { cva } from 'class-variance-authority';
|
|
5
|
+
import { cn } from '../lib/cn.js';
|
|
6
|
+
const buttonVariants = cva('inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', {
|
|
7
|
+
variants: {
|
|
8
|
+
variant: {
|
|
9
|
+
default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
|
10
|
+
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
|
11
|
+
outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
|
12
|
+
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
|
13
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
14
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
15
|
+
},
|
|
16
|
+
size: {
|
|
17
|
+
default: 'h-9 px-4 py-2',
|
|
18
|
+
sm: 'h-8 rounded-md px-3 text-xs',
|
|
19
|
+
lg: 'h-10 rounded-md px-8',
|
|
20
|
+
icon: 'h-9 w-9',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultVariants: {
|
|
24
|
+
variant: 'default',
|
|
25
|
+
size: 'default',
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
29
|
+
const Comp = asChild ? Slot : 'button';
|
|
30
|
+
return (_jsx(Comp, { className: cn(buttonVariants({ variant, size, className })), ref: ref, ...props }));
|
|
31
|
+
});
|
|
32
|
+
Button.displayName = 'Button';
|
|
33
|
+
// eslint-disable-next-line react-refresh/only-export-components
|
|
34
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
|
3
|
+
declare const Checkbox: React.ForwardRefExoticComponent<Omit<CheckboxPrimitive.CheckboxProps & React.RefAttributes<HTMLButtonElement>, "ref"> & React.RefAttributes<HTMLButtonElement>>;
|
|
4
|
+
export { Checkbox };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
|
|
4
|
+
import { Check } from 'lucide-react';
|
|
5
|
+
import { cn } from '../lib/cn.js';
|
|
6
|
+
const Checkbox = React.forwardRef(({ className, ...props }, ref) => (_jsx(CheckboxPrimitive.Root, { ref: ref, className: cn('peer h-4 w-4 shrink-0 rounded-sm border border-border focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground', className), ...props, children: _jsx(CheckboxPrimitive.Indicator, { className: cn('flex items-center justify-center text-current'), children: _jsx(Check, { className: "h-3 w-3" }) }) })));
|
|
7
|
+
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
|
8
|
+
export { Checkbox };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
export interface ChoiceOption {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
color?: string;
|
|
6
|
+
}
|
|
7
|
+
export type ChoiceControlMode = 'single' | 'multiple';
|
|
8
|
+
export type ChoiceControlProfile = 'plain' | 'pill' | 'pill-cell' | 'menu';
|
|
9
|
+
export interface ChoiceControlProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
|
10
|
+
mode: ChoiceControlMode;
|
|
11
|
+
value: string[];
|
|
12
|
+
onValueChange: (next: string[]) => void;
|
|
13
|
+
options: ReadonlyArray<ChoiceOption>;
|
|
14
|
+
profile?: ChoiceControlProfile;
|
|
15
|
+
creatable?: boolean;
|
|
16
|
+
emptyOptionLabel?: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
readOnly?: boolean;
|
|
19
|
+
invalid?: boolean;
|
|
20
|
+
openOnMount?: boolean;
|
|
21
|
+
/** Lets a menu live inside another dismissible surface without blocking
|
|
22
|
+
* pointer events to that surface. Defaults to true for inline cell editors. */
|
|
23
|
+
nonModal?: boolean;
|
|
24
|
+
/** Ends an edit when the menu is dismissed, independently of saving a choice. */
|
|
25
|
+
onDismiss?: () => void;
|
|
26
|
+
onSelectionBlur?: () => void;
|
|
27
|
+
onOptionColorChange?: (optionValue: string, color: string) => void;
|
|
28
|
+
}
|
|
29
|
+
export declare const ChoiceControl: React.ForwardRefExoticComponent<ChoiceControlProps & React.RefAttributes<HTMLDivElement>>;
|