@rokkit/states 1.0.0-next.133 → 1.0.0-next.134

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 ADDED
@@ -0,0 +1,161 @@
1
+ # @rokkit/states
2
+
3
+ Reactive state management for Rokkit UI components — ProxyItem, ProxyTree, Wrapper, ListController.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @rokkit/states
9
+ # or
10
+ bun add @rokkit/states
11
+ ```
12
+
13
+ ## Overview
14
+
15
+ `@rokkit/states` provides the reactive data layer that drives `@rokkit/ui` components. It includes:
16
+
17
+ - **ProxyItem / ProxyTree** — normalize arbitrary data objects into a unified, field-mapped interface for rendering
18
+ - **Wrapper** — navigation controller for persistent components (List, Tree, Tabs). Owns focus, movement, selection, and expansion state
19
+ - **ListController** — lower-level reactive base for selection and expansion state
20
+ - **Utilities** — i18n messages, theme mode tracking, media query breakpoints
21
+
22
+ These classes are used internally by `@rokkit/ui` components. You can also use them directly to build custom components or drive navigation logic outside of the standard components.
23
+
24
+ ## Usage
25
+
26
+ ### ProxyItem — normalize any data object
27
+
28
+ ```js
29
+ import { ProxyItem } from '@rokkit/states'
30
+
31
+ const item = { name: 'Dashboard', path: '/dashboard', icon: 'i-solar:home' }
32
+ const fields = { label: 'name', value: 'path' }
33
+
34
+ const proxy = new ProxyItem(item, fields)
35
+
36
+ proxy.label // 'Dashboard'
37
+ proxy.value // '/dashboard'
38
+ proxy.get('icon') // 'i-solar:home'
39
+ proxy.selected // reactive boolean ($state)
40
+ proxy.disabled // boolean
41
+ proxy.children // child ProxyItems (if item has children)
42
+ ```
43
+
44
+ ### ProxyTree — build a navigable tree from nested data
45
+
46
+ ```js
47
+ import { ProxyTree } from '@rokkit/states'
48
+
49
+ const items = [
50
+ {
51
+ label: 'Section A',
52
+ children: [
53
+ { label: 'Item 1', value: '1' },
54
+ { label: 'Item 2', value: '2' }
55
+ ]
56
+ },
57
+ { label: 'Item 3', value: '3' }
58
+ ]
59
+
60
+ const tree = new ProxyTree(items)
61
+ tree.flatView // flat array of visible nodes for rendering
62
+ tree.lookup // Map<pathKey, ProxyItem>
63
+ ```
64
+
65
+ ### Wrapper — navigation controller for persistent components
66
+
67
+ ```js
68
+ import { ProxyTree, Wrapper } from '@rokkit/states'
69
+
70
+ const tree = new ProxyTree(items)
71
+ const wrapper = new Wrapper(tree, {
72
+ onselect: (value, item) => console.log('selected', value),
73
+ onchange: (value) => console.log('changed', value)
74
+ })
75
+
76
+ wrapper.flatView // flat array of visible nodes
77
+ wrapper.focusedKey // currently focused path key (reactive)
78
+
79
+ wrapper.next() // move focus down
80
+ wrapper.prev() // move focus up
81
+ wrapper.first() // jump to first item
82
+ wrapper.last() // jump to last item
83
+ wrapper.select(pathKey) // select item by path key
84
+ wrapper.expand(pathKey) // expand a group node
85
+ wrapper.collapse(pathKey)
86
+ wrapper.moveToValue(value) // sync focus to match an external value
87
+ ```
88
+
89
+ ### ListController — base reactive state
90
+
91
+ ```js
92
+ import { ListController } from '@rokkit/states'
93
+
94
+ const ctrl = new ListController()
95
+
96
+ ctrl.selectedKeys // SvelteSet of selected path keys
97
+ ctrl.expandedKeys // SvelteSet of expanded path keys
98
+ ctrl.focusedKey // currently focused key
99
+ ctrl.data // flat array of visible nodes
100
+ ```
101
+
102
+ ### vibe — reactive theme mode
103
+
104
+ ```js
105
+ import { vibe } from '@rokkit/states'
106
+
107
+ vibe.mode // 'light' | 'dark'
108
+ vibe.toggle() // switch between light and dark
109
+ ```
110
+
111
+ ### messages — i18n message store
112
+
113
+ ```js
114
+ import { messages } from '@rokkit/states'
115
+
116
+ messages.setLocale('fr')
117
+ messages.get('no_results') // localized string
118
+ ```
119
+
120
+ ### watchMedia — responsive breakpoints
121
+
122
+ ```js
123
+ import { watchMedia, defaultBreakpoints } from '@rokkit/states'
124
+
125
+ const media = watchMedia(defaultBreakpoints)
126
+ media.sm // reactive boolean — true when viewport matches 'sm'
127
+ media.lg // reactive boolean
128
+ ```
129
+
130
+ ## API Reference
131
+
132
+ ### ProxyItem
133
+
134
+ | Member | Type | Description |
135
+ | ------------ | ------------- | ----------------------------------- |
136
+ | `label` | `string` | Display text (field-mapped) |
137
+ | `value` | `any` | Selection value (field-mapped) |
138
+ | `get(field)` | `any` | Read any field-mapped attribute |
139
+ | `expanded` | `boolean` | Reactive expansion state |
140
+ | `selected` | `boolean` | Reactive selection state |
141
+ | `disabled` | `boolean` | Whether the item is non-interactive |
142
+ | `children` | `ProxyItem[]` | Child items |
143
+
144
+ ### Wrapper
145
+
146
+ | Member | Type | Description |
147
+ | -------------------- | ---------------- | --------------------------------------------------------- |
148
+ | `flatView` | `Node[]` | Flat array of visible nodes for rendering |
149
+ | `focusedKey` | `string \| null` | Currently focused path key |
150
+ | `next()` | `void` | Move focus to next navigable item |
151
+ | `prev()` | `void` | Move focus to previous navigable item |
152
+ | `first()` | `void` | Move focus to first item |
153
+ | `last()` | `void` | Move focus to last item |
154
+ | `select(key)` | `void` | Select item by path key |
155
+ | `expand(key)` | `boolean` | Expand a group; returns false if already expanded or leaf |
156
+ | `collapse(key)` | `void` | Collapse a group or move focus to parent |
157
+ | `moveToValue(value)` | `void` | Sync focus to match an external bound value |
158
+
159
+ ---
160
+
161
+ Part of [Rokkit](https://github.com/jerrythomas/rokkit) — a Svelte 5 component library and design system.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/states",
3
- "version": "1.0.0-next.133",
3
+ "version": "1.0.0-next.134",
4
4
  "description": "Contains generic data manipulation functions that can be used in various components.",
5
5
  "author": "Jerry Thomas <me@jerrythomas.name>",
6
6
  "license": "MIT",
@@ -13,12 +13,14 @@
13
13
  },
14
14
  "scripts": {
15
15
  "prepublishOnly": "cp ../../LICENSE . && tsc --project tsconfig.build.json",
16
+ "postpublish": "rm -f LICENSE",
16
17
  "clean": "rm -rf dist",
17
18
  "build": "bun clean && bun prepublishOnly"
18
19
  },
19
20
  "files": [
20
21
  "src/**/*.js",
21
22
  "src/**/*.svelte",
23
+ "README.md",
22
24
  "LICENSE"
23
25
  ],
24
26
  "exports": {
@@ -31,8 +33,8 @@
31
33
  }
32
34
  },
33
35
  "dependencies": {
34
- "@rokkit/core": "1.0.0-next.133",
35
- "@rokkit/data": "1.0.0-next.133",
36
+ "@rokkit/core": "1.0.0-next.134",
37
+ "@rokkit/data": "1.0.0-next.134",
36
38
  "d3-array": "^3.2.4",
37
39
  "ramda": "^0.32.0",
38
40
  "svelte": "^5.53.5"
@@ -16,8 +16,7 @@ export function flatVisibleNodes(items, fields = DEFAULT_FIELDS, path = [], expa
16
16
  items.forEach((item, index) => {
17
17
  const itemPath = [...path, index]
18
18
  const key = getKeyFromPath(itemPath)
19
- const hasChildren =
20
- Array.isArray(item[fields.children]) && item[fields.children].length > 0
19
+ const hasChildren = Array.isArray(item[fields.children]) && item[fields.children].length > 0
21
20
  const expanded = hasChildren && (expandedKeys ? expandedKeys.has(key) : item[fields.expanded])
22
21
 
23
22
  data.push({ key, value: item, level, hasChildren })
@@ -86,7 +86,9 @@ export class LazyWrapper extends Wrapper {
86
86
 
87
87
  // Lazy sentinel: fetch children, then expand
88
88
  if (proxy.loaded === false) {
89
- proxy.fetch().then(() => { proxy.expanded = true })
89
+ proxy.fetch().then(() => {
90
+ proxy.expanded = true
91
+ })
90
92
  return
91
93
  }
92
94
 
@@ -112,7 +114,9 @@ export class LazyWrapper extends Wrapper {
112
114
 
113
115
  // Lazy sentinel: fetch children, then expand
114
116
  if (proxy.loaded === false) {
115
- proxy.fetch().then(() => { proxy.expanded = true })
117
+ proxy.fetch().then(() => {
118
+ proxy.expanded = true
119
+ })
116
120
  return
117
121
  }
118
122
  }
@@ -83,7 +83,10 @@ export class ListController {
83
83
  if (index < 0) {
84
84
  const valueField = this.fields.value
85
85
  index = this.data.findIndex(
86
- (row) => typeof row.value === 'object' && row.value !== null && equals(row.value[valueField], value)
86
+ (row) =>
87
+ typeof row.value === 'object' &&
88
+ row.value !== null &&
89
+ equals(row.value[valueField], value)
87
90
  )
88
91
  }
89
92
 
@@ -10,21 +10,38 @@ const defaultMessages = {
10
10
  select: 'Select an option',
11
11
  search: 'Search...',
12
12
  list: { label: 'List' },
13
- tree: { label: 'Tree', expand: 'Expand', collapse: 'Collapse', loading: 'Loading', loadMore: 'Load More' },
13
+ tree: {
14
+ label: 'Tree',
15
+ expand: 'Expand',
16
+ collapse: 'Collapse',
17
+ loading: 'Loading',
18
+ loadMore: 'Load More'
19
+ },
14
20
  toolbar: { label: 'Toolbar' },
15
21
  menu: { label: 'Menu' },
16
22
  toggle: { label: 'Selection' },
17
23
  rating: { label: 'Rating' },
18
24
  stepper: { label: 'Progress' },
19
25
  breadcrumbs: { label: 'Breadcrumb' },
20
- carousel: { label: 'Carousel', prev: 'Previous slide', next: 'Next slide', slides: 'Slide navigation' },
26
+ carousel: {
27
+ label: 'Carousel',
28
+ prev: 'Previous slide',
29
+ next: 'Next slide',
30
+ slides: 'Slide navigation'
31
+ },
21
32
  tabs: { add: 'Add tab', remove: 'Remove tab' },
22
33
  code: { copy: 'Copy code', copied: 'Copied!' },
23
34
  range: { lower: 'Lower bound', upper: 'Upper bound', value: 'Value' },
24
35
  search_: { clear: 'Clear search' },
25
36
  filter: { remove: 'Remove filter' },
26
37
  grid: { label: 'Grid' },
27
- uploadProgress: { label: 'Upload progress', clear: 'Clear all', cancel: 'Cancel', retry: 'Retry', remove: 'Remove' },
38
+ uploadProgress: {
39
+ label: 'Upload progress',
40
+ clear: 'Clear all',
41
+ cancel: 'Cancel',
42
+ retry: 'Retry',
43
+ remove: 'Remove'
44
+ },
28
45
  floatingNav: { label: 'Page navigation', pin: 'Pin navigation', unpin: 'Unpin navigation' },
29
46
  mode: { system: 'System', light: 'Light', dark: 'Dark' }
30
47
  }
@@ -51,7 +68,12 @@ class MessagesStore {
51
68
  set(custom) {
52
69
  const merged = { ...defaultMessages }
53
70
  for (const key of Object.keys(custom)) {
54
- if (typeof custom[key] === 'object' && custom[key] !== null && typeof merged[key] === 'object' && merged[key] !== null) {
71
+ if (
72
+ typeof custom[key] === 'object' &&
73
+ custom[key] !== null &&
74
+ typeof merged[key] === 'object' &&
75
+ merged[key] !== null
76
+ ) {
55
77
  merged[key] = { ...merged[key], ...custom[key] }
56
78
  } else {
57
79
  merged[key] = custom[key]
@@ -100,14 +100,13 @@ export class ProxyItem {
100
100
  void this.#version // reactive dependency — triggers recompute after set()
101
101
  const raw = this.#item[this.#fields.children]
102
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
- )
103
+ return raw.map((child, i) =>
104
+ this._createChild(
105
+ child,
106
+ this.#fields,
107
+ this.#key ? `${this.#key}-${i}` : String(i),
108
+ this.#level + 1
109
+ )
111
110
  )
