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.
- package/LICENSE +21 -0
- package/README.md +461 -0
- package/dist/format/format.d.ts +5 -0
- package/dist/format/format.js +25 -0
- package/dist/grid/AggregationBar.svelte +45 -0
- package/dist/grid/AggregationBar.svelte.d.ts +8 -0
- package/dist/grid/Cell.svelte +230 -0
- package/dist/grid/Cell.svelte.d.ts +32 -0
- package/dist/grid/Grid.svelte +1339 -0
- package/dist/grid/Grid.svelte.d.ts +76 -0
- package/dist/grid/GroupRow.svelte +110 -0
- package/dist/grid/GroupRow.svelte.d.ts +11 -0
- package/dist/grid/aggregate.d.ts +11 -0
- package/dist/grid/aggregate.js +23 -0
- package/dist/grid/clipboard.d.ts +9 -0
- package/dist/grid/clipboard.js +24 -0
- package/dist/grid/column.d.ts +95 -0
- package/dist/grid/column.js +62 -0
- package/dist/grid/export-xlsx.d.ts +8 -0
- package/dist/grid/export-xlsx.js +19 -0
- package/dist/grid/export.d.ts +19 -0
- package/dist/grid/export.js +48 -0
- package/dist/grid/grouping.d.ts +36 -0
- package/dist/grid/grouping.js +62 -0
- package/dist/grid/heatmap.d.ts +1 -0
- package/dist/grid/heatmap.js +12 -0
- package/dist/grid/pin.d.ts +23 -0
- package/dist/grid/pin.js +24 -0
- package/dist/grid/pivot.d.ts +27 -0
- package/dist/grid/pivot.js +0 -0
- package/dist/grid/reorder.d.ts +2 -0
- package/dist/grid/reorder.js +10 -0
- package/dist/grid/rowheight.d.ts +17 -0
- package/dist/grid/rowheight.js +41 -0
- package/dist/grid/selection.svelte.d.ts +30 -0
- package/dist/grid/selection.svelte.js +64 -0
- package/dist/grid/sizing.d.ts +17 -0
- package/dist/grid/sizing.js +28 -0
- package/dist/grid/source.d.ts +43 -0
- package/dist/grid/source.js +29 -0
- package/dist/grid/source.svelte.d.ts +21 -0
- package/dist/grid/source.svelte.js +53 -0
- package/dist/grid/theme.d.ts +27 -0
- package/dist/grid/theme.js +60 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +24 -0
- package/dist/sparkline/Sparkline.svelte +74 -0
- package/dist/sparkline/Sparkline.svelte.d.ts +9 -0
- package/dist/sparkline/sparkline-render.d.ts +16 -0
- package/dist/sparkline/sparkline-render.js +83 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.js +1 -0
- 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;
|