bo-grid 0.1.0 → 0.7.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.
@@ -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
- /** Sticky left offset (px) when pinned. */
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 with pinned ones moved to the front. */
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-left columns move to the front, and each
19
- * gets a sticky `left` offset (cumulative width of the pinned columns before it).
20
- * When nothing is pinned, column order is untouched and the grid stays in its
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-left columns move to the front, and each
4
- * gets a sticky `left` offset (cumulative width of the pinned columns before it).
5
- * When nothing is pinned, column order is untouched and the grid stays in its
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 pinned = cols.filter((c) => c.pinned);
10
- const anyPinned = pinned.length > 0;
11
- const columns = anyPinned ? [...pinned, ...cols.filter((c) => !c.pinned)] : [...cols];
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 left = 0;
14
- let totalWidth = 0;
26
+ let leftOff = 0;
15
27
  for (let i = 0; i < columns.length; i++) {
16
- const width = colWidth(columns[i]);
17
- const isPinned = anyPinned && i < pinned.length;
18
- info.push({ pinned: isPinned, left: isPinned ? left : 0, width });
19
- if (isPinned)
20
- left += width;
21
- totalWidth += width;
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
  }
@@ -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 the minimum. */
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
  /**
@@ -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 the minimum. */
4
- export function clampWidth(w) {
5
- return Math.max(MIN_COL_WIDTH, Math.round(w));
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) {
@@ -1,4 +1,5 @@
1
1
  import type { GridRow, SortState } from './column';
2
+ import { type ColumnFilter } from './filtering';
2
3
  export interface RowRange {
3
4
  /** First row index (inclusive). */
4
5
  start: number;
@@ -13,6 +14,8 @@ export interface RowSourceParams {
13
14
  /** Full sort order (primary first) for multi-column sort. May be empty. */
14
15
  sorts?: SortState[];
15
16
  filter: string;
17
+ /** Structured per-column filters (header filter menu), keyed by column key. */
18
+ columnFilters?: Record<string, ColumnFilter>;
16
19
  }
17
20
  export interface RowSourceResult {
18
21
  /** Rows for the requested range, in order. */
@@ -1,4 +1,5 @@
1
1
  import { compareBySorts } from './column';
2
+ import { passesFilters } from './filtering';
2
3
  /**
3
4
  * Adapt an in-memory array to the RowSource interface — applies sort/filter and
4
5
  * slices the requested range. Useful as a real client-side adapter and for
@@ -12,6 +13,10 @@ export function createArraySource(all, opts = {}) {
12
13
  if (f && filterKeys && filterKeys.length > 0) {
13
14
  rows = rows.filter((r) => filterKeys.some((k) => String(r[k] ?? '').toLowerCase().includes(f)));
14
15
  }
16
+ const cf = params.columnFilters;
17
+ if (cf && Object.keys(cf).length > 0) {
18
+ rows = rows.filter((r) => passesFilters(r, cf));
19
+ }
15
20
  const sorts = params.sorts?.length ? params.sorts : params.sort ? [params.sort] : [];
16
21
  if (sorts.length > 0) {
17
22
  rows = [...rows].sort((a, b) => compareBySorts(a, b, sorts));
@@ -1,5 +1,6 @@
1
1
  import type { GridRow, SortState } from './column';
2
2
  import type { RowRange, RowSource } from './source';
3
+ import type { ColumnFilter } from './filtering';
3
4
  /**
4
5
  * Drives a RowSource for the grid: fetches the visible window, caches rows by
5
6
  * index, tracks the total, and guards against stale responses. Cache is keyed by
@@ -17,5 +18,5 @@ export declare class RowSourceController {
17
18
  constructor(source: RowSource);
18
19
  rowAt(index: number): GridRow | null;
19
20
  private keyOf;
20
- fetch(range: RowRange, sorts: SortState[], filter: string): Promise<void>;
21
+ fetch(range: RowRange, sorts: SortState[], filter: string, columnFilters?: Record<string, ColumnFilter>): Promise<void>;
21
22
  }
@@ -18,11 +18,12 @@ export class RowSourceController {
18
18
  rowAt(index) {
19
19
  return this.cache.get(index) ?? null;
20
20
  }
21
- keyOf(sorts, filter) {
22
- return `${sorts.map((s) => `${s.key}:${s.dir}`).join(',')}|${filter}`;
21
+ keyOf(sorts, filter, columnFilters) {
22
+ const cf = columnFilters ? JSON.stringify(columnFilters) : '';
23
+ return `${sorts.map((s) => `${s.key}:${s.dir}`).join(',')}|${filter}|${cf}`;
23
24
  }
24
- async fetch(range, sorts, filter) {
25
- const key = this.keyOf(sorts, filter);
25
+ async fetch(range, sorts, filter, columnFilters) {
26
+ const key = this.keyOf(sorts, filter, columnFilters);
26
27
  if (key !== this.key) {
27
28
  this.key = key;
28
29
  this.cache.clear();
@@ -40,7 +41,7 @@ export class RowSourceController {
40
41
  return;
41
42
  const id = ++this.reqId;
42
43
  this.loading = true;
43
- const res = await this.source.getRows({ range, sort: sorts[0] ?? null, sorts, filter });
44
+ const res = await this.source.getRows({ range, sort: sorts[0] ?? null, sorts, filter, columnFilters });
44
45
  if (id !== this.reqId)
45
46
  return; // a newer request superseded this one
46
47
  this.total = res.total;
@@ -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/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export { default as Grid } from './grid/Grid.svelte';
2
2
  export { default as Sparkline } from './sparkline/Sparkline.svelte';
3
3
  export type { ColumnDef, Align, GridRow, SortDir, SortState, CellEditEvent } from './grid/column';
4
4
  export type { AggKind, AggResult } from './grid/aggregate';
5
+ export type { ColumnFilter, FilterKind, TextOp, NumberOp, DateOp } from './grid/filtering';
5
6
  export { fmtPrice, fmtPercent, fmtVolume, fmtDate } from './format/format';
6
7
  export type { DateStyle } from './format/format';
7
8
  export { aggregate } from './grid/aggregate';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bo-grid",
3
- "version": "0.1.0",
3
+ "version": "0.7.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"