@svgrid/grid 2.1.0 → 2.1.2
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/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +178 -178
- package/dist/SvGrid.css +2377 -2377
- package/dist/SvGrid.svelte +2764 -2764
- package/dist/SvGridBoard.svelte +2281 -2281
- package/dist/SvGridDropdown.svelte +666 -666
- package/dist/cdn/svgrid.js +2 -2
- package/dist/cdn/svgrid.svelte-external.js +2 -2
- package/package.json +92 -92
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +178 -178
- package/src/SvGrid.controller.svelte.ts +2553 -2553
- package/src/SvGrid.css +2377 -2377
- package/src/SvGrid.svelte +2764 -2764
- package/src/SvGrid.types.ts +944 -944
- package/src/SvGridBoard.svelte +2281 -2281
- package/src/SvGridDropdown.svelte +666 -666
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -61
- package/src/build-api.ts +798 -798
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1111 -1111
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/editing.test.ts +859 -859
- package/src/editing.ts +675 -675
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/flex-render.ts +3 -3
- package/src/index.ts +463 -463
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/menus.ts +582 -582
- package/src/merge-objects.ts +48 -48
- package/src/render-component.ts +28 -28
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -413
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +304 -304
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +439 -439
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/themes/index.ts +159 -159
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
package/src/themes/index.ts
CHANGED
|
@@ -1,159 +1,159 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SvGrid's built-in design-system theme presets (shadcn/ui, Tailwind, Material 3,
|
|
3
|
-
* Excel, Fluent 2, ...). Each preset carries an explicit light + dark palette;
|
|
4
|
-
* resolving one (plus a light/dark mode) yields the full `--sg-*` token set that
|
|
5
|
-
* restyles the whole grid - not just the accent.
|
|
6
|
-
*
|
|
7
|
-
* Pure, framework-agnostic data + helpers - safe to import from Node or the
|
|
8
|
-
* browser. `@svgrid/enterprise`'s Studio layer builds its `ProjectTheme`-aware
|
|
9
|
-
* wrapper on top of this module instead of keeping its own copy.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export type ThemeMode = 'light' | 'dark'
|
|
13
|
-
|
|
14
|
-
/** The core palette for one mode. */
|
|
15
|
-
export type ThemePalette = {
|
|
16
|
-
bg: string; fg: string; muted: string; border: string
|
|
17
|
-
headerBg: string; headerFg: string; accent: string
|
|
18
|
-
rowAlt: string; rowHover: string; selectionBg: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export type ThemePreset = {
|
|
22
|
-
/** Stable id, e.g. used as a CSS file name (`themes/<id>.css`). */
|
|
23
|
-
id: string
|
|
24
|
-
/** Display name shown on a theme picker. */
|
|
25
|
-
name: string
|
|
26
|
-
/** Control corner rounding (px). */
|
|
27
|
-
radius: number
|
|
28
|
-
/** Font stack applied to the app surface. */
|
|
29
|
-
font: string
|
|
30
|
-
light: ThemePalette
|
|
31
|
-
dark: ThemePalette
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** The built-in theme presets, in gallery order (default first). */
|
|
35
|
-
export const themePresets: ThemePreset[] = [
|
|
36
|
-
{ id: 'shadcn', name: 'shadcn/ui', radius: 6, font: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
|
|
37
|
-
light: { bg: '#ffffff', fg: '#09090b', muted: '#71717a', border: '#e4e4e7', headerBg: '#fafafa', headerFg: '#71717a', accent: '#18181b', rowAlt: '#ffffff', rowHover: '#f4f4f5', selectionBg: '#f4f4f5' },
|
|
38
|
-
dark: { bg: '#0a0a0a', fg: '#fafafa', muted: '#a1a1aa', border: '#27272a', headerBg: '#18181b', headerFg: '#a1a1aa', accent: '#fafafa', rowAlt: '#0a0a0a', rowHover: '#18181b', selectionBg: '#27272a' } },
|
|
39
|
-
{ id: 'tailwind', name: 'Tailwind', radius: 6, font: 'Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
40
|
-
light: { bg: '#ffffff', fg: '#0f172a', muted: '#64748b', border: '#e2e8f0', headerBg: '#f8fafc', headerFg: '#0f172a', accent: '#4f46e5', rowAlt: '#ffffff', rowHover: '#f1f5f9', selectionBg: '#e0e7ff' },
|
|
41
|
-
dark: { bg: '#0f172a', fg: '#f8fafc', muted: '#94a3b8', border: '#334155', headerBg: '#1e293b', headerFg: '#f8fafc', accent: '#818cf8', rowAlt: '#0f172a', rowHover: '#1e293b', selectionBg: '#312e81' } },
|
|
42
|
-
{ id: 'material', name: 'Material 3', radius: 8, font: '"Roboto Flex", Roboto, "Segoe UI", system-ui, sans-serif',
|
|
43
|
-
light: { bg: '#fef7ff', fg: '#1d1b20', muted: '#49454f', border: '#cac4d0', headerBg: '#f3edf7', headerFg: '#1d1b20', accent: '#6750a4', rowAlt: '#fffbfe', rowHover: '#ece6f0', selectionBg: '#e8def8' },
|
|
44
|
-
dark: { bg: '#141218', fg: '#e6e0e9', muted: '#cac4d0', border: '#49454f', headerBg: '#211f26', headerFg: '#e6e0e9', accent: '#d0bcff', rowAlt: '#141218', rowHover: '#2b2930', selectionBg: '#4a4458' } },
|
|
45
|
-
{ id: 'excel', name: 'Excel', radius: 0, font: '"Aptos", "Segoe UI", Calibri, Arial, sans-serif',
|
|
46
|
-
light: { bg: '#ffffff', fg: '#323130', muted: '#605e5c', border: '#d2d0ce', headerBg: '#f3f2f1', headerFg: '#444444', accent: '#107c41', rowAlt: '#ffffff', rowHover: '#edebe9', selectionBg: 'rgba(16, 124, 65, 0.10)' },
|
|
47
|
-
dark: { bg: '#1b1a19', fg: '#f3f2f1', muted: '#c8c6c4', border: '#3b3a39', headerBg: '#252423', headerFg: '#c8c6c4', accent: '#58c188', rowAlt: '#1b1a19', rowHover: '#323130', selectionBg: 'rgba(88, 193, 136, 0.16)' } },
|
|
48
|
-
{ id: 'fluent', name: 'Fluent 2', radius: 4, font: '"Segoe UI Variable", "Segoe UI", system-ui, sans-serif',
|
|
49
|
-
light: { bg: '#ffffff', fg: '#242424', muted: '#616161', border: '#e0e0e0', headerBg: '#f5f5f5', headerFg: '#242424', accent: '#0f6cbd', rowAlt: '#fafafa', rowHover: '#f0f0f0', selectionBg: '#ebebeb' },
|
|
50
|
-
dark: { bg: '#1f1f1f', fg: '#ffffff', muted: '#d6d6d6', border: '#666666', headerBg: '#141414', headerFg: '#ffffff', accent: '#479ef5', rowAlt: '#1a1a1a', rowHover: '#3d3d3d', selectionBg: '#383838' } },
|
|
51
|
-
{ id: 'carbon', name: 'Carbon', radius: 0, font: '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif',
|
|
52
|
-
light: { bg: '#ffffff', fg: '#161616', muted: '#525252', border: '#e0e0e0', headerBg: '#e0e0e0', headerFg: '#161616', accent: '#0f62fe', rowAlt: '#f4f4f4', rowHover: '#e8e8e8', selectionBg: '#d0e2ff' },
|
|
53
|
-
dark: { bg: '#161616', fg: '#f4f4f4', muted: '#c6c6c6', border: '#393939', headerBg: '#393939', headerFg: '#f4f4f4', accent: '#78a9ff', rowAlt: '#262626', rowHover: '#333333', selectionBg: '#393939' } },
|
|
54
|
-
{ id: 'sap', name: 'SAP Fiori', radius: 6, font: '"72", "72full", "Arial", sans-serif',
|
|
55
|
-
light: { bg: '#f5f6f7', fg: '#131e29', muted: '#556b82', border: '#e5e5e5', headerBg: '#ffffff', headerFg: '#131e29', accent: '#0070f2', rowAlt: '#f5f6f7', rowHover: '#eaecee', selectionBg: '#ebf8ff' },
|
|
56
|
-
dark: { bg: '#12171c', fg: '#f5f6f7', muted: '#8396a8', border: '#2e3742', headerBg: '#1d232a', headerFg: '#f5f6f7', accent: '#4db1ff', rowAlt: '#12171c', rowHover: '#222b35', selectionBg: '#1d2d3e' } },
|
|
57
|
-
{ id: 'salesforce', name: 'Salesforce', radius: 4, font: '"Salesforce Sans", "SF Pro", Helvetica, Arial, sans-serif',
|
|
58
|
-
light: { bg: '#ffffff', fg: '#181818', muted: '#757575', border: '#c9c9c9', headerBg: '#f3f3f3', headerFg: '#181818', accent: '#0176d3', rowAlt: '#ffffff', rowHover: '#f3f3f3', selectionBg: '#ecebea' },
|
|
59
|
-
dark: { bg: '#181818', fg: '#ffffff', muted: '#c9c9c9', border: '#3e3e3c', headerBg: '#2e2e2e', headerFg: '#ffffff', accent: '#1b96ff', rowAlt: '#181818', rowHover: '#2e2e2e', selectionBg: '#1e4d8e' } },
|
|
60
|
-
{ id: 'atlassian', name: 'Atlassian', radius: 3, font: '"Charlie Display", "Atlassian Sans", -apple-system, "Segoe UI", sans-serif',
|
|
61
|
-
light: { bg: '#ffffff', fg: '#172b4d', muted: '#44546f', border: '#dcdfe4', headerBg: '#fafbfc', headerFg: '#44546f', accent: '#0c66e4', rowAlt: '#ffffff', rowHover: '#f1f2f4', selectionBg: '#e9f2ff' },
|
|
62
|
-
dark: { bg: '#1d2125', fg: '#b6c2cf', muted: '#8590a2', border: '#38414a', headerBg: '#22272b', headerFg: '#8590a2', accent: '#579dff', rowAlt: '#1d2125', rowHover: '#2c333a', selectionBg: '#09326c' } },
|
|
63
|
-
{ id: 'github', name: 'GitHub', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif',
|
|
64
|
-
light: { bg: '#ffffff', fg: '#1f2328', muted: '#59636e', border: '#d1d9e0', headerBg: '#f6f8fa', headerFg: '#59636e', accent: '#0969da', rowAlt: '#ffffff', rowHover: '#f6f8fa', selectionBg: '#ddf4ff' },
|
|
65
|
-
dark: { bg: '#0d1117', fg: '#f0f6fc', muted: '#9198a1', border: '#3d444d', headerBg: '#151b23', headerFg: '#9198a1', accent: '#4493f8', rowAlt: '#0d1117', rowHover: '#151b23', selectionBg: 'rgba(56, 139, 253, 0.10)' } },
|
|
66
|
-
{ id: 'antd', name: 'Ant Design', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", sans-serif',
|
|
67
|
-
light: { bg: '#ffffff', fg: 'rgba(0, 0, 0, 0.88)', muted: 'rgba(0, 0, 0, 0.65)', border: '#d9d9d9', headerBg: '#fafafa', headerFg: 'rgba(0, 0, 0, 0.88)', accent: '#1677ff', rowAlt: '#ffffff', rowHover: '#f5f5f5', selectionBg: '#e6f4ff' },
|
|
68
|
-
dark: { bg: '#141414', fg: 'rgba(255, 255, 255, 0.85)', muted: 'rgba(255, 255, 255, 0.65)', border: '#424242', headerBg: '#1f1f1f', headerFg: 'rgba(255, 255, 255, 0.85)', accent: '#177ddc', rowAlt: '#141414', rowHover: '#262626', selectionBg: '#111a2c' } },
|
|
69
|
-
{ id: 'ag-alpine', name: 'Alpine', radius: 3, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
70
|
-
light: { bg: '#ffffff', fg: '#181d1f', muted: 'rgba(24, 29, 31, 0.5)', border: '#babfc7', headerBg: '#f8f8f8', headerFg: '#181d1f', accent: '#2196f3', rowAlt: '#fcfcfc', rowHover: 'rgba(33, 150, 243, 0.1)', selectionBg: 'rgba(33, 150, 243, 0.3)' },
|
|
71
|
-
dark: { bg: '#181d1f', fg: '#ffffff', muted: 'rgba(255, 255, 255, 0.5)', border: '#68686e', headerBg: '#222628', headerFg: '#ffffff', accent: '#2196f3', rowAlt: '#222628', rowHover: 'rgba(33, 150, 243, 0.1)', selectionBg: 'rgba(33, 150, 243, 0.3)' } },
|
|
72
|
-
{ id: 'bootstrap', name: 'Bootstrap 5', radius: 6, font: 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
73
|
-
light: { bg: '#ffffff', fg: '#212529', muted: '#6c757d', border: '#dee2e6', headerBg: '#f8f9fa', headerFg: '#212529', accent: '#0d6efd', rowAlt: '#fcfcfd', rowHover: '#f2f2f2', selectionBg: '#cfe2ff' },
|
|
74
|
-
dark: { bg: '#212529', fg: '#dee2e6', muted: '#adb5bd', border: '#495057', headerBg: '#2b3035', headerFg: '#dee2e6', accent: '#0d6efd', rowAlt: '#25292e', rowHover: '#2c3034', selectionBg: '#031633' } },
|
|
75
|
-
{ id: 'vercel', name: 'Vercel', radius: 6, font: '"Geist", "Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
76
|
-
light: { bg: '#ffffff', fg: '#000000', muted: '#666666', border: '#eaeaea', headerBg: '#fafafa', headerFg: '#666666', accent: '#0070f3', rowAlt: '#fafafa', rowHover: '#f5f5f5', selectionBg: '#e8f0fe' },
|
|
77
|
-
dark: { bg: '#000000', fg: '#ffffff', muted: '#888888', border: '#333333', headerBg: '#111111', headerFg: '#888888', accent: '#3291ff', rowAlt: '#0a0a0a', rowHover: '#1a1a1a', selectionBg: '#0d2847' } },
|
|
78
|
-
{ id: 'linear', name: 'Linear', radius: 6, font: '"Inter Variable", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
|
79
|
-
light: { bg: '#ffffff', fg: '#282a30', muted: '#6b6f76', border: '#e9e9eb', headerBg: '#f7f8f8', headerFg: '#6b6f76', accent: '#5e6ad2', rowAlt: '#fbfbfc', rowHover: '#f4f4f5', selectionBg: '#e8eafd' },
|
|
80
|
-
dark: { bg: '#08090a', fg: '#f7f8f8', muted: '#8a8f98', border: '#23252a', headerBg: '#0f1011', headerFg: '#8a8f98', accent: '#5e6ad2', rowAlt: '#0f1011', rowHover: '#1a1b1e', selectionBg: '#2a2d4a' } },
|
|
81
|
-
{ id: 'notion', name: 'Notion', radius: 4, font: 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
|
|
82
|
-
light: { bg: '#ffffff', fg: '#37352f', muted: '#787774', border: '#e9e9e7', headerBg: '#f7f6f3', headerFg: '#787774', accent: '#2383e2', rowAlt: '#fbfbfa', rowHover: '#f1f1ef', selectionBg: '#d9eaf7' },
|
|
83
|
-
dark: { bg: '#191919', fg: '#d4d4d4', muted: '#9b9b9b', border: '#2f2f2f', headerBg: '#202020', headerFg: '#9b9b9b', accent: '#529cca', rowAlt: '#1c1c1c', rowHover: '#252525', selectionBg: '#20415c' } },
|
|
84
|
-
{ id: 'nord', name: 'Nord', radius: 4, font: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
85
|
-
light: { bg: '#eceff4', fg: '#2e3440', muted: '#4c566a', border: '#d8dee9', headerBg: '#e5e9f0', headerFg: '#2e3440', accent: '#5e81ac', rowAlt: '#e5e9f0', rowHover: '#dde3ec', selectionBg: '#d8dee9' },
|
|
86
|
-
dark: { bg: '#2e3440', fg: '#d8dee9', muted: '#4c566a', border: '#434c5e', headerBg: '#3b4252', headerFg: '#e5e9f0', accent: '#88c0d0', rowAlt: '#333b48', rowHover: '#3b4252', selectionBg: '#434c5e' } },
|
|
87
|
-
{ id: 'dracula', name: 'Dracula', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
88
|
-
light: { bg: '#fffbeb', fg: '#1f1f1f', muted: '#6c664b', border: '#dedccf', headerBg: '#ece9df', headerFg: '#1f1f1f', accent: '#644ac9', rowAlt: '#fffdf5', rowHover: '#f2efe3', selectionBg: '#cfcfde' },
|
|
89
|
-
dark: { bg: '#282a36', fg: '#f8f8f2', muted: '#6272a4', border: '#44475a', headerBg: '#21222c', headerFg: '#f8f8f2', accent: '#bd93f9', rowAlt: '#2d2f3b', rowHover: '#343746', selectionBg: '#44475a' } },
|
|
90
|
-
{ id: 'catppuccin', name: 'Catppuccin', radius: 8, font: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
91
|
-
light: { bg: '#eff1f5', fg: '#4c4f69', muted: '#6c6f85', border: '#ccd0da', headerBg: '#e6e9ef', headerFg: '#4c4f69', accent: '#8839ef', rowAlt: '#e6e9ef', rowHover: '#dce0e8', selectionBg: '#dce0e8' },
|
|
92
|
-
dark: { bg: '#1e1e2e', fg: '#cdd6f4', muted: '#a6adc8', border: '#313244', headerBg: '#181825', headerFg: '#cdd6f4', accent: '#cba6f7', rowAlt: '#1a1a28', rowHover: '#313244', selectionBg: '#45475a' } },
|
|
93
|
-
]
|
|
94
|
-
|
|
95
|
-
/** The default preset (used when nothing is chosen). */
|
|
96
|
-
export const defaultThemePreset: ThemePreset = themePresets.find((t) => t.id === 'tailwind') ?? themePresets[0]!
|
|
97
|
-
|
|
98
|
-
/** Look up a preset by id. */
|
|
99
|
-
export const getThemePreset = (id?: string): ThemePreset | undefined =>
|
|
100
|
-
id ? themePresets.find((t) => t.id === id) : undefined
|
|
101
|
-
|
|
102
|
-
/** A readable (near-black / white) foreground for text placed on the accent. */
|
|
103
|
-
function onAccent(accent: string): string {
|
|
104
|
-
const m = /^#?([0-9a-f]{6})$/i.exec(accent.trim())
|
|
105
|
-
if (!m) return '#ffffff'
|
|
106
|
-
const n = parseInt(m[1]!, 16)
|
|
107
|
-
const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255
|
|
108
|
-
// Perceived luminance (sRGB, quick approximation).
|
|
109
|
-
const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
|
110
|
-
return lum > 0.6 ? '#18181b' : '#ffffff'
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Resolve the effective `--sg-*` tokens for a preset in a given mode, with an
|
|
114
|
-
* optional accent override applied. */
|
|
115
|
-
export function resolveThemeTokens(preset: ThemePreset | undefined, mode: ThemeMode, accentOverride?: string): Record<string, string> {
|
|
116
|
-
const resolved = preset ?? defaultThemePreset
|
|
117
|
-
const p = resolved[mode]
|
|
118
|
-
const accent = accentOverride || p.accent
|
|
119
|
-
return {
|
|
120
|
-
'--sg-bg': p.bg,
|
|
121
|
-
'--sg-fg': p.fg,
|
|
122
|
-
'--sg-muted': p.muted,
|
|
123
|
-
'--sg-border': p.border,
|
|
124
|
-
'--sg-header-bg': p.headerBg,
|
|
125
|
-
'--sg-header-fg': p.headerFg,
|
|
126
|
-
// A subtle inset surface (used by e.g. the master-detail region behind the
|
|
127
|
-
// nested grid). Themed so it isn't a fixed light grey on dark presets.
|
|
128
|
-
'--sg-bg-subtle': p.headerBg,
|
|
129
|
-
'--sg-row-alt-bg': p.rowAlt,
|
|
130
|
-
'--sg-row-hover-bg': p.rowHover,
|
|
131
|
-
'--sg-selection-bg': p.selectionBg,
|
|
132
|
-
// Inputs/checkboxes: pin to the theme's own surface so they never borrow the
|
|
133
|
-
// host page's input tokens (which is how a themed grid ended up with dark
|
|
134
|
-
// checkboxes on a light page when the host mode differed).
|
|
135
|
-
'--sg-input-bg': p.bg,
|
|
136
|
-
'--sg-input-border': p.border,
|
|
137
|
-
'--sg-accent': accent,
|
|
138
|
-
'--sg-on-accent': onAccent(accent),
|
|
139
|
-
'--sg-radius': `${resolved.radius}px`,
|
|
140
|
-
'--sg-font': resolved.font,
|
|
141
|
-
// The grid's custom scrollbar is a shadow-DOM web component that reads its
|
|
142
|
-
// OWN --sg-scrollbar-* tokens (with fixed light-mode fallbacks). Without
|
|
143
|
-
// these it ignores the theme entirely - a light scrollbar left on a dark
|
|
144
|
-
// grid. CSS custom properties pierce shadow DOM, so defining them here (on
|
|
145
|
-
// the themed container) reaches the scrollbar. Derive them from the palette
|
|
146
|
-
// so the scrollbar tracks the preset + light/dark mode like everything else.
|
|
147
|
-
'--sg-scrollbar-bg': p.headerBg,
|
|
148
|
-
'--sg-scrollbar-border': p.border,
|
|
149
|
-
'--sg-scrollbar-thumb': p.muted,
|
|
150
|
-
'--sg-scrollbar-thumb-hover': p.fg,
|
|
151
|
-
'--sg-scrollbar-thumb-active': p.fg,
|
|
152
|
-
'--sg-scrollbar-arrow': p.muted,
|
|
153
|
-
'--sg-scrollbar-arrow-hover': p.fg,
|
|
154
|
-
'--sg-scrollbar-arrow-hover-bg': p.rowHover,
|
|
155
|
-
'--sg-scrollbar-arrow-active': p.fg,
|
|
156
|
-
'--sg-scrollbar-arrow-active-bg': p.selectionBg,
|
|
157
|
-
'--sg-scrollbar-arrow-disabled': p.border,
|
|
158
|
-
}
|
|
159
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* SvGrid's built-in design-system theme presets (shadcn/ui, Tailwind, Material 3,
|
|
3
|
+
* Excel, Fluent 2, ...). Each preset carries an explicit light + dark palette;
|
|
4
|
+
* resolving one (plus a light/dark mode) yields the full `--sg-*` token set that
|
|
5
|
+
* restyles the whole grid - not just the accent.
|
|
6
|
+
*
|
|
7
|
+
* Pure, framework-agnostic data + helpers - safe to import from Node or the
|
|
8
|
+
* browser. `@svgrid/enterprise`'s Studio layer builds its `ProjectTheme`-aware
|
|
9
|
+
* wrapper on top of this module instead of keeping its own copy.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type ThemeMode = 'light' | 'dark'
|
|
13
|
+
|
|
14
|
+
/** The core palette for one mode. */
|
|
15
|
+
export type ThemePalette = {
|
|
16
|
+
bg: string; fg: string; muted: string; border: string
|
|
17
|
+
headerBg: string; headerFg: string; accent: string
|
|
18
|
+
rowAlt: string; rowHover: string; selectionBg: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ThemePreset = {
|
|
22
|
+
/** Stable id, e.g. used as a CSS file name (`themes/<id>.css`). */
|
|
23
|
+
id: string
|
|
24
|
+
/** Display name shown on a theme picker. */
|
|
25
|
+
name: string
|
|
26
|
+
/** Control corner rounding (px). */
|
|
27
|
+
radius: number
|
|
28
|
+
/** Font stack applied to the app surface. */
|
|
29
|
+
font: string
|
|
30
|
+
light: ThemePalette
|
|
31
|
+
dark: ThemePalette
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The built-in theme presets, in gallery order (default first). */
|
|
35
|
+
export const themePresets: ThemePreset[] = [
|
|
36
|
+
{ id: 'shadcn', name: 'shadcn/ui', radius: 6, font: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
|
|
37
|
+
light: { bg: '#ffffff', fg: '#09090b', muted: '#71717a', border: '#e4e4e7', headerBg: '#fafafa', headerFg: '#71717a', accent: '#18181b', rowAlt: '#ffffff', rowHover: '#f4f4f5', selectionBg: '#f4f4f5' },
|
|
38
|
+
dark: { bg: '#0a0a0a', fg: '#fafafa', muted: '#a1a1aa', border: '#27272a', headerBg: '#18181b', headerFg: '#a1a1aa', accent: '#fafafa', rowAlt: '#0a0a0a', rowHover: '#18181b', selectionBg: '#27272a' } },
|
|
39
|
+
{ id: 'tailwind', name: 'Tailwind', radius: 6, font: 'Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
40
|
+
light: { bg: '#ffffff', fg: '#0f172a', muted: '#64748b', border: '#e2e8f0', headerBg: '#f8fafc', headerFg: '#0f172a', accent: '#4f46e5', rowAlt: '#ffffff', rowHover: '#f1f5f9', selectionBg: '#e0e7ff' },
|
|
41
|
+
dark: { bg: '#0f172a', fg: '#f8fafc', muted: '#94a3b8', border: '#334155', headerBg: '#1e293b', headerFg: '#f8fafc', accent: '#818cf8', rowAlt: '#0f172a', rowHover: '#1e293b', selectionBg: '#312e81' } },
|
|
42
|
+
{ id: 'material', name: 'Material 3', radius: 8, font: '"Roboto Flex", Roboto, "Segoe UI", system-ui, sans-serif',
|
|
43
|
+
light: { bg: '#fef7ff', fg: '#1d1b20', muted: '#49454f', border: '#cac4d0', headerBg: '#f3edf7', headerFg: '#1d1b20', accent: '#6750a4', rowAlt: '#fffbfe', rowHover: '#ece6f0', selectionBg: '#e8def8' },
|
|
44
|
+
dark: { bg: '#141218', fg: '#e6e0e9', muted: '#cac4d0', border: '#49454f', headerBg: '#211f26', headerFg: '#e6e0e9', accent: '#d0bcff', rowAlt: '#141218', rowHover: '#2b2930', selectionBg: '#4a4458' } },
|
|
45
|
+
{ id: 'excel', name: 'Excel', radius: 0, font: '"Aptos", "Segoe UI", Calibri, Arial, sans-serif',
|
|
46
|
+
light: { bg: '#ffffff', fg: '#323130', muted: '#605e5c', border: '#d2d0ce', headerBg: '#f3f2f1', headerFg: '#444444', accent: '#107c41', rowAlt: '#ffffff', rowHover: '#edebe9', selectionBg: 'rgba(16, 124, 65, 0.10)' },
|
|
47
|
+
dark: { bg: '#1b1a19', fg: '#f3f2f1', muted: '#c8c6c4', border: '#3b3a39', headerBg: '#252423', headerFg: '#c8c6c4', accent: '#58c188', rowAlt: '#1b1a19', rowHover: '#323130', selectionBg: 'rgba(88, 193, 136, 0.16)' } },
|
|
48
|
+
{ id: 'fluent', name: 'Fluent 2', radius: 4, font: '"Segoe UI Variable", "Segoe UI", system-ui, sans-serif',
|
|
49
|
+
light: { bg: '#ffffff', fg: '#242424', muted: '#616161', border: '#e0e0e0', headerBg: '#f5f5f5', headerFg: '#242424', accent: '#0f6cbd', rowAlt: '#fafafa', rowHover: '#f0f0f0', selectionBg: '#ebebeb' },
|
|
50
|
+
dark: { bg: '#1f1f1f', fg: '#ffffff', muted: '#d6d6d6', border: '#666666', headerBg: '#141414', headerFg: '#ffffff', accent: '#479ef5', rowAlt: '#1a1a1a', rowHover: '#3d3d3d', selectionBg: '#383838' } },
|
|
51
|
+
{ id: 'carbon', name: 'Carbon', radius: 0, font: '"IBM Plex Sans", "Helvetica Neue", Arial, sans-serif',
|
|
52
|
+
light: { bg: '#ffffff', fg: '#161616', muted: '#525252', border: '#e0e0e0', headerBg: '#e0e0e0', headerFg: '#161616', accent: '#0f62fe', rowAlt: '#f4f4f4', rowHover: '#e8e8e8', selectionBg: '#d0e2ff' },
|
|
53
|
+
dark: { bg: '#161616', fg: '#f4f4f4', muted: '#c6c6c6', border: '#393939', headerBg: '#393939', headerFg: '#f4f4f4', accent: '#78a9ff', rowAlt: '#262626', rowHover: '#333333', selectionBg: '#393939' } },
|
|
54
|
+
{ id: 'sap', name: 'SAP Fiori', radius: 6, font: '"72", "72full", "Arial", sans-serif',
|
|
55
|
+
light: { bg: '#f5f6f7', fg: '#131e29', muted: '#556b82', border: '#e5e5e5', headerBg: '#ffffff', headerFg: '#131e29', accent: '#0070f2', rowAlt: '#f5f6f7', rowHover: '#eaecee', selectionBg: '#ebf8ff' },
|
|
56
|
+
dark: { bg: '#12171c', fg: '#f5f6f7', muted: '#8396a8', border: '#2e3742', headerBg: '#1d232a', headerFg: '#f5f6f7', accent: '#4db1ff', rowAlt: '#12171c', rowHover: '#222b35', selectionBg: '#1d2d3e' } },
|
|
57
|
+
{ id: 'salesforce', name: 'Salesforce', radius: 4, font: '"Salesforce Sans", "SF Pro", Helvetica, Arial, sans-serif',
|
|
58
|
+
light: { bg: '#ffffff', fg: '#181818', muted: '#757575', border: '#c9c9c9', headerBg: '#f3f3f3', headerFg: '#181818', accent: '#0176d3', rowAlt: '#ffffff', rowHover: '#f3f3f3', selectionBg: '#ecebea' },
|
|
59
|
+
dark: { bg: '#181818', fg: '#ffffff', muted: '#c9c9c9', border: '#3e3e3c', headerBg: '#2e2e2e', headerFg: '#ffffff', accent: '#1b96ff', rowAlt: '#181818', rowHover: '#2e2e2e', selectionBg: '#1e4d8e' } },
|
|
60
|
+
{ id: 'atlassian', name: 'Atlassian', radius: 3, font: '"Charlie Display", "Atlassian Sans", -apple-system, "Segoe UI", sans-serif',
|
|
61
|
+
light: { bg: '#ffffff', fg: '#172b4d', muted: '#44546f', border: '#dcdfe4', headerBg: '#fafbfc', headerFg: '#44546f', accent: '#0c66e4', rowAlt: '#ffffff', rowHover: '#f1f2f4', selectionBg: '#e9f2ff' },
|
|
62
|
+
dark: { bg: '#1d2125', fg: '#b6c2cf', muted: '#8590a2', border: '#38414a', headerBg: '#22272b', headerFg: '#8590a2', accent: '#579dff', rowAlt: '#1d2125', rowHover: '#2c333a', selectionBg: '#09326c' } },
|
|
63
|
+
{ id: 'github', name: 'GitHub', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif',
|
|
64
|
+
light: { bg: '#ffffff', fg: '#1f2328', muted: '#59636e', border: '#d1d9e0', headerBg: '#f6f8fa', headerFg: '#59636e', accent: '#0969da', rowAlt: '#ffffff', rowHover: '#f6f8fa', selectionBg: '#ddf4ff' },
|
|
65
|
+
dark: { bg: '#0d1117', fg: '#f0f6fc', muted: '#9198a1', border: '#3d444d', headerBg: '#151b23', headerFg: '#9198a1', accent: '#4493f8', rowAlt: '#0d1117', rowHover: '#151b23', selectionBg: 'rgba(56, 139, 253, 0.10)' } },
|
|
66
|
+
{ id: 'antd', name: 'Ant Design', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", sans-serif',
|
|
67
|
+
light: { bg: '#ffffff', fg: 'rgba(0, 0, 0, 0.88)', muted: 'rgba(0, 0, 0, 0.65)', border: '#d9d9d9', headerBg: '#fafafa', headerFg: 'rgba(0, 0, 0, 0.88)', accent: '#1677ff', rowAlt: '#ffffff', rowHover: '#f5f5f5', selectionBg: '#e6f4ff' },
|
|
68
|
+
dark: { bg: '#141414', fg: 'rgba(255, 255, 255, 0.85)', muted: 'rgba(255, 255, 255, 0.65)', border: '#424242', headerBg: '#1f1f1f', headerFg: 'rgba(255, 255, 255, 0.85)', accent: '#177ddc', rowAlt: '#141414', rowHover: '#262626', selectionBg: '#111a2c' } },
|
|
69
|
+
{ id: 'ag-alpine', name: 'Alpine', radius: 3, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
70
|
+
light: { bg: '#ffffff', fg: '#181d1f', muted: 'rgba(24, 29, 31, 0.5)', border: '#babfc7', headerBg: '#f8f8f8', headerFg: '#181d1f', accent: '#2196f3', rowAlt: '#fcfcfc', rowHover: 'rgba(33, 150, 243, 0.1)', selectionBg: 'rgba(33, 150, 243, 0.3)' },
|
|
71
|
+
dark: { bg: '#181d1f', fg: '#ffffff', muted: 'rgba(255, 255, 255, 0.5)', border: '#68686e', headerBg: '#222628', headerFg: '#ffffff', accent: '#2196f3', rowAlt: '#222628', rowHover: 'rgba(33, 150, 243, 0.1)', selectionBg: 'rgba(33, 150, 243, 0.3)' } },
|
|
72
|
+
{ id: 'bootstrap', name: 'Bootstrap 5', radius: 6, font: 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
|
73
|
+
light: { bg: '#ffffff', fg: '#212529', muted: '#6c757d', border: '#dee2e6', headerBg: '#f8f9fa', headerFg: '#212529', accent: '#0d6efd', rowAlt: '#fcfcfd', rowHover: '#f2f2f2', selectionBg: '#cfe2ff' },
|
|
74
|
+
dark: { bg: '#212529', fg: '#dee2e6', muted: '#adb5bd', border: '#495057', headerBg: '#2b3035', headerFg: '#dee2e6', accent: '#0d6efd', rowAlt: '#25292e', rowHover: '#2c3034', selectionBg: '#031633' } },
|
|
75
|
+
{ id: 'vercel', name: 'Vercel', radius: 6, font: '"Geist", "Geist Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
76
|
+
light: { bg: '#ffffff', fg: '#000000', muted: '#666666', border: '#eaeaea', headerBg: '#fafafa', headerFg: '#666666', accent: '#0070f3', rowAlt: '#fafafa', rowHover: '#f5f5f5', selectionBg: '#e8f0fe' },
|
|
77
|
+
dark: { bg: '#000000', fg: '#ffffff', muted: '#888888', border: '#333333', headerBg: '#111111', headerFg: '#888888', accent: '#3291ff', rowAlt: '#0a0a0a', rowHover: '#1a1a1a', selectionBg: '#0d2847' } },
|
|
78
|
+
{ id: 'linear', name: 'Linear', radius: 6, font: '"Inter Variable", "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
|
79
|
+
light: { bg: '#ffffff', fg: '#282a30', muted: '#6b6f76', border: '#e9e9eb', headerBg: '#f7f8f8', headerFg: '#6b6f76', accent: '#5e6ad2', rowAlt: '#fbfbfc', rowHover: '#f4f4f5', selectionBg: '#e8eafd' },
|
|
80
|
+
dark: { bg: '#08090a', fg: '#f7f8f8', muted: '#8a8f98', border: '#23252a', headerBg: '#0f1011', headerFg: '#8a8f98', accent: '#5e6ad2', rowAlt: '#0f1011', rowHover: '#1a1b1e', selectionBg: '#2a2d4a' } },
|
|
81
|
+
{ id: 'notion', name: 'Notion', radius: 4, font: 'ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif',
|
|
82
|
+
light: { bg: '#ffffff', fg: '#37352f', muted: '#787774', border: '#e9e9e7', headerBg: '#f7f6f3', headerFg: '#787774', accent: '#2383e2', rowAlt: '#fbfbfa', rowHover: '#f1f1ef', selectionBg: '#d9eaf7' },
|
|
83
|
+
dark: { bg: '#191919', fg: '#d4d4d4', muted: '#9b9b9b', border: '#2f2f2f', headerBg: '#202020', headerFg: '#9b9b9b', accent: '#529cca', rowAlt: '#1c1c1c', rowHover: '#252525', selectionBg: '#20415c' } },
|
|
84
|
+
{ id: 'nord', name: 'Nord', radius: 4, font: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
85
|
+
light: { bg: '#eceff4', fg: '#2e3440', muted: '#4c566a', border: '#d8dee9', headerBg: '#e5e9f0', headerFg: '#2e3440', accent: '#5e81ac', rowAlt: '#e5e9f0', rowHover: '#dde3ec', selectionBg: '#d8dee9' },
|
|
86
|
+
dark: { bg: '#2e3440', fg: '#d8dee9', muted: '#4c566a', border: '#434c5e', headerBg: '#3b4252', headerFg: '#e5e9f0', accent: '#88c0d0', rowAlt: '#333b48', rowHover: '#3b4252', selectionBg: '#434c5e' } },
|
|
87
|
+
{ id: 'dracula', name: 'Dracula', radius: 6, font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
88
|
+
light: { bg: '#fffbeb', fg: '#1f1f1f', muted: '#6c664b', border: '#dedccf', headerBg: '#ece9df', headerFg: '#1f1f1f', accent: '#644ac9', rowAlt: '#fffdf5', rowHover: '#f2efe3', selectionBg: '#cfcfde' },
|
|
89
|
+
dark: { bg: '#282a36', fg: '#f8f8f2', muted: '#6272a4', border: '#44475a', headerBg: '#21222c', headerFg: '#f8f8f2', accent: '#bd93f9', rowAlt: '#2d2f3b', rowHover: '#343746', selectionBg: '#44475a' } },
|
|
90
|
+
{ id: 'catppuccin', name: 'Catppuccin', radius: 8, font: '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
91
|
+
light: { bg: '#eff1f5', fg: '#4c4f69', muted: '#6c6f85', border: '#ccd0da', headerBg: '#e6e9ef', headerFg: '#4c4f69', accent: '#8839ef', rowAlt: '#e6e9ef', rowHover: '#dce0e8', selectionBg: '#dce0e8' },
|
|
92
|
+
dark: { bg: '#1e1e2e', fg: '#cdd6f4', muted: '#a6adc8', border: '#313244', headerBg: '#181825', headerFg: '#cdd6f4', accent: '#cba6f7', rowAlt: '#1a1a28', rowHover: '#313244', selectionBg: '#45475a' } },
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
/** The default preset (used when nothing is chosen). */
|
|
96
|
+
export const defaultThemePreset: ThemePreset = themePresets.find((t) => t.id === 'tailwind') ?? themePresets[0]!
|
|
97
|
+
|
|
98
|
+
/** Look up a preset by id. */
|
|
99
|
+
export const getThemePreset = (id?: string): ThemePreset | undefined =>
|
|
100
|
+
id ? themePresets.find((t) => t.id === id) : undefined
|
|
101
|
+
|
|
102
|
+
/** A readable (near-black / white) foreground for text placed on the accent. */
|
|
103
|
+
function onAccent(accent: string): string {
|
|
104
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(accent.trim())
|
|
105
|
+
if (!m) return '#ffffff'
|
|
106
|
+
const n = parseInt(m[1]!, 16)
|
|
107
|
+
const r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255
|
|
108
|
+
// Perceived luminance (sRGB, quick approximation).
|
|
109
|
+
const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255
|
|
110
|
+
return lum > 0.6 ? '#18181b' : '#ffffff'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Resolve the effective `--sg-*` tokens for a preset in a given mode, with an
|
|
114
|
+
* optional accent override applied. */
|
|
115
|
+
export function resolveThemeTokens(preset: ThemePreset | undefined, mode: ThemeMode, accentOverride?: string): Record<string, string> {
|
|
116
|
+
const resolved = preset ?? defaultThemePreset
|
|
117
|
+
const p = resolved[mode]
|
|
118
|
+
const accent = accentOverride || p.accent
|
|
119
|
+
return {
|
|
120
|
+
'--sg-bg': p.bg,
|
|
121
|
+
'--sg-fg': p.fg,
|
|
122
|
+
'--sg-muted': p.muted,
|
|
123
|
+
'--sg-border': p.border,
|
|
124
|
+
'--sg-header-bg': p.headerBg,
|
|
125
|
+
'--sg-header-fg': p.headerFg,
|
|
126
|
+
// A subtle inset surface (used by e.g. the master-detail region behind the
|
|
127
|
+
// nested grid). Themed so it isn't a fixed light grey on dark presets.
|
|
128
|
+
'--sg-bg-subtle': p.headerBg,
|
|
129
|
+
'--sg-row-alt-bg': p.rowAlt,
|
|
130
|
+
'--sg-row-hover-bg': p.rowHover,
|
|
131
|
+
'--sg-selection-bg': p.selectionBg,
|
|
132
|
+
// Inputs/checkboxes: pin to the theme's own surface so they never borrow the
|
|
133
|
+
// host page's input tokens (which is how a themed grid ended up with dark
|
|
134
|
+
// checkboxes on a light page when the host mode differed).
|
|
135
|
+
'--sg-input-bg': p.bg,
|
|
136
|
+
'--sg-input-border': p.border,
|
|
137
|
+
'--sg-accent': accent,
|
|
138
|
+
'--sg-on-accent': onAccent(accent),
|
|
139
|
+
'--sg-radius': `${resolved.radius}px`,
|
|
140
|
+
'--sg-font': resolved.font,
|
|
141
|
+
// The grid's custom scrollbar is a shadow-DOM web component that reads its
|
|
142
|
+
// OWN --sg-scrollbar-* tokens (with fixed light-mode fallbacks). Without
|
|
143
|
+
// these it ignores the theme entirely - a light scrollbar left on a dark
|
|
144
|
+
// grid. CSS custom properties pierce shadow DOM, so defining them here (on
|
|
145
|
+
// the themed container) reaches the scrollbar. Derive them from the palette
|
|
146
|
+
// so the scrollbar tracks the preset + light/dark mode like everything else.
|
|
147
|
+
'--sg-scrollbar-bg': p.headerBg,
|
|
148
|
+
'--sg-scrollbar-border': p.border,
|
|
149
|
+
'--sg-scrollbar-thumb': p.muted,
|
|
150
|
+
'--sg-scrollbar-thumb-hover': p.fg,
|
|
151
|
+
'--sg-scrollbar-thumb-active': p.fg,
|
|
152
|
+
'--sg-scrollbar-arrow': p.muted,
|
|
153
|
+
'--sg-scrollbar-arrow-hover': p.fg,
|
|
154
|
+
'--sg-scrollbar-arrow-hover-bg': p.rowHover,
|
|
155
|
+
'--sg-scrollbar-arrow-active': p.fg,
|
|
156
|
+
'--sg-scrollbar-arrow-active-bg': p.selectionBg,
|
|
157
|
+
'--sg-scrollbar-arrow-disabled': p.border,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { createColumnVirtualizer } from './column-virtualizer'
|
|
3
|
-
|
|
4
|
-
describe('column virtualizer', () => {
|
|
5
|
-
it('computes horizontal window', () => {
|
|
6
|
-
const v = createColumnVirtualizer({
|
|
7
|
-
count: 1000,
|
|
8
|
-
viewportWidth: 700,
|
|
9
|
-
overscan: 3,
|
|
10
|
-
estimateSize: () => 140,
|
|
11
|
-
})
|
|
12
|
-
const items = v.getVirtualItems()
|
|
13
|
-
expect(items[0]?.index).toBe(0)
|
|
14
|
-
expect(items.length).toBeGreaterThan(0)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it('updates indices on horizontal scroll', () => {
|
|
18
|
-
const v = createColumnVirtualizer({
|
|
19
|
-
count: 1000,
|
|
20
|
-
viewportWidth: 700,
|
|
21
|
-
estimateSize: () => 140,
|
|
22
|
-
})
|
|
23
|
-
v.setHorizontalOffset(2800)
|
|
24
|
-
const state = v.getState()
|
|
25
|
-
expect(state.startIndex).toBeGreaterThan(0)
|
|
26
|
-
})
|
|
27
|
-
})
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createColumnVirtualizer } from './column-virtualizer'
|
|
3
|
+
|
|
4
|
+
describe('column virtualizer', () => {
|
|
5
|
+
it('computes horizontal window', () => {
|
|
6
|
+
const v = createColumnVirtualizer({
|
|
7
|
+
count: 1000,
|
|
8
|
+
viewportWidth: 700,
|
|
9
|
+
overscan: 3,
|
|
10
|
+
estimateSize: () => 140,
|
|
11
|
+
})
|
|
12
|
+
const items = v.getVirtualItems()
|
|
13
|
+
expect(items[0]?.index).toBe(0)
|
|
14
|
+
expect(items.length).toBeGreaterThan(0)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('updates indices on horizontal scroll', () => {
|
|
18
|
+
const v = createColumnVirtualizer({
|
|
19
|
+
count: 1000,
|
|
20
|
+
viewportWidth: 700,
|
|
21
|
+
estimateSize: () => 140,
|
|
22
|
+
})
|
|
23
|
+
v.setHorizontalOffset(2800)
|
|
24
|
+
const state = v.getState()
|
|
25
|
+
expect(state.startIndex).toBeGreaterThan(0)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
import { createVirtualizer } from './virtualizer'
|
|
2
|
-
|
|
3
|
-
export type ColumnSizeEstimator = (index: number) => number
|
|
4
|
-
|
|
5
|
-
export function createColumnVirtualizer(input: {
|
|
6
|
-
count: number
|
|
7
|
-
viewportWidth: number
|
|
8
|
-
scrollOffset?: number
|
|
9
|
-
overscan?: number
|
|
10
|
-
/** Either a uniform size (number) or a per-column size function. */
|
|
11
|
-
estimateSize?: number | ColumnSizeEstimator
|
|
12
|
-
}) {
|
|
13
|
-
const virtualizer = createVirtualizer({
|
|
14
|
-
count: input.count,
|
|
15
|
-
estimateSize: input.estimateSize ?? 140,
|
|
16
|
-
viewportHeight: input.viewportWidth,
|
|
17
|
-
scrollOffset: input.scrollOffset ?? 0,
|
|
18
|
-
overscan: input.overscan ?? 4,
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
return {
|
|
22
|
-
...virtualizer,
|
|
23
|
-
setViewportWidth(viewportWidth: number) {
|
|
24
|
-
virtualizer.setViewportHeight(viewportWidth)
|
|
25
|
-
},
|
|
26
|
-
setHorizontalOffset(scrollOffset: number) {
|
|
27
|
-
virtualizer.setScrollOffset(scrollOffset)
|
|
28
|
-
},
|
|
29
|
-
}
|
|
30
|
-
}
|
|
1
|
+
import { createVirtualizer } from './virtualizer'
|
|
2
|
+
|
|
3
|
+
export type ColumnSizeEstimator = (index: number) => number
|
|
4
|
+
|
|
5
|
+
export function createColumnVirtualizer(input: {
|
|
6
|
+
count: number
|
|
7
|
+
viewportWidth: number
|
|
8
|
+
scrollOffset?: number
|
|
9
|
+
overscan?: number
|
|
10
|
+
/** Either a uniform size (number) or a per-column size function. */
|
|
11
|
+
estimateSize?: number | ColumnSizeEstimator
|
|
12
|
+
}) {
|
|
13
|
+
const virtualizer = createVirtualizer({
|
|
14
|
+
count: input.count,
|
|
15
|
+
estimateSize: input.estimateSize ?? 140,
|
|
16
|
+
viewportHeight: input.viewportWidth,
|
|
17
|
+
scrollOffset: input.scrollOffset ?? 0,
|
|
18
|
+
overscan: input.overscan ?? 4,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
...virtualizer,
|
|
23
|
+
setViewportWidth(viewportWidth: number) {
|
|
24
|
+
virtualizer.setViewportHeight(viewportWidth)
|
|
25
|
+
},
|
|
26
|
+
setHorizontalOffset(scrollOffset: number) {
|
|
27
|
+
virtualizer.setScrollOffset(scrollOffset)
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { createVirtualizer } from './virtualizer'
|
|
2
|
-
import type { VirtualizerOptions } from './types'
|
|
3
|
-
|
|
4
|
-
export function createSvelteVirtualizer(options: VirtualizerOptions) {
|
|
5
|
-
const virtualizer = createVirtualizer(options)
|
|
6
|
-
let version = $state(0)
|
|
7
|
-
|
|
8
|
-
virtualizer.subscribe(() => {
|
|
9
|
-
version += 1
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
return {
|
|
13
|
-
get version() {
|
|
14
|
-
return version
|
|
15
|
-
},
|
|
16
|
-
setOptions: virtualizer.setOptions,
|
|
17
|
-
setScrollOffset: virtualizer.setScrollOffset,
|
|
18
|
-
setViewportHeight: virtualizer.setViewportHeight,
|
|
19
|
-
scrollToIndex: virtualizer.scrollToIndex,
|
|
20
|
-
getVirtualItems: virtualizer.getVirtualItems,
|
|
21
|
-
getTotalSize: virtualizer.getTotalSize,
|
|
22
|
-
getOffsetForIndex: virtualizer.getOffsetForIndex,
|
|
23
|
-
getSizeForIndex: virtualizer.getSizeForIndex,
|
|
24
|
-
getState: virtualizer.getState,
|
|
25
|
-
}
|
|
26
|
-
}
|
|
1
|
+
import { createVirtualizer } from './virtualizer'
|
|
2
|
+
import type { VirtualizerOptions } from './types'
|
|
3
|
+
|
|
4
|
+
export function createSvelteVirtualizer(options: VirtualizerOptions) {
|
|
5
|
+
const virtualizer = createVirtualizer(options)
|
|
6
|
+
let version = $state(0)
|
|
7
|
+
|
|
8
|
+
virtualizer.subscribe(() => {
|
|
9
|
+
version += 1
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
get version() {
|
|
14
|
+
return version
|
|
15
|
+
},
|
|
16
|
+
setOptions: virtualizer.setOptions,
|
|
17
|
+
setScrollOffset: virtualizer.setScrollOffset,
|
|
18
|
+
setViewportHeight: virtualizer.setViewportHeight,
|
|
19
|
+
scrollToIndex: virtualizer.scrollToIndex,
|
|
20
|
+
getVirtualItems: virtualizer.getVirtualItems,
|
|
21
|
+
getTotalSize: virtualizer.getTotalSize,
|
|
22
|
+
getOffsetForIndex: virtualizer.getOffsetForIndex,
|
|
23
|
+
getSizeForIndex: virtualizer.getSizeForIndex,
|
|
24
|
+
getState: virtualizer.getState,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
export type VirtualItem = {
|
|
2
|
-
index: number
|
|
3
|
-
start: number
|
|
4
|
-
size: number
|
|
5
|
-
end: number
|
|
6
|
-
key: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export type VirtualizerOptions = {
|
|
10
|
-
count: number
|
|
11
|
-
/**
|
|
12
|
-
* Per-item size. Pass a number for a uniform layout (fast path) or a
|
|
13
|
-
* function for variable sizing - the virtualizer will build cumulative
|
|
14
|
-
* offsets and use binary lookup to find the visible window. The function
|
|
15
|
-
* receives the item index and must return its pixel size.
|
|
16
|
-
*/
|
|
17
|
-
estimateSize: number | ((index: number) => number)
|
|
18
|
-
overscan?: number
|
|
19
|
-
viewportHeight: number
|
|
20
|
-
scrollOffset?: number
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export type VirtualizerState = {
|
|
24
|
-
items: Array<VirtualItem>
|
|
25
|
-
totalSize: number
|
|
26
|
-
startIndex: number
|
|
27
|
-
endIndex: number
|
|
28
|
-
scrollOffset: number
|
|
29
|
-
viewportHeight: number
|
|
30
|
-
}
|
|
1
|
+
export type VirtualItem = {
|
|
2
|
+
index: number
|
|
3
|
+
start: number
|
|
4
|
+
size: number
|
|
5
|
+
end: number
|
|
6
|
+
key: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type VirtualizerOptions = {
|
|
10
|
+
count: number
|
|
11
|
+
/**
|
|
12
|
+
* Per-item size. Pass a number for a uniform layout (fast path) or a
|
|
13
|
+
* function for variable sizing - the virtualizer will build cumulative
|
|
14
|
+
* offsets and use binary lookup to find the visible window. The function
|
|
15
|
+
* receives the item index and must return its pixel size.
|
|
16
|
+
*/
|
|
17
|
+
estimateSize: number | ((index: number) => number)
|
|
18
|
+
overscan?: number
|
|
19
|
+
viewportHeight: number
|
|
20
|
+
scrollOffset?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type VirtualizerState = {
|
|
24
|
+
items: Array<VirtualItem>
|
|
25
|
+
totalSize: number
|
|
26
|
+
startIndex: number
|
|
27
|
+
endIndex: number
|
|
28
|
+
scrollOffset: number
|
|
29
|
+
viewportHeight: number
|
|
30
|
+
}
|