112
111
  }
113
112
 
@@ -292,8 +291,12 @@ export class LazyProxyItem extends ProxyItem {
292
291
  this.#loaded = lazyLoad === null || this.get('children') !== true
293
292
  }
294
293
 
295
- get loaded() { return this.#loaded }
296
- get loading() { return this.#loading }
294
+ get loaded() {
295
+ return this.#loaded
296
+ }
297
+ get loading() {
298
+ return this.#loading
299
+ }
297
300
 
298
301
  /**
299
302
  * Fetch children via the lazyLoad function.
@@ -317,4 +320,3 @@ export class LazyProxyItem extends ProxyItem {
317
320
  return new LazyProxyItem(raw, fields, key, level, this.#lazyLoad)
318
321
  }
319
322
  }
320
-
@@ -21,7 +21,13 @@ import { ProxyItem } from './proxy-item.svelte.js'
21
21
 
22
22
  // Maps a parent's line type to the continuation type shown at the same column
23
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' }
24
+ const NEXT_LINE = {
25
+ child: 'sibling',
26
+ last: 'empty',
27
+ sibling: 'sibling',
28
+ empty: 'empty',
29
+ icon: 'empty'
30
+ }
25
31
 
26
32
  // ─── Reactive tree traversal utilities ─────────────────────────────────────────
27
33
 
@@ -115,17 +121,24 @@ export class ProxyTree {
115
121
  */
116
122
  constructor(items = [], fields = {}, options = {}) {
117
123
  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))
124
+ this.#factory =
125
+ options.createProxy ?? ((raw, f, key, level) => new ProxyItem(raw, f, key, level))
126
+ this.#rootProxies = (items ?? []).map((raw, i) =>
127
+ this.#factory(raw, this.#fields, String(i), 1)
128
+ )
120
129
  }
