@rokkit/states 1.0.0-next.145 → 1.0.0-next.147

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/states",
3
- "version": "1.0.0-next.145",
3
+ "version": "1.0.0-next.147",
4
4
  "description": "Contains generic data manipulation functions that can be used in various components.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,46 @@
1
+ class AlertsStore {
2
+ /** @type {Array<{id: string, type: string, text?: string, dismissible: boolean, timeout: number, actions?: unknown}>} */
3
+ #items = $state([])
4
+ /** @type {Map<string, ReturnType<typeof setTimeout>>} */
5
+ #timers = new Map()
6
+
7
+ get current() {
8
+ return this.#items
9
+ }
10
+
11
+ /**
12
+ * Push a new alert. Returns the alert id.
13
+ * @param {{ type?: string, text?: string, dismissible?: boolean, timeout?: number, actions?: unknown }} alert
14
+ * @returns {string}
15
+ */
16
+ push({ type = 'info', text, dismissible = false, timeout = dismissible ? 0 : 4000, actions } = {}) {
17
+ const id = crypto.randomUUID()
18
+ this.#items = [...this.#items, { id, type, text, dismissible, timeout, actions }]
19
+ if (timeout > 0) {
20
+ const timer = setTimeout(() => this.dismiss(id), timeout)
21
+ this.#timers.set(id, timer)
22
+ }
23
+ return id
24
+ }
25
+
26
+ /**
27
+ * Dismiss an alert by id, cancelling its timer if any.
28
+ * @param {string} id
29
+ */
30
+ dismiss(id) {
31
+ clearTimeout(this.#timers.get(id))
32
+ this.#timers.delete(id)
33
+ this.#items = this.#items.filter((a) => a.id !== id)
34
+ }
35
+
36
+ /**
37
+ * Clear all alerts and cancel all timers.
38
+ */
39
+ clear() {
40
+ for (const timer of this.#timers.values()) clearTimeout(timer)
41
+ this.#timers.clear()
42
+ this.#items = []
43
+ }
44
+ }
45
+
46
+ export const alerts = new AlertsStore()
@@ -25,7 +25,9 @@ function visitNode(data, ctx) {
25
25
  const hasChildren = Array.isArray(item[fields.children]) && item[fields.children].length > 0
26
26
  data.push({ key, value: item, level, hasChildren })
27
27
  if (isExpanded(item, fields, key, expandedKeys)) {
28
- data.push(...flatVisibleNodes(item[fields.children], getNestedFields(fields), itemPath, expandedKeys))
28
+ data.push(
29
+ ...flatVisibleNodes(item[fields.children], getNestedFields(fields), itemPath, expandedKeys)
30
+ )
29
31
  }
30
32
  }
31
33
 
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export { alerts } from './alerts.svelte.js'
1
2
  export { TableController } from './table-controller.svelte.js'
2
3
  export { vibe } from './vibe.svelte.js'
3
4
  export { ListController } from './list-controller.svelte.js'
@@ -281,7 +281,11 @@ export class ListController {
281
281
  const indices = this.#findRangeIndices(anchorKey, key)
282
282
  if (!indices) return false
283
283
  const { anchorIndex, targetIndex } = indices
284
- this.#selectIndexRange(Math.min(anchorIndex, targetIndex), Math.max(anchorIndex, targetIndex), targetIndex)
284
+ this.#selectIndexRange(
285
+ Math.min(anchorIndex, targetIndex),
286
+ Math.max(anchorIndex, targetIndex),
287
+ targetIndex
288
+ )
285
289
  return true
286
290
  }
287
291
 
@@ -74,7 +74,15 @@ function visitProxy(result, proxy, parentLineTypes, position) {
74
74
  const hasChildren = children.length > 0
75
75
  const isExpandable = hasChildren || proxy.get('children') === true // sentinel: lazy-loadable
76
76
  const lineTypes = computeLineTypes(parentLineTypes, position, isExpandable)
77
- result.push({ key: proxy.key, proxy, level: proxy.level, hasChildren, isExpandable, type: proxy.type, lineTypes })
77
+ result.push({
78
+ key: proxy.key,
79
+ proxy,
80
+ level: proxy.level,
81
+ hasChildren,
82
+ isExpandable,
83
+ type: proxy.type,
84
+ lineTypes
85
+ })
78
86
  if (hasChildren && proxy.expanded) {
79
87
  result.push(...buildReactiveFlatView(children, lineTypes))
80
88
  }
@@ -42,14 +42,17 @@ export class Wrapper {
42
42
  #onchange
43
43
  #selectedValue = $state(undefined)
44
44
 
45
+ #collapsible
46
+
45
47
  /**
46
48
  * @param {import('./proxy-tree.svelte.js').ProxyTree} proxyTree
47
- * @param {{ onselect?: Function, onchange?: Function }} [options]
49
+ * @param {{ onselect?: Function, onchange?: Function, collapsible?: boolean }} [options]
48
50
  */
49
51
  constructor(proxyTree, options = {}) {
50
52
  this.#proxyTree = proxyTree
51
53
  this.#onselect = options.onselect
52
54
  this.#onchange = options.onchange
55
+ this.#collapsible = options.collapsible ?? true
53
56
  }
54
57
 
55
58
  // ─── IWrapper: state read by Navigator ─────────────────────────────────────
@@ -90,7 +93,8 @@ export class Wrapper {
90
93
 
91
94
  /**
92
95
  * Expand focused group, or move focus into it if already open.
93
- * No-op on leaf items.
96
+ * No-op on leaf items. When collapsible=false groups are always open,
97
+ * so this only ever moves focus into the first child.
94
98
  */
95
99
  expand(_path) {
96
100
  if (!this.#focusedKey) return
@@ -119,12 +123,13 @@ export class Wrapper {
119
123
  /**
120
124
  * Collapse focused group, or move focus to parent if already collapsed / leaf.
121
125
  * At root level with no parent: no-op.
126
+ * When collapsible=false, skips closing the group but still moves focus to parent.
122
127
  */
123
128
  collapse(_path) {
124
129
  if (!this.#focusedKey) return
125
130
  const node = this.flatView.find((n) => n.key === this.#focusedKey)
126
131
  if (!node) return
127
- if (node.hasChildren && node.proxy.expanded) {
132
+ if (node.hasChildren && node.proxy.expanded && this.#collapsible) {
128
133
  node.proxy.expanded = false
129
134
  } else {
130
135
  this.#focusParent()
@@ -147,7 +152,7 @@ export class Wrapper {
147
152
 
148
153
  /**
149
154
  * Select item at path (or focusedKey when path is null).
150
- * Groups toggle expanded. Leaves fire onchange (value differs) and onselect callbacks.
155
+ * Groups toggle expanded (only when collapsible=true). Leaves fire onchange and onselect callbacks.
151
156
  */
152
157
  select(path) {
153
158
  const key = path ?? this.#focusedKey
@@ -156,7 +161,7 @@ export class Wrapper {
156
161
  const proxy = this.#proxyTree.lookup.get(key)
157
162
  if (!proxy) return
158
163
  if (proxy.hasChildren) {
159
- proxy.expanded = !proxy.expanded
164
+ if (this.#collapsible) proxy.expanded = !proxy.expanded
160
165
  } else {
161
166
  this.#selectLeaf(proxy)
162
167
  }
@@ -165,8 +170,10 @@ export class Wrapper {
165
170
  /**
166
171
  * Toggle expansion of group at path — called by Navigator for accordion-trigger clicks.
167
172
  * Unlike select(), this only applies to groups and never fires onselect.
173
+ * No-op when collapsible=false.
168
174
  */
169
175
  toggle(path) {
176
+ if (!this.#collapsible) return
170
177
  const key = path ?? this.#focusedKey
171
178
  if (!key) return
172
179
  const proxy = this.#proxyTree.lookup.get(key)