@delightstack/components 0.1.0
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 +136 -0
- package/SKILL.md +149 -0
- package/bin/agents.js +63 -0
- package/dist/actions/Alert.svelte +202 -0
- package/dist/actions/Alert.svelte.d.ts +36 -0
- package/dist/actions/Alert.svelte.d.ts.map +1 -0
- package/dist/actions/Button.svelte +1450 -0
- package/dist/actions/Button.svelte.d.ts +56 -0
- package/dist/actions/Button.svelte.d.ts.map +1 -0
- package/dist/actions/ButtonGroup.svelte +111 -0
- package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
- package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
- package/dist/actions/CommandPalette.svelte +939 -0
- package/dist/actions/CommandPalette.svelte.d.ts +37 -0
- package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
- package/dist/actions/ContextMenu.svelte +138 -0
- package/dist/actions/ContextMenu.svelte.d.ts +54 -0
- package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
- package/dist/actions/Modal.svelte +474 -0
- package/dist/actions/Modal.svelte.d.ts +28 -0
- package/dist/actions/Modal.svelte.d.ts.map +1 -0
- package/dist/actions/Popover.svelte +1214 -0
- package/dist/actions/Popover.svelte.d.ts +31 -0
- package/dist/actions/Popover.svelte.d.ts.map +1 -0
- package/dist/actions/Portal.svelte +80 -0
- package/dist/actions/Portal.svelte.d.ts +17 -0
- package/dist/actions/Portal.svelte.d.ts.map +1 -0
- package/dist/actions/ThemeToggle.svelte +345 -0
- package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
- package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
- package/dist/actions/index.d.ts +13 -0
- package/dist/actions/index.d.ts.map +1 -0
- package/dist/actions/index.js +10 -0
- package/dist/actions/scrollbar.d.ts +48 -0
- package/dist/actions/scrollbar.d.ts.map +1 -0
- package/dist/actions/scrollbar.js +404 -0
- package/dist/display/Accordion.svelte +586 -0
- package/dist/display/Accordion.svelte.d.ts +41 -0
- package/dist/display/Accordion.svelte.d.ts.map +1 -0
- package/dist/display/Avatar.svelte +527 -0
- package/dist/display/Avatar.svelte.d.ts +22 -0
- package/dist/display/Avatar.svelte.d.ts.map +1 -0
- package/dist/display/AvatarGroup.svelte +298 -0
- package/dist/display/AvatarGroup.svelte.d.ts +31 -0
- package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
- package/dist/display/Calendar.svelte +1366 -0
- package/dist/display/Calendar.svelte.d.ts +58 -0
- package/dist/display/Calendar.svelte.d.ts.map +1 -0
- package/dist/display/Chart.svelte +1426 -0
- package/dist/display/Chart.svelte.d.ts +35 -0
- package/dist/display/Chart.svelte.d.ts.map +1 -0
- package/dist/display/Code.svelte +780 -0
- package/dist/display/Code.svelte.d.ts +19 -0
- package/dist/display/Code.svelte.d.ts.map +1 -0
- package/dist/display/Comparison.svelte +686 -0
- package/dist/display/Comparison.svelte.d.ts +22 -0
- package/dist/display/Comparison.svelte.d.ts.map +1 -0
- package/dist/display/Counter.svelte +285 -0
- package/dist/display/Counter.svelte.d.ts +21 -0
- package/dist/display/Counter.svelte.d.ts.map +1 -0
- package/dist/display/Expand.svelte +48 -0
- package/dist/display/Expand.svelte.d.ts +9 -0
- package/dist/display/Expand.svelte.d.ts.map +1 -0
- package/dist/display/List.svelte +294 -0
- package/dist/display/List.svelte.d.ts +40 -0
- package/dist/display/List.svelte.d.ts.map +1 -0
- package/dist/display/ListContextReset.svelte +19 -0
- package/dist/display/ListContextReset.svelte.d.ts +7 -0
- package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
- package/dist/display/ListItem.svelte +834 -0
- package/dist/display/ListItem.svelte.d.ts +22 -0
- package/dist/display/ListItem.svelte.d.ts.map +1 -0
- package/dist/display/QR.svelte +1193 -0
- package/dist/display/QR.svelte.d.ts +23 -0
- package/dist/display/QR.svelte.d.ts.map +1 -0
- package/dist/display/SplitPane.svelte +744 -0
- package/dist/display/SplitPane.svelte.d.ts +25 -0
- package/dist/display/SplitPane.svelte.d.ts.map +1 -0
- package/dist/display/Stat.svelte +439 -0
- package/dist/display/Stat.svelte.d.ts +24 -0
- package/dist/display/Stat.svelte.d.ts.map +1 -0
- package/dist/display/Table.svelte +4654 -0
- package/dist/display/Table.svelte.d.ts +249 -0
- package/dist/display/Table.svelte.d.ts.map +1 -0
- package/dist/display/TableCellEditor.svelte +935 -0
- package/dist/display/TableCellEditor.svelte.d.ts +58 -0
- package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
- package/dist/display/Timeline.svelte +1258 -0
- package/dist/display/Timeline.svelte.d.ts +43 -0
- package/dist/display/Timeline.svelte.d.ts.map +1 -0
- package/dist/display/Tree.svelte +1740 -0
- package/dist/display/Tree.svelte.d.ts +74 -0
- package/dist/display/Tree.svelte.d.ts.map +1 -0
- package/dist/display/Typewriter.svelte +338 -0
- package/dist/display/Typewriter.svelte.d.ts +22 -0
- package/dist/display/Typewriter.svelte.d.ts.map +1 -0
- package/dist/display/index.d.ts +24 -0
- package/dist/display/index.d.ts.map +1 -0
- package/dist/display/index.js +18 -0
- package/dist/feedback/Callout.svelte +529 -0
- package/dist/feedback/Callout.svelte.d.ts +24 -0
- package/dist/feedback/Callout.svelte.d.ts.map +1 -0
- package/dist/feedback/Confetti.svelte +631 -0
- package/dist/feedback/Confetti.svelte.d.ts +90 -0
- package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
- package/dist/feedback/Progress.svelte +382 -0
- package/dist/feedback/Progress.svelte.d.ts +25 -0
- package/dist/feedback/Progress.svelte.d.ts.map +1 -0
- package/dist/feedback/Toast.svelte +967 -0
- package/dist/feedback/Toast.svelte.d.ts +54 -0
- package/dist/feedback/Toast.svelte.d.ts.map +1 -0
- package/dist/feedback/index.d.ts +7 -0
- package/dist/feedback/index.d.ts.map +1 -0
- package/dist/feedback/index.js +4 -0
- package/dist/form/Checkbox.svelte +449 -0
- package/dist/form/Checkbox.svelte.d.ts +27 -0
- package/dist/form/Checkbox.svelte.d.ts.map +1 -0
- package/dist/form/Fieldset.svelte +410 -0
- package/dist/form/Fieldset.svelte.d.ts +22 -0
- package/dist/form/Fieldset.svelte.d.ts.map +1 -0
- package/dist/form/FileUpload.svelte +934 -0
- package/dist/form/FileUpload.svelte.d.ts +41 -0
- package/dist/form/FileUpload.svelte.d.ts.map +1 -0
- package/dist/form/Form.svelte +530 -0
- package/dist/form/Form.svelte.d.ts +120 -0
- package/dist/form/Form.svelte.d.ts.map +1 -0
- package/dist/form/Input.svelte +2858 -0
- package/dist/form/Input.svelte.d.ts +66 -0
- package/dist/form/Input.svelte.d.ts.map +1 -0
- package/dist/form/Radio.svelte +507 -0
- package/dist/form/Radio.svelte.d.ts +39 -0
- package/dist/form/Radio.svelte.d.ts.map +1 -0
- package/dist/form/Range.svelte +912 -0
- package/dist/form/Range.svelte.d.ts +33 -0
- package/dist/form/Range.svelte.d.ts.map +1 -0
- package/dist/form/Rating.svelte +429 -0
- package/dist/form/Rating.svelte.d.ts +28 -0
- package/dist/form/Rating.svelte.d.ts.map +1 -0
- package/dist/form/Select.svelte +1933 -0
- package/dist/form/Select.svelte.d.ts +54 -0
- package/dist/form/Select.svelte.d.ts.map +1 -0
- package/dist/form/Toggle.svelte +645 -0
- package/dist/form/Toggle.svelte.d.ts +50 -0
- package/dist/form/Toggle.svelte.d.ts.map +1 -0
- package/dist/form/index.d.ts +15 -0
- package/dist/form/index.d.ts.map +1 -0
- package/dist/form/index.js +10 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/layout/README.md +172 -0
- package/dist/media/Carousel.svelte +2424 -0
- package/dist/media/Carousel.svelte.d.ts +47 -0
- package/dist/media/Carousel.svelte.d.ts.map +1 -0
- package/dist/media/Gallery.svelte +2881 -0
- package/dist/media/Gallery.svelte.d.ts +82 -0
- package/dist/media/Gallery.svelte.d.ts.map +1 -0
- package/dist/media/Image.svelte +389 -0
- package/dist/media/Image.svelte.d.ts +33 -0
- package/dist/media/Image.svelte.d.ts.map +1 -0
- package/dist/media/PDF.svelte +1793 -0
- package/dist/media/PDF.svelte.d.ts +44 -0
- package/dist/media/PDF.svelte.d.ts.map +1 -0
- package/dist/media/Panorama.svelte +1391 -0
- package/dist/media/Panorama.svelte.d.ts +47 -0
- package/dist/media/Panorama.svelte.d.ts.map +1 -0
- package/dist/media/Video.svelte +2501 -0
- package/dist/media/Video.svelte.d.ts +58 -0
- package/dist/media/Video.svelte.d.ts.map +1 -0
- package/dist/media/carousel.d.ts +211 -0
- package/dist/media/carousel.d.ts.map +1 -0
- package/dist/media/carousel.js +408 -0
- package/dist/media/index.d.ts +11 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/index.js +5 -0
- package/dist/navigation/BottomSheet.svelte +636 -0
- package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
- package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
- package/dist/navigation/Breadcrumbs.svelte +611 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
- package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
- package/dist/navigation/Pagination.svelte +641 -0
- package/dist/navigation/Pagination.svelte.d.ts +27 -0
- package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
- package/dist/navigation/Steps.svelte +965 -0
- package/dist/navigation/Steps.svelte.d.ts +43 -0
- package/dist/navigation/Steps.svelte.d.ts.map +1 -0
- package/dist/navigation/Tabs.svelte +698 -0
- package/dist/navigation/Tabs.svelte.d.ts +41 -0
- package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
- package/dist/navigation/index.d.ts +8 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +5 -0
- package/package.json +139 -0
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
<script lang="ts" generics="T extends Record<string, unknown>">
|
|
2
|
+
/**
|
|
3
|
+
* The single inline editor mounted in a Table's currently-active cell. The
|
|
4
|
+
* Table mounts exactly one of these (keyed by cell identity) and owns active-cell
|
|
5
|
+
* movement; this component owns the editor control, the autocomplete popover, and
|
|
6
|
+
* inline validation. It emits navigation intents upward — it never moves the
|
|
7
|
+
* active cell itself.
|
|
8
|
+
*
|
|
9
|
+
* Autocomplete mirrors the Input component: a native `popover="manual"` placed
|
|
10
|
+
* with CSS anchor positioning (top layer → escapes the row's `overflow: hidden`),
|
|
11
|
+
* arrow/Enter/Escape keys, 300ms debounce for async results, and a loading row.
|
|
12
|
+
*/
|
|
13
|
+
import type { Column, CellOption, CellEditorContext } from './Table.svelte';
|
|
14
|
+
import { onMount, onDestroy, tick } from 'svelte';
|
|
15
|
+
import List from './List.svelte';
|
|
16
|
+
import ListItem from './ListItem.svelte';
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
/** The column definition for the cell being edited (editor kind, options, validation, etc.) */
|
|
20
|
+
column,
|
|
21
|
+
|
|
22
|
+
/** The row object the cell belongs to */
|
|
23
|
+
row,
|
|
24
|
+
|
|
25
|
+
/** The row index within the table data */
|
|
26
|
+
index,
|
|
27
|
+
|
|
28
|
+
/** The current cell value to seed the editor with */
|
|
29
|
+
value,
|
|
30
|
+
|
|
31
|
+
/** Save-error message from the Table (a rejected async `onedit`), shown inline. */
|
|
32
|
+
errorMessage = undefined,
|
|
33
|
+
|
|
34
|
+
/** Whether the table is in dense (compact) spacing mode */
|
|
35
|
+
dense = false,
|
|
36
|
+
|
|
37
|
+
/** Whether the table is in comfortable (roomy) spacing mode */
|
|
38
|
+
comfortable = false,
|
|
39
|
+
|
|
40
|
+
/** Whether this cell is the first / last navigable cell — lets Tab fall out of
|
|
41
|
+
* the grid at the edges instead of trapping focus. */
|
|
42
|
+
isFirstCell = false,
|
|
43
|
+
|
|
44
|
+
/** Whether this cell is the last navigable cell (see `isFirstCell`) */
|
|
45
|
+
isLastCell = false,
|
|
46
|
+
/** Whether an autocomplete/select editor should open its menu on mount.
|
|
47
|
+
* The Table passes `false` when the cell was entered by a keyboard ADVANCE
|
|
48
|
+
* (committing the previous cell), so the menu doesn't cascade open down the
|
|
49
|
+
* column; typing or Alt+ArrowDown still opens it. */
|
|
50
|
+
autoOpenMenu = true,
|
|
51
|
+
/** Value changed and passed validation — the Table runs `onedit`. */
|
|
52
|
+
oncommit = undefined,
|
|
53
|
+
/** Move the active cell. The Table has already let us commit. */
|
|
54
|
+
onnavigate = undefined,
|
|
55
|
+
/** Focus left the grid (blur / click-away) — the Table clears the active cell. */
|
|
56
|
+
onexit = undefined,
|
|
57
|
+
/** Per-keystroke notification — the Table fires `column.oninput`. */
|
|
58
|
+
onliveinput = undefined,
|
|
59
|
+
}: {
|
|
60
|
+
column: Column<T>;
|
|
61
|
+
row: T;
|
|
62
|
+
index: number;
|
|
63
|
+
value: unknown;
|
|
64
|
+
errorMessage?: string | undefined;
|
|
65
|
+
dense?: boolean;
|
|
66
|
+
comfortable?: boolean;
|
|
67
|
+
isFirstCell?: boolean;
|
|
68
|
+
isLastCell?: boolean;
|
|
69
|
+
autoOpenMenu?: boolean;
|
|
70
|
+
oncommit?: (detail: { value: unknown }) => void;
|
|
71
|
+
onnavigate?: (detail: {
|
|
72
|
+
dir: 'up' | 'down' | 'left' | 'right' | 'next' | 'prev';
|
|
73
|
+
}) => void;
|
|
74
|
+
onexit?: () => void;
|
|
75
|
+
onliveinput?: (detail: { value: unknown }) => void;
|
|
76
|
+
} = $props();
|
|
77
|
+
|
|
78
|
+
const uid = $props.id();
|
|
79
|
+
const anchorName = `--ds-cell-${String(uid).replace(/[^a-zA-Z0-9_-]/g, '')}`;
|
|
80
|
+
|
|
81
|
+
// ---- Editor kind ----
|
|
82
|
+
const customEditor = $derived(
|
|
83
|
+
typeof column.editor === 'function' ? column.editor : undefined,
|
|
84
|
+
);
|
|
85
|
+
const editorType = $derived(typeof column.editor === 'string' ? column.editor : 'text');
|
|
86
|
+
const isBoolean = $derived(editorType === 'boolean' && !customEditor);
|
|
87
|
+
const isSelect = $derived(editorType === 'select' && !customEditor);
|
|
88
|
+
const isNumber = $derived(editorType === 'number' && !customEditor);
|
|
89
|
+
const isDate = $derived(editorType === 'date' && !customEditor);
|
|
90
|
+
|
|
91
|
+
// ---- Value helpers ----
|
|
92
|
+
function formatValue(v: unknown): string {
|
|
93
|
+
if (column.format) return column.format(v, row);
|
|
94
|
+
return v == null ? '' : String(v);
|
|
95
|
+
}
|
|
96
|
+
function parseValue(raw: string): unknown {
|
|
97
|
+
if (column.parse) return column.parse(raw, row);
|
|
98
|
+
if (isNumber) {
|
|
99
|
+
const t = raw.trim();
|
|
100
|
+
if (t === '') return null;
|
|
101
|
+
return Number(t);
|
|
102
|
+
}
|
|
103
|
+
return raw;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// `column` is fixed for this editor's life (it remounts per cell via {#key}), so
|
|
107
|
+
// the initial editor kind is a plain (non-reactive) read.
|
|
108
|
+
const startsBoolean = typeof column.editor === 'string' && column.editor === 'boolean';
|
|
109
|
+
const initialText = formatValue(value);
|
|
110
|
+
const initialBool = !!value;
|
|
111
|
+
|
|
112
|
+
// ---- Local state ----
|
|
113
|
+
let draft = $state<string | boolean>(startsBoolean ? initialBool : initialText);
|
|
114
|
+
let localError = $state<string | null>(null);
|
|
115
|
+
let committed = false; // synchronous guard against double-commit on exit/destroy
|
|
116
|
+
let inputEl = $state<HTMLInputElement | undefined>(undefined);
|
|
117
|
+
let boolEl = $state<HTMLButtonElement | undefined>(undefined);
|
|
118
|
+
let rootEl = $state<HTMLDivElement | undefined>(undefined);
|
|
119
|
+
|
|
120
|
+
// ---- Autocomplete state (mirrors Input.svelte) ----
|
|
121
|
+
let ac_open = $state(false);
|
|
122
|
+
let ac_highlighted = $state(-1);
|
|
123
|
+
let ac_loading = $state(false);
|
|
124
|
+
let ac_filtered = $state<CellOption[]>([]);
|
|
125
|
+
let ac_above = $state(false);
|
|
126
|
+
let ac_debounce: ReturnType<typeof setTimeout> | undefined;
|
|
127
|
+
let dropdownEl = $state<HTMLElement | undefined>(undefined);
|
|
128
|
+
let selectedOption: CellOption | null = null;
|
|
129
|
+
|
|
130
|
+
const hasStaticOptions = $derived(!!(column.options && column.options.length));
|
|
131
|
+
const hasAutocomplete = $derived(
|
|
132
|
+
!isBoolean &&
|
|
133
|
+
!customEditor &&
|
|
134
|
+
(hasStaticOptions || !!column.onautocomplete || isSelect),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
function normalizeOptions(opts: CellOption[] | string[] | undefined): CellOption[] {
|
|
138
|
+
if (!opts) return [];
|
|
139
|
+
return opts.map((o) => (typeof o === 'string' ? { value: o, label: o } : o));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const ac_options = $derived.by((): CellOption[] => {
|
|
143
|
+
if (column.onautocomplete) return ac_filtered;
|
|
144
|
+
const opts = normalizeOptions(column.options as CellOption[] | string[] | undefined);
|
|
145
|
+
// A `select` always shows the full option list (typing jumps the highlight,
|
|
146
|
+
// it doesn't filter). Plain autocomplete filters by the current value.
|
|
147
|
+
if (isSelect) return opts;
|
|
148
|
+
const q = typeof draft === 'string' ? draft.trim().toLowerCase() : '';
|
|
149
|
+
if (!q) return opts;
|
|
150
|
+
return opts.filter((o) => (o.label ?? o.value).toLowerCase().includes(q));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Mirror `ac_open` onto the native popover, and detect a flip-above so the panel
|
|
154
|
+
// expands from the edge nearest the cell (same technique as Input).
|
|
155
|
+
$effect(() => {
|
|
156
|
+
const el = dropdownEl;
|
|
157
|
+
if (!el) return;
|
|
158
|
+
const shown = el.matches(':popover-open');
|
|
159
|
+
if (ac_open && !shown) {
|
|
160
|
+
try {
|
|
161
|
+
el.showPopover();
|
|
162
|
+
if (rootEl) {
|
|
163
|
+
const t = rootEl.getBoundingClientRect();
|
|
164
|
+
const d = el.getBoundingClientRect();
|
|
165
|
+
ac_above = d.top < t.top;
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
/* not connected yet */
|
|
169
|
+
}
|
|
170
|
+
} else if (!ac_open && shown) {
|
|
171
|
+
try {
|
|
172
|
+
el.hidePopover();
|
|
173
|
+
} catch {
|
|
174
|
+
/* already hidden */
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// When the panel opens, park the highlight: a `select` starts on its current
|
|
180
|
+
// value (so arrows step from there); autocomplete starts on the first match (so
|
|
181
|
+
// Enter picks the top result without arrowing). Reads `ac_open`/`ac_options`
|
|
182
|
+
// only — not `ac_highlighted` — so arrow moves never re-trigger it.
|
|
183
|
+
$effect(() => {
|
|
184
|
+
const opts = ac_options;
|
|
185
|
+
if (!ac_open) return;
|
|
186
|
+
if (isSelect) {
|
|
187
|
+
const cur = opts.findIndex(
|
|
188
|
+
(o) => (o.label ?? o.value) === initialText || o.value === initialText,
|
|
189
|
+
);
|
|
190
|
+
ac_highlighted = cur >= 0 ? cur : opts.findIndex((o) => !o.disabled);
|
|
191
|
+
} else {
|
|
192
|
+
ac_highlighted = opts.findIndex((o) => !o.disabled);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Pull the option <button>s (ListItem) out of the tab order so focus stays in the
|
|
197
|
+
// input; clicks still work (their pointerdown is prevented on the panel).
|
|
198
|
+
$effect(() => {
|
|
199
|
+
if (!dropdownEl || ac_options.length === 0) return;
|
|
200
|
+
dropdownEl.querySelectorAll('button').forEach((b) => (b.tabIndex = -1));
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
function openAutocomplete() {
|
|
204
|
+
if (!hasAutocomplete) return;
|
|
205
|
+
ac_open = true;
|
|
206
|
+
if (column.onautocomplete) filterAutocomplete(typeof draft === 'string' ? draft : '');
|
|
207
|
+
}
|
|
208
|
+
function closeAutocomplete() {
|
|
209
|
+
ac_open = false;
|
|
210
|
+
ac_highlighted = -1;
|
|
211
|
+
}
|
|
212
|
+
async function filterAutocomplete(query: string) {
|
|
213
|
+
if (!column.onautocomplete) return;
|
|
214
|
+
ac_loading = true;
|
|
215
|
+
try {
|
|
216
|
+
const r = await column.onautocomplete({
|
|
217
|
+
query,
|
|
218
|
+
value: parseValue(query),
|
|
219
|
+
row,
|
|
220
|
+
index,
|
|
221
|
+
column,
|
|
222
|
+
});
|
|
223
|
+
ac_filtered = normalizeOptions(r);
|
|
224
|
+
} finally {
|
|
225
|
+
ac_loading = false;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function moveHighlight(delta: number) {
|
|
229
|
+
const opts = ac_options;
|
|
230
|
+
if (!opts.length) return;
|
|
231
|
+
let i = ac_highlighted;
|
|
232
|
+
for (let n = 0; n < opts.length; n++) {
|
|
233
|
+
i = (i + delta + opts.length) % opts.length;
|
|
234
|
+
if (!opts[i].disabled) break;
|
|
235
|
+
}
|
|
236
|
+
ac_highlighted = i;
|
|
237
|
+
requestAnimationFrame(() => {
|
|
238
|
+
const items = dropdownEl?.querySelectorAll('.list-item');
|
|
239
|
+
items?.[ac_highlighted]?.scrollIntoView({ block: 'nearest' });
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function highlightMatch(text: string): string {
|
|
244
|
+
const q = typeof draft === 'string' ? draft.trim() : '';
|
|
245
|
+
if (!q) return escapeHtml(text);
|
|
246
|
+
const idx = text.toLowerCase().indexOf(q.toLowerCase());
|
|
247
|
+
if (idx === -1) return escapeHtml(text);
|
|
248
|
+
return `${escapeHtml(text.slice(0, idx))}<strong>${escapeHtml(
|
|
249
|
+
text.slice(idx, idx + q.length),
|
|
250
|
+
)}</strong>${escapeHtml(text.slice(idx + q.length))}`;
|
|
251
|
+
}
|
|
252
|
+
function escapeHtml(s: string): string {
|
|
253
|
+
return s.replace(/[&<>"']/g, (c) =>
|
|
254
|
+
c === '&'
|
|
255
|
+
? '&'
|
|
256
|
+
: c === '<'
|
|
257
|
+
? '<'
|
|
258
|
+
: c === '>'
|
|
259
|
+
? '>'
|
|
260
|
+
: c === '"'
|
|
261
|
+
? '"'
|
|
262
|
+
: ''',
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ---- Validation ----
|
|
267
|
+
async function runValidate(parsed: unknown): Promise<string | null> {
|
|
268
|
+
if (isNumber && typeof parsed === 'number' && Number.isNaN(parsed)) {
|
|
269
|
+
return 'Enter a valid number';
|
|
270
|
+
}
|
|
271
|
+
if (column.validate) {
|
|
272
|
+
const r = await column.validate(parsed, row, index);
|
|
273
|
+
return r && r.length ? r : null;
|
|
274
|
+
}
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---- Commit ----
|
|
279
|
+
async function tryCommit(): Promise<boolean> {
|
|
280
|
+
if (committed || isBoolean) return true;
|
|
281
|
+
const text = String(draft);
|
|
282
|
+
if (isSelect && !optionFor(text)) {
|
|
283
|
+
// Constrained: an unmatched value reverts rather than commits.
|
|
284
|
+
draft = initialText;
|
|
285
|
+
committed = true;
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
if (text === initialText) {
|
|
289
|
+
committed = true;
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
committed = true; // synchronous guard
|
|
293
|
+
const parsed =
|
|
294
|
+
selectedOption && (selectedOption.label ?? selectedOption.value) === text
|
|
295
|
+
? selectedOption.value
|
|
296
|
+
: parseValue(text);
|
|
297
|
+
const err = await runValidate(parsed);
|
|
298
|
+
if (err) {
|
|
299
|
+
committed = false;
|
|
300
|
+
localError = err;
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
oncommit?.({ value: parsed });
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function optionFor(text: string): CellOption | undefined {
|
|
308
|
+
const opts = column.onautocomplete
|
|
309
|
+
? ac_filtered
|
|
310
|
+
: normalizeOptions(column.options as never);
|
|
311
|
+
return opts.find((o) => (o.label ?? o.value) === text || o.value === text);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async function commitAndNavigate(
|
|
315
|
+
dir: 'up' | 'down' | 'left' | 'right' | 'next' | 'prev',
|
|
316
|
+
) {
|
|
317
|
+
const ok = await tryCommit();
|
|
318
|
+
if (ok) onnavigate?.({ dir });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// `via` distinguishes how the option was chosen. Keyboard (Enter on the
|
|
322
|
+
// highlight) keeps the spreadsheet flow: commit and advance to the cell below.
|
|
323
|
+
// Pointer (click/tap) commits and CLOSES the edit — no advance, and definitely
|
|
324
|
+
// no next-cell menu popping open under the cursor.
|
|
325
|
+
function selectOption(opt: CellOption, via: 'keyboard' | 'pointer' = 'keyboard') {
|
|
326
|
+
if (opt.disabled) return;
|
|
327
|
+
selectedOption = opt;
|
|
328
|
+
draft = opt.label ?? opt.value;
|
|
329
|
+
localError = null;
|
|
330
|
+
closeAutocomplete();
|
|
331
|
+
committed = false;
|
|
332
|
+
if (via === 'pointer') {
|
|
333
|
+
void (async () => {
|
|
334
|
+
const ok = await tryCommit();
|
|
335
|
+
if (ok) onexit?.();
|
|
336
|
+
})();
|
|
337
|
+
} else {
|
|
338
|
+
void commitAndNavigate('down');
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function toggleBoolean() {
|
|
343
|
+
draft = !(draft as boolean);
|
|
344
|
+
oncommit?.({ value: draft });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function cancel() {
|
|
348
|
+
if (ac_open) {
|
|
349
|
+
closeAutocomplete();
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
draft = isBoolean ? initialBool : initialText;
|
|
353
|
+
localError = null;
|
|
354
|
+
selectedOption = null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ---- Caret edge detection (for ←/→ cell jumps) ----
|
|
358
|
+
function caretAtStart(): boolean {
|
|
359
|
+
const el = inputEl;
|
|
360
|
+
if (!el) return true;
|
|
361
|
+
try {
|
|
362
|
+
return el.selectionStart === 0 && el.selectionEnd === 0;
|
|
363
|
+
} catch {
|
|
364
|
+
return true; // inputs without text selection (number/date) jump freely
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
function caretAtEnd(): boolean {
|
|
368
|
+
const el = inputEl;
|
|
369
|
+
if (!el) return true;
|
|
370
|
+
try {
|
|
371
|
+
return el.selectionStart === el.value.length && el.selectionEnd === el.value.length;
|
|
372
|
+
} catch {
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ---- Keyboard ----
|
|
378
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
379
|
+
if (e.isComposing || e.keyCode === 229) return; // IME composition
|
|
380
|
+
const k = e.key;
|
|
381
|
+
|
|
382
|
+
// Alt+ArrowDown opens a closed panel (the standard combobox affordance) —
|
|
383
|
+
// needed when the cell was entered via a keyboard advance (no auto-open).
|
|
384
|
+
if (hasAutocomplete && !ac_open && k === 'ArrowDown' && e.altKey) {
|
|
385
|
+
e.preventDefault();
|
|
386
|
+
openAutocomplete();
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Autocomplete navigation takes priority while the panel is open.
|
|
391
|
+
if (ac_open && ac_options.length) {
|
|
392
|
+
if (k === 'ArrowDown') {
|
|
393
|
+
e.preventDefault();
|
|
394
|
+
moveHighlight(1);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (k === 'ArrowUp') {
|
|
398
|
+
e.preventDefault();
|
|
399
|
+
moveHighlight(-1);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (k === 'Enter' && ac_highlighted >= 0) {
|
|
403
|
+
e.preventDefault();
|
|
404
|
+
selectOption(ac_options[ac_highlighted]);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (k === 'Escape') {
|
|
408
|
+
e.preventDefault();
|
|
409
|
+
closeAutocomplete();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
switch (k) {
|
|
415
|
+
case 'Enter':
|
|
416
|
+
e.preventDefault();
|
|
417
|
+
void commitAndNavigate('down');
|
|
418
|
+
break;
|
|
419
|
+
case 'Tab':
|
|
420
|
+
if ((!e.shiftKey && isLastCell) || (e.shiftKey && isFirstCell)) {
|
|
421
|
+
void tryCommit(); // let focus fall out of the grid
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
e.preventDefault();
|
|
425
|
+
void commitAndNavigate(e.shiftKey ? 'prev' : 'next');
|
|
426
|
+
break;
|
|
427
|
+
case 'Escape':
|
|
428
|
+
e.preventDefault();
|
|
429
|
+
cancel();
|
|
430
|
+
break;
|
|
431
|
+
case 'ArrowUp':
|
|
432
|
+
e.preventDefault();
|
|
433
|
+
void commitAndNavigate('up');
|
|
434
|
+
break;
|
|
435
|
+
case 'ArrowDown':
|
|
436
|
+
e.preventDefault();
|
|
437
|
+
void commitAndNavigate('down');
|
|
438
|
+
break;
|
|
439
|
+
case 'ArrowLeft':
|
|
440
|
+
if (caretAtStart()) {
|
|
441
|
+
e.preventDefault();
|
|
442
|
+
void commitAndNavigate('left');
|
|
443
|
+
}
|
|
444
|
+
break;
|
|
445
|
+
case 'ArrowRight':
|
|
446
|
+
if (caretAtEnd()) {
|
|
447
|
+
e.preventDefault();
|
|
448
|
+
void commitAndNavigate('right');
|
|
449
|
+
}
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function handleBooleanKeydown(e: KeyboardEvent) {
|
|
455
|
+
if (e.isComposing) return;
|
|
456
|
+
switch (e.key) {
|
|
457
|
+
case ' ':
|
|
458
|
+
e.preventDefault();
|
|
459
|
+
toggleBoolean();
|
|
460
|
+
break;
|
|
461
|
+
case 'Enter':
|
|
462
|
+
e.preventDefault();
|
|
463
|
+
toggleBoolean();
|
|
464
|
+
onnavigate?.({ dir: 'down' });
|
|
465
|
+
break;
|
|
466
|
+
case 'Tab':
|
|
467
|
+
if ((!e.shiftKey && isLastCell) || (e.shiftKey && isFirstCell)) return;
|
|
468
|
+
e.preventDefault();
|
|
469
|
+
onnavigate?.({ dir: e.shiftKey ? 'prev' : 'next' });
|
|
470
|
+
break;
|
|
471
|
+
case 'ArrowUp':
|
|
472
|
+
e.preventDefault();
|
|
473
|
+
onnavigate?.({ dir: 'up' });
|
|
474
|
+
break;
|
|
475
|
+
case 'ArrowDown':
|
|
476
|
+
e.preventDefault();
|
|
477
|
+
onnavigate?.({ dir: 'down' });
|
|
478
|
+
break;
|
|
479
|
+
case 'ArrowLeft':
|
|
480
|
+
e.preventDefault();
|
|
481
|
+
onnavigate?.({ dir: 'left' });
|
|
482
|
+
break;
|
|
483
|
+
case 'ArrowRight':
|
|
484
|
+
e.preventDefault();
|
|
485
|
+
onnavigate?.({ dir: 'right' });
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function handleInput() {
|
|
491
|
+
localError = null;
|
|
492
|
+
selectedOption = null;
|
|
493
|
+
onliveinput?.({ value: isNumber ? parseValue(String(draft)) : draft });
|
|
494
|
+
if (!hasAutocomplete) return;
|
|
495
|
+
ac_open = true;
|
|
496
|
+
if (isSelect) {
|
|
497
|
+
// Type-ahead: jump the highlight to the first option that starts with what
|
|
498
|
+
// was typed; the list itself stays unfiltered.
|
|
499
|
+
const q = String(draft).trim().toLowerCase();
|
|
500
|
+
if (q) {
|
|
501
|
+
const i = ac_options.findIndex(
|
|
502
|
+
(o) => !o.disabled && (o.label ?? o.value).toLowerCase().startsWith(q),
|
|
503
|
+
);
|
|
504
|
+
if (i >= 0) ac_highlighted = i;
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (column.onautocomplete) {
|
|
509
|
+
clearTimeout(ac_debounce);
|
|
510
|
+
const q = String(draft);
|
|
511
|
+
ac_debounce = setTimeout(() => filterAutocomplete(q), 300);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Real blur (focus left the editor + popover) → commit and exit edit mode.
|
|
516
|
+
function handleFocusOut(e: FocusEvent) {
|
|
517
|
+
const next = e.relatedTarget as Node | null;
|
|
518
|
+
if (next && (rootEl?.contains(next) || dropdownEl?.contains(next))) return;
|
|
519
|
+
closeAutocomplete();
|
|
520
|
+
void tryCommit();
|
|
521
|
+
onexit?.();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ---- Lifecycle: autofocus + select on mount; commit a dirty draft if torn down
|
|
525
|
+
// (e.g. the row scrolled out of a virtual window) before an explicit commit. ----
|
|
526
|
+
onMount(() => {
|
|
527
|
+
void (async () => {
|
|
528
|
+
await tick();
|
|
529
|
+
if (isBoolean) {
|
|
530
|
+
boolEl?.focus();
|
|
531
|
+
} else {
|
|
532
|
+
inputEl?.focus();
|
|
533
|
+
try {
|
|
534
|
+
inputEl?.select();
|
|
535
|
+
} catch {
|
|
536
|
+
/* noop */
|
|
537
|
+
}
|
|
538
|
+
if (hasAutocomplete && autoOpenMenu) openAutocomplete();
|
|
539
|
+
}
|
|
540
|
+
})();
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
onDestroy(() => {
|
|
544
|
+
clearTimeout(ac_debounce);
|
|
545
|
+
if (!committed && !isBoolean) void tryCommit();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
const editorCtx = $derived<CellEditorContext<T>>({
|
|
549
|
+
value,
|
|
550
|
+
row,
|
|
551
|
+
index,
|
|
552
|
+
column,
|
|
553
|
+
setValue: (v: unknown) => {
|
|
554
|
+
draft = (isBoolean ? !!v : String(v ?? '')) as typeof draft;
|
|
555
|
+
},
|
|
556
|
+
commit: () => void commitAndNavigate('down'),
|
|
557
|
+
cancel,
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
const showError = $derived(localError ?? errorMessage ?? null);
|
|
561
|
+
const placeholder = $derived(column.placeholder ?? '');
|
|
562
|
+
</script>
|
|
563
|
+
|
|
564
|
+
{#snippet checkmark(checked: boolean)}
|
|
565
|
+
<span class="checkmark" class:checked>
|
|
566
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" aria-hidden="true">
|
|
567
|
+
<rect class="box" x="2" y="2" width="20" height="20" rx="3" stroke-width="2" />
|
|
568
|
+
<path
|
|
569
|
+
class="check"
|
|
570
|
+
d="M6 12.5 L10 16.5 L18 8"
|
|
571
|
+
stroke-width="2.5"
|
|
572
|
+
stroke-linecap="round"
|
|
573
|
+
stroke-linejoin="round" />
|
|
574
|
+
</svg>
|
|
575
|
+
</span>
|
|
576
|
+
{/snippet}
|
|
577
|
+
|
|
578
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
579
|
+
<div
|
|
580
|
+
class="editor"
|
|
581
|
+
class:has-error={!!showError}
|
|
582
|
+
class:dense
|
|
583
|
+
class:comfortable
|
|
584
|
+
class:is-bool={isBoolean}
|
|
585
|
+
bind:this={rootEl}
|
|
586
|
+
style:anchor-name={anchorName}
|
|
587
|
+
onfocusout={handleFocusOut}>
|
|
588
|
+
{#if customEditor}
|
|
589
|
+
{@render customEditor(editorCtx)}
|
|
590
|
+
{:else if isBoolean}
|
|
591
|
+
<button
|
|
592
|
+
type="button"
|
|
593
|
+
class="bool-toggle"
|
|
594
|
+
role="switch"
|
|
595
|
+
aria-checked={draft as boolean}
|
|
596
|
+
aria-label={column.label}
|
|
597
|
+
bind:this={boolEl}
|
|
598
|
+
onclick={(e) => {
|
|
599
|
+
e.stopPropagation();
|
|
600
|
+
toggleBoolean();
|
|
601
|
+
}}
|
|
602
|
+
onkeydown={handleBooleanKeydown}>
|
|
603
|
+
{@render checkmark(draft as boolean)}
|
|
604
|
+
</button>
|
|
605
|
+
{:else}
|
|
606
|
+
<input
|
|
607
|
+
class="cell-input"
|
|
608
|
+
type={isDate ? 'date' : 'text'}
|
|
609
|
+
inputmode={isNumber ? 'decimal' : undefined}
|
|
610
|
+
role={hasAutocomplete ? 'combobox' : undefined}
|
|
611
|
+
aria-expanded={hasAutocomplete ? ac_open : undefined}
|
|
612
|
+
aria-controls={hasAutocomplete ? `${uid}-cell-listbox` : undefined}
|
|
613
|
+
aria-autocomplete={hasAutocomplete ? 'list' : undefined}
|
|
614
|
+
aria-invalid={!!showError}
|
|
615
|
+
{placeholder}
|
|
616
|
+
bind:this={inputEl}
|
|
617
|
+
bind:value={draft}
|
|
618
|
+
oninput={handleInput}
|
|
619
|
+
onkeydown={handleKeydown} />
|
|
620
|
+
{/if}
|
|
621
|
+
|
|
622
|
+
{#if hasAutocomplete}
|
|
623
|
+
<!-- Autocomplete popover — native popover + CSS anchor positioning (top layer).
|
|
624
|
+
Uses List/ListItem; ListItem's active highlight snaps in instantly (see
|
|
625
|
+
ListItem's `.active` ::before rule), so arrowing feels immediate. -->
|
|
626
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
627
|
+
<div
|
|
628
|
+
class="ac-dropdown"
|
|
629
|
+
class:above={ac_above}
|
|
630
|
+
popover="manual"
|
|
631
|
+
bind:this={dropdownEl}
|
|
632
|
+
role="listbox"
|
|
633
|
+
id="{uid}-cell-listbox"
|
|
634
|
+
style:position-anchor={anchorName}
|
|
635
|
+
onpointerdown={(e) => e.preventDefault()}>
|
|
636
|
+
{#if ac_loading}
|
|
637
|
+
<div class="ac-status">
|
|
638
|
+
<span class="ac-spinner" aria-hidden="true"></span>
|
|
639
|
+
Loading…
|
|
640
|
+
</div>
|
|
641
|
+
{:else if ac_options.length === 0}
|
|
642
|
+
<div class="ac-status">No results</div>
|
|
643
|
+
{:else}
|
|
644
|
+
<List dense>
|
|
645
|
+
{#each ac_options as opt, i (opt.value)}
|
|
646
|
+
<ListItem
|
|
647
|
+
active={ac_highlighted === i}
|
|
648
|
+
disabled={opt.disabled}
|
|
649
|
+
onclick={() => selectOption(opt, 'pointer')}>
|
|
650
|
+
<span class="ac-option">
|
|
651
|
+
<span class="ac-option-label">
|
|
652
|
+
{@html highlightMatch(opt.label ?? opt.value)}
|
|
653
|
+
</span>
|
|
654
|
+
{#if opt.description}
|
|
655
|
+
<span class="ac-option-desc">{opt.description}</span>
|
|
656
|
+
{/if}
|
|
657
|
+
</span>
|
|
658
|
+
</ListItem>
|
|
659
|
+
{/each}
|
|
660
|
+
</List>
|
|
661
|
+
{/if}
|
|
662
|
+
</div>
|
|
663
|
+
{/if}
|
|
664
|
+
|
|
665
|
+
{#if showError}
|
|
666
|
+
<!-- Error tooltip as a top-layer popover so it can't be hidden behind the cells
|
|
667
|
+
stacked below it. -->
|
|
668
|
+
<div
|
|
669
|
+
class="error"
|
|
670
|
+
popover="manual"
|
|
671
|
+
role="alert"
|
|
672
|
+
style:position-anchor={anchorName}
|
|
673
|
+
{@attach (node) => {
|
|
674
|
+
try {
|
|
675
|
+
(node as HTMLElement & { showPopover(): void }).showPopover();
|
|
676
|
+
} catch {
|
|
677
|
+
/* not connected */
|
|
678
|
+
}
|
|
679
|
+
}}>
|
|
680
|
+
{showError}
|
|
681
|
+
</div>
|
|
682
|
+
{/if}
|
|
683
|
+
</div>
|
|
684
|
+
|
|
685
|
+
<style>
|
|
686
|
+
.editor {
|
|
687
|
+
position: relative;
|
|
688
|
+
display: flex;
|
|
689
|
+
align-items: center;
|
|
690
|
+
flex: 1;
|
|
691
|
+
min-width: 0;
|
|
692
|
+
/* Bleed to the cell edges (the td keeps its padding for the resting text; the
|
|
693
|
+
editor cancels it so the caret sits where the display text was). */
|
|
694
|
+
margin: -0.75rem -1rem;
|
|
695
|
+
padding: 0.75rem 1rem;
|
|
696
|
+
gap: 0.5rem;
|
|
697
|
+
|
|
698
|
+
&.dense {
|
|
699
|
+
margin: -0.375rem -0.75rem;
|
|
700
|
+
padding: 0.375rem 0.75rem;
|
|
701
|
+
}
|
|
702
|
+
&.comfortable {
|
|
703
|
+
margin: -1rem -1.25rem;
|
|
704
|
+
padding: 1rem 1.25rem;
|
|
705
|
+
}
|
|
706
|
+
&.has-error .cell-input {
|
|
707
|
+
color: var(--color-error, #dc2626);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/* `.cell-input` (not a bare element selector): the class doubles as the JS hook
|
|
712
|
+
the Table uses to focus the editor after a keyboard move. */
|
|
713
|
+
.cell-input {
|
|
714
|
+
flex: 1;
|
|
715
|
+
min-width: 0;
|
|
716
|
+
width: 100%;
|
|
717
|
+
border: none;
|
|
718
|
+
outline: none;
|
|
719
|
+
background: transparent;
|
|
720
|
+
padding: 0;
|
|
721
|
+
margin: 0;
|
|
722
|
+
font: inherit;
|
|
723
|
+
color: inherit;
|
|
724
|
+
line-height: inherit;
|
|
725
|
+
|
|
726
|
+
&::placeholder {
|
|
727
|
+
color: light-dark(
|
|
728
|
+
var(--color-text-muted, #9ca3af),
|
|
729
|
+
var(--color-text-muted, #6b7280)
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/* Error tooltip — a top-layer popover (CSS anchor positioned) so it paints over
|
|
735
|
+
the cells below it, never behind them. */
|
|
736
|
+
.error {
|
|
737
|
+
position: fixed;
|
|
738
|
+
top: anchor(bottom);
|
|
739
|
+
left: anchor(left);
|
|
740
|
+
margin: 4px 0 0 0;
|
|
741
|
+
padding: 0.3em 0.55em;
|
|
742
|
+
border: none;
|
|
743
|
+
font-size: 0.75rem;
|
|
744
|
+
white-space: nowrap;
|
|
745
|
+
color: #fff;
|
|
746
|
+
background: var(--color-error, #dc2626);
|
|
747
|
+
border-radius: var(--radius-md, 6px);
|
|
748
|
+
@supports (corner-shape: squircle) {
|
|
749
|
+
corner-shape: squircle;
|
|
750
|
+
border-radius: calc(var(--radius-md, 6px) * var(--squircle-ratio, 2));
|
|
751
|
+
}
|
|
752
|
+
box-shadow: var(--shadow-md, 0 8px 28px -8px rgb(0 0 0 / 0.35));
|
|
753
|
+
pointer-events: none;
|
|
754
|
+
position-try-fallbacks: flip-block;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/* ---- Boolean toggle: fills the whole cell so a click anywhere toggles ---- */
|
|
758
|
+
/* `position: static` lets the button's `inset: 0` resolve to the <td> (which is
|
|
759
|
+
`position: relative`), so the toggle covers the entire cell including padding. */
|
|
760
|
+
.editor.is-bool {
|
|
761
|
+
position: static;
|
|
762
|
+
margin: 0;
|
|
763
|
+
padding: 0;
|
|
764
|
+
}
|
|
765
|
+
.bool-toggle {
|
|
766
|
+
position: absolute;
|
|
767
|
+
inset: 0;
|
|
768
|
+
display: flex;
|
|
769
|
+
align-items: center;
|
|
770
|
+
justify-content: center;
|
|
771
|
+
padding: 0;
|
|
772
|
+
border: none;
|
|
773
|
+
background: transparent;
|
|
774
|
+
cursor: pointer;
|
|
775
|
+
|
|
776
|
+
&:focus-visible {
|
|
777
|
+
outline: none;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/* Checkbox visual — matches the Checkbox component (accent-filled box + drawn
|
|
782
|
+
check), so editable boolean cells read identically to a real Checkbox. */
|
|
783
|
+
.checkmark {
|
|
784
|
+
display: inline-flex;
|
|
785
|
+
flex-shrink: 0;
|
|
786
|
+
line-height: 0;
|
|
787
|
+
|
|
788
|
+
.box {
|
|
789
|
+
stroke: light-dark(
|
|
790
|
+
var(--color-text-disabled, #999),
|
|
791
|
+
var(--color-text-disabled, #777)
|
|
792
|
+
);
|
|
793
|
+
fill: transparent;
|
|
794
|
+
transition:
|
|
795
|
+
stroke 150ms ease,
|
|
796
|
+
fill 150ms ease;
|
|
797
|
+
}
|
|
798
|
+
.check {
|
|
799
|
+
stroke: transparent;
|
|
800
|
+
fill: none;
|
|
801
|
+
stroke-dasharray: 28;
|
|
802
|
+
stroke-dashoffset: 28;
|
|
803
|
+
transition:
|
|
804
|
+
stroke-dashoffset 250ms ease,
|
|
805
|
+
stroke 150ms ease;
|
|
806
|
+
}
|
|
807
|
+
&.checked .box {
|
|
808
|
+
stroke: var(--color-action, #1976d2);
|
|
809
|
+
fill: var(--color-action, #1976d2);
|
|
810
|
+
}
|
|
811
|
+
&.checked .check {
|
|
812
|
+
stroke: var(--color-action-text, #fff);
|
|
813
|
+
stroke-dashoffset: 0;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/* ================================================================== */
|
|
818
|
+
/* Autocomplete popover — mirrors Input.svelte's autocomplete dropdown */
|
|
819
|
+
/* ================================================================== */
|
|
820
|
+
.ac-dropdown {
|
|
821
|
+
position: fixed;
|
|
822
|
+
top: anchor(bottom);
|
|
823
|
+
bottom: auto;
|
|
824
|
+
left: anchor(left);
|
|
825
|
+
right: auto;
|
|
826
|
+
width: max(anchor-size(width), 12rem);
|
|
827
|
+
margin: 0.35em 0 0 0;
|
|
828
|
+
padding: 0;
|
|
829
|
+
box-sizing: border-box;
|
|
830
|
+
max-height: 16em;
|
|
831
|
+
overflow-y: auto;
|
|
832
|
+
/* Border + shadow together: in light mode the shadow lifts the panel and
|
|
833
|
+
the border is a faint edge; in dark mode --shadow-md is transparent, so
|
|
834
|
+
the border is what separates the panel from the page. */
|
|
835
|
+
border: 1px solid
|
|
836
|
+
light-dark(var(--color-border, #e5e7eb), var(--color-border, #404040));
|
|
837
|
+
background: light-dark(var(--color-bg, #fff), var(--color-surface, #262626));
|
|
838
|
+
color: light-dark(var(--color-text, #1a1a1a), var(--color-text, #f5f5f5));
|
|
839
|
+
border-radius: var(--radius-lg, 16px);
|
|
840
|
+
/* Keep the native (baseline-styled) thumb clear of the rounded corners.
|
|
841
|
+
scrollbar-width/scrollbar-color must NOT be set here — they disable the
|
|
842
|
+
::-webkit-scrollbar baseline styling in Chromium. */
|
|
843
|
+
--scrollbar-track-inset: calc(var(--radius-lg, 16px) / 2);
|
|
844
|
+
@supports (corner-shape: squircle) {
|
|
845
|
+
corner-shape: squircle;
|
|
846
|
+
border-radius: calc(var(--radius-lg, 16px) * var(--squircle-ratio, 2));
|
|
847
|
+
--scrollbar-track-inset: calc(
|
|
848
|
+
var(--radius-lg, 16px) * var(--squircle-ratio, 2) / 2
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
overscroll-behavior: contain;
|
|
852
|
+
box-shadow: var(--shadow-md, 0 8px 28px -8px rgb(0 0 0 / 0.3));
|
|
853
|
+
position-try-fallbacks: flip-block;
|
|
854
|
+
transform-origin: center top;
|
|
855
|
+
opacity: 1;
|
|
856
|
+
transform: scaleY(1);
|
|
857
|
+
transition:
|
|
858
|
+
opacity 200ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
859
|
+
transform 200ms cubic-bezier(0.16, 1, 0.3, 1),
|
|
860
|
+
display 200ms allow-discrete,
|
|
861
|
+
overlay 200ms allow-discrete;
|
|
862
|
+
}
|
|
863
|
+
.ac-dropdown.above {
|
|
864
|
+
transform-origin: center bottom;
|
|
865
|
+
}
|
|
866
|
+
.ac-dropdown:not(:popover-open) {
|
|
867
|
+
opacity: 0;
|
|
868
|
+
transform: scaleY(0.6);
|
|
869
|
+
}
|
|
870
|
+
@starting-style {
|
|
871
|
+
.ac-dropdown:popover-open {
|
|
872
|
+
opacity: 0;
|
|
873
|
+
transform: scaleY(0.6);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
.ac-option {
|
|
878
|
+
display: flex;
|
|
879
|
+
flex-direction: column;
|
|
880
|
+
min-width: 0;
|
|
881
|
+
flex: 1;
|
|
882
|
+
text-align: left;
|
|
883
|
+
}
|
|
884
|
+
.ac-option-label {
|
|
885
|
+
overflow: hidden;
|
|
886
|
+
text-overflow: ellipsis;
|
|
887
|
+
white-space: nowrap;
|
|
888
|
+
}
|
|
889
|
+
.ac-option-label :global(strong) {
|
|
890
|
+
color: var(--color-action, #1976d2);
|
|
891
|
+
font-weight: 700;
|
|
892
|
+
}
|
|
893
|
+
.ac-option-desc {
|
|
894
|
+
font-size: 0.8em;
|
|
895
|
+
color: light-dark(var(--color-text-muted, #6b7280), var(--color-text-muted, #9ca3af));
|
|
896
|
+
overflow: hidden;
|
|
897
|
+
text-overflow: ellipsis;
|
|
898
|
+
white-space: nowrap;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
.ac-status {
|
|
902
|
+
display: flex;
|
|
903
|
+
align-items: center;
|
|
904
|
+
justify-content: center;
|
|
905
|
+
gap: 0.5em;
|
|
906
|
+
padding: 0.85em;
|
|
907
|
+
color: light-dark(var(--color-text-muted, #6b7280), var(--color-text-muted, #9ca3af));
|
|
908
|
+
font-size: 0.9em;
|
|
909
|
+
}
|
|
910
|
+
.ac-spinner {
|
|
911
|
+
display: inline-block;
|
|
912
|
+
width: 14px;
|
|
913
|
+
height: 14px;
|
|
914
|
+
border: 2px solid
|
|
915
|
+
light-dark(var(--color-border, #e5e7eb), var(--color-border, #404040));
|
|
916
|
+
border-top-color: var(--color-action, #1976d2);
|
|
917
|
+
border-radius: 50%;
|
|
918
|
+
animation: cell-ac-spin 0.6s linear infinite;
|
|
919
|
+
flex-shrink: 0;
|
|
920
|
+
}
|
|
921
|
+
@keyframes cell-ac-spin {
|
|
922
|
+
to {
|
|
923
|
+
transform: rotate(360deg);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
@media (prefers-reduced-motion: reduce) {
|
|
928
|
+
.ac-dropdown {
|
|
929
|
+
transition: none;
|
|
930
|
+
}
|
|
931
|
+
.ac-spinner {
|
|
932
|
+
animation-duration: 1.2s;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
</style>
|