@svgrid/grid 2.2.28 → 2.2.29
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/GridMenus.svelte +148 -100
- package/dist/SvGrid.controller.svelte.d.ts +14 -1
- package/dist/SvGrid.controller.svelte.js +112 -28
- package/dist/SvGrid.css +15 -34
- package/dist/SvGrid.svelte +49 -21
- package/dist/SvGrid.types.d.ts +20 -5
- package/dist/SvListBox.svelte +42 -3
- package/dist/SvListBox.svelte.d.ts +10 -1
- package/dist/cdn/GridMenus-Da_p7SKS.js +486 -0
- package/dist/cdn/GridMenus-Ds3OpzZT.js +490 -0
- package/dist/cdn/{src-BxNEslEo.js → src-BLJfdyL0.js} +4014 -3869
- package/dist/cdn/{src-C4yKQZ5t.js → src-DYDCiC0D.js} +7036 -6891
- package/dist/cdn/svgrid.js +8 -8
- package/dist/cdn/svgrid.svelte-external.js +8 -8
- package/dist/column-groups.js +8 -10
- package/dist/column-id.d.ts +9 -0
- package/dist/column-id.js +25 -0
- package/dist/core.js +2 -1
- package/dist/createListbox.svelte.d.ts +15 -2
- package/dist/createListbox.svelte.js +27 -6
- package/dist/editing.d.ts +1 -0
- package/dist/editing.js +45 -10
- package/dist/filtering/excel-filters.d.ts +30 -10
- package/dist/filtering/excel-filters.js +96 -15
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/menus.d.ts +2 -0
- package/dist/menus.js +25 -0
- package/dist/row-resize.js +40 -0
- package/dist/selection.js +1 -1
- package/package.json +1 -1
- package/src/GridMenus.svelte +148 -100
- package/src/SvGrid.controller.svelte.ts +123 -27
- package/src/SvGrid.css +15 -34
- package/src/SvGrid.svelte +49 -21
- package/src/SvGrid.types.ts +20 -5
- package/src/SvListBox.svelte +42 -3
- package/src/column-groups.test.ts +48 -0
- package/src/column-groups.ts +8 -10
- package/src/column-id.ts +30 -0
- package/src/core.ts +2 -1
- package/src/createListbox.svelte.ts +39 -7
- package/src/editing.test.ts +90 -0
- package/src/editing.ts +46 -12
- package/src/filtering/excel-filters.test.ts +44 -1
- package/src/filtering/excel-filters.ts +91 -14
- package/src/index.ts +2 -0
- package/src/menus.test.ts +40 -4
- package/src/menus.ts +27 -0
- package/src/row-resize.test.ts +100 -0
- package/src/row-resize.ts +36 -0
- package/src/selection.ts +1 -1
- package/src/svgrid.api-extensions.test.ts +8 -3
- package/src/svgrid.cell-dom-ids.test.ts +96 -0
- package/src/svgrid.conditional-stat-scope.test.ts +111 -0
- package/src/svgrid.filter-menu-listbox.svelte.test.ts +308 -0
- package/src/svgrid.filter-menu-scroll.test.ts +3 -1
- package/src/svgrid.group-header-ids.test.ts +121 -0
- package/dist/cdn/GridMenus-BbzEvTYO.js +0 -421
- package/dist/cdn/GridMenus-E220X2kM.js +0 -417
|
@@ -16,6 +16,7 @@ import { createRowDrag, } from "./row-drag";
|
|
|
16
16
|
import { createAlignedGrids, } from "./aligned-grids";
|
|
17
17
|
import { resolveColumnTypes, } from "./column-types";
|
|
18
18
|
import { computeColumnGroupMeta, hiddenLeavesForCollapse, } from "./column-groups";
|
|
19
|
+
import { resolveColumnId } from "./column-id";
|
|
19
20
|
import { createGridApi, } from "./build-api";
|
|
20
21
|
import { createClipboard, } from "./clipboard";
|
|
21
22
|
import { resolveGridMessages } from "./grid-messages";
|
|
@@ -23,6 +24,7 @@ import { getPivotEngine, hasPivotEngine } from "./pivot-view.svelte";
|
|
|
23
24
|
import { filterOperatorOptions, fallbackOperatorOption, TEXT_OPERATORS, NUMBER_OPERATORS, DATE_OPERATORS, CHECKBOX_OPERATORS, operatorOption, operatorsForColumn, defaultOperatorFor, operatorLabelFor, } from "./filter-operators";
|
|
24
25
|
import { isBucketableColumn, buildBuckets, isInBucket, } from "./facet-buckets";
|
|
25
26
|
import { getColumnBaseValue, isGroupRow, toolPanelHeaderLabel, formatSummaryNumeric, getColumnAlign, getPinnedCellValue, getColumnAccessorValue, columnDefMatchesId, } from "./cell-values";
|
|
27
|
+
import { untrack } from "svelte";
|
|
26
28
|
/**
|
|
27
29
|
* Conservative fallback for the browser's max element height, used during SSR
|
|
28
30
|
* or if runtime detection fails. 8M is below every known engine cap (Firefox
|
|
@@ -145,7 +147,15 @@ function observeSizeRaf(el, cb) {
|
|
|
145
147
|
observer.disconnect();
|
|
146
148
|
};
|
|
147
149
|
}
|
|
148
|
-
export function createSvGridController(rawProps) {
|
|
150
|
+
export function createSvGridController(rawProps, domIdBase) {
|
|
151
|
+
/**
|
|
152
|
+
* Base for every DOM id this grid mints (`<base>_cell_<row>_<col>`). The view
|
|
153
|
+
* passes Svelte's per-instance `$props.id()`, which is stable across SSR and
|
|
154
|
+
* hydration; two `<SvGrid>` instances on one page used to emit identical cell
|
|
155
|
+
* ids, so `aria-activedescendant` on the second pointed into the first (#77).
|
|
156
|
+
* Falls back to the historical literal for direct controller construction.
|
|
157
|
+
*/
|
|
158
|
+
const gridDomId = domIdBase ?? "svgrid";
|
|
149
159
|
// Runtime option overrides set via the imperative api (`api.setOption(key, value)`).
|
|
150
160
|
// Every controller read of a prop goes through the `props` proxy below (and the view
|
|
151
161
|
// reads controller-resolved gates like `_features` / `editingEnabled`, not the raw
|
|
@@ -620,16 +630,13 @@ export function createSvGridController(rawProps) {
|
|
|
620
630
|
if (depth === 0)
|
|
621
631
|
return [];
|
|
622
632
|
const leafEntries = [];
|
|
623
|
-
function buildId(def, parentId, fallbackIx) {
|
|
624
|
-
return def.id ?? def.field ?? `${parentId ?? 'col'}_d_${fallbackIx}`;
|
|
625
|
-
}
|
|
626
633
|
// Leaves hidden by a collapsed/expanded group are skipped everywhere here,
|
|
627
634
|
// so group colSpan + widthPx exclude them and stay aligned with the leaves
|
|
628
635
|
// the body actually renders.
|
|
629
636
|
const hiddenLeaf = hiddenByGroupCollapse;
|
|
630
637
|
function collectLeaves(defs, parentId, depthHere) {
|
|
631
638
|
defs.forEach((def, ix) => {
|
|
632
|
-
const id =
|
|
639
|
+
const id = resolveColumnId(def, parentId, depthHere, ix);
|
|
633
640
|
if (def.columns?.length) {
|
|
634
641
|
collectLeaves(def.columns, id, depthHere + 1);
|
|
635
642
|
}
|
|
@@ -639,17 +646,22 @@ export function createSvGridController(rawProps) {
|
|
|
639
646
|
});
|
|
640
647
|
}
|
|
641
648
|
collectLeaves(userCols, undefined, 0);
|
|
642
|
-
function indexTree(defs, parentId, cursor) {
|
|
649
|
+
function indexTree(defs, parentId, cursor, depthHere) {
|
|
643
650
|
const nodes = [];
|
|
644
|
-
|
|
645
|
-
|
|
651
|
+
// Walk by array index, not by a count of pushed nodes: the `continue`
|
|
652
|
+
// below skips hidden leaves without pushing, so a counter would drift and
|
|
653
|
+
// hand the next unnamed sibling a different id than `collectLeaves` and
|
|
654
|
+
// the engine give it (#63).
|
|
655
|
+
for (let ix = 0; ix < defs.length; ix += 1) {
|
|
656
|
+
const def = defs[ix];
|
|
657
|
+
const id = resolveColumnId(def, parentId, depthHere, ix);
|
|
646
658
|
// Skip leaves the collapse state hides, so leaf indices/colSpans match
|
|
647
659
|
// `leafEntries` (and the body's rendered columns) exactly.
|
|
648
660
|
if (!def.columns?.length && hiddenLeaf[id])
|
|
649
661
|
continue;
|
|
650
662
|
const leafStart = cursor.leaf;
|
|
651
663
|
if (def.columns?.length) {
|
|
652
|
-
indexTree(def.columns, id, cursor);
|
|
664
|
+
indexTree(def.columns, id, cursor, depthHere + 1);
|
|
653
665
|
}
|
|
654
666
|
else {
|
|
655
667
|
cursor.leaf += 1;
|
|
@@ -660,7 +672,7 @@ export function createSvGridController(rawProps) {
|
|
|
660
672
|
return nodes;
|
|
661
673
|
}
|
|
662
674
|
const cursor = { leaf: 0 };
|
|
663
|
-
const topNodes = indexTree(userCols, undefined, cursor);
|
|
675
|
+
const topNodes = indexTree(userCols, undefined, cursor, 0);
|
|
664
676
|
function nodesAtDepth(nodes, currentDepth, targetDepth) {
|
|
665
677
|
if (currentDepth === targetDepth)
|
|
666
678
|
return nodes;
|
|
@@ -668,7 +680,7 @@ export function createSvGridController(rawProps) {
|
|
|
668
680
|
for (const n of nodes) {
|
|
669
681
|
if (n.def.columns?.length) {
|
|
670
682
|
const childCursor = { leaf: n.leafStart };
|
|
671
|
-
const children = indexTree(n.def.columns, n.id, childCursor);
|
|
683
|
+
const children = indexTree(n.def.columns, n.id, childCursor, currentDepth + 1);
|
|
672
684
|
out.push(...nodesAtDepth(children, currentDepth + 1, targetDepth));
|
|
673
685
|
}
|
|
674
686
|
else {
|
|
@@ -760,10 +772,13 @@ export function createSvGridController(rawProps) {
|
|
|
760
772
|
const formats = props.conditionalFormats;
|
|
761
773
|
if (!formats?.length || !formatsNeedingStats(formats))
|
|
762
774
|
return map;
|
|
763
|
-
// `
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
|
|
775
|
+
// `filtered` (default): every row that survives the filters, ignoring the
|
|
776
|
+
// page slice - so a value keeps the same colour as you page through (#61).
|
|
777
|
+
// `visible`: only the rows on screen, rescaling per page. `all`: the full
|
|
778
|
+
// unfiltered dataset, for a scale that stays put as you filter.
|
|
779
|
+
const scope = props.conditionalStatScope ?? "filtered";
|
|
780
|
+
const scanAll = scope === "all";
|
|
781
|
+
const scanRows = scope === "visible" ? allRows : allRowsBeforePagination;
|
|
767
782
|
for (const column of allColumns) {
|
|
768
783
|
const needs = formats.some((f) => formatNeedsStats(f) && (!f.columns || f.columns.includes(column.id)));
|
|
769
784
|
if (!needs)
|
|
@@ -779,7 +794,7 @@ export function createSvGridController(rawProps) {
|
|
|
779
794
|
}
|
|
780
795
|
})()
|
|
781
796
|
: (function* () {
|
|
782
|
-
for (const row of
|
|
797
|
+
for (const row of scanRows) {
|
|
783
798
|
yield fieldFn
|
|
784
799
|
? fieldFn(row.original)
|
|
785
800
|
: field
|
|
@@ -1740,7 +1755,7 @@ export function createSvGridController(rawProps) {
|
|
|
1740
1755
|
const inCols = active.colIndex >= 0 && active.colIndex < allColumns.length;
|
|
1741
1756
|
if (!inRows || !inCols)
|
|
1742
1757
|
return null;
|
|
1743
|
-
return getGridCellDomId(
|
|
1758
|
+
return getGridCellDomId(gridDomId, active.rowIndex, active.colIndex);
|
|
1744
1759
|
});
|
|
1745
1760
|
// Above this many cells (rows x columns) the summary aggregation is
|
|
1746
1761
|
// deferred one animation frame so it never blocks first paint - a
|
|
@@ -1808,7 +1823,7 @@ export function createSvGridController(rawProps) {
|
|
|
1808
1823
|
grid.setActiveCell({
|
|
1809
1824
|
rowIndex: 0,
|
|
1810
1825
|
colIndex: 0,
|
|
1811
|
-
cellId: getGridCellDomId(
|
|
1826
|
+
cellId: getGridCellDomId(gridDomId, 0, 0),
|
|
1812
1827
|
});
|
|
1813
1828
|
});
|
|
1814
1829
|
$effect(() => {
|
|
@@ -2166,13 +2181,55 @@ export function createSvGridController(rawProps) {
|
|
|
2166
2181
|
}
|
|
2167
2182
|
return Array.from(seen).sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
|
2168
2183
|
}
|
|
2184
|
+
/**
|
|
2185
|
+
* Facet values for the open menu, SNAPSHOTTED when it opens.
|
|
2186
|
+
*
|
|
2187
|
+
* `facetValuesForColumn` walks every row and natural-sorts the distinct
|
|
2188
|
+
* values, so deriving it live re-ran that whole scan on every data change.
|
|
2189
|
+
* On a streaming grid (the trading-desk demo replaces its row array a couple
|
|
2190
|
+
* of times a second) that meant rebuilding a 10k-entry set + sort - and
|
|
2191
|
+
* re-keying the rendered value list - continuously for as long as the menu
|
|
2192
|
+
* sat open. The offered values are frozen for the life of the popover
|
|
2193
|
+
* instead, which is also what a spreadsheet's filter checklist does.
|
|
2194
|
+
* Filtering itself is unaffected: `valueFilters` matches live row values.
|
|
2195
|
+
*
|
|
2196
|
+
* The cache is a plain local, not `$state`, so writing it here can't feed
|
|
2197
|
+
* back into this derived.
|
|
2198
|
+
*/
|
|
2199
|
+
// Stable identities for the closed-menu case, so the derives below don't hand
|
|
2200
|
+
// out a fresh empty array/set on every read.
|
|
2201
|
+
const EMPTY_FACETS = [];
|
|
2202
|
+
const EMPTY_FACET_SET = new Set();
|
|
2203
|
+
let facetCache = null;
|
|
2169
2204
|
const columnMenuFacetValues = $derived.by(() => {
|
|
2170
2205
|
// The funnel popover drives via `filterMenuFor`; the column menu's Filter
|
|
2171
2206
|
// tab drives via `columnMenuFor`. Support whichever is open.
|
|
2172
2207
|
const columnId = filterMenuFor ?? columnMenuFor;
|
|
2173
2208
|
if (!columnId)
|
|
2174
|
-
return
|
|
2175
|
-
|
|
2209
|
+
return EMPTY_FACETS;
|
|
2210
|
+
// Server-provided values land asynchronously, so they stay TRACKED and
|
|
2211
|
+
// re-snapshot when they arrive. Locally derived values are read untracked
|
|
2212
|
+
// so a live data tick can't retrigger the scan.
|
|
2213
|
+
const source = props.serverFilterValues
|
|
2214
|
+
? (serverFacetValues[columnId] ?? null)
|
|
2215
|
+
: null;
|
|
2216
|
+
if (facetCache?.columnId === columnId && facetCache.source === source) {
|
|
2217
|
+
return facetCache.values;
|
|
2218
|
+
}
|
|
2219
|
+
facetCache = {
|
|
2220
|
+
columnId,
|
|
2221
|
+
source,
|
|
2222
|
+
values: untrack(() => facetValuesForColumn(columnId)),
|
|
2223
|
+
};
|
|
2224
|
+
return facetCache.values;
|
|
2225
|
+
});
|
|
2226
|
+
// Drop the snapshot when the menu closes, so reopening the SAME column
|
|
2227
|
+
// re-scans and picks up whatever the data has become meanwhile. This has to
|
|
2228
|
+
// be an effect rather than a branch in the derived above: the derived is
|
|
2229
|
+
// lazy, and nothing reads it while the menu is shut.
|
|
2230
|
+
$effect(() => {
|
|
2231
|
+
if (!(filterMenuFor ?? columnMenuFor))
|
|
2232
|
+
facetCache = null;
|
|
2176
2233
|
});
|
|
2177
2234
|
const columnMenuVisibleFacets = $derived.by(() => {
|
|
2178
2235
|
const query = columnMenuSearch.trim().toLowerCase();
|
|
@@ -2180,20 +2237,40 @@ export function createSvGridController(rawProps) {
|
|
|
2180
2237
|
return columnMenuFacetValues;
|
|
2181
2238
|
return columnMenuFacetValues.filter((value) => value.toLowerCase().includes(query));
|
|
2182
2239
|
});
|
|
2240
|
+
/** Visible facets as listbox options ((Blanks) label for the empty value). */
|
|
2241
|
+
const columnMenuFacetOptions = $derived(columnMenuVisibleFacets.map((value) => ({
|
|
2242
|
+
value,
|
|
2243
|
+
label: value === "" ? "(Blanks)" : value,
|
|
2244
|
+
})));
|
|
2245
|
+
/**
|
|
2246
|
+
* Checked facets for the open menu. An ABSENT `valueFilters` entry means
|
|
2247
|
+
* "everything checked", so that case materializes the set once here rather
|
|
2248
|
+
* than every consumer re-deriving it.
|
|
2249
|
+
*/
|
|
2250
|
+
const columnMenuSelectedFacets = $derived.by(() => {
|
|
2251
|
+
const columnId = filterMenuFor ?? columnMenuFor;
|
|
2252
|
+
if (!columnId)
|
|
2253
|
+
return EMPTY_FACET_SET;
|
|
2254
|
+
return valueFilters[columnId] ?? new Set(columnMenuFacetValues);
|
|
2255
|
+
});
|
|
2183
2256
|
// Distinct values offered in the `in` / `notIn` suggestions dropdown for the
|
|
2184
|
-
// active chip input, narrowed by whatever the user has typed.
|
|
2185
|
-
// high-cardinality column
|
|
2186
|
-
|
|
2257
|
+
// active chip input, narrowed by whatever the user has typed. Uncapped - the
|
|
2258
|
+
// dropdown windows its rows, so a high-cardinality column costs a list of
|
|
2259
|
+
// strings, not thousands of nodes.
|
|
2187
2260
|
const inSuggestValues = $derived.by(() => {
|
|
2188
2261
|
if (!inSuggestFor)
|
|
2189
|
-
return
|
|
2262
|
+
return EMPTY_FACETS;
|
|
2190
2263
|
const query = inSuggestQuery.trim().toLowerCase();
|
|
2191
2264
|
const all = facetValuesForColumn(inSuggestFor);
|
|
2192
|
-
|
|
2265
|
+
return query
|
|
2193
2266
|
? all.filter((value) => value.toLowerCase().includes(query))
|
|
2194
2267
|
: all;
|
|
2195
|
-
return matched.slice(0, IN_SUGGEST_LIMIT);
|
|
2196
2268
|
});
|
|
2269
|
+
/** `in` / `notIn` suggestions as listbox options. */
|
|
2270
|
+
const inSuggestOptions = $derived(inSuggestValues.map((value) => ({
|
|
2271
|
+
value,
|
|
2272
|
+
label: value === "" ? "(Blanks)" : value,
|
|
2273
|
+
})));
|
|
2197
2274
|
// Fire onApiReady exactly once when the grid is first ready. Wrapping in
|
|
2198
2275
|
// an effect that tracks `props.onApiReady` was racy - every parent render
|
|
2199
2276
|
// creates a new inline arrow, the effect re-fired, and any synchronous
|
|
@@ -2332,6 +2409,7 @@ export function createSvGridController(rawProps) {
|
|
|
2332
2409
|
get inSuggestQuery() { return inSuggestQuery; },
|
|
2333
2410
|
set inSuggestQuery(v) { inSuggestQuery = v; },
|
|
2334
2411
|
get inSuggestValues() { return inSuggestValues; },
|
|
2412
|
+
get inSuggestOptions() { return inSuggestOptions; },
|
|
2335
2413
|
get facetValuesForColumn() { return facetValuesForColumn; },
|
|
2336
2414
|
get chooseColumnsPos() { return chooseColumnsPos; },
|
|
2337
2415
|
set chooseColumnsPos(v) { chooseColumnsPos = v; },
|
|
@@ -2621,6 +2699,7 @@ export function createSvGridController(rawProps) {
|
|
|
2621
2699
|
get columnWindowRightSpacer() { return columnWindowRightSpacer; },
|
|
2622
2700
|
get activeCell() { return activeCell; },
|
|
2623
2701
|
get activeDescendantId() { return activeDescendantId; },
|
|
2702
|
+
get gridDomId() { return gridDomId; },
|
|
2624
2703
|
get formatSummaryNumeric() { return formatSummaryNumeric; },
|
|
2625
2704
|
get computeSummaries() { return computeSummaries; },
|
|
2626
2705
|
get SUMMARY_DEFER_CELL_LIMIT() { return SUMMARY_DEFER_CELL_LIMIT; },
|
|
@@ -2703,6 +2782,7 @@ export function createSvGridController(rawProps) {
|
|
|
2703
2782
|
get applyHistoryStep() { return applyHistoryStep; },
|
|
2704
2783
|
get updateEditingCellValue() { return updateEditingCellValue; },
|
|
2705
2784
|
get onEditorKeyDown() { return onEditorKeyDown; },
|
|
2785
|
+
get commitAndMoveByTab() { return commitAndMoveByTab; },
|
|
2706
2786
|
get focusOnMount() { return focusOnMount; },
|
|
2707
2787
|
get onHeaderSortClick() { return onHeaderSortClick; },
|
|
2708
2788
|
get onGridKeyDown() { return onGridKeyDown; },
|
|
@@ -2743,6 +2823,7 @@ export function createSvGridController(rawProps) {
|
|
|
2743
2823
|
get addFilterToken() { return addFilterToken; },
|
|
2744
2824
|
get removeFilterToken() { return removeFilterToken; },
|
|
2745
2825
|
get toggleFilterToken() { return toggleFilterToken; },
|
|
2826
|
+
get setFilterTokens() { return setFilterTokens; },
|
|
2746
2827
|
get sortColumnFromMenu() { return sortColumnFromMenu; },
|
|
2747
2828
|
get clearColumnSort() { return clearColumnSort; },
|
|
2748
2829
|
get groupByColumnFromMenu() { return groupByColumnFromMenu; },
|
|
@@ -2754,8 +2835,11 @@ export function createSvGridController(rawProps) {
|
|
|
2754
2835
|
get serverFacetLoading() { return serverFacetLoading; },
|
|
2755
2836
|
get columnMenuFacetValues() { return columnMenuFacetValues; },
|
|
2756
2837
|
get columnMenuVisibleFacets() { return columnMenuVisibleFacets; },
|
|
2838
|
+
get columnMenuFacetOptions() { return columnMenuFacetOptions; },
|
|
2839
|
+
get columnMenuSelectedFacets() { return columnMenuSelectedFacets; },
|
|
2757
2840
|
get isFacetChecked() { return isFacetChecked; },
|
|
2758
2841
|
get toggleFacetValue() { return toggleFacetValue; },
|
|
2842
|
+
get setFacetSelection() { return setFacetSelection; },
|
|
2759
2843
|
get isAllFacetsChecked() { return isAllFacetsChecked; },
|
|
2760
2844
|
get toggleAllFacets() { return toggleAllFacets; },
|
|
2761
2845
|
get clearColumnFilter() { return clearColumnFilter; },
|
|
@@ -2769,9 +2853,9 @@ export function createSvGridController(rawProps) {
|
|
|
2769
2853
|
const { showTooltipFor, hideTooltip, flushScheduledScrollSync, scheduleScrollSync, onBodyScroll } = createScrollSync(ctx);
|
|
2770
2854
|
const { onGridKeyDown, onWindowKeydown, onHeaderSortClick } = createKeyboard(ctx);
|
|
2771
2855
|
const { computeSummaries, hasRenderedColumn } = createSummaries(ctx);
|
|
2772
|
-
const { updateFilterRow, updateFilterOperator, updateFilterMenuValue, updateFilterMenuValueTo, addFilterToken, removeFilterToken, toggleFilterToken, toggleCheckboxWithKeyboard, isColumnFiltered, closeMenus, openInSuggest, closeInSuggest, openChooseColumns, openColumnMenu, openFilterMenu, openOperatorMenu, sortColumnFromMenu, clearColumnSort, groupByColumnFromMenu, clearGroupingFromMenu, isFacetChecked, toggleFacetValue, isAllFacetsChecked, toggleAllFacets, clearColumnFilter, changePage, goToPage, setPageSize, openContextMenu, closeContextMenu, contextMenuItems, saveComment, removeComment, closeCommentEditor } = createMenus(ctx);
|
|
2856
|
+
const { updateFilterRow, updateFilterOperator, updateFilterMenuValue, updateFilterMenuValueTo, addFilterToken, removeFilterToken, toggleFilterToken, toggleCheckboxWithKeyboard, isColumnFiltered, closeMenus, openInSuggest, closeInSuggest, openChooseColumns, openColumnMenu, openFilterMenu, openOperatorMenu, sortColumnFromMenu, clearColumnSort, groupByColumnFromMenu, clearGroupingFromMenu, isFacetChecked, toggleFacetValue, setFacetSelection, setFilterTokens, isAllFacetsChecked, toggleAllFacets, clearColumnFilter, changePage, goToPage, setPageSize, openContextMenu, closeContextMenu, contextMenuItems, saveComment, removeComment, closeCommentEditor } = createMenus(ctx);
|
|
2773
2857
|
const { cellConditionalFormat, computeRowClass, computeCellClass, computeCellTooltip, computeCellValidity, computeCellNote, getColumnEditorOptions, formatListCellValue, formatCellValue, formatPinnedValue, computePinnedCellClass } = createCellRender(ctx);
|
|
2774
|
-
const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste } = createEditing(ctx);
|
|
2858
|
+
const { isCellEditable, isCellEditableAt, getRowColumnValue, getCellDisplayValue, startEditingWithChar, startEditing, stopEditing, startFullRowEdit, setFullRowDraft, commitFullRowEdit, cancelFullRowEdit, saveEditingCell, applyHistoryStep, updateEditingCellValue, onEditorKeyDown, commitAndMoveByTab, focusOnMount, onCellDoubleClick, pasteFromClipboard, onGridPaste } = createEditing(ctx);
|
|
2775
2859
|
const { isRowSelected, toggleRowSelectionById, toggleSelectAllRows, setActiveCell, scrollActiveCellIntoView, setSelection, extendSelection, isCellInSelectedRange, getCellRangeEdges, getSelectionRects, isInFillPreview, fillMarqueeEdges, findColumnById, onCellPointerDown, onCellPointerEnter, endDragSelection, onWindowPointerMove, onCellClick, emitCellDoubleClick } = createSelection(ctx);
|
|
2776
2860
|
const { cellPinStyle, isColumnPinned, getCurrentColumnOrder, emitColumnOrder, setColumnOrderInternal, applyColumnDrop, onColumnHeaderDragStart, onColumnHeaderDragOver, onColumnHeaderDragLeave, onColumnHeaderDrop, onColumnHeaderDragEnd, pinColumnLeft, pinColumnRight, unpinColumn, toggleColumnVisibleInPanel, moveColumnInPanel, toggleGroupInPanel, getColumnBaseWidth, getColumnWidth, startColumnResize, onColumnResizeMove, endColumnResize, measureText, autosizeColumn, autosizeAllColumns, resetColumns } = createColumns(ctx);
|
|
2777
2861
|
const { onRowDragStart, onRowDragOver, onRowDragLeave, onRowDrop, onRowsContainerDragOver, onRowsContainerDrop, onRowDragEnd, destroyRowDrag } = createRowDrag(ctx);
|
package/dist/SvGrid.css
CHANGED
|
@@ -1899,35 +1899,15 @@ select.sv-grid-fr-editor {
|
|
|
1899
1899
|
|
|
1900
1900
|
/* In / Not-in value-suggestions dropdown (anchored under the chip input).
|
|
1901
1901
|
Defined AFTER .sv-grid-menu so its tighter sizing wins - the element
|
|
1902
|
-
carries both classes.
|
|
1902
|
+
carries both classes. The rows come from SvListBox, which owns its own
|
|
1903
|
+
scrolling (windowed past ~40 values), so this is just the frame. */
|
|
1903
1904
|
.sv-grid-menu.sv-grid-in-suggest {
|
|
1904
1905
|
min-width: 140px;
|
|
1905
1906
|
max-width: 260px;
|
|
1906
|
-
|
|
1907
|
-
overflow
|
|
1907
|
+
padding: 0;
|
|
1908
|
+
overflow: visible;
|
|
1908
1909
|
font-size: 12px;
|
|
1909
1910
|
}
|
|
1910
|
-
.sv-grid-in-suggest-item {
|
|
1911
|
-
gap: 6px;
|
|
1912
|
-
padding: 3px 8px;
|
|
1913
|
-
border-radius: 4px;
|
|
1914
|
-
line-height: 1.4;
|
|
1915
|
-
}
|
|
1916
|
-
.sv-grid-in-suggest-item[aria-selected="true"] {
|
|
1917
|
-
color: var(--sg-accent, #0b63f3);
|
|
1918
|
-
font-weight: 600;
|
|
1919
|
-
}
|
|
1920
|
-
.sv-grid-in-suggest-check {
|
|
1921
|
-
flex: none;
|
|
1922
|
-
width: 12px;
|
|
1923
|
-
text-align: center;
|
|
1924
|
-
color: var(--sg-accent, #0b63f3);
|
|
1925
|
-
}
|
|
1926
|
-
.sv-grid-in-suggest-label {
|
|
1927
|
-
overflow: hidden;
|
|
1928
|
-
text-overflow: ellipsis;
|
|
1929
|
-
white-space: nowrap;
|
|
1930
|
-
}
|
|
1931
1911
|
|
|
1932
1912
|
.sv-grid-menu-item[aria-checked="true"] {
|
|
1933
1913
|
background: var(--sg-selection-bg, #eaf2ff);
|
|
@@ -2062,12 +2042,13 @@ select.sv-grid-fr-editor {
|
|
|
2062
2042
|
padding: 2px 7px !important;
|
|
2063
2043
|
}
|
|
2064
2044
|
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2045
|
+
/* The value / column checklists are SvListBox instances (windowed past ~40
|
|
2046
|
+
rows). They draw their own box off the shared --sg-input-border/--sg-border
|
|
2047
|
+
tokens, so all the menu adds is scroll containment - without it a wheel past
|
|
2048
|
+
the end of a non-virtual list scrolls the page behind the popover, which
|
|
2049
|
+
closes it. */
|
|
2050
|
+
.sv-grid-menu .sv-listbox {
|
|
2068
2051
|
overscroll-behavior: contain;
|
|
2069
|
-
border: 1px solid var(--sg-border, #eaeef4);
|
|
2070
|
-
border-radius: 5px;
|
|
2071
2052
|
}
|
|
2072
2053
|
|
|
2073
2054
|
.sv-grid-facet {
|
|
@@ -2082,9 +2063,11 @@ select.sv-grid-fr-editor {
|
|
|
2082
2063
|
background: var(--sg-row-hover-bg, #f4f6fa);
|
|
2083
2064
|
}
|
|
2084
2065
|
|
|
2066
|
+
/* Sits directly above the checklist, which draws its own frame - so no rule
|
|
2067
|
+
here, or the two borders stack. */
|
|
2085
2068
|
.sv-grid-facet-all {
|
|
2086
|
-
border-bottom: 1px solid var(--sg-border, #eaeef4);
|
|
2087
2069
|
font-weight: 600;
|
|
2070
|
+
padding-inline-start: 3px;
|
|
2088
2071
|
}
|
|
2089
2072
|
|
|
2090
2073
|
.sv-grid-facet input {
|
|
@@ -2464,11 +2447,9 @@ select.sv-grid-fr-editor {
|
|
|
2464
2447
|
.sv-grid-choose-columns-menu {
|
|
2465
2448
|
width: 240px;
|
|
2466
2449
|
}
|
|
2467
|
-
.sv-grid-choose-columns-menu .sv-grid-facet-list {
|
|
2468
|
-
max-height: 280px;
|
|
2469
|
-
}
|
|
2470
2450
|
|
|
2471
|
-
/* Native
|
|
2451
|
+
/* Native checkbox on the "(Select all)" row picks up the theme accent. The
|
|
2452
|
+
checklist rows below it draw SvListBox's own box, not a native input. */
|
|
2472
2453
|
.sv-grid-facet input[type="checkbox"] {
|
|
2473
2454
|
accent-color: var(--sg-accent, #2563eb);
|
|
2474
2455
|
width: 14px;
|
package/dist/SvGrid.svelte
CHANGED
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
} from "./SvGrid.helpers";
|
|
57
57
|
import {
|
|
58
58
|
splitInTokens,
|
|
59
|
+
trailingInToken,
|
|
59
60
|
joinInTokens,
|
|
60
61
|
} from "./filtering/excel-filters";
|
|
61
62
|
import { createSvGridController } from "./SvGrid.controller.svelte";
|
|
@@ -68,7 +69,13 @@
|
|
|
68
69
|
import { getBoardView } from "./board-view.svelte";
|
|
69
70
|
import { getChartView } from "./chart-view.svelte";
|
|
70
71
|
let props: Props<TFeatures, TData> = $props();
|
|
71
|
-
|
|
72
|
+
// Per-instance base for every DOM id the grid mints. `$props.id()` is stable
|
|
73
|
+
// across SSR and hydration, so two grids on one page no longer collide on
|
|
74
|
+
// `svgrid_cell_0_0` and point `aria-activedescendant` at each other (#77).
|
|
75
|
+
// `$props.id()` must be its own declaration initializer - it can't be passed
|
|
76
|
+
// inline as a call argument.
|
|
77
|
+
const gridInstanceId = $props.id();
|
|
78
|
+
const ctrl = createSvGridController(props, gridInstanceId);
|
|
72
79
|
// Effective props: the controller's `override ?? prop` proxy that backs
|
|
73
80
|
// `api.setOption(...)`. Reading `opt.X` (instead of `props.X`) makes a view-direct
|
|
74
81
|
// prop honor a runtime override. Svelte binds template `props.X` reads to the prop
|
|
@@ -151,6 +158,10 @@
|
|
|
151
158
|
ctrl.contextMenuFor ||
|
|
152
159
|
ctrl.filterMenuFor ||
|
|
153
160
|
ctrl.operatorMenuFor ||
|
|
161
|
+
// The value-suggestions dropdown lives in the overlay too, and it can be
|
|
162
|
+
// the FIRST thing a user opens (focusing an in/notIn filter input), so it
|
|
163
|
+
// has to pull the chunk in rather than assume a menu already did.
|
|
164
|
+
ctrl.inSuggestFor ||
|
|
154
165
|
ctrl.chooseColumnsPos;
|
|
155
166
|
if (menuOpen && !MenusOverlay)
|
|
156
167
|
import("./GridMenus.svelte").then((m) => (MenusOverlay = m.default));
|
|
@@ -474,7 +485,6 @@
|
|
|
474
485
|
const closeInSuggest = $derived(ctrl.closeInSuggest);
|
|
475
486
|
const addFilterToken = $derived(ctrl.addFilterToken);
|
|
476
487
|
const removeFilterToken = $derived(ctrl.removeFilterToken);
|
|
477
|
-
const facetValuesForColumn = $derived(ctrl.facetValuesForColumn);
|
|
478
488
|
const onWindowKeydown = $derived(ctrl.onWindowKeydown);
|
|
479
489
|
|
|
480
490
|
// True when a `regex` filter's pattern won't compile - used to flag the
|
|
@@ -522,6 +532,21 @@
|
|
|
522
532
|
if (ctrl.inSuggestFor !== columnId) openInSuggest(input, columnId);
|
|
523
533
|
}
|
|
524
534
|
|
|
535
|
+
// The tool-panel / filter-menu `in` / `notIn` inputs hold the WHOLE token
|
|
536
|
+
// list ("AAPL, MSFT"), unlike the chip input which holds one token. So the
|
|
537
|
+
// suggestion query is the trailing fragment being typed, not the full value.
|
|
538
|
+
function onSetOpFilterFocus(event: FocusEvent, columnId: string): void {
|
|
539
|
+
const input = event.currentTarget as HTMLInputElement;
|
|
540
|
+
openInSuggest(input, columnId);
|
|
541
|
+
ctrl.inSuggestQuery = trailingInToken(input.value);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function onSetOpFilterInput(event: Event, columnId: string): void {
|
|
545
|
+
const input = event.currentTarget as HTMLInputElement;
|
|
546
|
+
ctrl.inSuggestQuery = trailingInToken(input.value);
|
|
547
|
+
if (ctrl.inSuggestFor !== columnId) openInSuggest(input, columnId);
|
|
548
|
+
}
|
|
549
|
+
|
|
525
550
|
// Overflow layout for the `in` / `notIn` chip row: render every chip, then
|
|
526
551
|
// measure how many fit on one line and collapse the rest into the "+N" pill.
|
|
527
552
|
// A guess-by-width heuristic can't work (labels vary), so we measure real
|
|
@@ -1280,10 +1305,14 @@
|
|
|
1280
1305
|
return;
|
|
1281
1306
|
}
|
|
1282
1307
|
// Tab and Ctrl/Cmd+Enter both commit. Plain Enter inserts a newline.
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1308
|
+
// Tab goes through the shared helper so it advances the active cell
|
|
1309
|
+
// like every other editor does (#48).
|
|
1310
|
+
if (event.key === "Tab") {
|
|
1311
|
+
event.preventDefault();
|
|
1312
|
+
ctrl.commitAndMoveByTab(event.shiftKey);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
|
|
1287
1316
|
event.preventDefault();
|
|
1288
1317
|
saveEditingCell();
|
|
1289
1318
|
ctrl.gridRootEl?.focus({ preventScroll: true });
|
|
@@ -2583,7 +2612,7 @@
|
|
|
2583
2612
|
),
|
|
2584
2613
|
}}
|
|
2585
2614
|
{...getGridCellA11yProps({
|
|
2586
|
-
id: getGridCellDomId(
|
|
2615
|
+
id: getGridCellDomId(ctrl.gridDomId, rowIndex, colIndex),
|
|
2587
2616
|
rowIndex: rowIndex + 1,
|
|
2588
2617
|
colIndex: colIndex + 1,
|
|
2589
2618
|
selected: isRowSelected(row.id),
|
|
@@ -2833,7 +2862,7 @@
|
|
|
2833
2862
|
),
|
|
2834
2863
|
}}
|
|
2835
2864
|
{...getGridCellA11yProps({
|
|
2836
|
-
id: getGridCellDomId(
|
|
2865
|
+
id: getGridCellDomId(ctrl.gridDomId, rowIndex, colIndex),
|
|
2837
2866
|
rowIndex: rowIndex + 1,
|
|
2838
2867
|
colIndex: colIndex + 1,
|
|
2839
2868
|
selected: isRowSelected(row.id),
|
|
@@ -3273,10 +3302,13 @@
|
|
|
3273
3302
|
</select>
|
|
3274
3303
|
{#if active !== "isBlank" && active !== "isNotBlank"}
|
|
3275
3304
|
{@const tpIsSetOp = active === "in" || active === "notIn"}
|
|
3305
|
+
<!-- in/notIn drives the shared value-suggestions dropdown
|
|
3306
|
+
(a windowed checklist) rather than a native datalist,
|
|
3307
|
+
which capped at 200 values and couldn't show which
|
|
3308
|
+
are already selected. -->
|
|
3276
3309
|
<input
|
|
3277
3310
|
class="sv-grid-tp-filter-input"
|
|
3278
3311
|
type={active === "regex" ? "text" : fType}
|
|
3279
|
-
list={tpIsSetOp ? `sv-tp-in-list-${column.id}` : undefined}
|
|
3280
3312
|
placeholder={active === "between"
|
|
3281
3313
|
? "From"
|
|
3282
3314
|
: tpIsSetOp
|
|
@@ -3285,19 +3317,15 @@
|
|
|
3285
3317
|
? "Pattern…"
|
|
3286
3318
|
: "Filter…"}
|
|
3287
3319
|
value={filterMenuValues[column.id]?.value ?? ""}
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3320
|
+
onfocus={tpIsSetOp
|
|
3321
|
+
? (e) => onSetOpFilterFocus(e, column.id)
|
|
3322
|
+
: undefined}
|
|
3323
|
+
onblur={tpIsSetOp ? () => closeInSuggest() : undefined}
|
|
3324
|
+
oninput={(e) => {
|
|
3325
|
+
updateFilterMenuValue(column.id, e.currentTarget.value);
|
|
3326
|
+
if (tpIsSetOp) onSetOpFilterInput(e, column.id);
|
|
3327
|
+
}}
|
|
3293
3328
|
/>
|
|
3294
|
-
{#if tpIsSetOp}
|
|
3295
|
-
<datalist id={`sv-tp-in-list-${column.id}`}>
|
|
3296
|
-
{#each facetValuesForColumn(column.id).slice(0, 200) as v (v)}
|
|
3297
|
-
<option value={v}></option>
|
|
3298
|
-
{/each}
|
|
3299
|
-
</datalist>
|
|
3300
|
-
{/if}
|
|
3301
3329
|
{#if active === "between"}
|
|
3302
3330
|
<input
|
|
3303
3331
|
class="sv-grid-tp-filter-input"
|
package/dist/SvGrid.types.d.ts
CHANGED
|
@@ -1339,11 +1339,18 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
|
|
|
1339
1339
|
conditionalFormats?: ReadonlyArray<ConditionalFormat<TData>>;
|
|
1340
1340
|
/**
|
|
1341
1341
|
* Which rows feed the min/max range that `colorScale` / `dataBar` formats
|
|
1342
|
-
* scale against.
|
|
1343
|
-
*
|
|
1344
|
-
*
|
|
1345
|
-
|
|
1346
|
-
|
|
1342
|
+
* scale against.
|
|
1343
|
+
*
|
|
1344
|
+
* - `filtered` (default): every row that survives the filters, ignoring the
|
|
1345
|
+
* page slice. The scale follows what you filtered to but does not shift
|
|
1346
|
+
* when you page, so the same value keeps the same colour on page 1 and
|
|
1347
|
+
* page 2.
|
|
1348
|
+
* - `visible`: only the rows currently on screen. Rescales per page - use it
|
|
1349
|
+
* when you want each page's heat map normalized to that page.
|
|
1350
|
+
* - `all`: the full unfiltered dataset, for a scale that stays put as you
|
|
1351
|
+
* filter.
|
|
1352
|
+
*/
|
|
1353
|
+
conditionalStatScope?: "filtered" | "visible" | "all";
|
|
1347
1354
|
onCellValueChange?: (event: {
|
|
1348
1355
|
rowIndex: number;
|
|
1349
1356
|
columnId: string;
|
|
@@ -1562,6 +1569,14 @@ export type CellEditState = {
|
|
|
1562
1569
|
columnId: string;
|
|
1563
1570
|
editorType: CellEditorType;
|
|
1564
1571
|
value: unknown;
|
|
1572
|
+
/**
|
|
1573
|
+
* The row's underlying data object, captured when the edit started. The
|
|
1574
|
+
* commit path resolves the row by id first; this is the fallback for when
|
|
1575
|
+
* the row has left the row model mid-edit (a filter typed into the filter
|
|
1576
|
+
* row while an editor is open), so the typed value still lands instead of
|
|
1577
|
+
* being silently dropped.
|
|
1578
|
+
*/
|
|
1579
|
+
rowRef?: unknown;
|
|
1565
1580
|
} | null;
|
|
1566
1581
|
export type FilterOperator = "contains" | "notContains" | "equals" | "notEquals" | "startsWith" | "endsWith" | "regex" | "in" | "notIn" | "greaterThan" | "lessThan" | "between" | "isBlank" | "isNotBlank";
|
|
1567
1582
|
export type FilterOption = {
|
package/dist/SvListBox.svelte
CHANGED
|
@@ -23,9 +23,18 @@
|
|
|
23
23
|
|
|
24
24
|
type Props = SvEditorProps & {
|
|
25
25
|
options: ReadonlyArray<ListOption>
|
|
26
|
-
value?: string | number |
|
|
26
|
+
value?: string | number | ReadonlyArray<string | number> | ReadonlySet<string | number> | null
|
|
27
27
|
onChange?: (value: any) => void
|
|
28
28
|
multiple?: boolean
|
|
29
|
+
/**
|
|
30
|
+
* Checklist styling: each row draws a checkbox and the selected-row accent
|
|
31
|
+
* highlight is suppressed, because a list that starts fully selected (a
|
|
32
|
+
* filter checklist) would otherwise paint every row. State reads off the
|
|
33
|
+
* box; `aria-selected` still carries it for assistive tech.
|
|
34
|
+
*/
|
|
35
|
+
checkbox?: boolean
|
|
36
|
+
/** Stretch to the container width instead of the default 220px. */
|
|
37
|
+
block?: boolean
|
|
29
38
|
/** Visible height in rows before scrolling. */
|
|
30
39
|
rows?: number
|
|
31
40
|
/** Window the list (render only visible rows) for large option sets. */
|
|
@@ -46,6 +55,8 @@
|
|
|
46
55
|
value = null,
|
|
47
56
|
onChange,
|
|
48
57
|
multiple = false,
|
|
58
|
+
checkbox = false,
|
|
59
|
+
block = false,
|
|
49
60
|
disabled = false,
|
|
50
61
|
size = 'md',
|
|
51
62
|
rows = 7,
|
|
@@ -152,13 +163,15 @@
|
|
|
152
163
|
})
|
|
153
164
|
</script>
|
|
154
165
|
|
|
155
|
-
<SvField id={uid} {label} {hint} {error} {required} {dir}>
|
|
166
|
+
<SvField id={uid} {label} {hint} {error} {required} {dir} {block}>
|
|
156
167
|
<!-- Div-based listbox (role comes from `lb.rootProps()`). -->
|
|
157
168
|
<div
|
|
158
169
|
bind:this={listEl}
|
|
159
170
|
class="sv-listbox sv-listbox--{size}"
|
|
160
171
|
class:is-invalid={invalid}
|
|
161
172
|
class:is-virtual={useVirtual}
|
|
173
|
+
class:is-checkbox={checkbox}
|
|
174
|
+
class:is-block={block}
|
|
162
175
|
style:--sv-rows={rows}
|
|
163
176
|
style:--sv-row-h={`${rowHeightBase}px`}
|
|
164
177
|
onwheel={useVirtual ? scroller.onWheel : undefined}
|
|
@@ -211,7 +224,11 @@
|
|
|
211
224
|
</SvField>
|
|
212
225
|
|
|
213
226
|
{#snippet optionInner(opt: ListOption)}
|
|
214
|
-
{#if
|
|
227
|
+
{#if checkbox}
|
|
228
|
+
<span class="sv-listbox__box" class:is-on={isSel(opt)} aria-hidden="true"></span>
|
|
229
|
+
{:else if multiple}
|
|
230
|
+
<span class="sv-listbox__check" aria-hidden="true">{isSel(opt) ? '✓' : ''}</span>
|
|
231
|
+
{/if}
|
|
215
232
|
{#if item ?? itemTemplate}{@render (item ?? itemTemplate)!(opt)}{:else}<span class="sv-listbox__label">{opt.label}</span>{/if}
|
|
216
233
|
{/snippet}
|
|
217
234
|
|
|
@@ -249,6 +266,7 @@
|
|
|
249
266
|
padding: 0; overflow: hidden; position: relative;
|
|
250
267
|
height: calc(var(--sv-rows, 7) * var(--sv-row-h, 32px) + 8px);
|
|
251
268
|
}
|
|
269
|
+
.sv-listbox.is-block { width: 100%; }
|
|
252
270
|
.sv-listbox--sm { --_fs: 12px; }
|
|
253
271
|
.sv-listbox--lg { --_fs: 15px; }
|
|
254
272
|
.sv-listbox:focus-visible { border-color: var(--_accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--_accent) 22%, transparent); }
|
|
@@ -284,4 +302,25 @@
|
|
|
284
302
|
}
|
|
285
303
|
.sv-listbox__check { width: 14px; text-align: center; color: var(--_accent); }
|
|
286
304
|
.sv-listbox__label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
305
|
+
|
|
306
|
+
/* ---- Checklist variant ---- */
|
|
307
|
+
.sv-listbox__box {
|
|
308
|
+
width: 14px; height: 14px; flex: none; box-sizing: border-box; position: relative;
|
|
309
|
+
border: 1px solid var(--sg-input-border, var(--sg-border, #cbd5e1));
|
|
310
|
+
border-radius: 3px; background: var(--sg-input-bg, #fff);
|
|
311
|
+
}
|
|
312
|
+
.sv-listbox__box.is-on { background: var(--_accent); border-color: var(--_accent); }
|
|
313
|
+
.sv-listbox__box.is-on::after {
|
|
314
|
+
content: ''; position: absolute; left: 4px; top: 1px; width: 3px; height: 7px;
|
|
315
|
+
border: solid var(--sg-input-bg, #fff); border-width: 0 2px 2px 0; transform: rotate(45deg);
|
|
316
|
+
}
|
|
317
|
+
/* The box carries the state, so selected rows keep neutral chrome - only the
|
|
318
|
+
roving highlight tints a row. */
|
|
319
|
+
.sv-listbox.is-checkbox .sv-listbox__opt.is-selected {
|
|
320
|
+
background: none; color: inherit; font-weight: inherit;
|
|
321
|
+
}
|
|
322
|
+
.sv-listbox.is-checkbox .sv-listbox__opt.is-active,
|
|
323
|
+
.sv-listbox.is-checkbox .sv-listbox__opt.is-selected.is-active {
|
|
324
|
+
background: var(--sg-row-hover-bg, #f1f5f9);
|
|
325
|
+
}
|
|
287
326
|
</style>
|