121
130
 
122
131
  // ─── Read accessors ──────────────────────────────────────────────────────
123
132
 
124
133
  /** @returns {ProxyItem[]} Root proxy array */
125
- get roots() { return this.#rootProxies }
134
+ get roots() {
135
+ return this.#rootProxies
136
+ }
126
137
 
127
138
  /** @returns {Map<string, ProxyItem>} Lookup map of all proxies by key */
128
- get lookup() { return this.#lookup }
139
+ get lookup() {
140
+ return this.#lookup
141
+ }
129
142
 
130
143
  // ─── Mutation methods ────────────────────────────────────────────────────
131
144
 
@@ -137,9 +150,7 @@ export class ProxyTree {
137
150
  */
138
151
  append(items) {
139
152
  const start = this.#rootProxies.length
140
- const newProxies = items.map((raw, i) =>
141
- this.#factory(raw, this.#fields, String(start + i), 1)
142
- )
153
+ const newProxies = items.map((raw, i) => this.#factory(raw, this.#fields, String(start + i), 1))
143
154
  this.#rootProxies = [...this.#rootProxies, ...newProxies]
144
155
  }
145
156
 
@@ -68,7 +68,8 @@ export class TableController {
68
68
  }
69
69
  } else {
70
70
  // Single column sort: replace entire sort state
71
- this.sortState = nextDirection === 'none' ? [] : [{ column: columnName, direction: nextDirection }]
71
+ this.sortState =
72
+ nextDirection === 'none' ? [] : [{ column: columnName, direction: nextDirection }]
72
73
  }
73
74
 
74
75
  // Update column sorted flags
@@ -29,9 +29,7 @@ export class Wrapper {
29
29
  // Navigable items: exclude separators, spacers, and disabled items.
30
30
  // This is the subset that keyboard navigation moves through.
31
31
  #navigable = $derived(
32
- this.flatView.filter(
33
- (n) => n.type !== 'separator' && n.type !== 'spacer' && !n.proxy.disabled
34
- )
32
+ this.flatView.filter((n) => n.type !== 'separator' && n.type !== 'spacer' && !n.proxy.disabled)
35
33
  )
36
34
 
37
35
  // ─── State ──────────────────────────────────────────────────────────────────
@@ -56,7 +54,9 @@ export class Wrapper {
56
54
 
57
55
  // ─── IWrapper: state read by Navigator ─────────────────────────────────────
58
56
 
59
- get focusedKey() { return this.#focusedKey }
57
+ get focusedKey() {
58
+ return this.#focusedKey
59
+ }
60
60
 
61
61
  // ─── IWrapper: movement (path passed through but ignored) ──────────────────
62
62
 
@@ -211,9 +211,7 @@ export class Wrapper {
211
211
  const nav = this.#navigable
212
212
  if (!nav.length) return null
213
213
  const q = query.toLowerCase()
214
- const startIdx = startAfterKey
215
- ? nav.findIndex((n) => n.key === startAfterKey) + 1
216
- : 0
214
+ const startIdx = startAfterKey ? nav.findIndex((n) => n.key === startAfterKey) + 1 : 0
217
215
  for (let i = 0; i < nav.length; i++) {
218
216
  const node = nav[(startIdx + i) % nav.length]
219
217
  if (node.proxy.label.toLowerCase().startsWith(q)) return node.key
@@ -224,8 +222,12 @@ export class Wrapper {
224
222
  // ─── Helpers for the component ─────────────────────────────────────────────
225
223
 
226
224
  /** @returns {Map<string, import('./proxy-item.svelte.js').ProxyItem>} */
227
- get lookup() { return this.#proxyTree.lookup }
225
+ get lookup() {
226
+ return this.#proxyTree.lookup
227
+ }
228
228
 
229
229
  /** @returns {import('./proxy-tree.svelte.js').ProxyTree} */
230
- get proxyTree() { return this.#proxyTree }
230
+ get proxyTree() {
231
+ return this.#proxyTree
232
+ }
231
233
  }