bo-grid 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.
Files changed (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +461 -0
  3. package/dist/format/format.d.ts +5 -0
  4. package/dist/format/format.js +25 -0
  5. package/dist/grid/AggregationBar.svelte +45 -0
  6. package/dist/grid/AggregationBar.svelte.d.ts +8 -0
  7. package/dist/grid/Cell.svelte +230 -0
  8. package/dist/grid/Cell.svelte.d.ts +32 -0
  9. package/dist/grid/Grid.svelte +1339 -0
  10. package/dist/grid/Grid.svelte.d.ts +76 -0
  11. package/dist/grid/GroupRow.svelte +110 -0
  12. package/dist/grid/GroupRow.svelte.d.ts +11 -0
  13. package/dist/grid/aggregate.d.ts +11 -0
  14. package/dist/grid/aggregate.js +23 -0
  15. package/dist/grid/clipboard.d.ts +9 -0
  16. package/dist/grid/clipboard.js +24 -0
  17. package/dist/grid/column.d.ts +95 -0
  18. package/dist/grid/column.js +62 -0
  19. package/dist/grid/export-xlsx.d.ts +8 -0
  20. package/dist/grid/export-xlsx.js +19 -0
  21. package/dist/grid/export.d.ts +19 -0
  22. package/dist/grid/export.js +48 -0
  23. package/dist/grid/grouping.d.ts +36 -0
  24. package/dist/grid/grouping.js +62 -0
  25. package/dist/grid/heatmap.d.ts +1 -0
  26. package/dist/grid/heatmap.js +12 -0
  27. package/dist/grid/pin.d.ts +23 -0
  28. package/dist/grid/pin.js +24 -0
  29. package/dist/grid/pivot.d.ts +27 -0
  30. package/dist/grid/pivot.js +0 -0
  31. package/dist/grid/reorder.d.ts +2 -0
  32. package/dist/grid/reorder.js +10 -0
  33. package/dist/grid/rowheight.d.ts +17 -0
  34. package/dist/grid/rowheight.js +41 -0
  35. package/dist/grid/selection.svelte.d.ts +30 -0
  36. package/dist/grid/selection.svelte.js +64 -0
  37. package/dist/grid/sizing.d.ts +17 -0
  38. package/dist/grid/sizing.js +28 -0
  39. package/dist/grid/source.d.ts +43 -0
  40. package/dist/grid/source.js +29 -0
  41. package/dist/grid/source.svelte.d.ts +21 -0
  42. package/dist/grid/source.svelte.js +53 -0
  43. package/dist/grid/theme.d.ts +27 -0
  44. package/dist/grid/theme.js +60 -0
  45. package/dist/index.d.ts +19 -0
  46. package/dist/index.js +24 -0
  47. package/dist/sparkline/Sparkline.svelte +74 -0
  48. package/dist/sparkline/Sparkline.svelte.d.ts +9 -0
  49. package/dist/sparkline/sparkline-render.d.ts +16 -0
  50. package/dist/sparkline/sparkline-render.js +83 -0
  51. package/dist/types.d.ts +7 -0
  52. package/dist/types.js +1 -0
  53. package/package.json +82 -0
@@ -0,0 +1,230 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { ColumnDef, GridRow } from './column';
4
+ import { formatCell, colStyle, candlesOf } from './column';
5
+ import { heatColor } from './heatmap';
6
+ import Sparkline from '../sparkline/Sparkline.svelte';
7
+
8
+ let {
9
+ col,
10
+ row,
11
+ r,
12
+ c,
13
+ selected = false,
14
+ focused = false,
15
+ pinned = false,
16
+ pinLeft = 0,
17
+ width,
18
+ alt = false,
19
+ editing = false,
20
+ colIndex,
21
+ cellId,
22
+ cellSnippet,
23
+ onCellDown,
24
+ onCellEnter,
25
+ onCellClick,
26
+ onCellDblClick,
27
+ onEditCommit,
28
+ onEditCancel,
29
+ }: {
30
+ col: ColumnDef;
31
+ row: GridRow;
32
+ r: number;
33
+ c: number;
34
+ selected?: boolean;
35
+ focused?: boolean;
36
+ pinned?: boolean;
37
+ pinLeft?: number;
38
+ /** Fixed pixel width (pinned/horizontal-scroll mode). */
39
+ width?: number;
40
+ alt?: boolean;
41
+ editing?: boolean;
42
+ colIndex?: number;
43
+ cellId?: string;
44
+ cellSnippet?: Snippet<[{ row: GridRow; column: ColumnDef; value: unknown }]>;
45
+ onCellDown?: (r: number, c: number, e: PointerEvent) => void;
46
+ onCellEnter?: (r: number, c: number, e: PointerEvent) => void;
47
+ onCellClick?: (r: number, c: number, e: MouseEvent) => void;
48
+ onCellDblClick?: (r: number, c: number) => void;
49
+ onEditCommit?: (raw: string) => void;
50
+ onEditCancel?: () => void;
51
+ } = $props();
52
+
53
+ let cancelled = false;
54
+ function focusSelect(node: HTMLInputElement) {
55
+ node.focus();
56
+ node.select();
57
+ }
58
+ function onEditKey(e: KeyboardEvent) {
59
+ e.stopPropagation(); // keep arrows/Enter in the input, not the grid
60
+ if (e.key === 'Enter') (e.currentTarget as HTMLInputElement).blur();
61
+ else if (e.key === 'Escape') {
62
+ cancelled = true;
63
+ (e.currentTarget as HTMLInputElement).blur();
64
+ }
65
+ }
66
+ function onEditBlur(e: FocusEvent) {
67
+ const v = (e.currentTarget as HTMLInputElement).value;
68
+ if (cancelled) {
69
+ cancelled = false;
70
+ onEditCancel?.();
71
+ } else {
72
+ onEditCommit?.(v);
73
+ }
74
+ }
75
+
76
+ // Dynamic field read. row is a runes class instance, so row[col.key] still
77
+ // goes through the $state getter — fine-grained reactivity is preserved even
78
+ // though the key is only known at runtime.
79
+ const value = $derived(row[col.key]);
80
+ const kind = $derived(col.type === 'text' ? 'text' : col.type === 'sparkline' ? 'spark' : 'num');
81
+
82
+ function cellStyle(): string {
83
+ let s = width != null ? `flex:0 0 ${width}px;width:${width}px;` : colStyle(col);
84
+ if (col.type === 'heatmap') s += `background:${heatColor(Number(value), col.min, col.max)};`;
85
+ if (pinned) {
86
+ s += `position:sticky;left:${pinLeft}px;z-index:1;`;
87
+ // Pinned cells must be opaque to cover scrolled content. Heatmap already
88
+ // set a background; otherwise match the (alternating) row colour.
89
+ if (col.type !== 'heatmap') s += `background:var(${alt ? '--bo-row-a' : '--bo-row-b'});`;
90
+ }
91
+ return s;
92
+ }
93
+ </script>
94
+
95
+ <!-- Keyboard interaction is handled at the grid level (arrow nav via aria-activedescendant + Enter); this cell click is a pointer affordance. -->
96
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
97
+ <span
98
+ class="c {kind}"
99
+ class:dim={col.type === 'volume'}
100
+ class:pos={col.type === 'percent' && Number(value) >= 0}
101
+ class:neg={col.type === 'percent' && Number(value) < 0}
102
+ class:sel={selected}
103
+ class:focus={focused}
104
+ style={cellStyle()}
105
+ role="gridcell"
106
+ tabindex="-1"
107
+ id={cellId}
108
+ aria-colindex={colIndex}
109
+ aria-selected={selected}
110
+ onpointerdown={(e) => onCellDown?.(r, c, e)}
111
+ onpointerenter={(e) => onCellEnter?.(r, c, e)}
112
+ onclick={(e) => onCellClick?.(r, c, e)}
113
+ ondblclick={() => onCellDblClick?.(r, c)}
114
+ >
115
+ {#if editing}
116
+ <input
117
+ class="bo-edit"
118
+ value={String(value ?? '')}
119
+ use:focusSelect
120
+ onkeydown={onEditKey}
121
+ onblur={onEditBlur}
122
+ onpointerdown={(e) => e.stopPropagation()}
123
+ onclick={(e) => e.stopPropagation()}
124
+ ondblclick={(e) => e.stopPropagation()}
125
+ />
126
+ {:else if col.type === 'custom'}
127
+ {#if cellSnippet}{@render cellSnippet({ row, column: col, value })}{:else}{value ?? ''}{/if}
128
+ {:else if col.type === 'sparkline'}
129
+ <Sparkline candles={candlesOf(row, col.sparkKey)} />
130
+ {:else if col.type === 'text'}
131
+ <strong>{formatCell(col, value)}</strong>{#if col.sub}<em>{row[col.sub]}</em>{/if}
132
+ {:else if col.flash}
133
+ {#key row.flashSeq}
134
+ <span class="flash {row.flashDir}">{formatCell(col, value)}</span>
135
+ {/key}
136
+ {:else}
137
+ {formatCell(col, value)}
138
+ {/if}
139
+ </span>
140
+
141
+ <style>
142
+ .c {
143
+ position: relative;
144
+ display: flex;
145
+ align-items: center;
146
+ padding: 0 8px;
147
+ height: 100%;
148
+ font-size: 13px;
149
+ line-height: 1.4;
150
+ overflow: hidden;
151
+ white-space: nowrap;
152
+ text-overflow: ellipsis;
153
+ }
154
+ .num {
155
+ justify-content: flex-end;
156
+ font-family: var(--bo-mono);
157
+ font-variant-numeric: tabular-nums;
158
+ }
159
+ .text {
160
+ gap: 6px;
161
+ }
162
+ .text strong {
163
+ font-family: var(--bo-mono);
164
+ font-weight: 600;
165
+ }
166
+ .text em {
167
+ font-style: normal;
168
+ font-size: 10px;
169
+ color: var(--bo-text-dim);
170
+ }
171
+ .spark {
172
+ overflow: visible;
173
+ }
174
+ .bo-edit {
175
+ width: 100%;
176
+ height: 100%;
177
+ padding: 0 7px;
178
+ font: inherit;
179
+ font-family: var(--bo-mono);
180
+ font-size: 13px;
181
+ text-align: inherit;
182
+ color: var(--bo-text);
183
+ background: var(--bo-bg);
184
+ border: 1px solid var(--bo-sel-border);
185
+ outline: none;
186
+ }
187
+ .dim {
188
+ color: var(--bo-text-dim);
189
+ }
190
+ .pos {
191
+ color: var(--bo-up);
192
+ }
193
+ .neg {
194
+ color: var(--bo-down);
195
+ }
196
+
197
+ /* Selection: a translucent fill layered via inset box-shadow so it tints even
198
+ over a heatmap background; the focus cell gets a 1px ring on top. */
199
+ .c.sel {
200
+ box-shadow: inset 0 0 0 1000px var(--bo-sel-fill);
201
+ }
202
+ .c.focus {
203
+ box-shadow:
204
+ inset 0 0 0 1000px var(--bo-sel-fill),
205
+ inset 0 0 0 1px var(--bo-sel-border);
206
+ }
207
+
208
+ .flash {
209
+ animation: flash 0.3s linear;
210
+ }
211
+ .flash.up {
212
+ color: var(--bo-up);
213
+ }
214
+ .flash.down {
215
+ color: var(--bo-down);
216
+ }
217
+ @keyframes flash {
218
+ 0% {
219
+ background: color-mix(in srgb, var(--bo-amber) 38%, transparent);
220
+ }
221
+ 100% {
222
+ background: transparent;
223
+ }
224
+ }
225
+ @media (prefers-reduced-motion: reduce) {
226
+ .flash {
227
+ animation: none;
228
+ }
229
+ }
230
+ </style>
@@ -0,0 +1,32 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { ColumnDef, GridRow } from './column';
3
+ type $$ComponentProps = {
4
+ col: ColumnDef;
5
+ row: GridRow;
6
+ r: number;
7
+ c: number;
8
+ selected?: boolean;
9
+ focused?: boolean;
10
+ pinned?: boolean;
11
+ pinLeft?: number;
12
+ /** Fixed pixel width (pinned/horizontal-scroll mode). */
13
+ width?: number;
14
+ alt?: boolean;
15
+ editing?: boolean;
16
+ colIndex?: number;
17
+ cellId?: string;
18
+ cellSnippet?: Snippet<[{
19
+ row: GridRow;
20
+ column: ColumnDef;
21
+ value: unknown;
22
+ }]>;
23
+ onCellDown?: (r: number, c: number, e: PointerEvent) => void;
24
+ onCellEnter?: (r: number, c: number, e: PointerEvent) => void;
25
+ onCellClick?: (r: number, c: number, e: MouseEvent) => void;
26
+ onCellDblClick?: (r: number, c: number) => void;
27
+ onEditCommit?: (raw: string) => void;
28
+ onEditCancel?: () => void;
29
+ };
30
+ declare const Cell: import("svelte").Component<$$ComponentProps, {}, "">;
31
+ type Cell = ReturnType<typeof Cell>;
32
+ export default Cell;