@svgrid/grid 2.1.21 → 2.1.23
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 +9 -2
- package/dist/DockNodeView.svelte +306 -0
- package/dist/DockNodeView.svelte.d.ts +7 -0
- package/dist/DockPopoutHost.svelte +17 -0
- package/dist/DockPopoutHost.svelte.d.ts +14 -0
- package/dist/GridMenus.svelte +83 -3
- package/dist/SvAutoComplete.svelte +5 -3
- package/dist/SvCarousel.svelte +20 -42
- package/dist/SvColorInput.svelte +4 -3
- package/dist/SvColorInput.svelte.d.ts +1 -1
- package/dist/SvComboBox.svelte +7 -3
- package/dist/SvCommand.svelte +28 -85
- package/dist/SvCountryInput.svelte +1 -1
- package/dist/SvDateRangeInput.svelte +1 -1
- package/dist/SvDateTimePicker.svelte +9 -3
- package/dist/SvDateTimePicker.svelte.d.ts +3 -0
- package/dist/SvDockLayout.svelte +244 -0
- package/dist/SvDockLayout.svelte.d.ts +77 -0
- package/dist/SvDockManager.svelte +922 -0
- package/dist/SvDockManager.svelte.d.ts +106 -0
- package/dist/SvDrawer.svelte +69 -47
- package/dist/SvDrawer.svelte.d.ts +4 -0
- package/dist/SvDropDownList.svelte +85 -13
- package/dist/SvDropDownList.svelte.d.ts +3 -0
- package/dist/SvField.svelte +21 -4
- package/dist/SvField.svelte.d.ts +1 -0
- package/dist/SvForm.svelte +201 -91
- package/dist/SvForm.svelte.d.ts +15 -22
- package/dist/SvGrid.controller.svelte.d.ts +32 -1
- package/dist/SvGrid.controller.svelte.js +156 -27
- package/dist/SvGrid.css +193 -5
- package/dist/SvGrid.svelte +357 -12
- package/dist/SvGrid.types.d.ts +452 -1
- package/dist/SvGridDropdown.svelte +8 -1
- package/dist/SvGridSelect.svelte +1 -0
- package/dist/SvListBox.svelte +36 -14
- package/dist/SvMaskedInput.svelte +4 -3
- package/dist/SvMaskedInput.svelte.d.ts +1 -1
- package/dist/SvMenu.svelte +1 -1
- package/dist/SvMenuList.svelte +23 -71
- package/dist/SvMenuList.svelte.d.ts +2 -13
- package/dist/SvModal.svelte +12 -29
- package/dist/SvMultiSelect.svelte +1 -0
- package/dist/SvNumberInput.svelte +4 -3
- package/dist/SvNumberInput.svelte.d.ts +1 -1
- package/dist/SvPagination.svelte +18 -18
- package/dist/SvPasswordInput.svelte +3 -2
- package/dist/SvPasswordInput.svelte.d.ts +1 -1
- package/dist/SvPhoneInput.svelte +3 -2
- package/dist/SvPhoneInput.svelte.d.ts +1 -1
- package/dist/SvPopover.svelte +1 -1
- package/dist/SvStepper.svelte +6 -9
- package/dist/SvTagsInput.svelte +3 -2
- package/dist/SvTagsInput.svelte.d.ts +1 -1
- package/dist/SvTooltip.svelte +12 -23
- package/dist/SvTour.svelte +32 -4
- package/dist/SvTreeSelect.svelte +1 -0
- package/dist/build-api.js +76 -54
- package/dist/cdn/svgrid.js +10907 -6444
- package/dist/cdn/svgrid.svelte-external.js +9622 -5475
- package/dist/columns.js +6 -0
- package/dist/conditional-formatting.d.ts +67 -3
- package/dist/conditional-formatting.js +124 -17
- package/dist/createCarousel.svelte.d.ts +72 -0
- package/dist/createCarousel.svelte.js +80 -0
- package/dist/createCommand.svelte.d.ts +62 -0
- package/dist/createCommand.svelte.js +109 -0
- package/dist/createForm.svelte.d.ts +62 -0
- package/dist/createForm.svelte.js +374 -0
- package/dist/createMenu.svelte.d.ts +88 -0
- package/dist/createMenu.svelte.js +153 -0
- package/dist/createOverlay.svelte.d.ts +38 -0
- package/dist/createOverlay.svelte.js +68 -0
- package/dist/createPagination.d.ts +74 -0
- package/dist/createPagination.js +71 -0
- package/dist/createPopoverSelect.svelte.js +6 -12
- package/dist/createStepper.d.ts +44 -0
- package/dist/createStepper.js +26 -0
- package/dist/createTooltip.svelte.d.ts +32 -0
- package/dist/createTooltip.svelte.js +61 -0
- package/dist/datetime/timezone.d.ts +36 -0
- package/dist/datetime/timezone.js +110 -0
- package/dist/dock-context.d.ts +52 -0
- package/dist/dock-context.js +1 -0
- package/dist/dock-manager-model.d.ts +136 -0
- package/dist/dock-manager-model.js +514 -0
- package/dist/dock-model.d.ts +105 -0
- package/dist/dock-model.js +288 -0
- package/dist/dock-popout.d.ts +30 -0
- package/dist/dock-popout.js +81 -0
- package/dist/editor-contract.d.ts +2 -0
- package/dist/filter-operators.d.ts +6 -0
- package/dist/filter-operators.js +35 -2
- package/dist/filtering/excel-filters.d.ts +18 -1
- package/dist/filtering/excel-filters.js +56 -0
- package/dist/form-field.d.ts +73 -0
- package/dist/form-field.js +15 -0
- package/dist/index.d.ts +25 -5
- package/dist/index.js +40 -4
- package/dist/list-nav.d.ts +18 -0
- package/dist/list-nav.js +30 -0
- package/dist/list-option.d.ts +2 -0
- package/dist/menu-item.d.ts +20 -0
- package/dist/menu-item.js +1 -0
- package/dist/menus.d.ts +5 -0
- package/dist/menus.js +68 -16
- package/dist/panel-resize.d.ts +25 -0
- package/dist/panel-resize.js +40 -0
- package/dist/popover.d.ts +29 -3
- package/dist/popover.js +22 -6
- package/dist/recurrence.d.ts +33 -7
- package/dist/recurrence.js +151 -13
- package/dist/row-drag.d.ts +8 -0
- package/dist/row-drag.js +11 -0
- package/dist/scheduler-ical.d.ts +30 -0
- package/dist/scheduler-ical.js +180 -0
- package/dist/scheduler-model.d.ts +269 -0
- package/dist/scheduler-model.js +602 -0
- package/dist/scheduler-view.svelte.d.ts +22 -0
- package/dist/scheduler-view.svelte.js +13 -0
- package/dist/spreadsheet.js +20 -12
- package/dist/ui-app.types.d.ts +3 -0
- package/package.json +5 -1
- package/src/DockNodeView.svelte +306 -0
- package/src/DockPopoutHost.svelte +17 -0
- package/src/GridMenus.svelte +83 -3
- package/src/SvAutoComplete.svelte +5 -3
- package/src/SvCarousel.svelte +20 -42
- package/src/SvCarousel.test.ts +68 -0
- package/src/SvColorInput.svelte +4 -3
- package/src/SvComboBox.svelte +7 -3
- package/src/SvCommand.svelte +28 -85
- package/src/SvCommand.test.ts +78 -0
- package/src/SvCountryInput.svelte +1 -1
- package/src/SvDateRangeInput.svelte +1 -1
- package/src/SvDateTimePicker.svelte +9 -3
- package/src/SvDockLayout.svelte +244 -0
- package/src/SvDockManager.svelte +922 -0
- package/src/SvDrawer.svelte +69 -47
- package/src/SvDropDownList.svelte +85 -13
- package/src/SvField.svelte +21 -4
- package/src/SvForm.svelte +201 -91
- package/src/SvForm.test.ts +299 -0
- package/src/SvGrid.controller.svelte.ts +152 -28
- package/src/SvGrid.css +193 -5
- package/src/SvGrid.svelte +357 -12
- package/src/SvGrid.types.ts +463 -1
- package/src/SvGridDropdown.svelte +8 -1
- package/src/SvGridSelect.svelte +1 -0
- package/src/SvListBox.svelte +36 -14
- package/src/SvMaskedInput.svelte +4 -3
- package/src/SvMenu.svelte +1 -1
- package/src/SvMenu.test.ts +24 -0
- package/src/SvMenuList.svelte +23 -71
- package/src/SvModal.svelte +12 -29
- package/src/SvModal.test.ts +82 -0
- package/src/SvMultiSelect.svelte +1 -0
- package/src/SvNumberInput.svelte +4 -3
- package/src/SvPagination.svelte +18 -18
- package/src/SvPasswordInput.svelte +3 -2
- package/src/SvPhoneInput.svelte +3 -2
- package/src/SvPopover.svelte +1 -1
- package/src/SvStepper.svelte +6 -9
- package/src/SvTagsInput.svelte +3 -2
- package/src/SvTooltip.svelte +12 -23
- package/src/SvTooltip.test.ts +73 -0
- package/src/SvTour.svelte +32 -4
- package/src/SvTreeSelect.svelte +1 -0
- package/src/build-api.ts +74 -51
- package/src/columns.ts +5 -0
- package/src/conditional-formatting.test.ts +104 -0
- package/src/conditional-formatting.ts +176 -19
- package/src/createCarousel.svelte.ts +104 -0
- package/src/createCommand.svelte.ts +125 -0
- package/src/createForm.svelte.ts +333 -0
- package/src/createMenu.svelte.ts +202 -0
- package/src/createOverlay.svelte.ts +101 -0
- package/src/createPagination.test.ts +65 -0
- package/src/createPagination.ts +84 -0
- package/src/createPopoverSelect.svelte.ts +5 -9
- package/src/createStepper.test.ts +62 -0
- package/src/createStepper.ts +61 -0
- package/src/createTooltip.svelte.ts +86 -0
- package/src/datetime/timezone.test.ts +69 -0
- package/src/datetime/timezone.ts +134 -0
- package/src/dock-context.ts +47 -0
- package/src/dock-manager-model.test.ts +275 -0
- package/src/dock-manager-model.ts +593 -0
- package/src/dock-model.test.ts +163 -0
- package/src/dock-model.ts +368 -0
- package/src/dock-popout.ts +94 -0
- package/src/editor-contract.ts +2 -0
- package/src/filter-operators.test.ts +59 -0
- package/src/filter-operators.ts +35 -2
- package/src/filtering/excel-filters.test.ts +86 -1
- package/src/filtering/excel-filters.ts +65 -0
- package/src/form-field.ts +87 -0
- package/src/index.ts +158 -4
- package/src/list-nav.test.ts +49 -0
- package/src/list-nav.ts +29 -0
- package/src/list-option.ts +2 -0
- package/src/menu-item.ts +21 -0
- package/src/menus.test.ts +48 -0
- package/src/menus.ts +73 -16
- package/src/panel-resize.ts +52 -0
- package/src/popover.bounds.test.ts +93 -0
- package/src/popover.ts +51 -7
- package/src/recurrence.test.ts +88 -1
- package/src/recurrence.ts +166 -15
- package/src/row-drag.ts +10 -0
- package/src/scheduler-ical.test.ts +68 -0
- package/src/scheduler-ical.ts +180 -0
- package/src/scheduler-model.test.ts +546 -0
- package/src/scheduler-model.ts +853 -0
- package/src/scheduler-view.svelte.ts +33 -0
- package/src/spreadsheet.ts +20 -12
- package/src/svgrid.effect-driven-filter.svelte.test.ts +116 -0
- package/src/ui-app.types.ts +3 -0
- package/src/virtual.test.ts +6 -4
package/dist/columns.js
CHANGED
|
@@ -155,6 +155,12 @@ export function createColumns(ctx) {
|
|
|
155
155
|
ctx.resizingColumnId = columnId;
|
|
156
156
|
ctx.resizeStartX = event.clientX;
|
|
157
157
|
ctx.resizeStartWidth = getColumnWidth(columnId);
|
|
158
|
+
// Capture the pointer so the drag keeps tracking when it leaves the 4px
|
|
159
|
+
// handle - without this, touch (and fast mouse) drags drop mid-resize (#59).
|
|
160
|
+
try {
|
|
161
|
+
event.currentTarget?.setPointerCapture?.(event.pointerId);
|
|
162
|
+
}
|
|
163
|
+
catch { /* capture is best-effort */ }
|
|
158
164
|
document.addEventListener("pointermove", onColumnResizeMove);
|
|
159
165
|
document.addEventListener("pointerup", endColumnResize);
|
|
160
166
|
document.addEventListener("pointercancel", endColumnResize);
|
|
@@ -9,15 +9,60 @@
|
|
|
9
9
|
* numeric range and returns the visual primitives, so the `<SvGrid>` render
|
|
10
10
|
* component just paints the result and the logic is unit-testable.
|
|
11
11
|
*/
|
|
12
|
+
/** A single gradient stop along the normalized 0..1 domain. */
|
|
13
|
+
export type ColorScaleStop = {
|
|
14
|
+
offset: number;
|
|
15
|
+
color: string;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* How `minValue`/`maxValue` (and the derived column extremes) are read:
|
|
19
|
+
* - `absolute` (default): literal data values.
|
|
20
|
+
* - `percent`: 0..100 positions along the column's own min..max span, so you
|
|
21
|
+
* can say "tint the top 20%" without knowing the numbers up front.
|
|
22
|
+
*/
|
|
23
|
+
export type ScaleBounds = 'absolute' | 'percent';
|
|
12
24
|
export type ColorScaleFormat = {
|
|
13
25
|
type: 'colorScale';
|
|
14
26
|
/** 2-stop (min/max) or 3-stop (min/mid/max) gradient. Hex colors. */
|
|
15
|
-
min
|
|
27
|
+
min?: string;
|
|
16
28
|
mid?: string;
|
|
17
|
-
max
|
|
29
|
+
max?: string;
|
|
30
|
+
/**
|
|
31
|
+
* N-stop gradient along the normalized domain (offsets 0..1). When present
|
|
32
|
+
* this overrides `min`/`mid`/`max`, enabling banded / traffic-light scales.
|
|
33
|
+
*/
|
|
34
|
+
stops?: ReadonlyArray<ColorScaleStop>;
|
|
35
|
+
/**
|
|
36
|
+
* `hue` (default): interpolate between stop colors into an opaque fill.
|
|
37
|
+
* `alpha`: keep a single `base` color and interpolate its *opacity*, so the
|
|
38
|
+
* tint composites over zebra striping, selection, and pinned backgrounds
|
|
39
|
+
* instead of painting over them - Adaptable's "live heat map" look.
|
|
40
|
+
*/
|
|
41
|
+
mode?: 'hue' | 'alpha';
|
|
42
|
+
/** Base color for `alpha` mode. Default `#2563eb`. */
|
|
43
|
+
base?: string;
|
|
44
|
+
/** [min, max] opacity for `alpha` mode. Default [0.05, 0.85]. */
|
|
45
|
+
alphaBounds?: readonly [number, number];
|
|
18
46
|
/** Fix the scale; otherwise derived from the column's data. */
|
|
19
47
|
minValue?: number;
|
|
20
48
|
maxValue?: number;
|
|
49
|
+
/** Interpret `minValue`/`maxValue` as absolute values or 0..100 percents. */
|
|
50
|
+
bounds?: ScaleBounds;
|
|
51
|
+
/**
|
|
52
|
+
* Diverging scale pinned at 0: negatives and positives shade outward from a
|
|
53
|
+
* neutral midpoint, symmetric around zero. Ideal for P&L / price deltas.
|
|
54
|
+
*/
|
|
55
|
+
zeroCentred?: boolean;
|
|
56
|
+
/** Flip the ramp so the lowest values attract the most attention. */
|
|
57
|
+
reverse?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Tint by this cell's value as a proportion of another column's value on the
|
|
60
|
+
* SAME row (a field key on the row object), instead of the column extremes.
|
|
61
|
+
* Column comparison - e.g. filled vs target, open vs total.
|
|
62
|
+
*/
|
|
63
|
+
compareColumn?: string;
|
|
64
|
+
/** Attach a tooltip showing the raw value or its % position on the ramp. */
|
|
65
|
+
tooltip?: 'value' | 'percent';
|
|
21
66
|
};
|
|
22
67
|
export type DataBarFormat = {
|
|
23
68
|
type: 'dataBar';
|
|
@@ -25,6 +70,15 @@ export type DataBarFormat = {
|
|
|
25
70
|
negativeColor?: string;
|
|
26
71
|
minValue?: number;
|
|
27
72
|
maxValue?: number;
|
|
73
|
+
/** Interpret `minValue`/`maxValue` as absolute values or 0..100 percents. */
|
|
74
|
+
bounds?: ScaleBounds;
|
|
75
|
+
/**
|
|
76
|
+
* Size the bar by this cell's value as a proportion of another column's
|
|
77
|
+
* value on the same row (a field key), instead of the column extremes.
|
|
78
|
+
*/
|
|
79
|
+
compareColumn?: string;
|
|
80
|
+
/** Fill the bar with a left-to-right gradient rather than a flat color. */
|
|
81
|
+
gradient?: boolean;
|
|
28
82
|
/** Show the cell's text on top of the bar. Default true. */
|
|
29
83
|
showValue?: boolean;
|
|
30
84
|
};
|
|
@@ -61,9 +115,12 @@ export type ResolvedCellFormat = {
|
|
|
61
115
|
percent: number;
|
|
62
116
|
color: string;
|
|
63
117
|
fromRight: boolean;
|
|
118
|
+
gradient?: boolean;
|
|
64
119
|
};
|
|
65
120
|
icon?: string;
|
|
66
121
|
iconOnly?: boolean;
|
|
122
|
+
/** Tooltip text (value / percentile), when a format requests one. */
|
|
123
|
+
title?: string;
|
|
67
124
|
};
|
|
68
125
|
export type ColumnStat = {
|
|
69
126
|
min: number;
|
|
@@ -81,11 +138,18 @@ export declare function computeColumnStat(values: Iterable<unknown>): ColumnStat
|
|
|
81
138
|
export declare function contrastText(bg: string): string | null;
|
|
82
139
|
/** Linear interpolate two hex colors. Falls back to `a` if parsing fails. */
|
|
83
140
|
export declare function lerpColor(a: string, b: string, t: number): string;
|
|
141
|
+
/** Build an `rgba()` string from a hex color + opacity. Falls back to the hex. */
|
|
142
|
+
export declare function rgba(hex: string, alpha: number): string;
|
|
84
143
|
/**
|
|
85
144
|
* Resolve every format that applies to one cell into a single set of visual
|
|
86
145
|
* primitives. Later-listed formats override earlier ones for the same
|
|
87
146
|
* property, so order your `conditionalFormats` from general to specific.
|
|
88
147
|
*/
|
|
89
148
|
export declare function resolveCellFormat<TData = unknown>(value: unknown, row: TData, columnId: string, formats: ReadonlyArray<ConditionalFormat<TData>>, stat: ColumnStat | null): ResolvedCellFormat;
|
|
90
|
-
/**
|
|
149
|
+
/**
|
|
150
|
+
* Whether any format needs a numeric min/max precomputed. Color-scale and
|
|
151
|
+
* data-bar formats do, EXCEPT when they derive their range from another column
|
|
152
|
+
* on the same row (`compareColumn`) - those read the row, not column stats.
|
|
153
|
+
*/
|
|
154
|
+
export declare function formatNeedsStats(f: ConditionalFormat<any>): boolean;
|
|
91
155
|
export declare function formatsNeedingStats(formats: ReadonlyArray<ConditionalFormat<any>>): boolean;
|
|
@@ -92,13 +92,85 @@ export function lerpColor(a, b, t) {
|
|
|
92
92
|
ca[2] + (cb[2] - ca[2]) * k,
|
|
93
93
|
]);
|
|
94
94
|
}
|
|
95
|
+
/** Build an `rgba()` string from a hex color + opacity. Falls back to the hex. */
|
|
96
|
+
export function rgba(hex, alpha) {
|
|
97
|
+
const rgb = parseHex(hex);
|
|
98
|
+
if (!rgb)
|
|
99
|
+
return hex;
|
|
100
|
+
return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${clamp01(alpha).toFixed(3)})`;
|
|
101
|
+
}
|
|
102
|
+
/** Color of an N-stop ramp at normalized position `t`. Stops sorted by offset. */
|
|
103
|
+
function stopsColorAt(stops, t) {
|
|
104
|
+
const sorted = [...stops].sort((a, b) => a.offset - b.offset);
|
|
105
|
+
if (sorted.length === 0)
|
|
106
|
+
return '#000000';
|
|
107
|
+
if (t <= sorted[0].offset)
|
|
108
|
+
return sorted[0].color;
|
|
109
|
+
const last = sorted[sorted.length - 1];
|
|
110
|
+
if (t >= last.offset)
|
|
111
|
+
return last.color;
|
|
112
|
+
for (let i = 0; i < sorted.length - 1; i += 1) {
|
|
113
|
+
const a = sorted[i];
|
|
114
|
+
const b = sorted[i + 1];
|
|
115
|
+
if (t >= a.offset && t <= b.offset) {
|
|
116
|
+
const span = b.offset - a.offset || 1;
|
|
117
|
+
return lerpColor(a.color, b.color, (t - a.offset) / span);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return last.color;
|
|
121
|
+
}
|
|
122
|
+
/** The interpolated color for a color-scale at normalized position `t`. */
|
|
95
123
|
function colorScaleAt(fmt, t) {
|
|
124
|
+
if (fmt.stops && fmt.stops.length >= 2)
|
|
125
|
+
return stopsColorAt(fmt.stops, t);
|
|
126
|
+
const min = fmt.min ?? '#ffffff';
|
|
127
|
+
const max = fmt.max ?? '#000000';
|
|
96
128
|
if (fmt.mid) {
|
|
97
129
|
return t <= 0.5
|
|
98
|
-
? lerpColor(
|
|
99
|
-
: lerpColor(fmt.mid,
|
|
130
|
+
? lerpColor(min, fmt.mid, t / 0.5)
|
|
131
|
+
: lerpColor(fmt.mid, max, (t - 0.5) / 0.5);
|
|
100
132
|
}
|
|
101
|
-
return lerpColor(
|
|
133
|
+
return lerpColor(min, max, t);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Normalized position (0..1) of `n` within a format's domain, honoring
|
|
137
|
+
* absolute/percent bounds, zero-centred diverging scales, cross-column
|
|
138
|
+
* comparison, and reverse. Returns null when the domain can't be resolved
|
|
139
|
+
* (e.g. compareColumn missing / zero on the row).
|
|
140
|
+
*/
|
|
141
|
+
function domainT(fmt, n, row, stat) {
|
|
142
|
+
let t;
|
|
143
|
+
if (fmt.compareColumn) {
|
|
144
|
+
const other = Number(row?.[fmt.compareColumn]);
|
|
145
|
+
if (!Number.isFinite(other) || other === 0)
|
|
146
|
+
return null;
|
|
147
|
+
t = clamp01(n / other);
|
|
148
|
+
}
|
|
149
|
+
else if (fmt.type === 'colorScale' && fmt.zeroCentred) {
|
|
150
|
+
const dMin = fmt.minValue ?? stat?.min ?? n;
|
|
151
|
+
const dMax = fmt.maxValue ?? stat?.max ?? n;
|
|
152
|
+
const bound = Math.max(Math.abs(dMin), Math.abs(dMax)) || 1;
|
|
153
|
+
t = clamp01(0.5 + n / bound / 2);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const dMin = stat?.min ?? n;
|
|
157
|
+
const dMax = stat?.max ?? n;
|
|
158
|
+
let lo;
|
|
159
|
+
let hi;
|
|
160
|
+
if (fmt.bounds === 'percent') {
|
|
161
|
+
const span = dMax - dMin;
|
|
162
|
+
lo = dMin + ((fmt.minValue ?? 0) / 100) * span;
|
|
163
|
+
hi = dMin + ((fmt.maxValue ?? 100) / 100) * span;
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
lo = fmt.minValue ?? dMin;
|
|
167
|
+
hi = fmt.maxValue ?? dMax;
|
|
168
|
+
}
|
|
169
|
+
t = hi === lo ? 0.5 : clamp01((n - lo) / (hi - lo));
|
|
170
|
+
}
|
|
171
|
+
if (fmt.type === 'colorScale' && fmt.reverse)
|
|
172
|
+
t = 1 - t;
|
|
173
|
+
return t;
|
|
102
174
|
}
|
|
103
175
|
/**
|
|
104
176
|
* Resolve every format that applies to one cell into a single set of visual
|
|
@@ -132,25 +204,52 @@ export function resolveCellFormat(value, row, columnId, formats, stat) {
|
|
|
132
204
|
if (value == null || value === '' || !Number.isFinite(n))
|
|
133
205
|
continue;
|
|
134
206
|
if (fmt.type === 'colorScale') {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
207
|
+
const t = domainT(fmt, n, row, stat);
|
|
208
|
+
if (t == null)
|
|
209
|
+
continue;
|
|
210
|
+
if (fmt.mode === 'alpha') {
|
|
211
|
+
const [alo, ahi] = fmt.alphaBounds ?? [0.05, 0.85];
|
|
212
|
+
out.background = rgba(fmt.base ?? '#2563eb', alo + (ahi - alo) * t);
|
|
213
|
+
// Translucent tint composites over the row bg; leave text color alone.
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
out.background = colorScaleAt(fmt, t);
|
|
217
|
+
// The scale fills the whole cell, so keep the text legible against it.
|
|
218
|
+
const c = contrastText(out.background);
|
|
219
|
+
if (c)
|
|
220
|
+
out.color = c;
|
|
221
|
+
}
|
|
222
|
+
if (fmt.tooltip === 'value')
|
|
223
|
+
out.title = String(value);
|
|
224
|
+
else if (fmt.tooltip === 'percent')
|
|
225
|
+
out.title = `${Math.round(t * 100)}%`;
|
|
143
226
|
}
|
|
144
227
|
else if (fmt.type === 'dataBar') {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
228
|
+
let percent;
|
|
229
|
+
if (fmt.compareColumn) {
|
|
230
|
+
const t = domainT(fmt, n, row, stat);
|
|
231
|
+
if (t == null)
|
|
232
|
+
continue;
|
|
233
|
+
percent = t * 100;
|
|
234
|
+
}
|
|
235
|
+
else if (fmt.bounds === 'percent') {
|
|
236
|
+
const t = domainT(fmt, n, row, stat);
|
|
237
|
+
if (t == null)
|
|
238
|
+
continue;
|
|
239
|
+
percent = t * 100;
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
const lo = fmt.minValue ?? Math.min(0, stat?.min ?? 0);
|
|
243
|
+
const hi = fmt.maxValue ?? stat?.max ?? n;
|
|
244
|
+
const span = hi - lo || 1;
|
|
245
|
+
percent = clamp01((n - lo) / span) * 100;
|
|
246
|
+
}
|
|
149
247
|
const negative = n < 0;
|
|
150
248
|
out.dataBar = {
|
|
151
249
|
percent,
|
|
152
250
|
color: negative ? (fmt.negativeColor ?? '#ef4444') : fmt.color,
|
|
153
251
|
fromRight: false,
|
|
252
|
+
gradient: fmt.gradient,
|
|
154
253
|
};
|
|
155
254
|
if (fmt.showValue === false)
|
|
156
255
|
out.color = 'transparent';
|
|
@@ -164,7 +263,15 @@ export function resolveCellFormat(value, row, columnId, formats, stat) {
|
|
|
164
263
|
}
|
|
165
264
|
return out;
|
|
166
265
|
}
|
|
167
|
-
/**
|
|
266
|
+
/**
|
|
267
|
+
* Whether any format needs a numeric min/max precomputed. Color-scale and
|
|
268
|
+
* data-bar formats do, EXCEPT when they derive their range from another column
|
|
269
|
+
* on the same row (`compareColumn`) - those read the row, not column stats.
|
|
270
|
+
*/
|
|
271
|
+
export function formatNeedsStats(f) {
|
|
272
|
+
return ((f.type === 'colorScale' || f.type === 'dataBar') &&
|
|
273
|
+
!('compareColumn' in f && f.compareColumn));
|
|
274
|
+
}
|
|
168
275
|
export function formatsNeedingStats(formats) {
|
|
169
|
-
return formats.some(
|
|
276
|
+
return formats.some(formatNeedsStats);
|
|
170
277
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createCarousel - the HEADLESS core behind <SvCarousel>: slide navigation
|
|
3
|
+
* (with optional looping), autoplay that pauses on hover/focus and via a
|
|
4
|
+
* persistent stop toggle (WCAG 2.2.2), and rtl-aware arrow wiring - as
|
|
5
|
+
* prop-getters you spread onto your own markup. The component keeps only the
|
|
6
|
+
* visual track + swipe. Runes-based, like `createListbox`.
|
|
7
|
+
*/
|
|
8
|
+
import { type EditorDir } from './editor-contract';
|
|
9
|
+
export type CarouselConfig = {
|
|
10
|
+
count: () => number;
|
|
11
|
+
/** Active slide index (the component owns the state; core calls onChange). */
|
|
12
|
+
current: () => number;
|
|
13
|
+
onChange?: (index: number) => void;
|
|
14
|
+
loop?: () => boolean;
|
|
15
|
+
/** Autoplay interval in ms; 0 = off. */
|
|
16
|
+
autoplay?: () => number;
|
|
17
|
+
dir?: () => EditorDir | undefined;
|
|
18
|
+
};
|
|
19
|
+
export declare function createCarousel(config: CarouselConfig): {
|
|
20
|
+
readonly paused: boolean;
|
|
21
|
+
readonly userPaused: boolean;
|
|
22
|
+
rtl: () => boolean;
|
|
23
|
+
isActive: (i: number) => boolean;
|
|
24
|
+
go: (i: number) => void;
|
|
25
|
+
next: () => void;
|
|
26
|
+
prev: () => void;
|
|
27
|
+
canGoPrev: () => boolean;
|
|
28
|
+
canGoNext: () => boolean;
|
|
29
|
+
/** Spread onto the carousel container. Pauses autoplay on hover/focus. */
|
|
30
|
+
rootProps: (ariaLabel?: string) => {
|
|
31
|
+
role: "group";
|
|
32
|
+
'aria-roledescription': string;
|
|
33
|
+
'aria-label': string;
|
|
34
|
+
onpointerenter: () => void;
|
|
35
|
+
onpointerleave: () => void;
|
|
36
|
+
onfocusin: () => void;
|
|
37
|
+
onfocusout: () => void;
|
|
38
|
+
};
|
|
39
|
+
/** Spread onto each slide element. */
|
|
40
|
+
slideProps: (i: number) => {
|
|
41
|
+
role: "group";
|
|
42
|
+
'aria-roledescription': string;
|
|
43
|
+
'aria-label': string;
|
|
44
|
+
'aria-hidden': boolean;
|
|
45
|
+
inert: boolean;
|
|
46
|
+
};
|
|
47
|
+
/** The visually-leading (inline-start) arrow: logical prev in ltr, next in rtl. */
|
|
48
|
+
prevArrowProps: () => {
|
|
49
|
+
'aria-label': string;
|
|
50
|
+
onclick: () => void;
|
|
51
|
+
disabled: boolean;
|
|
52
|
+
};
|
|
53
|
+
/** The visually-trailing (inline-end) arrow: logical next in ltr, prev in rtl. */
|
|
54
|
+
nextArrowProps: () => {
|
|
55
|
+
'aria-label': string;
|
|
56
|
+
onclick: () => void;
|
|
57
|
+
disabled: boolean;
|
|
58
|
+
};
|
|
59
|
+
/** Spread onto a dot indicator. */
|
|
60
|
+
dotProps: (i: number) => {
|
|
61
|
+
'aria-label': string;
|
|
62
|
+
'aria-current': "true" | undefined;
|
|
63
|
+
onclick: () => void;
|
|
64
|
+
};
|
|
65
|
+
/** Spread onto the persistent play/pause toggle (render only when autoplay > 0). */
|
|
66
|
+
playPauseProps: () => {
|
|
67
|
+
'aria-label': string;
|
|
68
|
+
'aria-pressed': boolean;
|
|
69
|
+
onclick: () => void;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
export type Carousel = ReturnType<typeof createCarousel>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export function createCarousel(config) {
|
|
2
|
+
const count = () => config.count();
|
|
3
|
+
const current = () => config.current();
|
|
4
|
+
const loop = () => config.loop?.() ?? true;
|
|
5
|
+
const rtl = () => config.dir?.() === 'rtl';
|
|
6
|
+
let paused = $state(false); // hover/focus
|
|
7
|
+
let userPaused = $state(false); // explicit stop toggle
|
|
8
|
+
function go(i) {
|
|
9
|
+
const n = count();
|
|
10
|
+
if (n === 0)
|
|
11
|
+
return;
|
|
12
|
+
const target = loop() ? ((i % n) + n) % n : Math.max(0, Math.min(i, n - 1));
|
|
13
|
+
config.onChange?.(target);
|
|
14
|
+
}
|
|
15
|
+
const next = () => go(current() + 1);
|
|
16
|
+
const prev = () => go(current() - 1);
|
|
17
|
+
const canGoPrev = () => loop() || current() > 0;
|
|
18
|
+
const canGoNext = () => loop() || current() < count() - 1;
|
|
19
|
+
// Autoplay, paused while hovered/focused or explicitly stopped (WCAG 2.2.2).
|
|
20
|
+
$effect(() => {
|
|
21
|
+
const ms = config.autoplay?.() ?? 0;
|
|
22
|
+
if (!ms || paused || userPaused || count() < 2)
|
|
23
|
+
return;
|
|
24
|
+
const t = setInterval(() => next(), ms);
|
|
25
|
+
return () => clearInterval(t);
|
|
26
|
+
});
|
|
27
|
+
return {
|
|
28
|
+
get paused() { return paused; },
|
|
29
|
+
get userPaused() { return userPaused; },
|
|
30
|
+
rtl,
|
|
31
|
+
isActive: (i) => i === current(),
|
|
32
|
+
go,
|
|
33
|
+
next,
|
|
34
|
+
prev,
|
|
35
|
+
canGoPrev,
|
|
36
|
+
canGoNext,
|
|
37
|
+
/** Spread onto the carousel container. Pauses autoplay on hover/focus. */
|
|
38
|
+
rootProps: (ariaLabel = 'Carousel') => ({
|
|
39
|
+
role: 'group',
|
|
40
|
+
'aria-roledescription': 'carousel',
|
|
41
|
+
'aria-label': ariaLabel,
|
|
42
|
+
onpointerenter: () => { paused = true; },
|
|
43
|
+
onpointerleave: () => { paused = false; },
|
|
44
|
+
onfocusin: () => { paused = true; },
|
|
45
|
+
onfocusout: () => { paused = false; },
|
|
46
|
+
}),
|
|
47
|
+
/** Spread onto each slide element. */
|
|
48
|
+
slideProps: (i) => ({
|
|
49
|
+
role: 'group',
|
|
50
|
+
'aria-roledescription': 'slide',
|
|
51
|
+
'aria-label': `${i + 1} of ${count()}`,
|
|
52
|
+
'aria-hidden': i !== current(),
|
|
53
|
+
inert: i !== current(),
|
|
54
|
+
}),
|
|
55
|
+
/** The visually-leading (inline-start) arrow: logical prev in ltr, next in rtl. */
|
|
56
|
+
prevArrowProps: () => ({
|
|
57
|
+
'aria-label': rtl() ? 'Next slide' : 'Previous slide',
|
|
58
|
+
onclick: rtl() ? next : prev,
|
|
59
|
+
disabled: rtl() ? !canGoNext() : !canGoPrev(),
|
|
60
|
+
}),
|
|
61
|
+
/** The visually-trailing (inline-end) arrow: logical next in ltr, prev in rtl. */
|
|
62
|
+
nextArrowProps: () => ({
|
|
63
|
+
'aria-label': rtl() ? 'Previous slide' : 'Next slide',
|
|
64
|
+
onclick: rtl() ? prev : next,
|
|
65
|
+
disabled: rtl() ? !canGoPrev() : !canGoNext(),
|
|
66
|
+
}),
|
|
67
|
+
/** Spread onto a dot indicator. */
|
|
68
|
+
dotProps: (i) => ({
|
|
69
|
+
'aria-label': `Go to slide ${i + 1}`,
|
|
70
|
+
'aria-current': (i === current() ? 'true' : undefined),
|
|
71
|
+
onclick: () => go(i),
|
|
72
|
+
}),
|
|
73
|
+
/** Spread onto the persistent play/pause toggle (render only when autoplay > 0). */
|
|
74
|
+
playPauseProps: () => ({
|
|
75
|
+
'aria-label': userPaused ? 'Play autoplay' : 'Pause autoplay',
|
|
76
|
+
'aria-pressed': userPaused,
|
|
77
|
+
onclick: () => { userPaused = !userPaused; },
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createCommand - the HEADLESS core behind <SvCommand> (the ⌘K palette): the
|
|
3
|
+
* fuzzy-filtered result list, roving active index, run/close, the global toggle
|
|
4
|
+
* hotkey, and the WAI-ARIA combobox wiring (activedescendant) - as prop-getters
|
|
5
|
+
* you spread onto your own input/list/option markup. The overlay lifecycle
|
|
6
|
+
* (focus-trap + scroll-lock + dismissal) is a separate concern; pair this with
|
|
7
|
+
* `createOverlay`. Runes-based, like `createListbox`.
|
|
8
|
+
*/
|
|
9
|
+
import type { CommandItem } from './ui-app.types';
|
|
10
|
+
export type CommandConfig = {
|
|
11
|
+
commands: () => ReadonlyArray<CommandItem>;
|
|
12
|
+
/** Whether the palette is open (used to reset query/active on open). */
|
|
13
|
+
open: () => boolean;
|
|
14
|
+
onRun?: (cmd: CommandItem) => void;
|
|
15
|
+
/** Close the palette (a command ran, or requested by the input). */
|
|
16
|
+
onClose?: () => void;
|
|
17
|
+
/** Toggle open/closed - fired by the global hotkey. */
|
|
18
|
+
onToggleOpen?: () => void;
|
|
19
|
+
/** Global toggle hotkey (Cmd/Ctrl + key). `false` disables it. Default 'mod+k'. */
|
|
20
|
+
hotkey?: () => 'mod+k' | 'mod+p' | false;
|
|
21
|
+
/** DOM hook to scroll the newly-active option into view. */
|
|
22
|
+
scrollActiveIntoView?: (index: number) => void;
|
|
23
|
+
};
|
|
24
|
+
export declare function createCommand(config: CommandConfig): {
|
|
25
|
+
query: string;
|
|
26
|
+
readonly active: number;
|
|
27
|
+
readonly results: CommandItem[];
|
|
28
|
+
isActive: (i: number) => boolean;
|
|
29
|
+
move: (delta: number) => void;
|
|
30
|
+
run: (cmd: CommandItem | undefined) => void;
|
|
31
|
+
/** Arrow/Enter handler - attach to the panel (catches events bubbled from the
|
|
32
|
+
* input) so keys work whether dispatched on the input or the dialog. */
|
|
33
|
+
onKeydown: (e: KeyboardEvent) => void;
|
|
34
|
+
/** Id of the list element (for aria-controls). */
|
|
35
|
+
listId: string;
|
|
36
|
+
/** Id of the option at `i` (for aria-activedescendant + item id). */
|
|
37
|
+
optionId: (i: number) => string;
|
|
38
|
+
/** Spread onto the search `<input>`. */
|
|
39
|
+
inputProps: () => {
|
|
40
|
+
role: "combobox";
|
|
41
|
+
'aria-expanded': true;
|
|
42
|
+
'aria-controls': string;
|
|
43
|
+
'aria-activedescendant': string | undefined;
|
|
44
|
+
value: string;
|
|
45
|
+
oninput: (e: Event) => void;
|
|
46
|
+
};
|
|
47
|
+
/** Spread onto the `<ul>` list. */
|
|
48
|
+
listProps: () => {
|
|
49
|
+
id: string;
|
|
50
|
+
role: "listbox";
|
|
51
|
+
};
|
|
52
|
+
/** Spread onto the option element at `i`. */
|
|
53
|
+
optionProps: (i: number) => {
|
|
54
|
+
id: string;
|
|
55
|
+
'data-idx': number;
|
|
56
|
+
role: "option";
|
|
57
|
+
'aria-selected': boolean;
|
|
58
|
+
onpointermove: () => void;
|
|
59
|
+
onclick: () => void;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
export type Command = ReturnType<typeof createCommand>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { fuzzyScore } from './fuzzy';
|
|
2
|
+
import { nextEditorId } from './editor-contract';
|
|
3
|
+
export function createCommand(config) {
|
|
4
|
+
const id = nextEditorId('sv-cmd');
|
|
5
|
+
const listId = `${id}-list`;
|
|
6
|
+
const optionId = (i) => `${id}-opt-${i}`;
|
|
7
|
+
let query = $state('');
|
|
8
|
+
let active = $state(0);
|
|
9
|
+
const results = $derived.by(() => {
|
|
10
|
+
const q = query.trim();
|
|
11
|
+
const list = config.commands().filter((c) => !c.disabled);
|
|
12
|
+
if (!q)
|
|
13
|
+
return list;
|
|
14
|
+
return list
|
|
15
|
+
.map((c) => ({ c, s: fuzzyScore(`${c.label} ${c.keywords ?? ''} ${c.group ?? ''}`, q) }))
|
|
16
|
+
.filter((x) => x.s !== null)
|
|
17
|
+
.sort((a, b) => b.s - a.s)
|
|
18
|
+
.map((x) => x.c);
|
|
19
|
+
});
|
|
20
|
+
function run(cmd) {
|
|
21
|
+
if (!cmd || cmd.disabled)
|
|
22
|
+
return;
|
|
23
|
+
config.onClose?.();
|
|
24
|
+
config.onRun?.(cmd);
|
|
25
|
+
cmd.onRun?.();
|
|
26
|
+
}
|
|
27
|
+
function move(delta) {
|
|
28
|
+
const n = results.length;
|
|
29
|
+
if (!n)
|
|
30
|
+
return;
|
|
31
|
+
active = (active + delta + n) % n;
|
|
32
|
+
config.scrollActiveIntoView?.(active);
|
|
33
|
+
}
|
|
34
|
+
function onInputKeydown(e) {
|
|
35
|
+
if (e.key === 'ArrowDown') {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
move(1);
|
|
38
|
+
}
|
|
39
|
+
else if (e.key === 'ArrowUp') {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
move(-1);
|
|
42
|
+
}
|
|
43
|
+
else if (e.key === 'Enter') {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
run(results[active]);
|
|
46
|
+
}
|
|
47
|
+
// Escape + outside-click are owned by the overlay/dismissable layer.
|
|
48
|
+
}
|
|
49
|
+
// Reset the active row as the query changes.
|
|
50
|
+
$effect(() => { void query; active = 0; });
|
|
51
|
+
// Reset query + active each time the palette opens.
|
|
52
|
+
$effect(() => { if (config.open()) {
|
|
53
|
+
query = '';
|
|
54
|
+
active = 0;
|
|
55
|
+
} });
|
|
56
|
+
// Global toggle hotkey.
|
|
57
|
+
$effect(() => {
|
|
58
|
+
const hk = config.hotkey?.() ?? 'mod+k';
|
|
59
|
+
if (!hk)
|
|
60
|
+
return;
|
|
61
|
+
const key = hk === 'mod+p' ? 'p' : 'k';
|
|
62
|
+
const onKey = (e) => {
|
|
63
|
+
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === key) {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
config.onToggleOpen?.();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
window.addEventListener('keydown', onKey);
|
|
69
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
get query() { return query; },
|
|
73
|
+
set query(v) { query = v; },
|
|
74
|
+
get active() { return active; },
|
|
75
|
+
get results() { return results; },
|
|
76
|
+
isActive: (i) => i === active,
|
|
77
|
+
move,
|
|
78
|
+
run,
|
|
79
|
+
/** Arrow/Enter handler - attach to the panel (catches events bubbled from the
|
|
80
|
+
* input) so keys work whether dispatched on the input or the dialog. */
|
|
81
|
+
onKeydown: onInputKeydown,
|
|
82
|
+
/** Id of the list element (for aria-controls). */
|
|
83
|
+
listId,
|
|
84
|
+
/** Id of the option at `i` (for aria-activedescendant + item id). */
|
|
85
|
+
optionId,
|
|
86
|
+
/** Spread onto the search `<input>`. */
|
|
87
|
+
inputProps: () => ({
|
|
88
|
+
role: 'combobox',
|
|
89
|
+
'aria-expanded': true,
|
|
90
|
+
'aria-controls': listId,
|
|
91
|
+
'aria-activedescendant': results[active] ? optionId(active) : undefined,
|
|
92
|
+
value: query,
|
|
93
|
+
oninput: (e) => { query = e.currentTarget.value; },
|
|
94
|
+
// Arrow/Enter is handled on the panel (see `onKeydown`) via bubbling, so it
|
|
95
|
+
// works for both input-dispatched and panel-dispatched key events.
|
|
96
|
+
}),
|
|
97
|
+
/** Spread onto the `<ul>` list. */
|
|
98
|
+
listProps: () => ({ id: listId, role: 'listbox' }),
|
|
99
|
+
/** Spread onto the option element at `i`. */
|
|
100
|
+
optionProps: (i) => ({
|
|
101
|
+
id: optionId(i),
|
|
102
|
+
'data-idx': i,
|
|
103
|
+
role: 'option',
|
|
104
|
+
'aria-selected': i === active,
|
|
105
|
+
onpointermove: () => { active = i; },
|
|
106
|
+
onclick: () => run(results[i]),
|
|
107
|
+
}),
|
|
108
|
+
};
|
|
109
|
+
}
|