@rokkit/states 1.0.0-next.151 → 1.0.0-next.152

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.151",
3
+ "version": "1.0.0-next.152",
4
4
  "description": "Contains generic data manipulation functions that can be used in various components.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -15,12 +15,21 @@ class AlertsStore {
15
15
  */
16
16
  #scheduleTimer(id, timeout) {
17
17
  if (timeout > 0) {
18
- this.#timers.set(id, setTimeout(() => this.dismiss(id), timeout))
18
+ this.#timers.set(
19
+ id,
20
+ setTimeout(() => this.dismiss(id), timeout)
21
+ )
19
22
  }
20
23
  }
21
24
 
22
- // eslint-disable-next-line complexity
23
- push({ type = 'info', text, dismissible = false, timeout = dismissible ? 0 : 4000, actions } = {}) {
25
+
26
+ push({
27
+ type = 'info',
28
+ text,
29
+ dismissible = false,
30
+ timeout = dismissible ? 0 : 4000,
31
+ actions
32
+ } = {}) {
24
33
  const id = crypto.randomUUID()
25
34
  this.#items = [...this.#items, { id, type, text, dismissible, timeout, actions }]
26
35
  this.#scheduleTimer(id, timeout)
@@ -47,7 +47,7 @@ function collectVisibleNodes(items, fields, path, expandedKeys) {
47
47
  return data
48
48
  }
49
49
 
50
- // eslint-disable-next-line complexity
50
+
51
51
  export function flatVisibleNodes(items, fields = DEFAULT_FIELDS, path = [], expandedKeys = null) {
52
52
  if (!items || !Array.isArray(items)) return []
53
53
  return collectVisibleNodes(items, fields, path, expandedKeys)
@@ -84,7 +84,7 @@ function makeLookupEntry(item, norm, fields) {
84
84
  * @param {SvelteMap} lookup Accumulator map
85
85
  * @param {{ item: *, index: number, fields: *, path: Array<number> }} ctx
86
86
  */
87
- // eslint-disable-next-line complexity
87
+
88
88
  function visitLookupNode(lookup, ctx) {
89
89
  const { item, index, fields, path } = ctx
90
90
  const itemPath = [...path, index]
@@ -44,7 +44,7 @@ export class ListController {
44
44
  * Process a single item for expanded key initialization.
45
45
  * @private
46
46
  */
47
- // eslint-disable-next-line complexity
47
+
48
48
  #initExpandedItem(item, index, fields, path) {
49
49
  if (item === null || item === undefined || typeof item !== 'object') return
50
50
  const children = item[fields.children]
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Default messages for UI components
2
+ * Default (English) messages for all UI components.
3
3
  * @type {import('./types').Messages}
4
4
  */
5
5
  const defaultMessages = {
@@ -29,7 +29,8 @@ const defaultMessages = {
29
29
  next: 'Next slide',
30
30
  slides: 'Slide navigation'
31
31
  },
32
- tabs: { add: 'Add tab', remove: 'Remove tab' },
32
+ tabs: { add: 'Add tab', remove: 'Remove tab', placeholder: 'Select a tab to view its content.' },
33
+ table: { empty: 'No data', sortAscending: 'Sort ascending', sortDescending: 'Sort descending' },
33
34
  code: { copy: 'Copy code', copied: 'Copied!' },
34
35
  range: { lower: 'Lower bound', upper: 'Upper bound', value: 'Value' },
35
36
  search_: { clear: 'Clear search' },
@@ -47,53 +48,118 @@ const defaultMessages = {
47
48
  }
48
49
 
49
50
  /**
50
- * Messages store for localized UI strings
51
+ * Deep-merge `overrides` onto `base`. One level deep for nested objects.
52
+ * @param {Record<string, unknown>} base
53
+ * @param {Record<string, unknown>} overrides
54
+ * @returns {Record<string, unknown>}
51
55
  */
56
+ function deepMerge(base, overrides) {
57
+ const result = { ...base }
58
+ for (const key of Object.keys(overrides)) {
59
+ const isObj = (v) => typeof v === 'object' && v !== null
60
+ result[key] =
61
+ isObj(overrides[key]) && isObj(result[key])
62
+ ? { ...result[key], ...overrides[key] }
63
+ : overrides[key]
64
+ }
65
+ return result
66
+ }
67
+
52
68
  class MessagesStore {
53
- /** @type {import('./types').Messages} */
54
- #messages = $state({ ...defaultMessages })
69
+ #registry = {}
70
+ #locale = 'en'
71
+
72
+ // ─── Flat string keys ─────────────────────────────────────────────────────
73
+
74
+ emptyList = $state(defaultMessages.emptyList)
75
+ emptyTree = $state(defaultMessages.emptyTree)
76
+ loading = $state(defaultMessages.loading)
77
+ noResults = $state(defaultMessages.noResults)
78
+ select = $state(defaultMessages.select)
79
+ search = $state(defaultMessages.search)
80
+
81
+ // ─── Nested component namespaces ──────────────────────────────────────────
82
+
83
+ list = $state({ ...defaultMessages.list })
84
+ tree = $state({ ...defaultMessages.tree })
85
+ toolbar = $state({ ...defaultMessages.toolbar })
86
+ menu = $state({ ...defaultMessages.menu })
87
+ toggle = $state({ ...defaultMessages.toggle })
88
+ rating = $state({ ...defaultMessages.rating })
89
+ stepper = $state({ ...defaultMessages.stepper })
90
+ breadcrumbs = $state({ ...defaultMessages.breadcrumbs })
91
+ carousel = $state({ ...defaultMessages.carousel })
92
+ tabs = $state({ ...defaultMessages.tabs })
93
+ table = $state({ ...defaultMessages.table })
94
+ code = $state({ ...defaultMessages.code })
95
+ range = $state({ ...defaultMessages.range })
96
+ search_ = $state({ ...defaultMessages.search_ })
97
+ filter = $state({ ...defaultMessages.filter })
98
+ grid = $state({ ...defaultMessages.grid })
99
+ uploadProgress = $state({ ...defaultMessages.uploadProgress })
100
+ floatingNav = $state({ ...defaultMessages.floatingNav })
101
+ mode = $state({ ...defaultMessages.mode })
102
+
103
+ // ─── Active locale ─────────────────────────────────────────────────────────
104
+
105
+ /** Currently active locale tag (read-only). */
106
+ get locale() {
107
+ return this.#locale
108
+ }
109
+
110
+ // ─── Internals ─────────────────────────────────────────────────────────────
111
+
112
+ #apply() {
113
+ const overrides = this.#registry[this.#locale]
114
+ const computed = overrides
115
+ ? /** @type {import('./types').Messages} */ (deepMerge(defaultMessages, overrides))
116
+ : structuredClone(defaultMessages)
117
+ Object.assign(this, computed)
118
+ }
119
+
120
+ // ─── Public API ────────────────────────────────────────────────────────────
55
121
 
56
122
  /**
57
- * Get the current messages
58
- * @returns {import('./types').Messages}
123
+ * Register translation overrides for a locale. Unspecified keys fall back to English defaults.
124
+ * @param {string} locale — BCP 47 tag or any identifier (e.g. 'de', 'fr-CA')
125
+ * @param {Partial<import('./types').Messages>} overrides
59
126
  */
60
- get current() {
61
- return this.#messages
127
+ register(locale, overrides) {
128
+ this.#registry = { ...this.#registry, [locale]: overrides }
129
+ this.#apply()
62
130
  }
63
131
 
64
132
  /**
65
- * Merge a single key from custom into merged target.
66
- * @param {Record<string, unknown>} merged
67
- * @param {Record<string, unknown>} custom
68
- * @param {string} key
133
+ * Switch to a previously registered locale. Use 'en' to restore English defaults.
134
+ * @param {string} locale
69
135
  */
70
- #mergeKey(merged, custom, key) {
71
- const isObject = (v) => typeof v === 'object' && v !== null
72
- if (isObject(custom[key]) && isObject(merged[key])) {
73
- merged[key] = { ...merged[key], ...custom[key] }
74
- } else {
75
- merged[key] = custom[key]
76
- }
136
+ setLocale(locale) {
137
+ this.#locale = locale
138
+ this.#apply()
77
139
  }
78
140
 
79
141
  /**
80
- * Set custom messages (merges with defaults)
81
- * @param {Partial<import('./types').Messages>} custom
142
+ * Apply one-off overrides without naming a locale (convenience / backward compat).
143
+ * @param {Partial<import('./types').Messages>} overrides
82
144
  */
83
- set(custom) {
84
- const merged = { ...defaultMessages }
85
- for (const key of Object.keys(custom)) {
86
- this.#mergeKey(merged, custom, key)
87
- }
88
- this.#messages = merged
145
+ set(overrides) {
146
+ this.#registry = { ...this.#registry, _custom: overrides }
147
+ this.#locale = '_custom'
148
+ this.#apply()
89
149
  }
90
150
 
91
151
  /**
92
- * Reset to default messages
152
+ * Reset to English defaults and clear all registered locales.
93
153
  */
94
154
  reset() {
95
- this.#messages = { ...defaultMessages }
155
+ this.#registry = {}
156
+ this.#locale = 'en'
157
+ this.#apply()
96
158
  }
97
159
  }
98
160
 
161
+ /**
162
+ * Reactive messages store. Access strings directly: `messages.select`, `messages.tree.expand`.
163
+ * Configure locale: `messages.register('de', {...})` + `messages.setLocale('de')`.
164
+ */
99
165
  export const messages = new MessagesStore()
@@ -125,7 +125,7 @@ export class Wrapper {
125
125
  * At root level with no parent: no-op.
126
126
  * When collapsible=false, skips closing the group but still moves focus to parent.
127
127
  */
128
- // eslint-disable-next-line complexity
128
+
129
129
  collapse(_path) {
130
130
  if (!this.#focusedKey) return
131
131
  const node = this.flatView.find((n) => n.key === this.#focusedKey)
@@ -177,7 +177,7 @@ export class Wrapper {
177
177
  * Unlike select(), this only applies to groups and never fires onselect.
178
178
  * No-op when collapsible=false.
179
179
  */
180
- // eslint-disable-next-line complexity
180
+
181
181
  toggle(path) {
182
182
  if (!this.#collapsible) return
183
183
  const key = path ?? this.#focusedKey