@rokkit/states 1.0.0-next.125 → 1.0.0-next.128

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.
@@ -0,0 +1,320 @@
1
+ /**
2
+ * ProxyItem
3
+ *
4
+ * Wraps a raw item (object or primitive) and provides uniform field access.
5
+ *
6
+ * #raw — always the original input, never mutated.
7
+ * #item — the object used for all field accesses:
8
+ * for objects: same reference as #raw
9
+ * for primitives: { [fields.text]: raw, [fields.value]: raw }
10
+ * This normalisation means get() and all getters work through the
11
+ * same field-mapping path with no special-casing.
12
+ * #key — path-based identifier ('0', '0-1', '0-1-2', …) assigned by
13
+ * ProxyTree and propagated into children automatically.
14
+ * #level — nesting depth: always equals key.split('-').length.
15
+ * (1 = root, 2 = first-level children, 3 = grandchildren, …)
16
+ *
17
+ * get(fieldName) — maps semantic name → raw key → #item value.
18
+ * For field-mapped attributes only (text, value, icon, …).
19
+ * Structural props (key, level) and control state
20
+ * (expanded, selected) are accessed directly as properties.
21
+ *
22
+ * Direct getters: label, value, id — primary access.
23
+ * All other fields via get(fieldName).
24
+ *
25
+ * children — auto-wrapped as ProxyItem instances via $derived, with their
26
+ * keys and levels already set. Stable references so
27
+ * $derived correctly tracks nested expanded state.
28
+ *
29
+ * Control state (expanded / selected) — two modes:
30
+ * external: item has the field → proxy reads/writes through to #item
31
+ * internal: item lacks the field → proxy owns it as $state
32
+ * Primitive items always use internal mode (their #item has no state fields).
33
+ *
34
+ * ProxyItems are created once and never recreated — this keeps $state
35
+ * signals stable so $derived computations track them correctly.
36
+ */
37
+
38
+ import { BASE_FIELDS, normalizeFields } from '@rokkit/core'
39
+ export { BASE_FIELDS }
40
+
41
+ // Auto-increment counter for generating stable unique IDs.
42
+ let _nextId = 1
43
+
44
+ // ─── ProxyItem ────────────────────────────────────────────────────────────────
45
+
46
+ export class ProxyItem {
47
+ #raw // original input — never touched after construction
48
+ #item // normalised object used for all field accesses
49
+ #fields
50
+ #id // stable unique identifier — from item field or auto-generated
51
+ #key // path-based key e.g. '0', '0-1', '0-1-2'
52
+ #level // nesting depth: 1 = root
53
+
54
+ // Control state — always read from here so $derived tracks changes.
55
+ // Initialised from #item field when present (external mode).
56
+ #expanded = $state(false)
57
+ #selected = $state(false)
58
+
59
+ // Version counter — incremented by set() to trigger #children recomputation.
60
+ // $derived reads this so it re-derives when set('children', ...) is called.
61
+ #version = $state(0)
62
+
63
+ // Children auto-wrapped as ProxyItem instances with keys + levels assigned.
64
+ // $derived ensures stable references: same ProxyItem instances returned on
65
+ // every access, so $derived can track their expanded state.
66
+ #children = $derived(this.#buildChildren())
67
+
68
+ /**
69
+ * @param {*} raw Raw item — object or primitive (string, number, …)
70
+ * @param {Partial<typeof BASE_FIELDS>} [fields]
71
+ * @param {string} [key] Path-based key assigned by ProxyTree
72
+ * @param {number} [level] Nesting depth (1 = root)
73
+ */
74
+ constructor(raw, fields = {}, key = '', level = 0) {
75
+ this.#fields = { ...BASE_FIELDS, ...normalizeFields(fields) }
76
+ this.#raw = raw
77
+ this.#key = key
78
+ this.#level = level
79
+
80
+ // Normalise primitives: #item is always an object.
81
+ // Both text and value fields point to the primitive so all accessors work uniformly.
82
+ this.#item =
83
+ raw !== null && typeof raw === 'object'
84
+ ? raw
85
+ : { [this.#fields.label]: raw, [this.#fields.value]: raw }
86
+
87
+ // Stable unique id: read from item field, or auto-generate
88
+ this.#id = this.#item[this.#fields.id] ?? `proxy-${_nextId++}`
89
+
90
+ // Sync initial control state from #item fields when present
91
+ const ef = this.#fields.expanded
92
+ const sf = this.#fields.selected
93
+ if (ef in this.#item) this.#expanded = Boolean(this.#item[ef])
94
+ if (sf in this.#item) this.#selected = Boolean(this.#item[sf])
95
+ }
96
+
97
+ // ─── Internal: build wrapped children ────────────────────────────────────
98
+
99
+ #buildChildren() {
100
+ void this.#version // reactive dependency — triggers recompute after set()
101
+ const raw = this.#item[this.#fields.children]
102
+ if (!Array.isArray(raw) || raw.length === 0) return []
103
+ return raw.map(
104
+ (child, i) =>
105
+ this._createChild(
106
+ child,
107
+ this.#fields,
108
+ this.#key ? `${this.#key}-${i}` : String(i),
109
+ this.#level + 1
110
+ )
111
+ )
112
+ }
113
+
114
+ /**
115
+ * Factory method for creating child proxies. Override in subclasses
116
+ * to produce specialised children (e.g. LazyProxyItem).
117
+ * @param {*} raw
118
+ * @param {Partial<typeof BASE_FIELDS>} fields
119
+ * @param {string} key
120
+ * @param {number} level
121
+ * @returns {ProxyItem}
122
+ */
123
+ _createChild(raw, fields, key, level) {
124
+ return new ProxyItem(raw, fields, key, level)
125
+ }
126
+
127
+ // ─── Structural props ─────────────────────────────────────────────────────
128
+
129
+ get key() {
130
+ return this.#key
131
+ }
132
+ get level() {
133
+ return this.#level
134
+ }
135
+ /** Stable unique identifier — from item's id field, or auto-generated. */
136
+ get id() {
137
+ return this.#id
138
+ }
139
+ /** The original input passed to the constructor — never mutated. */
140
+ get original() {
141
+ return this.#raw
142
+ }
143
+ /** The merged field-mapping configuration. */
144
+ get fields() {
145
+ return this.#fields
146
+ }
147
+
148
+ // ─── Generic field accessor ───────────────────────────────────────────────
149
+ //
150
+ // Maps a semantic field name to the #item value via the fields config.
151
+ // For field-mapped attributes only: text, value, icon, href, description, …
152
+ // Falls back to using fieldName directly as a raw key when not in config.
153
+
154
+ /**
155
+ * @param {string} fieldName Semantic name, e.g. 'icon', 'href', 'description'
156
+ * @returns {*}
157
+ */
158
+ get(fieldName) {
159
+ const rawKey = this.#fields[fieldName] ?? fieldName
160
+ return this.#item[rawKey]
161
+ }
162
+
163
+ /**
164
+ * Write a value back to the underlying item through the field mapping.
165
+ * For object items, this modifies the original raw item (since #item === #raw).
166
+ * Increments the version counter so $derived(#buildChildren()) re-computes.
167
+ *
168
+ * @param {string} fieldName Semantic name, e.g. 'children', 'text'
169
+ * @param {*} value
170
+ */
171
+ set(fieldName, value) {
172
+ const rawKey = this.#fields[fieldName] ?? fieldName
173
+ this.#item[rawKey] = value
174
+ this.#version++
175
+ }
176
+
177
+ /**
178
+ * Write directly to the original raw item, bypassing field mapping.
179
+ * Advanced operation for when the caller needs to update the source data.
180
+ * Accepts either (field, value) or an object for batch updates.
181
+ * Increments version so $derived(#buildChildren()) re-computes.
182
+ *
183
+ * @param {string|object} fieldOrBatch Raw key name, or { key: value, … }
184
+ * @param {*} [value]
185
+ */
186
+ mutate(fieldOrBatch, value) {
187
+ if (typeof fieldOrBatch === 'object' && fieldOrBatch !== null) {
188
+ for (const [k, v] of Object.entries(fieldOrBatch)) {
189
+ this.#raw[k] = v
190
+ }
191
+ } else {
192
+ this.#raw[fieldOrBatch] = value
193
+ }
194
+ this.#version++
195
+ }
196
+
197
+ // ─── Field-mapped accessors ───────────────────────────────────────────────
198
+
199
+ get label() {
200
+ return this.#item[this.#fields.label] ?? ''
201
+ }
202
+ get value() {
203
+ return this.#item[this.#fields.value] ?? this.#raw
204
+ }
205
+ // All other fields via get('icon'), get('href'), get('snippet'), etc.
206
+
207
+ // ─── Computed props ───────────────────────────────────────────────────────
208
+
209
+ get disabled() {
210
+ return this.#item[this.#fields.disabled] === true
211
+ }
212
+
213
+ /** True only for object items with a non-empty children array. */
214
+ get hasChildren() {
215
+ return (
216
+ this.#raw !== null &&
217
+ typeof this.#raw === 'object' &&
218
+ Array.isArray(this.#item[this.#fields.children]) &&
219
+ this.#item[this.#fields.children].length > 0
220
+ )
221
+ }
222
+
223
+ /** Returns wrapped ProxyItem children (empty array for primitives and leaf items). */
224
+ get children() {
225
+ return this.#children
226
+ }
227
+
228
+ get type() {
229
+ const t = this.#item[this.#fields.type]
230
+ if (t === 'separator' || t === 'spacer') return t
231
+ return this.hasChildren ? 'group' : 'item'
232
+ }
233
+
234
+ // ─── Control state (expanded / selected) ─────────────────────────────────
235
+
236
+ get expanded() {
237
+ return this.#expanded
238
+ }
239
+ set expanded(v) {
240
+ this.#expanded = v
241
+ const ef = this.#fields.expanded
242
+ if (ef in this.#item) this.#item[ef] = v
243
+ }
244
+
245
+ get selected() {
246
+ return this.#selected
247
+ }
248
+ set selected(v) {
249
+ this.#selected = v
250
+ const sf = this.#fields.selected
251
+ if (sf in this.#item) this.#item[sf] = v
252
+ }
253
+ }
254
+
255
+ // ─── LazyProxyItem ───────────────────────────────────────────────────────────
256
+
257
+ /**
258
+ * LazyProxyItem
259
+ *
260
+ * Extends ProxyItem with lazy-loading support. When a lazyLoad function
261
+ * is provided, children are fetched on demand via fetch().
262
+ *
263
+ * #lazyLoad — async function (value, raw) => children[] — null when not lazy
264
+ * #loaded — true when: lazyLoad is null, or node already has children array,
265
+ * or after successful fetch(). false only for sentinel nodes
266
+ * (children === true) that need fetching.
267
+ * #loading — true during async fetch(), false otherwise. Used for spinner UI.
268
+ *
269
+ * After fetch(), uses set('children', result) to update the underlying item
270
+ * and trigger #children recomputation via the version counter.
271
+ *
272
+ * The lazyLoad function is propagated to all children automatically via
273
+ * _createChild override.
274
+ */
275
+ export class LazyProxyItem extends ProxyItem {
276
+ #lazyLoad
277
+ #loaded = $state(true)
278
+ #loading = $state(false)
279
+
280
+ /**
281
+ * @param {*} raw
282
+ * @param {Partial<typeof BASE_FIELDS>} [fields]
283
+ * @param {string} [key]
284
+ * @param {number} [level]
285
+ * @param {((value: unknown, raw: unknown) => Promise<unknown[]>) | null} [lazyLoad]
286
+ */
287
+ constructor(raw, fields = {}, key = '', level = 0, lazyLoad = null) {
288
+ super(raw, fields, key, level)
289
+ this.#lazyLoad = lazyLoad
290
+ // Loaded if: no lazyLoad function, children already exist as an array, or no children field (leaf)
291
+ // Only sentinel nodes (children: true) are considered unloaded
292
+ this.#loaded = lazyLoad === null || this.get('children') !== true
293
+ }
294
+
295
+ get loaded() { return this.#loaded }
296
+ get loading() { return this.#loading }
297
+
298
+ /**
299
+ * Fetch children via the lazyLoad function.
300
+ * No-op if lazyLoad is null, already loaded, or currently loading.
301
+ * After fetching, writes children to the underlying item via set().
302
+ */
303
+ async fetch() {
304
+ if (!this.#lazyLoad || this.#loaded || this.#loading) return
305
+ this.#loading = true
306
+ try {
307
+ const children = await this.#lazyLoad(this.value, this.original)
308
+ this.set('children', children)
309
+ this.#loaded = true
310
+ } finally {
311
+ this.#loading = false
312
+ }
313
+ }
314
+
315
+ /** @override — propagate lazyLoad to children */
316
+ _createChild(raw, fields, key, level) {
317
+ return new LazyProxyItem(raw, fields, key, level, this.#lazyLoad)
318
+ }
319
+ }
320
+
@@ -0,0 +1,158 @@
1
+ /**
2
+ * ProxyTree
3
+ *
4
+ * Reactive data layer that manages a tree of ProxyItem instances.
5
+ * Derives flatView (for rendering) and lookup (for O(1) access) reactively
6
+ * from the root proxies and their children.
7
+ *
8
+ * Used by both Wrapper and LazyWrapper as the shared data model.
9
+ *
10
+ * Key design:
11
+ * #rootProxies is $state([]) — reassigned (not mutated) so $derived re-computes.
12
+ * flatView and lookup are $derived from #rootProxies, reading proxy.children
13
+ * and proxy.expanded recursively, so they automatically re-derive on any
14
+ * expansion or children change anywhere in the tree.
15
+ */
16
+
17
+ import { BASE_FIELDS, normalizeFields } from '@rokkit/core'
18
+ import { ProxyItem } from './proxy-item.svelte.js'
19
+
20
+ // ─── Tree line type computation ────────────────────────────────────────────────
21
+
22
+ // Maps a parent's line type to the continuation type shown at the same column
23
+ // in child rows below it. 'child'→'sibling' (line continues), 'last'→'empty' (branch ended).
24
+ const NEXT_LINE = { child: 'sibling', last: 'empty', sibling: 'sibling', empty: 'empty', icon: 'empty' }
25
+
26
+ // ─── Reactive tree traversal utilities ─────────────────────────────────────────
27
+
28
+ /**
29
+ * Build flat view by walking proxy.children ($derived) recursively.
30
+ * Reads proxy.expanded ($state) and proxy.children ($derived), so any
31
+ * $derived wrapping this function re-computes on expansion or children changes.
32
+ *
33
+ * Computes lineTypes per node during the walk — no second pass needed.
34
+ * lineTypes is an array of connector types for rendering tree lines:
35
+ * 'child' — branch connector
36
+ * 'last' — last branch connector
37
+ * 'sibling' — vertical continuation line
38
+ * 'empty' — (blank space)
39
+ * 'icon' — expand/collapse toggle slot
40
+ *
41
+ * @param {ProxyItem[]} proxies
42
+ * @param {string[]} [parentLineTypes] Line types of the parent node (for computing inherited connectors)
43
+ * @returns {{ key: string, proxy: ProxyItem, level: number, hasChildren: boolean, isExpandable: boolean, type: string, lineTypes: string[] }[]}
44
+ */
45
+ function buildReactiveFlatView(proxies, parentLineTypes = []) {
46
+ const result = []
47
+ for (let i = 0; i < proxies.length; i++) {
48
+ const proxy = proxies[i]
49
+ const children = proxy.children // reads $derived — registers dependency
50
+ const hasChildren = children.length > 0
51
+ const isExpandable = hasChildren || proxy.get('children') === true // sentinel: lazy-loadable
52
+ const isLast = i === proxies.length - 1
53
+ const position = isLast ? 'last' : 'child'
54
+
55
+ // Compute line types: inherit parent's continuations + current position + icon/empty
56
+ const inherited = parentLineTypes.slice(0, -1).map((t) => NEXT_LINE[t] ?? 'empty')
57
+ if (parentLineTypes.length > 0) inherited.push(position)
58
+ const lineTypes = isExpandable ? [...inherited, 'icon'] : inherited
59
+
60
+ result.push({
61
+ key: proxy.key,
62
+ proxy,
63
+ level: proxy.level,
64
+ hasChildren,
65
+ isExpandable,
66
+ type: proxy.type,
67
+ lineTypes
68
+ })
69
+ if (hasChildren && proxy.expanded) {
70
+ result.push(...buildReactiveFlatView(children, lineTypes))
71
+ }
72
+ }
73
+ return result
74
+ }
75
+
76
+ /**
77
+ * Build lookup Map by walking proxy.children ($derived) recursively.
78
+ * Traverses ALL children (not just expanded) so keys are available
79
+ * for selection and navigation even before a group is opened.
80
+ *
81
+ * @param {ProxyItem[]} proxies
82
+ * @param {Map<string, ProxyItem>} [map]
83
+ * @returns {Map<string, ProxyItem>}
84
+ */
85
+ function buildReactiveLookup(proxies, map = new Map()) {
86
+ for (const proxy of proxies) {
87
+ map.set(proxy.key, proxy)
88
+ const children = proxy.children
89
+ if (children.length > 0) {
90
+ buildReactiveLookup(children, map)
91
+ }
92
+ }
93
+ return map
94
+ }
95
+
96
+ // ─── ProxyTree ─────────────────────────────────────────────────────────────────
97
+
98
+ export class ProxyTree {
99
+ #fields
100
+ #factory
101
+
102
+ // Root proxies — $state so reassignment triggers $derived recomputation.
103
+ #rootProxies = $state([])
104
+
105
+ // Reactive flatView: re-derives when proxy.expanded OR proxy.children changes.
106
+ flatView = $derived(buildReactiveFlatView(this.#rootProxies))
107
+
108
+ // Reactive lookup: re-derives when proxy.children changes anywhere in the tree.
109
+ #lookup = $derived(buildReactiveLookup(this.#rootProxies))
110
+
111
+ /**
112
+ * @param {unknown[]} [items]
113
+ * @param {Partial<typeof BASE_FIELDS>} [fields]
114
+ * @param {{ createProxy?: (raw: *, fields: object, key: string, level: number) => ProxyItem }} [options]
115
+ */
116
+ constructor(items = [], fields = {}, options = {}) {
117
+ this.#fields = { ...BASE_FIELDS, ...normalizeFields(fields) }
118
+ this.#factory = options.createProxy ?? ((raw, f, key, level) => new ProxyItem(raw, f, key, level))
119
+ this.#rootProxies = (items ?? []).map((raw, i) => this.#factory(raw, this.#fields, String(i), 1))
120
+ }
121
+
122
+ // ─── Read accessors ──────────────────────────────────────────────────────
123
+
124
+ /** @returns {ProxyItem[]} Root proxy array */
125
+ get roots() { return this.#rootProxies }
126
+
127
+ /** @returns {Map<string, ProxyItem>} Lookup map of all proxies by key */
128
+ get lookup() { return this.#lookup }
129
+
130
+ // ─── Mutation methods ────────────────────────────────────────────────────
131
+
132
+ /**
133
+ * Append new root items. Creates proxies with keys continuing from
134
+ * the current length. Reassigns #rootProxies to trigger $derived.
135
+ *
136
+ * @param {unknown[]} items Raw items to append as roots
137
+ */
138
+ append(items) {
139
+ const start = this.#rootProxies.length
140
+ const newProxies = items.map((raw, i) =>
141
+ this.#factory(raw, this.#fields, String(start + i), 1)
142
+ )
143
+ this.#rootProxies = [...this.#rootProxies, ...newProxies]
144
+ }
145
+
146
+ /**
147
+ * Add children to an existing proxy node.
148
+ * Uses proxy.set('children', rawItems) so ProxyItem's version counter
149
+ * triggers #buildChildren() recomputation. The flatView and lookup
150
+ * derive from proxy.children reactively.
151
+ *
152
+ * @param {ProxyItem} proxy The proxy to add children to
153
+ * @param {unknown[]} items Raw child items
154
+ */
155
+ addChildren(proxy, items) {
156
+ proxy.set('children', items)
157
+ }
158
+ }
@@ -0,0 +1,199 @@
1
+ import { ascending, descending } from 'd3-array'
2
+ import { deriveColumns } from '@rokkit/data'
3
+ import { ListController } from './list-controller.svelte.js'
4
+
5
+ /**
6
+ * TableController — manages table state via composition over ListController.
7
+ *
8
+ * Handles column metadata, sorting (single and multi-column), and delegates
9
+ * row focus/selection/navigation to an internal ListController.
10
+ */
11
+ export class TableController {
12
+ columns = $state([])
13
+ sortState = $state([])
14
+
15
+ #list
16
+ #rawData
17
+ #fields
18
+
19
+ /**
20
+ * @param {Array<Record<string, unknown>>} data - Row data
21
+ * @param {Object} [options]
22
+ * @param {Array} [options.columns] - Column definitions (auto-derived if empty)
23
+ * @param {Object} [options.fields] - Row-level field mapping
24
+ * @param {*} [options.value] - Initial selected value
25
+ * @param {boolean} [options.multiselect] - Enable multi-row selection
26
+ */
27
+ constructor(data = [], options = {}) {
28
+ const { columns, fields, value, multiselect } = options
29
+ this.#rawData = data
30
+ this.#fields = fields
31
+ this.columns = columns?.length
32
+ ? columns.map((c) => ({ sortable: true, sorted: 'none', ...c }))
33
+ : deriveColumns(data)
34
+ this.#list = new ListController(data, value, fields, { multiselect })
35
+ }
36
+
37
+ // =========================================================================
38
+ // Sort
39
+ // =========================================================================
40
+
41
+ /**
42
+ * Toggle sort on a column. Cycles: none → ascending → descending → none.
43
+ * @param {string} columnName - Column to sort by
44
+ * @param {boolean} [extend=false] - If true (Shift+click), add to sort stack
45
+ */
46
+ sortBy(columnName, extend = false) {
47
+ const col = this.columns.find((c) => c.name === columnName)
48
+ if (!col || col.sortable === false) return
49
+
50
+ // Determine next direction
51
+ const cycle = { none: 'ascending', ascending: 'descending', descending: 'none' }
52
+ const nextDirection = cycle[col.sorted ?? 'none']
53
+
54
+ if (extend) {
55
+ // Multi-column sort: add/update/remove this column in the sort stack
56
+ const existing = this.sortState.findIndex((s) => s.column === columnName)
57
+ if (nextDirection === 'none') {
58
+ // Remove from stack
59
+ this.sortState = this.sortState.filter((s) => s.column !== columnName)
60
+ } else if (existing >= 0) {
61
+ // Update direction in place
62
+ this.sortState = this.sortState.map((s) =>
63
+ s.column === columnName ? { ...s, direction: nextDirection } : s
64
+ )
65
+ } else {
66
+ // Add to stack
67
+ this.sortState = [...this.sortState, { column: columnName, direction: nextDirection }]
68
+ }
69
+ } else {
70
+ // Single column sort: replace entire sort state
71
+ this.sortState = nextDirection === 'none' ? [] : [{ column: columnName, direction: nextDirection }]
72
+ }
73
+
74
+ // Update column sorted flags
75
+ this.columns = this.columns.map((c) => {
76
+ const sort = this.sortState.find((s) => s.column === c.name)
77
+ return { ...c, sorted: sort ? sort.direction : 'none' }
78
+ })
79
+
80
+ // Apply sort and update list
81
+ this.#applySortAndUpdate()
82
+ }
83
+
84
+ /**
85
+ * Clear all sort state and restore original data order.
86
+ */
87
+ clearSort() {
88
+ this.sortState = []
89
+ this.columns = this.columns.map((c) => ({ ...c, sorted: 'none' }))
90
+ this.#list.update(this.#rawData)
91
+ }
92
+
93
+ /**
94
+ * Apply current sortState to rawData and feed sorted data to list controller.
95
+ * @private
96
+ */
97
+ #applySortAndUpdate() {
98
+ if (this.sortState.length === 0) {
99
+ this.#list.update(this.#rawData)
100
+ return
101
+ }
102
+
103
+ const sorted = [...this.#rawData].sort((a, b) => {
104
+ for (const { column, direction } of this.sortState) {
105
+ const comparator = direction === 'ascending' ? ascending : descending
106
+ const result = comparator(a[column], b[column])
107
+ if (result !== 0) return result
108
+ }
109
+ return 0
110
+ })
111
+
112
+ this.#list.update(sorted)
113
+ }
114
+
115
+ // =========================================================================
116
+ // Data access (delegated to ListController)
117
+ // =========================================================================
118
+
119
+ get data() {
120
+ return this.#list.data
121
+ }
122
+
123
+ get lookup() {
124
+ return this.#list.lookup
125
+ }
126
+
127
+ get focusedKey() {
128
+ return this.#list.focusedKey
129
+ }
130
+
131
+ set focusedKey(v) {
132
+ this.#list.focusedKey = v
133
+ }
134
+
135
+ get focused() {
136
+ return this.#list.focused
137
+ }
138
+
139
+ get selected() {
140
+ return this.#list.selected
141
+ }
142
+
143
+ get selectedKeys() {
144
+ return this.#list.selectedKeys
145
+ }
146
+
147
+ // =========================================================================
148
+ // Navigation (delegated to ListController)
149
+ // =========================================================================
150
+
151
+ moveFirst() {
152
+ return this.#list.moveFirst()
153
+ }
154
+
155
+ moveLast() {
156
+ return this.#list.moveLast()
157
+ }
158
+
159
+ moveNext() {
160
+ return this.#list.moveNext()
161
+ }
162
+
163
+ movePrev() {
164
+ return this.#list.movePrev()
165
+ }
166
+
167
+ moveTo(path) {
168
+ return this.#list.moveTo(path)
169
+ }
170
+
171
+ moveToIndex(index) {
172
+ return this.#list.moveToIndex(index)
173
+ }
174
+
175
+ // =========================================================================
176
+ // Selection (delegated to ListController)
177
+ // =========================================================================
178
+
179
+ select(key) {
180
+ return this.#list.select(key)
181
+ }
182
+
183
+ extendSelection(key) {
184
+ return this.#list.extendSelection(key)
185
+ }
186
+
187
+ // =========================================================================
188
+ // Update
189
+ // =========================================================================
190
+
191
+ /**
192
+ * Update the data source. Re-applies current sort if active.
193
+ * @param {Array<Record<string, unknown>>} data
194
+ */
195
+ update(data) {
196
+ this.#rawData = data
197
+ this.#applySortAndUpdate()
198
+ }
199
+ }