svelte-fluentui 1.0.0-rc14 → 1.0.0-rc15
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 +10 -7
- package/dist/components/QuickGrid.svelte +488 -62
- package/dist/components/QuickGrid.svelte.d.ts +29 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
A comprehensive Svelte wrapper library for Microsoft FluentUI web components (v2.6.x), providing a seamless way to use FluentUI components in Svelte applications.
|
|
4
4
|
|
|
5
|
-
## What's New in v1.0.0-
|
|
5
|
+
## What's New in v1.0.0-rc15
|
|
6
6
|
|
|
7
|
-
- **`
|
|
8
|
-
- **`
|
|
9
|
-
-
|
|
10
|
-
- **
|
|
7
|
+
- **`QuickGrid` tree mode** — pass `treePathMember="path"` and mark one column with `isTree: true` to render hierarchical data with indentation + expand/collapse. Supports PostgreSQL ltree (`"1.2.3"`), POSIX (`"/1/2/3"`), and Windows (`"C:\foo\bar"`) path styles with auto-detection. Optional `treeLevelMember` / `treeParentMember` skip path parsing when those values are pre-computed in the database. `treeDataSorted` skips internal sort. `expandedPaths` is `$bindable`. `defaultExpandDepth` is interpreted relative to the dataset's shallowest level so subtree views always show their roots. Filter is ancestor-aware. Optional `treeDoubleClickBehavior="toggle"` lets users smash the row instead of aiming at the chevron
|
|
8
|
+
- **`QuickGrid` per-column `filter` callback** — `(filterValue: string, row: T) => boolean | null` replaces the built-in substring match for one column. Use for numeric ranges, date ranges, regex, multi-field search. Returning `null` signals "input is syntactically incomplete" — the grid then ignores the filter (all rows pass) AND adds a `.invalid` class to the input so the user gets a red border instead of an empty grid
|
|
9
|
+
- **`QuickGrid` grid-level `onfilterchange`** — `(filters: Record<string, string>) => void` for server-side filtering. When set, internal filtering is fully bypassed; the grid still renders the inputs, fires this on every keystroke, and trusts the caller to update `items`
|
|
10
|
+
- **`QuickGrid` `idMember` prop** — stable row identity. Drafts and invalid-cell markers now key by this value (or `treePathMember` in tree mode, or displayed-row index as a last-resort fallback that triggers a one-shot console warning) so they survive pagination, filter, sort, and tree expand/collapse re-orderings
|
|
11
|
+
- **`QuickGrid` `columnMinWidth` prop** — default `min-width` for any column without its own. Stops content-sized columns from collapsing too far when paired with `fillerColumn`
|
|
12
|
+
- **`QuickGrid` `fillerColumn` actually absorbs leftover space now** — was `width: auto` (a no-op for absorption), now `width: 100%`. Affects every demo using `fillerColumn`, not just tree
|
|
13
|
+
- **BREAKING — `QuickGrid` `invalidCells` shape: `rowIndex` → `rowKey`** — bindable `invalidCells: CellValidationState[]` now carries `rowKey: string` instead of `rowIndex: number`, matching the new stable-id keying. Migration: callers binding `invalidCells` need to read `c.rowKey` instead of `c.rowIndex`. The `onvalidationerror` callback's `detail.rowIndex` is unchanged
|
|
11
14
|
|
|
12
15
|
## Features
|
|
13
16
|
|
|
@@ -42,7 +45,7 @@ A comprehensive Svelte wrapper library for Microsoft FluentUI web components (v2
|
|
|
42
45
|
- **Toast Service** - Programmatic notifications with `toast.success()`, `toast.error()`, etc. - 6 positions, progress bars, auto-dismiss
|
|
43
46
|
- **InputFile** - Drag-and-drop file upload with validation and progress tracking
|
|
44
47
|
- **Autocomplete** - Multiple selection with tag/chip display, async search with AbortSignal, initial options, Ctrl+Space to show all
|
|
45
|
-
- **QuickGrid** - Advanced data grid with sorting, filtering, pagination, editable rows, row toolbar, context menu
|
|
48
|
+
- **QuickGrid** - Advanced data grid with sorting, filtering, pagination, editable rows, row toolbar, context menu, tree mode with ltree-style paths, per-column custom filter predicates, and stable row identity via `idMember`
|
|
46
49
|
- **Three-State Checkbox** - Checkbox with indeterminate state support
|
|
47
50
|
- **Responsive Layout** - Complete layout system with Grid, Stack, and responsive components
|
|
48
51
|
|
|
@@ -86,7 +89,7 @@ npm install svelte-fluentui
|
|
|
86
89
|
|
|
87
90
|
### Data Display
|
|
88
91
|
- `DataGrid` / `DataGridRow` / `DataGridCell` - Data table components
|
|
89
|
-
- `QuickGrid` - Advanced data grid with sorting, filtering, and
|
|
92
|
+
- `QuickGrid` - Advanced data grid with sorting, filtering, pagination, inline editing, tree mode, and custom filter predicates
|
|
90
93
|
- `Card` - Content container
|
|
91
94
|
- `Badge` - Status indicators
|
|
92
95
|
- `ProgressBar` - Progress indication
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
|
|
78
78
|
// Validation types
|
|
79
79
|
type CellValidationState = {
|
|
80
|
-
|
|
80
|
+
rowKey: string
|
|
81
81
|
field: string
|
|
82
82
|
error: string
|
|
83
83
|
}
|
|
@@ -124,6 +124,15 @@
|
|
|
124
124
|
format?: (value: any, row: T) => string
|
|
125
125
|
template?: (row: T) => string
|
|
126
126
|
snippet?: Snippet<[T]>
|
|
127
|
+
// Custom filter predicate. When set, replaces the built-in substring match for this
|
|
128
|
+
// column. Called once per row with the raw filter input string and the row.
|
|
129
|
+
// Returns `true` to keep the row, `false` to exclude it, or `null` when the input
|
|
130
|
+
// is syntactically invalid (e.g. user has only typed `>` so far) — `null` causes
|
|
131
|
+
// the filter to be ignored entirely (all rows pass) AND the filter input gets a
|
|
132
|
+
// `.invalid` class so the UI can surface the bad-syntax state. Assumes the
|
|
133
|
+
// validity is a property of the input string, not the row, so the grid only
|
|
134
|
+
// probes the first row to decide validity.
|
|
135
|
+
filter?: (filterValue: string, row: T) => boolean | null
|
|
127
136
|
// Editing props
|
|
128
137
|
editable?: boolean
|
|
129
138
|
editor?: EditorType
|
|
@@ -137,6 +146,8 @@
|
|
|
137
146
|
oncelledit?: (context: CustomEditorContext<T>) => void
|
|
138
147
|
// Show edit button in cell
|
|
139
148
|
showEditButton?: boolean
|
|
149
|
+
// Tree mode: render this column with indent + expand/collapse chevron
|
|
150
|
+
isTree?: boolean
|
|
140
151
|
}
|
|
141
152
|
|
|
142
153
|
type RowChangeDetail<T> = {
|
|
@@ -228,6 +239,10 @@
|
|
|
228
239
|
* horizontal space. Pair with per-column `width` / `minWidth` / `maxWidth` / `autoWidth` when you
|
|
229
240
|
* want columns to keep predefined widths instead of stretching to justify across the table. */
|
|
230
241
|
fillerColumn?: boolean
|
|
242
|
+
/** Default `min-width` applied to every column header that does not specify its own `minWidth`.
|
|
243
|
+
* Useful with `fillerColumn` to stop content-sized columns from collapsing to a one-word column.
|
|
244
|
+
* Accepts any CSS length (e.g. `"100px"`, `"8rem"`). */
|
|
245
|
+
columnMinWidth?: string
|
|
231
246
|
class?: string
|
|
232
247
|
style?: string
|
|
233
248
|
cellTemplate?: SlotType
|
|
@@ -249,6 +264,25 @@
|
|
|
249
264
|
// Context menu
|
|
250
265
|
contextMenu?: ContextMenuItem<T>[]
|
|
251
266
|
oncontextmenuopen?: (context: ContextMenuContext<T>) => void
|
|
267
|
+
// Stable row identity — keys internal state (drafts, edits) so they survive
|
|
268
|
+
// re-orderings (pagination, filter, tree expand/collapse). When omitted, the grid
|
|
269
|
+
// falls back to treePathMember in tree mode, then to the displayed-row index, and
|
|
270
|
+
// console.warns once if neither stable key is available while editing is enabled.
|
|
271
|
+
idMember?: keyof T
|
|
272
|
+
// Tree mode (ltree-style path hierarchy)
|
|
273
|
+
treePathMember?: keyof T // Required to enable tree mode — field holding the path string ("1.2.3", "/1/2/3", "C:\\foo\\bar")
|
|
274
|
+
treeLevelMember?: keyof T // Optional — pre-computed depth (0-based). Falls back to deriving from path.
|
|
275
|
+
treeParentMember?: keyof T // Optional — pre-computed parent path. Falls back to deriving from path.
|
|
276
|
+
treeSeparator?: string // Path separator. Auto-detected from first row if omitted.
|
|
277
|
+
treeDataSorted?: boolean // True when caller already sorted items so parents precede children. Default false → grid sorts internally.
|
|
278
|
+
expandedPaths?: Set<string> // Bindable set of expanded path strings. Default: managed internally.
|
|
279
|
+
defaultExpandDepth?: number // Initial expansion depth when expandedPaths not bound. Omit to expand all.
|
|
280
|
+
treeDoubleClickBehavior?: "none" | "toggle" // "toggle": double-clicking the tree column toggles expand/collapse on rows with children. Default "none".
|
|
281
|
+
// Filter mode. When set, internal client-side filtering is bypassed and the caller
|
|
282
|
+
// owns the filtered dataset (typical for server-side search). Filter inputs still
|
|
283
|
+
// render and fire this callback with a copy of the current `{field: value}` map on
|
|
284
|
+
// every keystroke — debounce + post to your backend, then update `items`.
|
|
285
|
+
onfilterchange?: (filters: Record<string, string>) => void
|
|
252
286
|
// Callbacks
|
|
253
287
|
onrowchange?: (detail: RowChangeDetail<T>) => void
|
|
254
288
|
onroweditstart?: (detail: { row: T, rowIndex: number, field: string }) => void
|
|
@@ -268,6 +302,7 @@
|
|
|
268
302
|
striped = true,
|
|
269
303
|
hoverable = true,
|
|
270
304
|
fillerColumn = false,
|
|
305
|
+
columnMinWidth = undefined,
|
|
271
306
|
class: className = "",
|
|
272
307
|
style = "",
|
|
273
308
|
cellTemplate = undefined,
|
|
@@ -288,6 +323,19 @@
|
|
|
288
323
|
// Context menu
|
|
289
324
|
contextMenu = undefined,
|
|
290
325
|
oncontextmenuopen = undefined,
|
|
326
|
+
// Stable row identity
|
|
327
|
+
idMember = undefined,
|
|
328
|
+
// Tree mode
|
|
329
|
+
treePathMember = undefined,
|
|
330
|
+
treeLevelMember = undefined,
|
|
331
|
+
treeParentMember = undefined,
|
|
332
|
+
treeSeparator = undefined,
|
|
333
|
+
treeDataSorted = false,
|
|
334
|
+
expandedPaths = $bindable(undefined),
|
|
335
|
+
defaultExpandDepth = undefined,
|
|
336
|
+
treeDoubleClickBehavior = "none",
|
|
337
|
+
// Filter delegation
|
|
338
|
+
onfilterchange = undefined,
|
|
291
339
|
// Callbacks
|
|
292
340
|
onrowchange = undefined,
|
|
293
341
|
onroweditstart = undefined,
|
|
@@ -314,7 +362,11 @@
|
|
|
314
362
|
} else if (column.width) {
|
|
315
363
|
parts.push(`width: ${column.width}`)
|
|
316
364
|
}
|
|
365
|
+
// Per-column minWidth wins; otherwise fall back to the grid-level default. Skip both
|
|
366
|
+
// when the column opted into autoWidth — that mode is supposed to shrink to content
|
|
367
|
+
// and a min-width would break the trick.
|
|
317
368
|
if (column.minWidth) parts.push(`min-width: ${column.minWidth}`)
|
|
369
|
+
else if (columnMinWidth && !column.autoWidth) parts.push(`min-width: ${columnMinWidth}`)
|
|
318
370
|
if (column.maxWidth) parts.push(`max-width: ${column.maxWidth}`)
|
|
319
371
|
if (includeAlign) parts.push(`text-align: ${column.align || "left"}`)
|
|
320
372
|
return parts.join("; ")
|
|
@@ -335,8 +387,42 @@
|
|
|
335
387
|
let currentCellError = $state<string | null>(null) // Error for currently editing cell
|
|
336
388
|
let isValidating = $state(false)
|
|
337
389
|
|
|
338
|
-
// Draft rows - clones of rows being edited (preserves dirty values including invalid ones)
|
|
339
|
-
|
|
390
|
+
// Draft rows - clones of rows being edited (preserves dirty values including invalid ones).
|
|
391
|
+
// Keyed by stable row id (idMember → treePathMember → displayed index fallback) so drafts
|
|
392
|
+
// survive pagination / filter / tree expand-collapse re-orderings.
|
|
393
|
+
let draftRows = $state<Map<string, T>>(new Map())
|
|
394
|
+
|
|
395
|
+
// Returns a stable key for a row. Coalesces idMember → treePathMember → displayed index.
|
|
396
|
+
// The index fallback is lossy (drifts on re-order) and triggers a one-shot console warning
|
|
397
|
+
// via the effect below when editing is enabled without a real stable id.
|
|
398
|
+
function getRowId(item: T, displayedIndex: number): string {
|
|
399
|
+
if (idMember !== undefined) {
|
|
400
|
+
const v = item[idMember]
|
|
401
|
+
if (v !== undefined && v !== null) return String(v)
|
|
402
|
+
}
|
|
403
|
+
if (treePathMember !== undefined) {
|
|
404
|
+
const v = item[treePathMember]
|
|
405
|
+
if (v !== undefined && v !== null) return String(v)
|
|
406
|
+
}
|
|
407
|
+
return String(displayedIndex)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let idWarningEmitted = $state(false)
|
|
411
|
+
$effect(() => {
|
|
412
|
+
if (idWarningEmitted) return
|
|
413
|
+
if (!editable) return
|
|
414
|
+
if (idMember !== undefined || treePathMember !== undefined) return
|
|
415
|
+
// eslint-disable-next-line no-console
|
|
416
|
+
console.warn(
|
|
417
|
+
"[QuickGrid] No `idMember` was provided while `editable` is enabled. Row state " +
|
|
418
|
+
"(drafts, in-progress edits) will be keyed by displayed-row index, which means " +
|
|
419
|
+
"pagination, filtering, sorting, or tree expand/collapse can shift the displayed " +
|
|
420
|
+
"order and cause an in-flight edit to land on the wrong row. Pass `idMember=\"id\"` " +
|
|
421
|
+
"(or any field that uniquely identifies a row) to fix this. In tree mode, " +
|
|
422
|
+
"`treePathMember` is also accepted as a fallback identity."
|
|
423
|
+
)
|
|
424
|
+
idWarningEmitted = true
|
|
425
|
+
})
|
|
340
426
|
|
|
341
427
|
// Navigation mode state (for "navigate" editTrigger)
|
|
342
428
|
let focusedCell = $state<{ rowIndex: number; colIndex: number } | null>(null)
|
|
@@ -384,21 +470,218 @@
|
|
|
384
470
|
})
|
|
385
471
|
})
|
|
386
472
|
|
|
387
|
-
//
|
|
473
|
+
// ============ Tree mode helpers ============
|
|
474
|
+
let isTreeMode = $derived(treePathMember !== undefined)
|
|
475
|
+
let internalExpandedPaths = $state(new Set<string>())
|
|
476
|
+
let treeInitialized = $state(false)
|
|
477
|
+
|
|
478
|
+
let resolvedTreeSeparator = $derived.by(() => {
|
|
479
|
+
if (treeSeparator) return treeSeparator
|
|
480
|
+
if (!isTreeMode) return "/"
|
|
481
|
+
// Scan items until we find a path that contains a recognizable separator —
|
|
482
|
+
// inspecting only items[0] would miss the case where the root is "1" and the
|
|
483
|
+
// first path that actually contains a separator is "1.1" (the second row).
|
|
484
|
+
for (const item of items) {
|
|
485
|
+
const p = String(item[treePathMember as keyof T] ?? "")
|
|
486
|
+
if (p.includes("/")) return "/"
|
|
487
|
+
if (p.includes("\\")) return "\\"
|
|
488
|
+
if (p.includes(".")) return "."
|
|
489
|
+
}
|
|
490
|
+
return "/"
|
|
491
|
+
})
|
|
492
|
+
|
|
493
|
+
function getRowPath(item: T): string {
|
|
494
|
+
if (!treePathMember) return ""
|
|
495
|
+
const raw = String(item[treePathMember] ?? "")
|
|
496
|
+
// Strip trailing separator so "C:\foo\" and "C:\foo" behave the same
|
|
497
|
+
const sep = resolvedTreeSeparator
|
|
498
|
+
return raw.endsWith(sep) ? raw.slice(0, -sep.length) : raw
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function getRowLevel(item: T): number {
|
|
502
|
+
if (treeLevelMember) {
|
|
503
|
+
const v = item[treeLevelMember]
|
|
504
|
+
const n = Number(v)
|
|
505
|
+
return Number.isFinite(n) ? n : 0
|
|
506
|
+
}
|
|
507
|
+
const path = getRowPath(item)
|
|
508
|
+
if (!path) return 0
|
|
509
|
+
const sep = resolvedTreeSeparator
|
|
510
|
+
return path.split(sep).filter(Boolean).length - 1
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function getRowParentPath(item: T): string {
|
|
514
|
+
if (treeParentMember) {
|
|
515
|
+
const raw = String(item[treeParentMember] ?? "")
|
|
516
|
+
const sep = resolvedTreeSeparator
|
|
517
|
+
return raw.endsWith(sep) ? raw.slice(0, -sep.length) : raw
|
|
518
|
+
}
|
|
519
|
+
const path = getRowPath(item)
|
|
520
|
+
if (!path) return ""
|
|
521
|
+
const sep = resolvedTreeSeparator
|
|
522
|
+
const idx = path.lastIndexOf(sep)
|
|
523
|
+
return idx >= 0 ? path.slice(0, idx) : ""
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Path-aware comparison: numeric segments compare numerically (so "1.2" < "1.10")
|
|
527
|
+
function compareTreePaths(a: string, b: string): number {
|
|
528
|
+
const sep = resolvedTreeSeparator
|
|
529
|
+
const aSegs = a.split(sep).filter(Boolean)
|
|
530
|
+
const bSegs = b.split(sep).filter(Boolean)
|
|
531
|
+
const min = Math.min(aSegs.length, bSegs.length)
|
|
532
|
+
for (let i = 0; i < min; i++) {
|
|
533
|
+
const aSeg = aSegs[i]
|
|
534
|
+
const bSeg = bSegs[i]
|
|
535
|
+
const aNum = Number(aSeg)
|
|
536
|
+
const bNum = Number(bSeg)
|
|
537
|
+
if (!isNaN(aNum) && !isNaN(bNum) && aSeg !== "" && bSeg !== "") {
|
|
538
|
+
if (aNum !== bNum) return aNum - bNum
|
|
539
|
+
} else if (aSeg !== bSeg) {
|
|
540
|
+
return aSeg.localeCompare(bSeg)
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return aSegs.length - bSegs.length
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Set of every path that exists as an actual row (used to distinguish "real" ancestors
|
|
547
|
+
// from virtual roots — e.g. "C:" when only "C:\\Windows" and below are in the dataset).
|
|
548
|
+
let treePathSet = $derived.by(() => {
|
|
549
|
+
const result = new Set<string>()
|
|
550
|
+
if (!isTreeMode) return result
|
|
551
|
+
for (const item of items) result.add(getRowPath(item))
|
|
552
|
+
return result
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
// Set of paths that have at least one child in the dataset
|
|
556
|
+
let treeParentPathSet = $derived.by(() => {
|
|
557
|
+
const result = new Set<string>()
|
|
558
|
+
if (!isTreeMode) return result
|
|
559
|
+
for (const item of items) {
|
|
560
|
+
const parent = getRowParentPath(item)
|
|
561
|
+
if (parent) result.add(parent)
|
|
562
|
+
}
|
|
563
|
+
return result
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
function rowHasChildren(item: T): boolean {
|
|
567
|
+
return treeParentPathSet.has(getRowPath(item))
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
let resolvedExpandedPaths = $derived(expandedPaths ?? internalExpandedPaths)
|
|
571
|
+
|
|
572
|
+
function isPathExpanded(path: string): boolean {
|
|
573
|
+
return resolvedExpandedPaths.has(path)
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function toggleExpand(path: string) {
|
|
577
|
+
if (expandedPaths !== undefined) {
|
|
578
|
+
const next = new Set(expandedPaths)
|
|
579
|
+
if (next.has(path)) next.delete(path)
|
|
580
|
+
else next.add(path)
|
|
581
|
+
expandedPaths = next
|
|
582
|
+
} else {
|
|
583
|
+
const next = new Set(internalExpandedPaths)
|
|
584
|
+
if (next.has(path)) next.delete(path)
|
|
585
|
+
else next.add(path)
|
|
586
|
+
internalExpandedPaths = next
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Initialize internal expansion when items first arrive (only if no external binding).
|
|
591
|
+
// `defaultExpandDepth` is interpreted relative to the shallowest level present in the
|
|
592
|
+
// dataset, not absolute level 0 — so a partial tree where the shallowest row is level 3
|
|
593
|
+
// still gets its own "roots" expanded.
|
|
594
|
+
$effect(() => {
|
|
595
|
+
if (!isTreeMode) return
|
|
596
|
+
if (expandedPaths !== undefined) return
|
|
597
|
+
if (treeInitialized) return
|
|
598
|
+
if (items.length === 0) return
|
|
599
|
+
let minLevel = Infinity
|
|
600
|
+
for (const item of items) minLevel = Math.min(minLevel, getRowLevel(item))
|
|
601
|
+
if (!Number.isFinite(minLevel)) minLevel = 0
|
|
602
|
+
const next = new Set<string>()
|
|
603
|
+
for (const item of items) {
|
|
604
|
+
const level = getRowLevel(item) - minLevel
|
|
605
|
+
if (defaultExpandDepth === undefined || level < defaultExpandDepth) {
|
|
606
|
+
next.add(getRowPath(item))
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
internalExpandedPaths = next
|
|
610
|
+
treeInitialized = true
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
// ============ Display chain ============
|
|
614
|
+
|
|
615
|
+
// Tree-ordered: caller-sorted, or sorted internally by path
|
|
616
|
+
let treeOrderedItems = $derived.by(() => {
|
|
617
|
+
if (!isTreeMode) return items
|
|
618
|
+
if (treeDataSorted) return items
|
|
619
|
+
return [...items].sort((a, b) => compareTreePaths(getRowPath(a), getRowPath(b)))
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
// Per-field validity for custom filters — `false` when the column's filter predicate
|
|
623
|
+
// returned null on the first row (probe), meaning the input is syntactically invalid
|
|
624
|
+
// (e.g. `>` typed so far without a number). Surfaced to the template so the input gets
|
|
625
|
+
// a `.invalid` class.
|
|
626
|
+
let filterValidity = $derived.by(() => {
|
|
627
|
+
const result: Record<string, boolean> = {}
|
|
628
|
+
if (items.length === 0) return result
|
|
629
|
+
for (const [field, filterValue] of Object.entries(filters)) {
|
|
630
|
+
if (!filterValue) continue
|
|
631
|
+
const col = columns.find((c) => String(c.field) === field)
|
|
632
|
+
if (!col?.filter) continue
|
|
633
|
+
const probe = col.filter(filterValue, items[0])
|
|
634
|
+
if (probe === null) result[field] = false
|
|
635
|
+
}
|
|
636
|
+
return result
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
// Computed: filtered items (tree-aware: includes ancestors of matches).
|
|
640
|
+
// When `onfilterchange` is provided the caller owns filtering — we render the inputs
|
|
641
|
+
// and fire the callback, but skip internal filtering entirely (server-side mode).
|
|
388
642
|
let filteredItems = $derived.by(() => {
|
|
389
|
-
if (
|
|
643
|
+
if (onfilterchange) return treeOrderedItems
|
|
644
|
+
if (!filterable || Object.keys(filters).length === 0) return treeOrderedItems
|
|
390
645
|
|
|
391
|
-
|
|
392
|
-
|
|
646
|
+
const matchesFilter = (item: T) =>
|
|
647
|
+
Object.entries(filters).every(([field, filterValue]) => {
|
|
393
648
|
if (!filterValue) return true
|
|
649
|
+
// Per-column custom predicate wins over the built-in substring match.
|
|
650
|
+
const col = columns.find((c) => String(c.field) === field)
|
|
651
|
+
if (col?.filter) {
|
|
652
|
+
const result = col.filter(filterValue, item)
|
|
653
|
+
// null = invalid input → don't filter, treat row as a match.
|
|
654
|
+
return result === null ? true : result
|
|
655
|
+
}
|
|
394
656
|
const cellValue = String(item[field as keyof T] ?? "").toLowerCase()
|
|
395
657
|
return cellValue.includes(filterValue.toLowerCase())
|
|
396
658
|
})
|
|
397
|
-
|
|
659
|
+
|
|
660
|
+
if (!isTreeMode) return treeOrderedItems.filter(matchesFilter)
|
|
661
|
+
|
|
662
|
+
// Tree mode: include matched rows + all their ancestors so the hierarchy reads correctly
|
|
663
|
+
const matchedPaths = new Set<string>()
|
|
664
|
+
for (const item of treeOrderedItems) {
|
|
665
|
+
if (matchesFilter(item)) matchedPaths.add(getRowPath(item))
|
|
666
|
+
}
|
|
667
|
+
const includePaths = new Set(matchedPaths)
|
|
668
|
+
const sep = resolvedTreeSeparator
|
|
669
|
+
for (const path of matchedPaths) {
|
|
670
|
+
let parent = path
|
|
671
|
+
const idx0 = parent.lastIndexOf(sep)
|
|
672
|
+
parent = idx0 >= 0 ? parent.slice(0, idx0) : ""
|
|
673
|
+
while (parent) {
|
|
674
|
+
includePaths.add(parent)
|
|
675
|
+
const idx = parent.lastIndexOf(sep)
|
|
676
|
+
parent = idx >= 0 ? parent.slice(0, idx) : ""
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return treeOrderedItems.filter((i) => includePaths.has(getRowPath(i)))
|
|
398
680
|
})
|
|
399
681
|
|
|
400
|
-
// Computed: sorted items
|
|
682
|
+
// Computed: sorted items. Tree mode preserves hierarchy and ignores column sort.
|
|
401
683
|
let sortedItems = $derived.by(() => {
|
|
684
|
+
if (isTreeMode) return filteredItems
|
|
402
685
|
if (!sortColumn) return filteredItems
|
|
403
686
|
|
|
404
687
|
return [...filteredItems].sort((a, b) => {
|
|
@@ -420,17 +703,38 @@
|
|
|
420
703
|
})
|
|
421
704
|
})
|
|
422
705
|
|
|
706
|
+
// Computed: visible items — hide descendants of collapsed nodes (tree only).
|
|
707
|
+
// When a filter is active, ancestors are already auto-included above and we render the
|
|
708
|
+
// full filtered set so users can see matches inside otherwise-collapsed branches.
|
|
709
|
+
// Ancestors that aren't actually present as rows in the dataset are treated as virtual
|
|
710
|
+
// roots and never gate visibility (supports partial-tree / subtree views).
|
|
711
|
+
let visibleItems = $derived.by(() => {
|
|
712
|
+
if (!isTreeMode) return sortedItems
|
|
713
|
+
const filterActive = filterable && Object.values(filters).some((v) => !!v)
|
|
714
|
+
if (filterActive) return sortedItems
|
|
715
|
+
const sep = resolvedTreeSeparator
|
|
716
|
+
return sortedItems.filter((item) => {
|
|
717
|
+
let parent = getRowParentPath(item)
|
|
718
|
+
while (parent) {
|
|
719
|
+
if (treePathSet.has(parent) && !resolvedExpandedPaths.has(parent)) return false
|
|
720
|
+
const idx = parent.lastIndexOf(sep)
|
|
721
|
+
parent = idx >= 0 ? parent.slice(0, idx) : ""
|
|
722
|
+
}
|
|
723
|
+
return true
|
|
724
|
+
})
|
|
725
|
+
})
|
|
726
|
+
|
|
423
727
|
// Computed: paginated items
|
|
424
728
|
let paginatedItems = $derived.by(() => {
|
|
425
|
-
if (!pageable) return
|
|
729
|
+
if (!pageable) return visibleItems
|
|
426
730
|
|
|
427
731
|
const start = (currentPage - 1) * pageSize
|
|
428
732
|
const end = start + pageSize
|
|
429
|
-
return
|
|
733
|
+
return visibleItems.slice(start, end)
|
|
430
734
|
})
|
|
431
735
|
|
|
432
736
|
// Computed: total pages
|
|
433
|
-
let totalPages = $derived(Math.ceil(
|
|
737
|
+
let totalPages = $derived(Math.ceil(visibleItems.length / pageSize))
|
|
434
738
|
|
|
435
739
|
// Display items (final result)
|
|
436
740
|
let displayItems = $derived(paginatedItems)
|
|
@@ -450,6 +754,8 @@
|
|
|
450
754
|
function handleFilter(field: string, value: string) {
|
|
451
755
|
filters[field] = value
|
|
452
756
|
currentPage = 1 // Reset to first page when filtering
|
|
757
|
+
// Pass a shallow copy so callers can't mutate our internal state.
|
|
758
|
+
onfilterchange?.({ ...filters })
|
|
453
759
|
}
|
|
454
760
|
|
|
455
761
|
function goToPage(page: number) {
|
|
@@ -460,7 +766,7 @@
|
|
|
460
766
|
|
|
461
767
|
// Get raw value for a cell (checks draft row first, then original)
|
|
462
768
|
function getCellRawValue(item: T, rowIndex: number, field: string): unknown {
|
|
463
|
-
const draftRow = draftRows.get(rowIndex)
|
|
769
|
+
const draftRow = draftRows.get(getRowId(item, rowIndex))
|
|
464
770
|
if (draftRow) {
|
|
465
771
|
return draftRow[field as keyof T]
|
|
466
772
|
}
|
|
@@ -559,8 +865,9 @@
|
|
|
559
865
|
currentCellError = null
|
|
560
866
|
|
|
561
867
|
// Clone row if not already cloned (preserves dirty values across edits)
|
|
562
|
-
|
|
563
|
-
|
|
868
|
+
const rowKey = getRowId(item, rowIndex)
|
|
869
|
+
if (!draftRows.has(rowKey)) {
|
|
870
|
+
draftRows.set(rowKey, { ...item })
|
|
564
871
|
}
|
|
565
872
|
|
|
566
873
|
// Handle custom editor
|
|
@@ -608,67 +915,51 @@
|
|
|
608
915
|
}
|
|
609
916
|
}
|
|
610
917
|
|
|
611
|
-
// Helper functions for managing invalid cells
|
|
612
|
-
|
|
613
|
-
|
|
918
|
+
// Helper functions for managing invalid cells. Keyed by stable row id so markers survive
|
|
919
|
+
// pagination / filter / tree expand-collapse re-orderings.
|
|
920
|
+
function addInvalidCell(rowKey: string, field: string, error: string) {
|
|
921
|
+
const existingIndex = invalidCells.findIndex(c => c.rowKey === rowKey && c.field === field)
|
|
614
922
|
if (existingIndex >= 0) {
|
|
615
|
-
invalidCells[existingIndex] = {
|
|
923
|
+
invalidCells[existingIndex] = { rowKey, field, error }
|
|
616
924
|
} else {
|
|
617
|
-
invalidCells = [...invalidCells, {
|
|
925
|
+
invalidCells = [...invalidCells, { rowKey, field, error }]
|
|
618
926
|
}
|
|
619
927
|
}
|
|
620
928
|
|
|
621
|
-
function removeInvalidCell(
|
|
622
|
-
invalidCells = invalidCells.filter(c => !(c.
|
|
929
|
+
function removeInvalidCell(rowKey: string, field: string) {
|
|
930
|
+
invalidCells = invalidCells.filter(c => !(c.rowKey === rowKey && c.field === field))
|
|
623
931
|
}
|
|
624
932
|
|
|
625
|
-
function getCellValidationError(
|
|
626
|
-
const cell = invalidCells.find(c => c.
|
|
933
|
+
function getCellValidationError(rowKey: string, field: string): string | null {
|
|
934
|
+
const cell = invalidCells.find(c => c.rowKey === rowKey && c.field === field)
|
|
627
935
|
return cell?.error || null
|
|
628
936
|
}
|
|
629
937
|
|
|
630
|
-
function isCellInvalid(
|
|
631
|
-
return invalidCells.some(c => c.
|
|
938
|
+
function isCellInvalid(rowKey: string, field: string): boolean {
|
|
939
|
+
return invalidCells.some(c => c.rowKey === rowKey && c.field === field)
|
|
632
940
|
}
|
|
633
941
|
|
|
634
942
|
// ============ Draft Row Management Functions ============
|
|
635
|
-
//
|
|
943
|
+
// External-facing helpers (not currently called from inside the grid). Drafts are now
|
|
944
|
+
// keyed by the same stable id as everything else, so these accept a row key (string).
|
|
636
945
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
* Returns undefined if no draft exists.
|
|
640
|
-
*/
|
|
641
|
-
function getRowDraft(rowIndex: number): T | undefined {
|
|
642
|
-
return draftRows.get(rowIndex)
|
|
946
|
+
function getRowDraft(rowKey: string): T | undefined {
|
|
947
|
+
return draftRows.get(rowKey)
|
|
643
948
|
}
|
|
644
949
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
*/
|
|
648
|
-
function hasRowDraft(rowIndex: number): boolean {
|
|
649
|
-
return draftRows.has(rowIndex)
|
|
950
|
+
function hasRowDraft(rowKey: string): boolean {
|
|
951
|
+
return draftRows.has(rowKey)
|
|
650
952
|
}
|
|
651
953
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
*/
|
|
656
|
-
function discardRowDraft(rowIndex: number): void {
|
|
657
|
-
draftRows.delete(rowIndex)
|
|
658
|
-
// Remove invalid cell markers for this row
|
|
659
|
-
invalidCells = invalidCells.filter(c => c.rowIndex !== rowIndex)
|
|
954
|
+
function discardRowDraft(rowKey: string): void {
|
|
955
|
+
draftRows.delete(rowKey)
|
|
956
|
+
invalidCells = invalidCells.filter(c => c.rowKey !== rowKey)
|
|
660
957
|
}
|
|
661
958
|
|
|
662
|
-
|
|
663
|
-
* Get all row indices that have drafts.
|
|
664
|
-
*/
|
|
665
|
-
function getDraftRowIndices(): number[] {
|
|
959
|
+
function getDraftRowKeys(): string[] {
|
|
666
960
|
return Array.from(draftRows.keys())
|
|
667
961
|
}
|
|
668
962
|
|
|
669
|
-
/**
|
|
670
|
-
* Discard all drafts and invalid cell markers.
|
|
671
|
-
*/
|
|
672
963
|
function discardAllDrafts(): void {
|
|
673
964
|
draftRows.clear()
|
|
674
965
|
invalidCells = []
|
|
@@ -699,13 +990,14 @@
|
|
|
699
990
|
const oldValue = item[column.field as keyof T]
|
|
700
991
|
|
|
701
992
|
// Update draft row
|
|
702
|
-
const
|
|
993
|
+
const rowKey = getRowId(item, rowIndex)
|
|
994
|
+
const draftRow = draftRows.get(rowKey)
|
|
703
995
|
if (draftRow) {
|
|
704
996
|
;(draftRow as any)[field] = newValue
|
|
705
997
|
}
|
|
706
998
|
|
|
707
999
|
// Remove from invalid cells if it was invalid
|
|
708
|
-
removeInvalidCell(
|
|
1000
|
+
removeInvalidCell(rowKey, field)
|
|
709
1001
|
|
|
710
1002
|
// Always fire onrowchange with isValid: true
|
|
711
1003
|
onrowchange?.({
|
|
@@ -773,17 +1065,18 @@
|
|
|
773
1065
|
isValidating = false
|
|
774
1066
|
|
|
775
1067
|
// Update draft row with the new value (valid or invalid - preserves user input)
|
|
776
|
-
const
|
|
1068
|
+
const rowKey = getRowId(item, rowIndex)
|
|
1069
|
+
const draftRow = draftRows.get(rowKey)
|
|
777
1070
|
if (draftRow) {
|
|
778
1071
|
;(draftRow as any)[field] = finalValue
|
|
779
1072
|
}
|
|
780
1073
|
|
|
781
1074
|
// Update invalid cells tracking
|
|
782
1075
|
if (isValid) {
|
|
783
|
-
removeInvalidCell(
|
|
1076
|
+
removeInvalidCell(rowKey, field)
|
|
784
1077
|
currentCellError = null
|
|
785
1078
|
} else {
|
|
786
|
-
addInvalidCell(
|
|
1079
|
+
addInvalidCell(rowKey, field, validationErrorMsg || "Invalid value")
|
|
787
1080
|
currentCellError = validationErrorMsg
|
|
788
1081
|
onvalidationerror?.({ row: item, rowIndex, field, error: validationErrorMsg || "Invalid value" })
|
|
789
1082
|
}
|
|
@@ -827,6 +1120,18 @@
|
|
|
827
1120
|
}
|
|
828
1121
|
|
|
829
1122
|
function handleCellDblClick(e: MouseEvent, rowIndex: number, colIndex: number, column: Column<T>, item: T) {
|
|
1123
|
+
// Tree-toggle on dblclick wins on the tree column when enabled and the row has
|
|
1124
|
+
// children. Chevron's own ondblclick stops propagation, so this only fires for
|
|
1125
|
+
// dblclicks elsewhere in the cell. Leaves fall through to the edit logic below.
|
|
1126
|
+
if (
|
|
1127
|
+
isTreeMode &&
|
|
1128
|
+
column.isTree &&
|
|
1129
|
+
treeDoubleClickBehavior === "toggle" &&
|
|
1130
|
+
rowHasChildren(item)
|
|
1131
|
+
) {
|
|
1132
|
+
toggleExpand(getRowPath(item))
|
|
1133
|
+
return
|
|
1134
|
+
}
|
|
830
1135
|
if (!isCellEditable(column)) return
|
|
831
1136
|
const trigger = getColumnEditTrigger(column)
|
|
832
1137
|
// Double-click should work in both "dblclick" and "navigate" modes
|
|
@@ -1665,6 +1970,7 @@
|
|
|
1665
1970
|
<input
|
|
1666
1971
|
type="text"
|
|
1667
1972
|
class="filter-input"
|
|
1973
|
+
class:invalid={filterValidity[String(column.field)] === false}
|
|
1668
1974
|
placeholder="Filter..."
|
|
1669
1975
|
value={filters[String(column.field)] || ""}
|
|
1670
1976
|
oninput={(e) => handleFilter(String(column.field), e.currentTarget.value)}
|
|
@@ -1742,8 +2048,9 @@
|
|
|
1742
2048
|
{/if}
|
|
1743
2049
|
{#each columns as column, colIndex}
|
|
1744
2050
|
{@const cellField = String(column.field)}
|
|
1745
|
-
{@const
|
|
1746
|
-
{@const
|
|
2051
|
+
{@const cellRowKey = getRowId(item, rowIndex)}
|
|
2052
|
+
{@const cellInvalid = isCellInvalid(cellRowKey, cellField)}
|
|
2053
|
+
{@const cellError = getCellValidationError(cellRowKey, cellField)}
|
|
1747
2054
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_noninteractive_element_interactions -->
|
|
1748
2055
|
<td
|
|
1749
2056
|
data-row={rowIndex}
|
|
@@ -1758,6 +2065,7 @@
|
|
|
1758
2065
|
onkeydown={(e) => handleNavigationKeyDown(e, rowIndex, colIndex, column, item)}
|
|
1759
2066
|
title={cellError || undefined}
|
|
1760
2067
|
>
|
|
2068
|
+
{#snippet cellContent()}
|
|
1761
2069
|
{#if isEditing(rowIndex, cellField)}
|
|
1762
2070
|
{#if column.editor === "custom"}
|
|
1763
2071
|
<!-- Custom editor - handled via callback, show indicator -->
|
|
@@ -1837,6 +2145,46 @@
|
|
|
1837
2145
|
{/if}
|
|
1838
2146
|
</div>
|
|
1839
2147
|
{/if}
|
|
2148
|
+
{/snippet}
|
|
2149
|
+
{#if isTreeMode && column.isTree}
|
|
2150
|
+
{@const _path = getRowPath(item)}
|
|
2151
|
+
{@const _level = getRowLevel(item)}
|
|
2152
|
+
{@const _hasChildren = rowHasChildren(item)}
|
|
2153
|
+
{@const _expanded = isPathExpanded(_path)}
|
|
2154
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
2155
|
+
<div
|
|
2156
|
+
class="tree-cell"
|
|
2157
|
+
style={`padding-inline-start: ${_level * 1.25}rem`}
|
|
2158
|
+
onmousedown={(e) => {
|
|
2159
|
+
// Suppress the default text-selection extension that the second click of
|
|
2160
|
+
// a dblclick triggers — only when we're actually going to handle the
|
|
2161
|
+
// dblclick as a tree-toggle. e.detail counts clicks within the dblclick
|
|
2162
|
+
// interval, so > 1 == "this is the second+ click of a multi-click".
|
|
2163
|
+
if (e.detail > 1 && treeDoubleClickBehavior === "toggle" && _hasChildren) {
|
|
2164
|
+
e.preventDefault()
|
|
2165
|
+
}
|
|
2166
|
+
}}
|
|
2167
|
+
>
|
|
2168
|
+
{#if _hasChildren}
|
|
2169
|
+
<!-- svelte-ignore a11y_consider_explicit_label -->
|
|
2170
|
+
<button
|
|
2171
|
+
type="button"
|
|
2172
|
+
class="tree-chevron"
|
|
2173
|
+
class:expanded={_expanded}
|
|
2174
|
+
onclick={(e) => { e.stopPropagation(); toggleExpand(_path) }}
|
|
2175
|
+
ondblclick={(e) => e.stopPropagation()}
|
|
2176
|
+
title={_expanded ? "Collapse" : "Expand"}
|
|
2177
|
+
>
|
|
2178
|
+
▶
|
|
2179
|
+
</button>
|
|
2180
|
+
{:else}
|
|
2181
|
+
<span class="tree-leaf-spacer" aria-hidden="true"></span>
|
|
2182
|
+
{/if}
|
|
2183
|
+
<div class="tree-cell-body">{@render cellContent()}</div>
|
|
2184
|
+
</div>
|
|
2185
|
+
{:else}
|
|
2186
|
+
{@render cellContent()}
|
|
2187
|
+
{/if}
|
|
1840
2188
|
</td>
|
|
1841
2189
|
{/each}
|
|
1842
2190
|
{#if fillerColumn}
|
|
@@ -2002,9 +2350,12 @@
|
|
|
2002
2350
|
|
|
2003
2351
|
/* Filler column: an empty trailing cell whose sole job is to absorb any leftover
|
|
2004
2352
|
horizontal space so preceding columns keep their defined / auto widths instead
|
|
2005
|
-
of stretching.
|
|
2353
|
+
of stretching. `width: 100%` is the classic absorb-leftover trick in auto
|
|
2354
|
+
table-layout — other columns claim their content/defined widths first and the
|
|
2355
|
+
filler swallows whatever remains, instead of an arbitrary unspecified column
|
|
2356
|
+
ballooning to fill the table. */
|
|
2006
2357
|
.filler-column {
|
|
2007
|
-
width:
|
|
2358
|
+
width: 100%;
|
|
2008
2359
|
padding: 0;
|
|
2009
2360
|
background: transparent;
|
|
2010
2361
|
}
|
|
@@ -2084,6 +2435,27 @@
|
|
|
2084
2435
|
box-shadow: 0 0 0 1px var(--accent-fill-rest, #0078d4);
|
|
2085
2436
|
}
|
|
2086
2437
|
|
|
2438
|
+
/* Invalid state — surfaced when a column's `filter` callback returned null on the
|
|
2439
|
+
probe row (i.e. user typed something syntactically incomplete like `>`). Themes
|
|
2440
|
+
that override `--error-foreground` / `--error-fill-rest` get the right red. */
|
|
2441
|
+
.filter-input.invalid {
|
|
2442
|
+
border-color: var(--error-foreground, #d13438);
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
.filter-input.invalid:focus {
|
|
2446
|
+
border-color: var(--error-foreground, #d13438);
|
|
2447
|
+
box-shadow: 0 0 0 1px var(--error-foreground, #d13438);
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
[data-theme="dark"] .filter-input.invalid {
|
|
2451
|
+
border-color: var(--error-foreground, #f87c86);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
[data-theme="dark"] .filter-input.invalid:focus {
|
|
2455
|
+
border-color: var(--error-foreground, #f87c86);
|
|
2456
|
+
box-shadow: 0 0 0 1px var(--error-foreground, #f87c86);
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2087
2459
|
tbody tr {
|
|
2088
2460
|
border-bottom: 1px solid var(--neutral-stroke-layer-rest, #e0e0e0);
|
|
2089
2461
|
}
|
|
@@ -2108,6 +2480,60 @@
|
|
|
2108
2480
|
font-style: italic;
|
|
2109
2481
|
}
|
|
2110
2482
|
|
|
2483
|
+
/* Tree column: indentation + expand/collapse chevron.
|
|
2484
|
+
Use inline-flex (not flex) and don't set flex:1 on the body — otherwise the wrapper
|
|
2485
|
+
signals "fill all available space" to the table's auto layout and steals width from
|
|
2486
|
+
sibling columns. */
|
|
2487
|
+
.tree-cell {
|
|
2488
|
+
display: inline-flex;
|
|
2489
|
+
align-items: center;
|
|
2490
|
+
gap: 4px;
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
.tree-cell-body {
|
|
2494
|
+
min-width: 0;
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
.tree-chevron {
|
|
2498
|
+
all: unset;
|
|
2499
|
+
display: inline-flex;
|
|
2500
|
+
align-items: center;
|
|
2501
|
+
justify-content: center;
|
|
2502
|
+
width: 16px;
|
|
2503
|
+
height: 16px;
|
|
2504
|
+
border-radius: 2px;
|
|
2505
|
+
font-size: 9px;
|
|
2506
|
+
color: var(--neutral-foreground-hint, #707070);
|
|
2507
|
+
cursor: pointer;
|
|
2508
|
+
flex-shrink: 0;
|
|
2509
|
+
transition: transform 0.12s ease, background 0.1s ease;
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
.tree-chevron:hover {
|
|
2513
|
+
background: var(--neutral-fill-secondary-hover, #f0f0f0);
|
|
2514
|
+
color: var(--neutral-foreground-rest, #242424);
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
.tree-chevron.expanded {
|
|
2518
|
+
transform: rotate(90deg);
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
.tree-leaf-spacer {
|
|
2522
|
+
display: inline-block;
|
|
2523
|
+
width: 16px;
|
|
2524
|
+
height: 16px;
|
|
2525
|
+
flex-shrink: 0;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
[data-theme="dark"] .tree-chevron {
|
|
2529
|
+
color: var(--neutral-foreground-hint, #a0a0a0);
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
[data-theme="dark"] .tree-chevron:hover {
|
|
2533
|
+
background: var(--neutral-fill-secondary-hover, #3a3a3a);
|
|
2534
|
+
color: var(--neutral-foreground-rest, #e0e0e0);
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2111
2537
|
.pagination {
|
|
2112
2538
|
display: flex;
|
|
2113
2539
|
align-items: center;
|
|
@@ -24,6 +24,7 @@ declare function $$render<T>(): {
|
|
|
24
24
|
format?: ((value: any, row: T) => string) | undefined;
|
|
25
25
|
template?: ((row: T) => string) | undefined;
|
|
26
26
|
snippet?: Snippet<[T]> | undefined;
|
|
27
|
+
filter?: ((filterValue: string, row: T) => boolean | null) | undefined;
|
|
27
28
|
editable?: boolean;
|
|
28
29
|
editor?: "number" | "select" | "text" | "custom" | "autocomplete" | "combobox" | "checkbox" | "date";
|
|
29
30
|
editTrigger?: "button" | "click" | "dblclick" | "always" | "navigate";
|
|
@@ -97,6 +98,7 @@ declare function $$render<T>(): {
|
|
|
97
98
|
cancel: () => void;
|
|
98
99
|
}) => void) | undefined;
|
|
99
100
|
showEditButton?: boolean;
|
|
101
|
+
isTree?: boolean;
|
|
100
102
|
}[];
|
|
101
103
|
sortable?: boolean;
|
|
102
104
|
filterable?: boolean;
|
|
@@ -108,6 +110,10 @@ declare function $$render<T>(): {
|
|
|
108
110
|
* horizontal space. Pair with per-column `width` / `minWidth` / `maxWidth` / `autoWidth` when you
|
|
109
111
|
* want columns to keep predefined widths instead of stretching to justify across the table. */
|
|
110
112
|
fillerColumn?: boolean;
|
|
113
|
+
/** Default `min-width` applied to every column header that does not specify its own `minWidth`.
|
|
114
|
+
* Useful with `fillerColumn` to stop content-sized columns from collapsing to a one-word column.
|
|
115
|
+
* Accepts any CSS length (e.g. `"100px"`, `"8rem"`). */
|
|
116
|
+
columnMinWidth?: string;
|
|
111
117
|
class?: string;
|
|
112
118
|
style?: string;
|
|
113
119
|
cellTemplate?: SlotType;
|
|
@@ -116,7 +122,7 @@ declare function $$render<T>(): {
|
|
|
116
122
|
dropdownShowOnFocus?: boolean;
|
|
117
123
|
checkboxAlwaysEditable?: boolean;
|
|
118
124
|
invalidCells?: {
|
|
119
|
-
|
|
125
|
+
rowKey: string;
|
|
120
126
|
field: string;
|
|
121
127
|
error: string;
|
|
122
128
|
}[];
|
|
@@ -181,6 +187,7 @@ declare function $$render<T>(): {
|
|
|
181
187
|
format?: ((value: any, row: T) => string) | undefined;
|
|
182
188
|
template?: ((row: T) => string) | undefined;
|
|
183
189
|
snippet?: Snippet<[T]> | undefined;
|
|
190
|
+
filter?: ((filterValue: string, row: T) => boolean | null) | undefined;
|
|
184
191
|
editable?: boolean;
|
|
185
192
|
editor?: "number" | "select" | "text" | "custom" | "autocomplete" | "combobox" | "checkbox" | "date";
|
|
186
193
|
editTrigger?: "button" | "click" | "dblclick" | "always" | "navigate";
|
|
@@ -254,6 +261,7 @@ declare function $$render<T>(): {
|
|
|
254
261
|
cancel: () => void;
|
|
255
262
|
}) => void) | undefined;
|
|
256
263
|
showEditButton?: boolean;
|
|
264
|
+
isTree?: boolean;
|
|
257
265
|
};
|
|
258
266
|
cellValue: unknown;
|
|
259
267
|
}) => string);
|
|
@@ -283,6 +291,7 @@ declare function $$render<T>(): {
|
|
|
283
291
|
format?: ((value: any, row: T) => string) | undefined;
|
|
284
292
|
template?: ((row: T) => string) | undefined;
|
|
285
293
|
snippet?: Snippet<[T]> | undefined;
|
|
294
|
+
filter?: ((filterValue: string, row: T) => boolean | null) | undefined;
|
|
286
295
|
editable?: boolean;
|
|
287
296
|
editor?: "number" | "select" | "text" | "custom" | "autocomplete" | "combobox" | "checkbox" | "date";
|
|
288
297
|
editTrigger?: "button" | "click" | "dblclick" | "always" | "navigate";
|
|
@@ -356,6 +365,7 @@ declare function $$render<T>(): {
|
|
|
356
365
|
cancel: () => void;
|
|
357
366
|
}) => void) | undefined;
|
|
358
367
|
showEditButton?: boolean;
|
|
368
|
+
isTree?: boolean;
|
|
359
369
|
};
|
|
360
370
|
cellValue: unknown;
|
|
361
371
|
}) => boolean) | undefined;
|
|
@@ -384,6 +394,7 @@ declare function $$render<T>(): {
|
|
|
384
394
|
format?: ((value: any, row: T) => string) | undefined;
|
|
385
395
|
template?: ((row: T) => string) | undefined;
|
|
386
396
|
snippet?: Snippet<[T]> | undefined;
|
|
397
|
+
filter?: ((filterValue: string, row: T) => boolean | null) | undefined;
|
|
387
398
|
editable?: boolean;
|
|
388
399
|
editor?: "number" | "select" | "text" | "custom" | "autocomplete" | "combobox" | "checkbox" | "date";
|
|
389
400
|
editTrigger?: "button" | "click" | "dblclick" | "always" | "navigate";
|
|
@@ -457,6 +468,7 @@ declare function $$render<T>(): {
|
|
|
457
468
|
cancel: () => void;
|
|
458
469
|
}) => void) | undefined;
|
|
459
470
|
showEditButton?: boolean;
|
|
471
|
+
isTree?: boolean;
|
|
460
472
|
};
|
|
461
473
|
cellValue: unknown;
|
|
462
474
|
}) => boolean) | undefined;
|
|
@@ -487,6 +499,7 @@ declare function $$render<T>(): {
|
|
|
487
499
|
format?: ((value: any, row: T) => string) | undefined;
|
|
488
500
|
template?: ((row: T) => string) | undefined;
|
|
489
501
|
snippet?: Snippet<[T]> | undefined;
|
|
502
|
+
filter?: ((filterValue: string, row: T) => boolean | null) | undefined;
|
|
490
503
|
editable?: boolean;
|
|
491
504
|
editor?: "number" | "select" | "text" | "custom" | "autocomplete" | "combobox" | "checkbox" | "date";
|
|
492
505
|
editTrigger?: "button" | "click" | "dblclick" | "always" | "navigate";
|
|
@@ -560,6 +573,7 @@ declare function $$render<T>(): {
|
|
|
560
573
|
cancel: () => void;
|
|
561
574
|
}) => void) | undefined;
|
|
562
575
|
showEditButton?: boolean;
|
|
576
|
+
isTree?: boolean;
|
|
563
577
|
};
|
|
564
578
|
cellValue: unknown;
|
|
565
579
|
}) => void | Promise<void>) | undefined;
|
|
@@ -589,6 +603,7 @@ declare function $$render<T>(): {
|
|
|
589
603
|
format?: ((value: any, row: T) => string) | undefined;
|
|
590
604
|
template?: ((row: T) => string) | undefined;
|
|
591
605
|
snippet?: Snippet<[T]> | undefined;
|
|
606
|
+
filter?: ((filterValue: string, row: T) => boolean | null) | undefined;
|
|
592
607
|
editable?: boolean;
|
|
593
608
|
editor?: "number" | "select" | "text" | "custom" | "autocomplete" | "combobox" | "checkbox" | "date";
|
|
594
609
|
editTrigger?: "button" | "click" | "dblclick" | "always" | "navigate";
|
|
@@ -662,9 +677,20 @@ declare function $$render<T>(): {
|
|
|
662
677
|
cancel: () => void;
|
|
663
678
|
}) => void) | undefined;
|
|
664
679
|
showEditButton?: boolean;
|
|
680
|
+
isTree?: boolean;
|
|
665
681
|
};
|
|
666
682
|
cellValue: unknown;
|
|
667
683
|
}) => void) | undefined;
|
|
684
|
+
idMember?: keyof T | undefined;
|
|
685
|
+
treePathMember?: keyof T | undefined;
|
|
686
|
+
treeLevelMember?: keyof T | undefined;
|
|
687
|
+
treeParentMember?: keyof T | undefined;
|
|
688
|
+
treeSeparator?: string;
|
|
689
|
+
treeDataSorted?: boolean;
|
|
690
|
+
expandedPaths?: Set<string>;
|
|
691
|
+
defaultExpandDepth?: number;
|
|
692
|
+
treeDoubleClickBehavior?: "none" | "toggle";
|
|
693
|
+
onfilterchange?: (filters: Record<string, string>) => void;
|
|
668
694
|
onrowchange?: ((detail: {
|
|
669
695
|
row: T;
|
|
670
696
|
draftRow: T;
|
|
@@ -731,7 +757,7 @@ declare function $$render<T>(): {
|
|
|
731
757
|
}) => void) | undefined;
|
|
732
758
|
};
|
|
733
759
|
exports: {};
|
|
734
|
-
bindings: "invalidCells";
|
|
760
|
+
bindings: "invalidCells" | "expandedPaths";
|
|
735
761
|
slots: {};
|
|
736
762
|
events: {};
|
|
737
763
|
};
|
|
@@ -739,7 +765,7 @@ declare class __sveltets_Render<T> {
|
|
|
739
765
|
props(): ReturnType<typeof $$render<T>>['props'];
|
|
740
766
|
events(): ReturnType<typeof $$render<T>>['events'];
|
|
741
767
|
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
742
|
-
bindings(): "invalidCells";
|
|
768
|
+
bindings(): "invalidCells" | "expandedPaths";
|
|
743
769
|
exports(): {};
|
|
744
770
|
}
|
|
745
771
|
interface $$IsomorphicComponent {
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.0.0-
|
|
1
|
+
export declare const VERSION = "1.0.0-rc15";
|
package/dist/version.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// Auto-generated by scripts/pre-package.js — do NOT edit by hand.
|
|
2
2
|
// In dev this file holds whatever value was committed last; on each `npm run package`
|
|
3
3
|
// the pre-package script overwrites it with the current package.json version.
|
|
4
|
-
export const VERSION = "1.0.0-
|
|
4
|
+
export const VERSION = "1.0.0-rc15";
|