bo-grid 0.1.0 → 0.2.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 +110 -25
- package/dist/grid/Cell.svelte +124 -10
- package/dist/grid/Cell.svelte.d.ts +17 -1
- package/dist/grid/Grid.svelte +398 -31
- package/dist/grid/Grid.svelte.d.ts +39 -0
- package/dist/grid/Pager.svelte +59 -0
- package/dist/grid/Pager.svelte.d.ts +9 -0
- package/dist/grid/RowMenu.svelte +66 -0
- package/dist/grid/RowMenu.svelte.d.ts +12 -0
- package/dist/grid/column.d.ts +28 -5
- package/dist/grid/column.js +7 -5
- package/dist/grid/export.js +1 -1
- package/dist/grid/grouping.d.ts +2 -0
- package/dist/grid/pin.d.ts +10 -6
- package/dist/grid/pin.js +38 -15
- package/dist/grid/sizing.d.ts +2 -2
- package/dist/grid/sizing.js +4 -3
- package/dist/grid/tree.d.ts +12 -0
- package/dist/grid/tree.js +21 -0
- package/package.json +3 -1
|
@@ -54,6 +54,45 @@ type $$ComponentProps = {
|
|
|
54
54
|
/** Show a per-column filter input row under the header. Rows must match every
|
|
55
55
|
non-empty column filter (AND). In-memory mode only. Default false. */
|
|
56
56
|
filterRow?: boolean;
|
|
57
|
+
/** Message shown when there are no rows. Default 'No matching rows'. */
|
|
58
|
+
emptyMessage?: string;
|
|
59
|
+
/** Show a loading overlay over the grid (for consumer-driven async work in
|
|
60
|
+
in-memory mode; source mode shows skeleton rows automatically). */
|
|
61
|
+
loading?: boolean;
|
|
62
|
+
/** Right-click row menu. Return the items for a row; an empty array shows no
|
|
63
|
+
menu. Each item runs `onSelect` and closes the menu. */
|
|
64
|
+
rowMenu?: (row: GridRow) => Array<{
|
|
65
|
+
label: string;
|
|
66
|
+
onSelect: () => void;
|
|
67
|
+
}>;
|
|
68
|
+
/** Master-detail: render an expandable detail panel under a row. Adds a
|
|
69
|
+
leading expand-toggle column. In-memory mode only (overrides rowHeight). */
|
|
70
|
+
detail?: Snippet<[{
|
|
71
|
+
row: GridRow;
|
|
72
|
+
}]>;
|
|
73
|
+
/** Height (px) of the expanded detail panel. Default 160. */
|
|
74
|
+
detailHeight?: number;
|
|
75
|
+
/** Tree data: return a row's children (undefined/empty = leaf). When set,
|
|
76
|
+
`rows` are the roots; the grid renders an indented, expandable tree.
|
|
77
|
+
In-memory mode; filter/sort/group/paginate are not applied to the tree. */
|
|
78
|
+
getChildren?: (row: GridRow) => GridRow[] | undefined;
|
|
79
|
+
/** Enable drag-to-reorder rows via a handle in the first column. Called with
|
|
80
|
+
the from/to indices (into the visible rows) on drop — reorder your own
|
|
81
|
+
`rows` in here. Flat, unsorted, in-memory lists only. */
|
|
82
|
+
onRowReorder?: (fromIndex: number, toIndex: number) => void;
|
|
83
|
+
/** Rows per page. When > 0 (in-memory mode), shows a pager instead of one
|
|
84
|
+
long scroll; rows still virtualize within a page. Default 0 (off). */
|
|
85
|
+
pageSize?: number;
|
|
86
|
+
/** Controlled current page (0-based). Omit for uncontrolled paging. */
|
|
87
|
+
page?: number;
|
|
88
|
+
/** Called with the new page index when the pager is used. */
|
|
89
|
+
onPageChange?: (page: number) => void;
|
|
90
|
+
/** Called with the new column-key order after a header drag-reorder. */
|
|
91
|
+
onColumnReorder?: (keys: string[]) => void;
|
|
92
|
+
/** Called with a column key + new width after a drag-resize. */
|
|
93
|
+
onColumnResize?: (key: string, width: number) => void;
|
|
94
|
+
/** Accessible name for the grid (`aria-label` on the `role="grid"` root). */
|
|
95
|
+
ariaLabel?: string;
|
|
57
96
|
filter?: string;
|
|
58
97
|
groupBy?: string[];
|
|
59
98
|
aggregations?: AggKind[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Pagination bar for the grid. Presentation-only: the parent owns the page
|
|
3
|
+
// state and reorders via `onGoto`.
|
|
4
|
+
let {
|
|
5
|
+
page,
|
|
6
|
+
pageCount,
|
|
7
|
+
total,
|
|
8
|
+
onGoto,
|
|
9
|
+
}: {
|
|
10
|
+
page: number;
|
|
11
|
+
pageCount: number;
|
|
12
|
+
total: number;
|
|
13
|
+
onGoto: (page: number) => void;
|
|
14
|
+
} = $props();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div class="pager" role="navigation" aria-label="Pagination">
|
|
18
|
+
<button type="button" class="pg" disabled={page === 0} aria-label="First page" onclick={() => onGoto(0)}>«</button>
|
|
19
|
+
<button type="button" class="pg" disabled={page === 0} onclick={() => onGoto(page - 1)}>‹ Prev</button>
|
|
20
|
+
<span class="pageinfo">Page {page + 1} of {pageCount} · {total.toLocaleString()} rows</span>
|
|
21
|
+
<button type="button" class="pg" disabled={page >= pageCount - 1} onclick={() => onGoto(page + 1)}>Next ›</button>
|
|
22
|
+
<button type="button" class="pg" disabled={page >= pageCount - 1} aria-label="Last page" onclick={() => onGoto(pageCount - 1)}>»</button>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<style>
|
|
26
|
+
.pager {
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: 8px;
|
|
30
|
+
padding: 6px 10px;
|
|
31
|
+
background: var(--bo-header-bg);
|
|
32
|
+
border-top: 0.5px solid var(--bo-border);
|
|
33
|
+
}
|
|
34
|
+
.pg {
|
|
35
|
+
padding: 3px 9px;
|
|
36
|
+
font: inherit;
|
|
37
|
+
font-family: var(--bo-mono);
|
|
38
|
+
font-size: 11px;
|
|
39
|
+
color: var(--bo-text);
|
|
40
|
+
background: transparent;
|
|
41
|
+
border: 0.5px solid var(--bo-border);
|
|
42
|
+
border-radius: 6px;
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
}
|
|
45
|
+
.pg:hover:not(:disabled) {
|
|
46
|
+
background: var(--bo-row-hover);
|
|
47
|
+
border-color: var(--bo-sel-border);
|
|
48
|
+
}
|
|
49
|
+
.pg:disabled {
|
|
50
|
+
opacity: 0.4;
|
|
51
|
+
cursor: not-allowed;
|
|
52
|
+
}
|
|
53
|
+
.pageinfo {
|
|
54
|
+
font-family: var(--bo-mono);
|
|
55
|
+
font-size: 11px;
|
|
56
|
+
color: var(--bo-text-dim);
|
|
57
|
+
font-variant-numeric: tabular-nums;
|
|
58
|
+
}
|
|
59
|
+
</style>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Floating right-click row menu. Presentation-only: the parent owns open/close
|
|
3
|
+
// state and positions it via x/y.
|
|
4
|
+
let {
|
|
5
|
+
x,
|
|
6
|
+
y,
|
|
7
|
+
items,
|
|
8
|
+
onClose,
|
|
9
|
+
}: {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
items: Array<{ label: string; onSelect: () => void }>;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
} = $props();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div
|
|
18
|
+
class="rowmenu"
|
|
19
|
+
role="menu"
|
|
20
|
+
tabindex="-1"
|
|
21
|
+
style="left:{x}px;top:{y}px;"
|
|
22
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
23
|
+
>
|
|
24
|
+
{#each items as item (item.label)}
|
|
25
|
+
<button
|
|
26
|
+
class="rowmenu-item"
|
|
27
|
+
type="button"
|
|
28
|
+
role="menuitem"
|
|
29
|
+
onclick={() => {
|
|
30
|
+
item.onSelect();
|
|
31
|
+
onClose();
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
{item.label}
|
|
35
|
+
</button>
|
|
36
|
+
{/each}
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<style>
|
|
40
|
+
.rowmenu {
|
|
41
|
+
position: fixed;
|
|
42
|
+
z-index: 20;
|
|
43
|
+
min-width: 150px;
|
|
44
|
+
padding: 4px;
|
|
45
|
+
background: var(--bo-header-bg);
|
|
46
|
+
border: 0.5px solid var(--bo-border);
|
|
47
|
+
border-radius: 8px;
|
|
48
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
|
49
|
+
}
|
|
50
|
+
.rowmenu-item {
|
|
51
|
+
display: block;
|
|
52
|
+
width: 100%;
|
|
53
|
+
padding: 6px 10px;
|
|
54
|
+
font: inherit;
|
|
55
|
+
font-size: 12px;
|
|
56
|
+
text-align: left;
|
|
57
|
+
color: var(--bo-text);
|
|
58
|
+
background: transparent;
|
|
59
|
+
border: 0;
|
|
60
|
+
border-radius: 5px;
|
|
61
|
+
cursor: pointer;
|
|
62
|
+
}
|
|
63
|
+
.rowmenu-item:hover {
|
|
64
|
+
background: var(--bo-row-hover);
|
|
65
|
+
}
|
|
66
|
+
</style>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
items: Array<{
|
|
5
|
+
label: string;
|
|
6
|
+
onSelect: () => void;
|
|
7
|
+
}>;
|
|
8
|
+
onClose: () => void;
|
|
9
|
+
};
|
|
10
|
+
declare const RowMenu: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
11
|
+
type RowMenu = ReturnType<typeof RowMenu>;
|
|
12
|
+
export default RowMenu;
|
package/dist/grid/column.d.ts
CHANGED
|
@@ -8,22 +8,45 @@ interface ColBase {
|
|
|
8
8
|
header: string;
|
|
9
9
|
/** Fixed width in px. Ignored when `flex` is set. */
|
|
10
10
|
width?: number;
|
|
11
|
+
/** Min/max width (px) enforced while drag-resizing this column. */
|
|
12
|
+
minWidth?: number;
|
|
13
|
+
maxWidth?: number;
|
|
11
14
|
/** flex-grow weight; column stretches to fill remaining space. */
|
|
12
15
|
flex?: number;
|
|
13
16
|
align?: Align;
|
|
17
|
+
/** Extra class(es) for this column's data cells — a static string, or a
|
|
18
|
+
function of the cell value/row for conditional styling. Target via
|
|
19
|
+
`:global(.bo-grid .c.your-class)`. */
|
|
20
|
+
cellClass?: string | ((value: unknown, row: GridRow) => string | undefined);
|
|
21
|
+
/** Extra class(es) for this column's header. Target via `:global`. */
|
|
22
|
+
headerClass?: string;
|
|
14
23
|
/** Amber flash on value change (drives off the row's flashSeq/flashDir). */
|
|
15
24
|
flash?: boolean;
|
|
16
25
|
/** Set false to disable header-click sorting on this column. */
|
|
17
26
|
sortable?: boolean;
|
|
27
|
+
/** Custom ascending comparator for this column's values (e.g. enum priority or
|
|
28
|
+
natural sort). Direction is applied by the grid. In-memory mode only. */
|
|
29
|
+
compare?: (a: unknown, b: unknown) => number;
|
|
18
30
|
/** When grouping is active, show this aggregate of the column on group headers. */
|
|
19
31
|
groupAgg?: AggKind;
|
|
20
|
-
/** Pin this column
|
|
21
|
-
|
|
32
|
+
/** Pin this column so it stays visible during horizontal scroll. `true` /
|
|
33
|
+
`'left'` pins to the left edge, `'right'` to the right. */
|
|
34
|
+
pinned?: boolean | 'left' | 'right';
|
|
22
35
|
/** Allow inline editing (double-click or Enter on the focused cell). */
|
|
23
36
|
editable?: boolean;
|
|
24
37
|
/** Validate an edited value before it commits (inline edit or paste). Return
|
|
25
38
|
false to reject and keep the old value. Receives the coerced value. */
|
|
26
39
|
validate?: (value: string | number, row: GridRow) => boolean;
|
|
40
|
+
/** Editable choices: when set, editing renders a `<select>` of these options
|
|
41
|
+
instead of a text input (enum/status columns). */
|
|
42
|
+
options?: string[];
|
|
43
|
+
/** Set a native `title` tooltip on each cell (the full formatted value) — handy
|
|
44
|
+
when content truncates. */
|
|
45
|
+
tooltip?: boolean;
|
|
46
|
+
/** Custom display formatter, overriding the built-in type formatter. Applies
|
|
47
|
+
to display, tooltip, copy and (formatted) export. `row` is absent for
|
|
48
|
+
aggregate cells. */
|
|
49
|
+
format?: (value: unknown, row?: GridRow) => string;
|
|
27
50
|
/** Set false to disable drag-to-resize on this column (default on). */
|
|
28
51
|
resizable?: boolean;
|
|
29
52
|
/** Parent header label. Consecutive columns sharing a `group` render under a
|
|
@@ -78,15 +101,15 @@ export interface GridRow {
|
|
|
78
101
|
flashDir: 'up' | 'down';
|
|
79
102
|
[field: string]: unknown;
|
|
80
103
|
}
|
|
81
|
-
export declare function formatCell(col: ColumnDef, value: unknown): string;
|
|
104
|
+
export declare function formatCell(col: ColumnDef, value: unknown, row?: GridRow): string;
|
|
82
105
|
export declare function isSortable(col: ColumnDef): boolean;
|
|
83
106
|
export declare function isEditable(col: ColumnDef): boolean;
|
|
84
|
-
export declare function compareRows(a: GridRow, b: GridRow, sort: SortState): number;
|
|
107
|
+
export declare function compareRows(a: GridRow, b: GridRow, sort: SortState, col?: ColumnDef): number;
|
|
85
108
|
/**
|
|
86
109
|
* Multi-key comparison: apply each sort in order, returning the first that
|
|
87
110
|
* separates the rows. An empty list leaves rows in their original order.
|
|
88
111
|
*/
|
|
89
|
-
export declare function compareBySorts(a: GridRow, b: GridRow, sorts: readonly SortState[]): number;
|
|
112
|
+
export declare function compareBySorts(a: GridRow, b: GridRow, sorts: readonly SortState[], colOf?: (key: string) => ColumnDef | undefined): number;
|
|
90
113
|
export declare function colStyle(col: ColumnDef): string;
|
|
91
114
|
/** Concrete pixel width for a column (flex columns get a sensible default). */
|
|
92
115
|
export declare function colWidth(col: ColumnDef): number;
|
package/dist/grid/column.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { fmtPrice, fmtPercent, fmtVolume, fmtDate } from '../format/format';
|
|
2
|
-
export function formatCell(col, value) {
|
|
2
|
+
export function formatCell(col, value, row) {
|
|
3
|
+
if (col.format)
|
|
4
|
+
return col.format(value, row);
|
|
3
5
|
const n = typeof value === 'number' ? value : Number(value);
|
|
4
6
|
switch (col.type) {
|
|
5
7
|
case 'price':
|
|
@@ -29,17 +31,17 @@ function rawCompare(a, b) {
|
|
|
29
31
|
return a - b;
|
|
30
32
|
return String(a ?? '').localeCompare(String(b ?? ''));
|
|
31
33
|
}
|
|
32
|
-
export function compareRows(a, b, sort) {
|
|
33
|
-
const d = rawCompare(a[sort.key], b[sort.key]);
|
|
34
|
+
export function compareRows(a, b, sort, col) {
|
|
35
|
+
const d = col?.compare ? col.compare(a[sort.key], b[sort.key]) : rawCompare(a[sort.key], b[sort.key]);
|
|
34
36
|
return sort.dir === 'asc' ? d : -d;
|
|
35
37
|
}
|
|
36
38
|
/**
|
|
37
39
|
* Multi-key comparison: apply each sort in order, returning the first that
|
|
38
40
|
* separates the rows. An empty list leaves rows in their original order.
|
|
39
41
|
*/
|
|
40
|
-
export function compareBySorts(a, b, sorts) {
|
|
42
|
+
export function compareBySorts(a, b, sorts, colOf) {
|
|
41
43
|
for (const sort of sorts) {
|
|
42
|
-
const d = compareRows(a, b, sort);
|
|
44
|
+
const d = compareRows(a, b, sort, colOf?.(sort.key));
|
|
43
45
|
if (d !== 0)
|
|
44
46
|
return d;
|
|
45
47
|
}
|
package/dist/grid/export.js
CHANGED
|
@@ -19,7 +19,7 @@ export function rowsToMatrix(rows, columns, opts = {}) {
|
|
|
19
19
|
if (opts.header !== false)
|
|
20
20
|
matrix.push(cols.map((c) => c.header));
|
|
21
21
|
for (const row of rows) {
|
|
22
|
-
matrix.push(cols.map((c) => (opts.formatted ? formatCell(c, row[c.key]) : rawValue(c, row[c.key]))));
|
|
22
|
+
matrix.push(cols.map((c) => (opts.formatted ? formatCell(c, row[c.key], row) : rawValue(c, row[c.key]))));
|
|
23
23
|
}
|
|
24
24
|
return matrix;
|
|
25
25
|
}
|
package/dist/grid/grouping.d.ts
CHANGED
package/dist/grid/pin.d.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import type { ColumnDef } from './column';
|
|
2
|
+
export type PinSide = 'left' | 'right';
|
|
2
3
|
export interface PinInfo {
|
|
3
4
|
pinned: boolean;
|
|
4
|
-
/**
|
|
5
|
+
/** Which edge the column is pinned to, or null when not pinned. */
|
|
6
|
+
side: PinSide | null;
|
|
7
|
+
/** Sticky `left` offset (px) when side === 'left'. */
|
|
5
8
|
left: number;
|
|
9
|
+
/** Sticky `right` offset (px) when side === 'right'. */
|
|
10
|
+
right: number;
|
|
6
11
|
/** Concrete width (px). */
|
|
7
12
|
width: number;
|
|
8
13
|
}
|
|
9
14
|
export interface PinLayout {
|
|
10
|
-
/** Columns
|
|
15
|
+
/** Columns reordered: left-pinned first, then unpinned, then right-pinned. */
|
|
11
16
|
columns: ColumnDef[];
|
|
12
17
|
info: PinInfo[];
|
|
13
18
|
/** Sum of all column widths (the horizontally-scrollable content width). */
|
|
@@ -15,9 +20,8 @@ export interface PinLayout {
|
|
|
15
20
|
anyPinned: boolean;
|
|
16
21
|
}
|
|
17
22
|
/**
|
|
18
|
-
* Arrange columns for pinning: pinned
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* default fit-to-width layout.
|
|
23
|
+
* Arrange columns for pinning: left-pinned columns move to the front, right-
|
|
24
|
+
* pinned to the end, each with a cumulative sticky offset from its edge. When
|
|
25
|
+
* nothing is pinned, column order is untouched and the grid stays fit-to-width.
|
|
22
26
|
*/
|
|
23
27
|
export declare function arrangePinned(cols: readonly ColumnDef[]): PinLayout;
|
package/dist/grid/pin.js
CHANGED
|
@@ -1,24 +1,47 @@
|
|
|
1
1
|
import { colWidth } from './column';
|
|
2
|
+
function sideOf(c) {
|
|
3
|
+
if (c.pinned === 'right')
|
|
4
|
+
return 'right';
|
|
5
|
+
if (c.pinned)
|
|
6
|
+
return 'left'; // true | 'left'
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
2
9
|
/**
|
|
3
|
-
* Arrange columns for pinning: pinned
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* default fit-to-width layout.
|
|
10
|
+
* Arrange columns for pinning: left-pinned columns move to the front, right-
|
|
11
|
+
* pinned to the end, each with a cumulative sticky offset from its edge. When
|
|
12
|
+
* nothing is pinned, column order is untouched and the grid stays fit-to-width.
|
|
7
13
|
*/
|
|
8
14
|
export function arrangePinned(cols) {
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
15
|
+
const left = cols.filter((c) => sideOf(c) === 'left');
|
|
16
|
+
const right = cols.filter((c) => sideOf(c) === 'right');
|
|
17
|
+
const mid = cols.filter((c) => sideOf(c) === null);
|
|
18
|
+
const columns = left.length || right.length ? [...left, ...mid, ...right] : [...cols];
|
|
19
|
+
const anyPinned = left.length > 0 || right.length > 0;
|
|
20
|
+
const widths = columns.map(colWidth);
|
|
21
|
+
const totalWidth = widths.reduce((a, b) => a + b, 0);
|
|
22
|
+
const nLeft = left.length;
|
|
23
|
+
const nRight = right.length;
|
|
24
|
+
const firstRight = columns.length - nRight;
|
|
12
25
|
const info = [];
|
|
13
|
-
let
|
|
14
|
-
let totalWidth = 0;
|
|
26
|
+
let leftOff = 0;
|
|
15
27
|
for (let i = 0; i < columns.length; i++) {
|
|
16
|
-
const width =
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
const width = widths[i];
|
|
29
|
+
if (i < nLeft) {
|
|
30
|
+
info.push({ pinned: true, side: 'left', left: leftOff, right: 0, width });
|
|
31
|
+
leftOff += width;
|
|
32
|
+
}
|
|
33
|
+
else if (i >= firstRight) {
|
|
34
|
+
info.push({ pinned: true, side: 'right', left: 0, right: 0, width }); // right filled below
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
info.push({ pinned: false, side: null, left: 0, right: 0, width });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Right offsets accumulate from the right edge inward.
|
|
41
|
+
let rightOff = 0;
|
|
42
|
+
for (let i = columns.length - 1; i >= firstRight; i--) {
|
|
43
|
+
info[i].right = rightOff;
|
|
44
|
+
rightOff += widths[i];
|
|
22
45
|
}
|
|
23
46
|
return { columns, info, totalWidth, anyPinned };
|
|
24
47
|
}
|
package/dist/grid/sizing.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ import type { ColumnDef } from './column';
|
|
|
3
3
|
export declare const MIN_COL_WIDTH = 48;
|
|
4
4
|
/** User width overrides, keyed by column `key` so they survive reorder/pin. */
|
|
5
5
|
export type WidthMap = Record<string, number>;
|
|
6
|
-
/** Clamp + round a proposed drag width to
|
|
7
|
-
export declare function clampWidth(w: number): number;
|
|
6
|
+
/** Clamp + round a proposed drag width to [min, max], never below MIN_COL_WIDTH. */
|
|
7
|
+
export declare function clampWidth(w: number, min?: number, max?: number): number;
|
|
8
8
|
/** Whether a column may be resized, given the grid-level toggle. */
|
|
9
9
|
export declare function isResizable(col: ColumnDef, gridResizable: boolean): boolean;
|
|
10
10
|
/**
|
package/dist/grid/sizing.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Smallest width (px) a column may be dragged to. */
|
|
2
2
|
export const MIN_COL_WIDTH = 48;
|
|
3
|
-
/** Clamp + round a proposed drag width to
|
|
4
|
-
export function clampWidth(w) {
|
|
5
|
-
|
|
3
|
+
/** Clamp + round a proposed drag width to [min, max], never below MIN_COL_WIDTH. */
|
|
4
|
+
export function clampWidth(w, min = MIN_COL_WIDTH, max = Infinity) {
|
|
5
|
+
const lo = Math.max(MIN_COL_WIDTH, min);
|
|
6
|
+
return Math.min(Math.max(Math.round(w), lo), Math.max(lo, max));
|
|
6
7
|
}
|
|
7
8
|
/** Whether a column may be resized, given the grid-level toggle. */
|
|
8
9
|
export function isResizable(col, gridResizable) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { GridRow } from './column';
|
|
2
|
+
import type { VisualRow } from './grouping';
|
|
3
|
+
/** Resolve a row's children (undefined/empty = leaf). */
|
|
4
|
+
export type GetChildren = (row: GridRow) => GridRow[] | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Flatten a tree of rows into the visible, depth-tagged data rows the grid
|
|
7
|
+
* renders. Pre-order DFS: each node is emitted, then — if it has children and
|
|
8
|
+
* `isExpanded` returns true — its children, one level deeper. Collapsed or leaf
|
|
9
|
+
* nodes contribute only themselves. Pure: no row values are read, so a realtime
|
|
10
|
+
* tick never rebuilds this list.
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildTreeRows(roots: readonly GridRow[], getChildren: GetChildren, isExpanded: (row: GridRow) => boolean): VisualRow[];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flatten a tree of rows into the visible, depth-tagged data rows the grid
|
|
3
|
+
* renders. Pre-order DFS: each node is emitted, then — if it has children and
|
|
4
|
+
* `isExpanded` returns true — its children, one level deeper. Collapsed or leaf
|
|
5
|
+
* nodes contribute only themselves. Pure: no row values are read, so a realtime
|
|
6
|
+
* tick never rebuilds this list.
|
|
7
|
+
*/
|
|
8
|
+
export function buildTreeRows(roots, getChildren, isExpanded) {
|
|
9
|
+
const out = [];
|
|
10
|
+
const walk = (nodes, depth) => {
|
|
11
|
+
for (const row of nodes) {
|
|
12
|
+
const children = getChildren(row);
|
|
13
|
+
const hasChildren = !!children && children.length > 0;
|
|
14
|
+
out.push({ kind: 'data', row, depth, hasChildren });
|
|
15
|
+
if (hasChildren && isExpanded(row))
|
|
16
|
+
walk(children, depth + 1);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
walk(roots, 0);
|
|
20
|
+
return out;
|
|
21
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bo-grid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Tiny, fast Svelte 5 data grid: canvas sparklines, batched realtime cell updates, and virtual scrolling. A free, fintech-focused alternative to heavyweight grids.",
|
|
@@ -59,6 +59,8 @@
|
|
|
59
59
|
"size": "vite build && node scripts/size-check.mjs",
|
|
60
60
|
"size:lib": "vite build --config vite.lib.config.ts && node scripts/size-lib.mjs",
|
|
61
61
|
"smoke": "vite build --base=./ --outDir demo-dist && node scripts/smoke.mjs",
|
|
62
|
+
"ssr": "node scripts/ssr.mjs",
|
|
63
|
+
"bench": "node scripts/bench.mjs",
|
|
62
64
|
"test": "vitest run",
|
|
63
65
|
"release": "node scripts/release.mjs",
|
|
64
66
|
"release:dry": "node scripts/release.mjs --dry-run"
|