@ruckc/truss-ui 1.2.0 → 1.4.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/README.md +6 -1
- package/dist/commands.svelte.d.ts +41 -0
- package/dist/commands.svelte.js +59 -0
- package/dist/components/Accordion.svelte +1 -1
- package/dist/components/Button.svelte +65 -8
- package/dist/components/Button.svelte.d.ts +2 -0
- package/dist/components/Calendar.svelte +2 -2
- package/dist/components/Card.svelte +1 -0
- package/dist/components/Card.svelte.d.ts +1 -0
- package/dist/components/Checkbox.svelte +1 -1
- package/dist/components/CommandPalette.svelte +207 -0
- package/dist/components/CommandPalette.svelte.d.ts +39 -0
- package/dist/components/DataTable.svelte +201 -27
- package/dist/components/DataTable.svelte.d.ts +50 -4
- package/dist/components/Dialog.svelte +2 -1
- package/dist/components/Drawer.svelte +1 -1
- package/dist/components/EditableText.svelte +2 -2
- package/dist/components/Eyebrow.svelte +1 -1
- package/dist/components/FileUpload.svelte +1 -1
- package/dist/components/Footer.svelte +29 -2
- package/dist/components/Footer.svelte.d.ts +8 -0
- package/dist/components/GridLayout.svelte +242 -0
- package/dist/components/GridLayout.svelte.d.ts +30 -0
- package/dist/components/HoverCard.svelte +158 -0
- package/dist/components/HoverCard.svelte.d.ts +46 -0
- package/dist/components/Kbd.svelte +36 -0
- package/dist/components/Kbd.svelte.d.ts +8 -0
- package/dist/components/Link.svelte +1 -1
- package/dist/components/List.svelte +113 -0
- package/dist/components/List.svelte.d.ts +17 -0
- package/dist/components/ListItem.svelte +13 -0
- package/dist/components/ListItem.svelte.d.ts +9 -0
- package/dist/components/Menu.svelte +4 -3
- package/dist/components/MultiSelect.svelte +2 -2
- package/dist/components/Navbar.svelte +13 -3
- package/dist/components/Navbar.svelte.d.ts +5 -1
- package/dist/components/Panel.svelte +2 -2
- package/dist/components/Popover.svelte +22 -1
- package/dist/components/Popover.svelte.d.ts +2 -0
- package/dist/components/RadioGroup.svelte +1 -1
- package/dist/components/Select.svelte +1 -1
- package/dist/components/Shell.svelte +12 -1
- package/dist/components/Shell.svelte.d.ts +2 -0
- package/dist/components/ShortcutSheet.svelte +62 -0
- package/dist/components/ShortcutSheet.svelte.d.ts +9 -0
- package/dist/components/Sidebar.svelte +2 -2
- package/dist/components/Skeleton.svelte +80 -6
- package/dist/components/Skeleton.svelte.d.ts +8 -1
- package/dist/components/Slider.svelte +1 -1
- package/dist/components/Switch.svelte +1 -1
- package/dist/components/Tabs.svelte +252 -32
- package/dist/components/Tabs.svelte.d.ts +21 -9
- package/dist/components/Tag.svelte +7 -7
- package/dist/components/TagInput.svelte +1 -1
- package/dist/components/Text.svelte +1 -1
- package/dist/components/Toaster.svelte +33 -2
- package/dist/components/ToggleChip.svelte +1 -1
- package/dist/components/Toolbar.svelte +27 -0
- package/dist/components/Toolbar.svelte.d.ts +10 -0
- package/dist/components/ToolbarSeparator.svelte +15 -0
- package/dist/components/ToolbarSeparator.svelte.d.ts +3 -0
- package/dist/components/Tooltip.svelte +19 -28
- package/dist/components/Tooltip.svelte.d.ts +6 -1
- package/dist/components/TooltipTip.svelte +59 -0
- package/dist/components/TooltipTip.svelte.d.ts +12 -0
- package/dist/components/Tree.svelte +343 -44
- package/dist/components/Tree.svelte.d.ts +29 -2
- package/dist/grid-layout.d.ts +42 -0
- package/dist/grid-layout.js +100 -0
- package/dist/hotkeys.svelte.d.ts +45 -0
- package/dist/hotkeys.svelte.js +108 -0
- package/dist/index.d.ts +21 -2
- package/dist/index.js +13 -0
- package/dist/keys.d.ts +25 -0
- package/dist/keys.js +88 -0
- package/dist/roving.d.ts +25 -0
- package/dist/roving.js +129 -0
- package/dist/styles/base.css +2 -0
- package/dist/styles/tokens.css +49 -0
- package/dist/table-export.d.ts +24 -0
- package/dist/table-export.js +130 -0
- package/dist/toast.svelte.d.ts +25 -1
- package/dist/toast.svelte.js +42 -4
- package/dist/types.d.ts +6 -0
- package/package.json +7 -3
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import Kbd from './Kbd.svelte';
|
|
4
|
+
|
|
5
|
+
/** The floating bubble behind Tooltip and Button's `shortcut`: positions itself beside `anchor`, flipping to stay on screen. */
|
|
6
|
+
let {
|
|
7
|
+
anchor,
|
|
8
|
+
id,
|
|
9
|
+
text,
|
|
10
|
+
content,
|
|
11
|
+
shortcut,
|
|
12
|
+
placement = 'top'
|
|
13
|
+
}: {
|
|
14
|
+
anchor: HTMLElement;
|
|
15
|
+
id: string;
|
|
16
|
+
text?: string;
|
|
17
|
+
content?: Snippet;
|
|
18
|
+
shortcut?: string;
|
|
19
|
+
placement?: 'top' | 'bottom' | 'left' | 'right';
|
|
20
|
+
} = $props();
|
|
21
|
+
|
|
22
|
+
let ready = $state(false);
|
|
23
|
+
let x = $state(0);
|
|
24
|
+
let y = $state(0);
|
|
25
|
+
let tip = $state<HTMLDivElement>();
|
|
26
|
+
|
|
27
|
+
$effect(() => {
|
|
28
|
+
if (!tip) return;
|
|
29
|
+
const r = anchor.getBoundingClientRect();
|
|
30
|
+
const t = tip.getBoundingClientRect();
|
|
31
|
+
let p = placement;
|
|
32
|
+
if (p === 'top' && r.top < t.height + 10) p = 'bottom';
|
|
33
|
+
else if (p === 'bottom' && r.bottom + t.height + 10 > innerHeight) p = 'top';
|
|
34
|
+
else if (p === 'left' && r.left < t.width + 10) p = 'right';
|
|
35
|
+
else if (p === 'right' && r.right + t.width + 10 > innerWidth) p = 'left';
|
|
36
|
+
const cx = r.left + r.width / 2 - t.width / 2, cy = r.top + r.height / 2 - t.height / 2;
|
|
37
|
+
const pos = { top: [cx, r.top - t.height - 6], bottom: [cx, r.bottom + 6], left: [r.left - t.width - 6, cy], right: [r.right + 6, cy] }[p];
|
|
38
|
+
x = Math.min(Math.max(4, pos[0]), innerWidth - t.width - 4);
|
|
39
|
+
y = Math.max(4, pos[1]);
|
|
40
|
+
ready = true;
|
|
41
|
+
});
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<div class="tip" role="tooltip" {id} bind:this={tip} style:left="{x}px" style:top="{y}px" data-ready={ready}>
|
|
45
|
+
{#if text}<span class="txt">{text}</span>{/if}
|
|
46
|
+
{#if content}<span class="rich">{@render content()}</span>{/if}
|
|
47
|
+
{#if shortcut}<Kbd keys={shortcut} />{/if}
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<style>
|
|
51
|
+
.tip {
|
|
52
|
+
position: fixed; z-index: 200; max-width: 260px; padding: 4px 8px; pointer-events: none; visibility: hidden;
|
|
53
|
+
display: flex; align-items: center; flex-wrap: wrap; gap: 4px 8px;
|
|
54
|
+
background: var(--tr-on-surface); color: var(--tr-surface); border-radius: var(--tr-radius-sm);
|
|
55
|
+
font: 500 var(--tr-text-sm) / 1.35 var(--tr-font-sans); box-shadow: var(--tr-elevation-2);
|
|
56
|
+
}
|
|
57
|
+
.tip[data-ready='true'] { visibility: visible; animation: fade var(--tr-dur-fast) var(--tr-ease); }
|
|
58
|
+
@keyframes fade { from { opacity: 0; } }
|
|
59
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
anchor: HTMLElement;
|
|
4
|
+
id: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
content?: Snippet;
|
|
7
|
+
shortcut?: string;
|
|
8
|
+
placement?: 'top' | 'bottom' | 'left' | 'right';
|
|
9
|
+
};
|
|
10
|
+
declare const TooltipTip: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type TooltipTip = ReturnType<typeof TooltipTip>;
|
|
12
|
+
export default TooltipTip;
|
|
@@ -1,24 +1,45 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { tick } from 'svelte';
|
|
2
|
+
import { tick, type Snippet } from 'svelte';
|
|
3
3
|
import { SvelteMap } from 'svelte/reactivity';
|
|
4
4
|
import Icon from '../icons/Icon.svelte';
|
|
5
5
|
import Checkbox from './Checkbox.svelte';
|
|
6
6
|
import Spinner from './Spinner.svelte';
|
|
7
|
-
import
|
|
7
|
+
import Menu from './Menu.svelte';
|
|
8
|
+
import Popover from './Popover.svelte';
|
|
9
|
+
import type { TreeNode, MenuEntry } from '../types.js';
|
|
10
|
+
|
|
11
|
+
type DropPosition = 'before' | 'after' | 'inside';
|
|
8
12
|
|
|
9
13
|
let {
|
|
10
14
|
nodes,
|
|
11
15
|
expanded = $bindable([]),
|
|
12
16
|
selected = $bindable(),
|
|
17
|
+
multiple = false,
|
|
18
|
+
selectedIds = $bindable([]),
|
|
13
19
|
checkable = false,
|
|
14
20
|
checked = $bindable([]),
|
|
15
21
|
loadChildren,
|
|
16
22
|
label = 'Tree',
|
|
17
|
-
onselect
|
|
23
|
+
onselect,
|
|
24
|
+
onactivate,
|
|
25
|
+
nodeMenu,
|
|
26
|
+
nodeActions,
|
|
27
|
+
virtual = false,
|
|
28
|
+
height,
|
|
29
|
+
rowHeight,
|
|
30
|
+
overscan = 8,
|
|
31
|
+
draggable = false,
|
|
32
|
+
canDrop,
|
|
33
|
+
ondrop
|
|
18
34
|
}: {
|
|
19
35
|
nodes: TreeNode[];
|
|
20
36
|
expanded?: string[];
|
|
37
|
+
/** The single selected id, or with `multiple` the most recently picked ("lead") node. */
|
|
21
38
|
selected?: string;
|
|
39
|
+
/** Multi-select: click, Ctrl/Cmd+click toggles, Shift+click or Shift+Arrow selects a range, Ctrl+A selects all visible nodes. */
|
|
40
|
+
multiple?: boolean;
|
|
41
|
+
/** With `multiple`: the selected ids. */
|
|
42
|
+
selectedIds?: string[];
|
|
22
43
|
/** Tri-state checkboxes. `checked` holds leaf ids. */
|
|
23
44
|
checkable?: boolean;
|
|
24
45
|
checked?: string[];
|
|
@@ -26,15 +47,37 @@
|
|
|
26
47
|
loadChildren?: (node: TreeNode) => Promise<TreeNode[]>;
|
|
27
48
|
label?: string;
|
|
28
49
|
onselect?: (node: TreeNode) => void;
|
|
50
|
+
/** Enter or double-click on an enabled node ("open"). A double-click still toggles an expandable node as well. */
|
|
51
|
+
onactivate?: (node: TreeNode) => void;
|
|
52
|
+
/** Context menu for a node: right-click, the Menu key or Shift+F10. Return an empty list to keep the browser's menu. */
|
|
53
|
+
nodeMenu?: (node: TreeNode) => MenuEntry[];
|
|
54
|
+
/** Trailing actions, revealed on hover and when the row is focused (Tab moves from the row into them). */
|
|
55
|
+
nodeActions?: Snippet<[node: TreeNode]>;
|
|
56
|
+
/** Render only the rows in view (needs `height`; scrolls inside the tree). */
|
|
57
|
+
virtual?: boolean;
|
|
58
|
+
/** Height of the scrolling area (CSS length or px number). Also enables scrolling without `virtual`. */
|
|
59
|
+
height?: string | number;
|
|
60
|
+
/** Row height in px (default: the `--tr-tree-row-h` density token). */
|
|
61
|
+
rowHeight?: number;
|
|
62
|
+
/** Extra rows rendered above and below the visible window. */
|
|
63
|
+
overscan?: number;
|
|
64
|
+
/** Move nodes by pointer drag, or by keyboard: Ctrl+X to cut, then Ctrl+V (inside), Ctrl+Shift+V (after) or Ctrl+Alt+V (before) on the target. */
|
|
65
|
+
draggable?: boolean;
|
|
66
|
+
/** Veto a drop. A node is never offered as a target of itself or its descendants. */
|
|
67
|
+
canDrop?: (sources: TreeNode[], target: TreeNode, position: DropPosition) => boolean;
|
|
68
|
+
/** The tree does not rearrange `nodes`: update your data here. */
|
|
69
|
+
ondrop?: (sources: TreeNode[], target: TreeNode, position: DropPosition) => void;
|
|
29
70
|
} = $props();
|
|
30
71
|
|
|
31
72
|
const loaded = new SvelteMap<string, TreeNode[]>();
|
|
32
73
|
let loading = $state<string[]>([]);
|
|
33
74
|
let focusId = $state<string>();
|
|
34
75
|
let root: HTMLUListElement;
|
|
76
|
+
let scroller = $state<HTMLDivElement>();
|
|
35
77
|
|
|
36
78
|
const kids = (n: TreeNode) => n.children ?? loaded.get(n.id);
|
|
37
79
|
const expandable = (n: TreeNode) => !!kids(n)?.length || (!!n.hasChildren && !kids(n));
|
|
80
|
+
const openSet = $derived(new Set(expanded));
|
|
38
81
|
|
|
39
82
|
type Row = { node: TreeNode; depth: number; parent?: TreeNode; pos: number; size: number };
|
|
40
83
|
const rows = $derived.by(() => {
|
|
@@ -42,15 +85,48 @@
|
|
|
42
85
|
const walk = (list: TreeNode[], depth: number, parent?: TreeNode) =>
|
|
43
86
|
list.forEach((node, i) => {
|
|
44
87
|
out.push({ node, depth, parent, pos: i + 1, size: list.length });
|
|
45
|
-
if (
|
|
88
|
+
if (openSet.has(node.id) && kids(node)) walk(kids(node)!, depth + 1, node);
|
|
46
89
|
});
|
|
47
90
|
walk(nodes, 0);
|
|
48
91
|
return out;
|
|
49
92
|
});
|
|
50
|
-
const
|
|
93
|
+
const indexOf = $derived(new Map(rows.map((r, i) => [r.node.id, i])));
|
|
94
|
+
|
|
95
|
+
// ---------- windowing
|
|
96
|
+
let probe = $state<HTMLElement>();
|
|
97
|
+
let probeH = $state(28);
|
|
98
|
+
$effect(() => {
|
|
99
|
+
if (!probe) return;
|
|
100
|
+
const read = () => { const h = probe!.getBoundingClientRect().height; if (h > 0) probeH = h; };
|
|
101
|
+
read();
|
|
102
|
+
const ro = new ResizeObserver(read);
|
|
103
|
+
ro.observe(probe);
|
|
104
|
+
return () => ro.disconnect();
|
|
105
|
+
});
|
|
106
|
+
const rowH = $derived(rowHeight ?? probeH);
|
|
107
|
+
let viewH = $state(0);
|
|
108
|
+
let scrollTop = $state(0);
|
|
109
|
+
const start = $derived(virtual ? Math.max(0, Math.min(rows.length, Math.floor(scrollTop / rowH) - overscan)) : 0);
|
|
110
|
+
const end = $derived(virtual ? Math.min(rows.length, Math.ceil((scrollTop + (viewH || 320)) / rowH) + overscan) : rows.length);
|
|
111
|
+
const shown = $derived(rows.slice(start, end));
|
|
112
|
+
const tabbable = $derived.by(() => {
|
|
113
|
+
const i = focusId == null ? undefined : indexOf.get(focusId);
|
|
114
|
+
if (i !== undefined && i >= start && i < end) return focusId;
|
|
115
|
+
return rows[Math.min(start, rows.length - 1)]?.node.id;
|
|
116
|
+
});
|
|
117
|
+
const heightCss = $derived(height == null ? (virtual ? '20rem' : undefined) : typeof height === 'number' ? `${height}px` : height);
|
|
118
|
+
|
|
119
|
+
function reveal(i: number) {
|
|
120
|
+
if (!virtual || !scroller) return;
|
|
121
|
+
const top = i * rowH, bottom = top + rowH;
|
|
122
|
+
let next = scrollTop;
|
|
123
|
+
if (top < scrollTop) next = top;
|
|
124
|
+
else if (bottom > scrollTop + scroller.clientHeight) next = bottom - scroller.clientHeight;
|
|
125
|
+
if (next !== scrollTop) scroller.scrollTop = scrollTop = next;
|
|
126
|
+
}
|
|
51
127
|
|
|
52
128
|
async function expand(n: TreeNode) {
|
|
53
|
-
if (!
|
|
129
|
+
if (!openSet.has(n.id)) expanded = [...expanded, n.id];
|
|
54
130
|
if (loadChildren && n.hasChildren && !kids(n) && !loading.includes(n.id)) {
|
|
55
131
|
loading = [...loading, n.id];
|
|
56
132
|
try { loaded.set(n.id, await loadChildren(n)); }
|
|
@@ -59,7 +135,7 @@
|
|
|
59
135
|
}
|
|
60
136
|
}
|
|
61
137
|
const collapse = (n: TreeNode) => (expanded = expanded.filter((x) => x !== n.id));
|
|
62
|
-
const toggle = (n: TreeNode) => (
|
|
138
|
+
const toggle = (n: TreeNode) => (openSet.has(n.id) ? collapse(n) : expand(n));
|
|
63
139
|
|
|
64
140
|
function leaves(n: TreeNode): string[] {
|
|
65
141
|
const k = kids(n);
|
|
@@ -77,71 +153,294 @@
|
|
|
77
153
|
|
|
78
154
|
async function focusRow(id: string) {
|
|
79
155
|
focusId = id;
|
|
156
|
+
const i = indexOf.get(id);
|
|
157
|
+
if (i !== undefined) reveal(i);
|
|
80
158
|
await tick();
|
|
81
159
|
root.querySelector<HTMLElement>(`[data-id="${CSS.escape(id)}"]`)?.focus();
|
|
82
160
|
}
|
|
83
|
-
|
|
161
|
+
|
|
162
|
+
// ---------- selection
|
|
163
|
+
const selSet = $derived(new Set(multiple ? (selectedIds.length || !selected ? selectedIds : [selected]) : selected ? [selected] : []));
|
|
164
|
+
let anchor: string | undefined;
|
|
165
|
+
type Mods = { shift?: boolean; mod?: boolean };
|
|
166
|
+
function rangeIds(a: string, b: string) {
|
|
167
|
+
const i = indexOf.get(a) ?? indexOf.get(b)!, j = indexOf.get(b)!;
|
|
168
|
+
return rows.slice(Math.min(i, j), Math.max(i, j) + 1).filter((r) => !r.node.disabled).map((r) => r.node.id);
|
|
169
|
+
}
|
|
170
|
+
function pick(n: TreeNode, m: Mods = {}) {
|
|
84
171
|
if (n.disabled) return;
|
|
85
|
-
|
|
172
|
+
if (multiple) {
|
|
173
|
+
if (m.shift) selectedIds = rangeIds(anchor ?? selected ?? n.id, n.id);
|
|
174
|
+
else if (m.mod) {
|
|
175
|
+
const cur = [...selSet];
|
|
176
|
+
selectedIds = cur.includes(n.id) ? cur.filter((x) => x !== n.id) : [...cur, n.id];
|
|
177
|
+
anchor = n.id;
|
|
178
|
+
} else { selectedIds = [n.id]; anchor = n.id; }
|
|
179
|
+
selected = selectedIds.includes(n.id) ? n.id : selectedIds.at(-1);
|
|
180
|
+
} else selected = n.id;
|
|
86
181
|
onselect?.(n);
|
|
87
182
|
}
|
|
183
|
+
function selectAll() {
|
|
184
|
+
selectedIds = rows.filter((r) => !r.node.disabled).map((r) => r.node.id);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ---------- activation
|
|
188
|
+
function activate(n: TreeNode) {
|
|
189
|
+
if (n.disabled) return;
|
|
190
|
+
// Enter on a node inside a multi-selection acts on that selection instead of collapsing it
|
|
191
|
+
if (!(multiple && selSet.has(n.id))) pick(n);
|
|
192
|
+
onactivate?.(n);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------- node menu
|
|
196
|
+
let ctx = $state<{ x: number; y: number; items: MenuEntry[]; opener: HTMLElement } | null>(null);
|
|
197
|
+
let ctxAnchor = $state<HTMLElement>();
|
|
198
|
+
function openMenu(n: TreeNode, x: number, y: number, opener: HTMLElement) {
|
|
199
|
+
const items = nodeMenu?.(n) ?? [];
|
|
200
|
+
if (!items.length) return false; // nothing to offer: keep the browser's own menu
|
|
201
|
+
if (!selSet.has(n.id)) pick(n);
|
|
202
|
+
ctx = { x, y, items, opener };
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
function closeMenu(refocus: boolean) {
|
|
206
|
+
const o = ctx?.opener;
|
|
207
|
+
ctx = null;
|
|
208
|
+
if (refocus) o?.focus();
|
|
209
|
+
}
|
|
210
|
+
function onContext(e: MouseEvent, n: TreeNode) {
|
|
211
|
+
if (!nodeMenu || (e.target as Element).closest('.acts')) return;
|
|
212
|
+
if (openMenu(n, e.clientX, e.clientY, e.currentTarget as HTMLElement)) e.preventDefault();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---------- move: pointer drag + keyboard cut / paste
|
|
216
|
+
let live = $state('');
|
|
217
|
+
let flip = false;
|
|
218
|
+
function announce(msg: string) { live = msg + ((flip = !flip) ? '' : ' '); }
|
|
219
|
+
const nameOf = (list: TreeNode[]) => (list.length === 1 ? `"${list[0].label}"` : `${list.length} items`);
|
|
220
|
+
const rowOf = (id: string) => rows[indexOf.get(id)!];
|
|
221
|
+
function inside(id: string, ancestor: string) {
|
|
222
|
+
for (let r: Row | undefined = rowOf(id); r; r = r.parent ? rowOf(r.parent.id) : undefined) if (r.node.id === ancestor) return true;
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
/** Dragging a selection moves the whole selection; dragging an unselected node moves just it. */
|
|
226
|
+
function sourcesFor(id: string) {
|
|
227
|
+
const ids = multiple && selSet.has(id) ? rows.filter((r) => selSet.has(r.node.id) && !r.node.disabled).map((r) => r.node.id) : [id];
|
|
228
|
+
return ids.filter((x) => !ids.some((o) => o !== x && inside(x, o))); // a child travels with its dragged parent
|
|
229
|
+
}
|
|
230
|
+
function allowed(ids: string[], targetId: string, pos: DropPosition) {
|
|
231
|
+
if (!ids.length || !rowOf(targetId) || ids.some((s) => inside(targetId, s))) return false;
|
|
232
|
+
const t = rowOf(targetId).node;
|
|
233
|
+
if (t.disabled) return false;
|
|
234
|
+
return canDrop ? canDrop(ids.map((s) => rowOf(s).node), t, pos) !== false : true;
|
|
235
|
+
}
|
|
236
|
+
function drop(ids: string[], targetId: string, pos: DropPosition) {
|
|
237
|
+
const src = ids.map((s) => rowOf(s).node), t = rowOf(targetId).node;
|
|
238
|
+
ondrop?.(src, t, pos);
|
|
239
|
+
announce(`Moved ${nameOf(src)} ${pos === 'inside' ? 'into' : pos} "${t.label}"`);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let cut = $state<string[]>([]);
|
|
243
|
+
let drag = $state<{ ids: string[]; x: number; y: number; over?: { id: string; pos: DropPosition } } | null>(null);
|
|
244
|
+
let pending: { id: string; x: number; y: number } | null = null;
|
|
245
|
+
let suppressClick = false;
|
|
246
|
+
|
|
247
|
+
function pointerDown(e: PointerEvent, n: TreeNode) {
|
|
248
|
+
if (!draggable || e.button !== 0 || n.disabled || (e.target as Element).closest('.acts, .tw, .ck')) return;
|
|
249
|
+
pending = { id: n.id, x: e.clientX, y: e.clientY };
|
|
250
|
+
window.addEventListener('pointermove', pointerMove);
|
|
251
|
+
window.addEventListener('pointerup', pointerUp);
|
|
252
|
+
window.addEventListener('pointercancel', endDrag);
|
|
253
|
+
window.addEventListener('keydown', dragKey, true);
|
|
254
|
+
}
|
|
255
|
+
function pointerMove(e: PointerEvent) {
|
|
256
|
+
if (!pending) return;
|
|
257
|
+
if (!drag) {
|
|
258
|
+
if (Math.hypot(e.clientX - pending.x, e.clientY - pending.y) < 5) return;
|
|
259
|
+
drag = { ids: sourcesFor(pending.id), x: e.clientX, y: e.clientY };
|
|
260
|
+
}
|
|
261
|
+
let over: { id: string; pos: DropPosition } | undefined;
|
|
262
|
+
const li = document.elementFromPoint(e.clientX, e.clientY)?.closest<HTMLElement>('li[role="treeitem"]');
|
|
263
|
+
if (li && root.contains(li) && li.dataset.id) {
|
|
264
|
+
const r = li.getBoundingClientRect(), f = (e.clientY - r.top) / r.height;
|
|
265
|
+
const pos: DropPosition = f < 0.25 ? 'before' : f > 0.75 ? 'after' : 'inside';
|
|
266
|
+
for (const p of [pos, f < 0.5 ? 'before' : 'after'] as DropPosition[]) // fall back to the nearest edge
|
|
267
|
+
if (allowed(drag.ids, li.dataset.id, p)) { over = { id: li.dataset.id, pos: p }; break; }
|
|
268
|
+
}
|
|
269
|
+
drag = { ...drag, x: e.clientX, y: e.clientY, over };
|
|
270
|
+
if (scroller) { // edge auto-scroll
|
|
271
|
+
const r = scroller.getBoundingClientRect();
|
|
272
|
+
if (e.clientY < r.top + 24) scroller.scrollTop -= 12;
|
|
273
|
+
else if (e.clientY > r.bottom - 24) scroller.scrollTop += 12;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function swallowClick() { suppressClick = true; setTimeout(() => (suppressClick = false)); }
|
|
277
|
+
function pointerUp() {
|
|
278
|
+
const d = drag;
|
|
279
|
+
if (d) swallowClick();
|
|
280
|
+
endDrag();
|
|
281
|
+
if (d?.over) drop(d.ids, d.over.id, d.over.pos);
|
|
282
|
+
}
|
|
283
|
+
function dragKey(e: KeyboardEvent) {
|
|
284
|
+
if (e.key === 'Escape' && drag) { e.preventDefault(); e.stopPropagation(); swallowClick(); endDrag(); announce('Move cancelled'); }
|
|
285
|
+
}
|
|
286
|
+
function endDrag() {
|
|
287
|
+
pending = null;
|
|
288
|
+
drag = null;
|
|
289
|
+
window.removeEventListener('pointermove', pointerMove);
|
|
290
|
+
window.removeEventListener('pointerup', pointerUp);
|
|
291
|
+
window.removeEventListener('pointercancel', endDrag);
|
|
292
|
+
window.removeEventListener('keydown', dragKey, true);
|
|
293
|
+
}
|
|
294
|
+
$effect(() => endDrag);
|
|
295
|
+
|
|
296
|
+
function pasteOn(n: TreeNode, pos: DropPosition) {
|
|
297
|
+
if (!cut.length) return;
|
|
298
|
+
const ids = cut.filter((s) => rowOf(s));
|
|
299
|
+
if (!allowed(ids, n.id, pos)) { announce(`Cannot move ${nameOf(ids.map((s) => rowOf(s).node))} here`); return; }
|
|
300
|
+
cut = [];
|
|
301
|
+
drop(ids, n.id, pos);
|
|
302
|
+
}
|
|
303
|
+
|
|
88
304
|
function onKey(e: KeyboardEvent, i: number) {
|
|
89
305
|
const r = rows[i], n = r.node;
|
|
90
306
|
if (e.target !== e.currentTarget) return;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
307
|
+
const mod = e.ctrlKey || e.metaKey;
|
|
308
|
+
const go = (j: number) => {
|
|
309
|
+
const t = rows[Math.max(0, Math.min(rows.length - 1, j))];
|
|
310
|
+
if (!t) return;
|
|
311
|
+
focusRow(t.node.id);
|
|
312
|
+
if (multiple && e.shiftKey && !t.node.disabled) {
|
|
313
|
+
anchor ??= n.id;
|
|
314
|
+
selectedIds = rangeIds(anchor, t.node.id);
|
|
315
|
+
selected = t.node.id;
|
|
316
|
+
onselect?.(t.node);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
const page = Math.max(1, Math.floor((viewH || 280) / rowH) - 1);
|
|
320
|
+
if (e.key === 'ContextMenu' || (e.shiftKey && e.key === 'F10')) {
|
|
321
|
+
if (!nodeMenu) return;
|
|
322
|
+
const b = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
323
|
+
if (!openMenu(n, b.left + 24, b.top + b.height / 2, e.currentTarget as HTMLElement)) return;
|
|
324
|
+
}
|
|
325
|
+
else if (draggable && mod && !e.shiftKey && !e.altKey && e.key.toLowerCase() === 'x') {
|
|
326
|
+
if (n.disabled) return;
|
|
327
|
+
cut = sourcesFor(n.id);
|
|
328
|
+
announce(`${nameOf(cut.map((s) => rowOf(s).node))} cut. Move to a node and press Control V to move inside it.`);
|
|
329
|
+
}
|
|
330
|
+
else if (draggable && mod && e.key.toLowerCase() === 'v' && cut.length) pasteOn(n, e.altKey ? 'before' : e.shiftKey ? 'after' : 'inside');
|
|
331
|
+
else if (draggable && e.key === 'Escape' && cut.length) { cut = []; announce('Cut cancelled'); }
|
|
332
|
+
else if (multiple && mod && !e.shiftKey && e.key.toLowerCase() === 'a') selectAll();
|
|
333
|
+
else if (e.key === 'ArrowDown') go(i + 1);
|
|
334
|
+
else if (e.key === 'ArrowUp') go(i - 1);
|
|
335
|
+
else if (e.key === 'PageDown') go(i + page);
|
|
336
|
+
else if (e.key === 'PageUp') go(i - page);
|
|
337
|
+
else if (e.key === 'Home') go(0);
|
|
338
|
+
else if (e.key === 'End') go(rows.length - 1);
|
|
95
339
|
else if (e.key === 'ArrowRight') {
|
|
96
|
-
if (expandable(n) && !
|
|
97
|
-
else if (
|
|
340
|
+
if (expandable(n) && !openSet.has(n.id)) expand(n);
|
|
341
|
+
else if (openSet.has(n.id) && rows[i + 1]?.parent === n) focusRow(rows[i + 1].node.id);
|
|
98
342
|
} else if (e.key === 'ArrowLeft') {
|
|
99
|
-
if (
|
|
343
|
+
if (openSet.has(n.id)) collapse(n);
|
|
100
344
|
else if (r.parent) focusRow(r.parent.id);
|
|
101
|
-
} else if (e.key === 'Enter')
|
|
102
|
-
else if (e.key === ' ') {
|
|
345
|
+
} else if (e.key === 'Enter') activate(n);
|
|
346
|
+
else if (e.key === ' ') {
|
|
347
|
+
if (checkable) toggleCheck(n);
|
|
348
|
+
else if (multiple) pick(n, { mod: true });
|
|
349
|
+
else pick(n);
|
|
350
|
+
}
|
|
103
351
|
else return;
|
|
104
352
|
e.preventDefault();
|
|
105
353
|
}
|
|
106
354
|
</script>
|
|
107
355
|
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
aria-
|
|
116
|
-
|
|
117
|
-
|
|
356
|
+
<div class="wrap" style:--tr-tree-row-h={rowHeight != null ? `${rowHeight}px` : undefined}>
|
|
357
|
+
<span class="probe" bind:this={probe} aria-hidden="true"></span>
|
|
358
|
+
<div
|
|
359
|
+
class="scroll" data-scroll={heightCss ? '' : undefined} style:height={heightCss} bind:this={scroller} bind:clientHeight={viewH}
|
|
360
|
+
onscroll={(e) => { if (virtual) scrollTop = e.currentTarget.scrollTop; }}
|
|
361
|
+
>
|
|
362
|
+
<ul
|
|
363
|
+
class="tree" role="tree" aria-label={label} aria-multiselectable={multiple || checkable || undefined} bind:this={root}
|
|
364
|
+
data-virtual={virtual || undefined} data-dragging={drag ? '' : undefined}
|
|
365
|
+
style:padding-block-start={virtual ? `${start * rowH}px` : undefined}
|
|
366
|
+
style:padding-block-end={virtual ? `${(rows.length - end) * rowH}px` : undefined}
|
|
118
367
|
>
|
|
119
|
-
{#
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
368
|
+
{#each shown as r, k (r.node.id)}
|
|
369
|
+
{@const n = r.node}
|
|
370
|
+
{@const i = start + k}
|
|
371
|
+
{@const sel = selSet.has(n.id)}
|
|
372
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
373
|
+
<li
|
|
374
|
+
role="treeitem" data-id={n.id} aria-level={r.depth + 1} aria-setsize={r.size} aria-posinset={r.pos}
|
|
375
|
+
aria-expanded={expandable(n) ? openSet.has(n.id) : undefined} aria-selected={sel}
|
|
376
|
+
aria-disabled={n.disabled} tabindex={n.id === tabbable ? 0 : -1} data-selected={sel}
|
|
377
|
+
data-cut={cut.includes(n.id) || (drag?.ids.includes(n.id) ?? false) || undefined}
|
|
378
|
+
data-drop={drag?.over?.id === n.id ? drag.over.pos : undefined}
|
|
379
|
+
style:padding-inline-start="{r.depth * 18 + 4}px"
|
|
380
|
+
onkeydown={(e) => onKey(e, i)} onfocus={() => (focusId = n.id)}
|
|
381
|
+
onclick={(e) => { if (suppressClick) return; focusId = n.id; pick(n, { shift: e.shiftKey, mod: e.ctrlKey || e.metaKey }); }}
|
|
382
|
+
ondblclick={() => { if (expandable(n)) toggle(n); activate(n); }}
|
|
383
|
+
oncontextmenu={(e) => onContext(e, n)}
|
|
384
|
+
onpointerdown={(e) => pointerDown(e, n)}
|
|
385
|
+
>
|
|
386
|
+
{#if expandable(n)}
|
|
387
|
+
<button type="button" class="tw" tabindex="-1" aria-hidden="true" onclick={(e) => { e.stopPropagation(); toggle(n); }}>
|
|
388
|
+
{#if loading.includes(n.id)}<Spinner size={14} />{:else}<Icon name={openSet.has(n.id) ? 'chevron-down' : 'chevron-right'} />{/if}
|
|
389
|
+
</button>
|
|
390
|
+
{:else}<span class="tw"></span>{/if}
|
|
391
|
+
{#if checkable}
|
|
392
|
+
<span class="ck" onclick={(e) => e.stopPropagation()} role="presentation">
|
|
393
|
+
<Checkbox tabindex={-1} aria-label="Select {n.label}" disabled={n.disabled} checked={checkState(n) === 'all'} indeterminate={checkState(n) === 'some'} onchange={() => toggleCheck(n)} />
|
|
394
|
+
</span>
|
|
395
|
+
{/if}
|
|
396
|
+
{#if n.icon}<Icon name={n.icon} />{:else if expandable(n) || kids(n)}<Icon name="folder" />{:else}<Icon name="file" />{/if}
|
|
397
|
+
<span class="lbl">{n.label}</span>
|
|
398
|
+
{#if nodeActions && !n.disabled}
|
|
399
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
400
|
+
<span class="acts" onclick={(e) => e.stopPropagation()} ondblclick={(e) => e.stopPropagation()} onpointerdown={(e) => e.stopPropagation()}>{@render nodeActions(n)}</span>
|
|
401
|
+
{/if}
|
|
402
|
+
</li>
|
|
403
|
+
{/each}
|
|
404
|
+
</ul>
|
|
405
|
+
</div>
|
|
406
|
+
<div class="sr" role="status" aria-live="polite">{live}</div>
|
|
407
|
+
{#if drag}<div class="ghost" aria-hidden="true" style:left="{drag.x + 12}px" style:top="{drag.y + 12}px">{drag.ids.length === 1 ? rowOf(drag.ids[0])?.node.label : `${drag.ids.length} items`}</div>{/if}
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
{#if ctx}
|
|
411
|
+
<div class="ctxpt" bind:this={ctxAnchor} style:left="{ctx.x}px" style:top="{ctx.y}px" aria-hidden="true"></div>
|
|
412
|
+
{#if ctxAnchor}
|
|
413
|
+
<Popover open={true} fixed anchor={ctxAnchor} onclose={closeMenu}>
|
|
414
|
+
<Menu items={ctx.items} label="Node actions" autofocus onclose={() => closeMenu(true)} />
|
|
415
|
+
</Popover>
|
|
416
|
+
{/if}
|
|
417
|
+
{/if}
|
|
134
418
|
|
|
135
419
|
<style>
|
|
420
|
+
.wrap { position: relative; }
|
|
421
|
+
.probe { position: absolute; width: 0; height: var(--tr-tree-row-h); visibility: hidden; pointer-events: none; }
|
|
422
|
+
.scroll[data-scroll] { overflow: auto; }
|
|
136
423
|
.tree { margin: 0; padding: 4px; list-style: none; }
|
|
137
|
-
|
|
424
|
+
.tree[data-virtual] { padding-block: 0; }
|
|
425
|
+
li { position: relative; display: flex; align-items: center; gap: 6px; height: var(--tr-tree-row-h); box-sizing: border-box; padding-inline-end: 8px; border-radius: var(--tr-radius-sm); color: var(--tr-on-surface); cursor: default; outline: none; user-select: none; }
|
|
138
426
|
li:hover { background: var(--tr-surface-container-high); }
|
|
139
427
|
li[data-selected='true'] { background: color-mix(in srgb, var(--tr-primary-container) 60%, transparent); box-shadow: inset 3px 0 0 var(--tr-primary); }
|
|
140
428
|
li:focus-visible { box-shadow: inset 0 0 0 2px var(--tr-primary); }
|
|
141
429
|
li[aria-disabled='true'] { opacity: 0.45; }
|
|
430
|
+
li[data-cut] { opacity: 0.5; }
|
|
431
|
+
li[data-drop='inside'] { outline: 2px solid var(--tr-primary); outline-offset: -2px; background: color-mix(in srgb, var(--tr-primary-container) 40%, transparent); }
|
|
432
|
+
li[data-drop='before']::after, li[data-drop='after']::after { content: ''; position: absolute; inset-inline: 0; height: 2px; background: var(--tr-primary); pointer-events: none; }
|
|
433
|
+
li[data-drop='before']::after { top: -1px; }
|
|
434
|
+
li[data-drop='after']::after { bottom: -1px; }
|
|
142
435
|
.tw { display: inline-grid; place-items: center; flex: none; width: 20px; height: 20px; padding: 0; border: 0; border-radius: var(--tr-radius-sm); background: none; color: var(--tr-on-surface-variant); cursor: pointer; }
|
|
143
436
|
button.tw:hover { background: var(--tr-surface-container-highest); }
|
|
144
437
|
.ck { display: inline-flex; }
|
|
145
438
|
li > :global(svg) { color: var(--tr-on-surface-variant); }
|
|
146
439
|
.lbl { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
440
|
+
/* hidden actions are also unfocusable, so Tab only enters the actions of the focused / hovered row */
|
|
441
|
+
.acts { display: inline-flex; flex: none; align-items: center; gap: 2px; visibility: hidden; }
|
|
442
|
+
li:hover .acts, li:focus-within .acts { visibility: visible; }
|
|
443
|
+
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
444
|
+
.ghost { position: fixed; z-index: 400; pointer-events: none; padding: 2px 10px; border-radius: var(--tr-radius-sm); background: var(--tr-surface-container-highest); color: var(--tr-on-surface); box-shadow: 0 2px 8px rgb(0 0 0 / 0.3); font-size: 0.85em; white-space: nowrap; }
|
|
445
|
+
.ctxpt { position: fixed; width: 0; height: 0; pointer-events: none; }
|
|
147
446
|
</style>
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import type { TreeNode, MenuEntry } from '../types.js';
|
|
3
|
+
type DropPosition = 'before' | 'after' | 'inside';
|
|
2
4
|
type $$ComponentProps = {
|
|
3
5
|
nodes: TreeNode[];
|
|
4
6
|
expanded?: string[];
|
|
7
|
+
/** The single selected id, or with `multiple` the most recently picked ("lead") node. */
|
|
5
8
|
selected?: string;
|
|
9
|
+
/** Multi-select: click, Ctrl/Cmd+click toggles, Shift+click or Shift+Arrow selects a range, Ctrl+A selects all visible nodes. */
|
|
10
|
+
multiple?: boolean;
|
|
11
|
+
/** With `multiple`: the selected ids. */
|
|
12
|
+
selectedIds?: string[];
|
|
6
13
|
/** Tri-state checkboxes. `checked` holds leaf ids. */
|
|
7
14
|
checkable?: boolean;
|
|
8
15
|
checked?: string[];
|
|
@@ -10,7 +17,27 @@ type $$ComponentProps = {
|
|
|
10
17
|
loadChildren?: (node: TreeNode) => Promise<TreeNode[]>;
|
|
11
18
|
label?: string;
|
|
12
19
|
onselect?: (node: TreeNode) => void;
|
|
20
|
+
/** Enter or double-click on an enabled node ("open"). A double-click still toggles an expandable node as well. */
|
|
21
|
+
onactivate?: (node: TreeNode) => void;
|
|
22
|
+
/** Context menu for a node: right-click, the Menu key or Shift+F10. Return an empty list to keep the browser's menu. */
|
|
23
|
+
nodeMenu?: (node: TreeNode) => MenuEntry[];
|
|
24
|
+
/** Trailing actions, revealed on hover and when the row is focused (Tab moves from the row into them). */
|
|
25
|
+
nodeActions?: Snippet<[node: TreeNode]>;
|
|
26
|
+
/** Render only the rows in view (needs `height`; scrolls inside the tree). */
|
|
27
|
+
virtual?: boolean;
|
|
28
|
+
/** Height of the scrolling area (CSS length or px number). Also enables scrolling without `virtual`. */
|
|
29
|
+
height?: string | number;
|
|
30
|
+
/** Row height in px (default: the `--tr-tree-row-h` density token). */
|
|
31
|
+
rowHeight?: number;
|
|
32
|
+
/** Extra rows rendered above and below the visible window. */
|
|
33
|
+
overscan?: number;
|
|
34
|
+
/** Move nodes by pointer drag, or by keyboard: Ctrl+X to cut, then Ctrl+V (inside), Ctrl+Shift+V (after) or Ctrl+Alt+V (before) on the target. */
|
|
35
|
+
draggable?: boolean;
|
|
36
|
+
/** Veto a drop. A node is never offered as a target of itself or its descendants. */
|
|
37
|
+
canDrop?: (sources: TreeNode[], target: TreeNode, position: DropPosition) => boolean;
|
|
38
|
+
/** The tree does not rearrange `nodes`: update your data here. */
|
|
39
|
+
ondrop?: (sources: TreeNode[], target: TreeNode, position: DropPosition) => void;
|
|
13
40
|
};
|
|
14
|
-
declare const Tree: import("svelte").Component<$$ComponentProps, {}, "checked" | "expanded" | "selected">;
|
|
41
|
+
declare const Tree: import("svelte").Component<$$ComponentProps, {}, "checked" | "expanded" | "selected" | "selectedIds">;
|
|
15
42
|
type Tree = ReturnType<typeof Tree>;
|
|
16
43
|
export default Tree;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure layout engine behind GridLayout: integer grid cells, collision resolution, vertical compaction,
|
|
3
|
+
* size clamping and responsive column mapping. Nothing here touches the DOM and no function mutates its input.
|
|
4
|
+
*/
|
|
5
|
+
export type GridItem = {
|
|
6
|
+
id: string;
|
|
7
|
+
/** Column of the left edge (0-based). */
|
|
8
|
+
x: number;
|
|
9
|
+
/** Row of the top edge (0-based). */
|
|
10
|
+
y: number;
|
|
11
|
+
/** Width in columns. */
|
|
12
|
+
w: number;
|
|
13
|
+
/** Height in rows. */
|
|
14
|
+
h: number;
|
|
15
|
+
minW?: number;
|
|
16
|
+
minH?: number;
|
|
17
|
+
maxW?: number;
|
|
18
|
+
maxH?: number;
|
|
19
|
+
/** Never moved or resized, and other tiles flow around it. */
|
|
20
|
+
static?: boolean;
|
|
21
|
+
};
|
|
22
|
+
/** Whether two items overlap (touching edges do not count). */
|
|
23
|
+
export declare function collides(a: GridItem, b: GridItem): boolean;
|
|
24
|
+
/** Number of rows the layout occupies. */
|
|
25
|
+
export declare function bottom(items: GridItem[]): number;
|
|
26
|
+
/** Clamp one item into `cols` columns: size within its min/max (and the grid), position inside the grid. */
|
|
27
|
+
export declare function clampItem(item: GridItem, cols: number): GridItem;
|
|
28
|
+
/** Clamp every item and compact the layout vertically. */
|
|
29
|
+
export declare function compact(items: GridItem[], cols: number): GridItem[];
|
|
30
|
+
/** Fit a layout to a (possibly narrower) column count: clamp, resolve collisions, compact. */
|
|
31
|
+
export declare const fitToCols: typeof compact;
|
|
32
|
+
/** Move an item to (x, y): clamped to the grid, colliding items are pushed down, the rest compact around it. */
|
|
33
|
+
export declare function moveItem(items: GridItem[], id: string, x: number, y: number, cols: number): GridItem[];
|
|
34
|
+
/** Resize an item to w by h cells, within its min/max and the grid edge; colliding items are pushed down. */
|
|
35
|
+
export declare function resizeItem(items: GridItem[], id: string, w: number, h: number, cols: number): GridItem[];
|
|
36
|
+
/**
|
|
37
|
+
* Columns for a container width. `breakpoints` maps a minimum width in px to a column count; the largest
|
|
38
|
+
* key not above `width` wins, and a width below every key uses the smallest key. Without breakpoints it is `fallback`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function colsFor(width: number, breakpoints: Record<number, number> | undefined, fallback: number): number;
|
|
41
|
+
/** Whether two layouts place every item identically. */
|
|
42
|
+
export declare function sameLayout(a: GridItem[], b: GridItem[]): boolean;
|