@rokkit/states 1.1.13 → 1.1.15

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.1.13",
3
+ "version": "1.1.15",
4
4
  "description": "Contains generic data manipulation functions that can be used in various components.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,8 +37,8 @@
37
37
  }
38
38
  },
39
39
  "dependencies": {
40
- "@rokkit/core": "1.1.13",
41
- "@rokkit/data": "1.1.13",
40
+ "@rokkit/core": "1.1.15",
41
+ "@rokkit/data": "1.1.15",
42
42
  "d3-array": "^3.2.4",
43
43
  "ramda": "^0.32.0",
44
44
  "svelte": "^5.53.5"
@@ -0,0 +1,123 @@
1
+ /**
2
+ * App command registry — keyboard shortcuts + actions in one place.
3
+ * Singleton, like `vibe`/`messages`. See docs/design/19-command-system.md.
4
+ *
5
+ * @typedef {Object} Command
6
+ * @property {string} id Unique id; re-registering the same id replaces (warns).
7
+ * @property {string} label Display text (consumer-localized).
8
+ * @property {() => void} run Executed on shortcut match or palette select.
9
+ * @property {string} [shortcut] e.g. 'mod+k' | 'mod+shift+p' | '?'. `mod` = Cmd on macOS / Ctrl elsewhere.
10
+ * @property {string} [group] Palette category.
11
+ * @property {boolean} [global] Fire even while a text input is focused (default false).
12
+ * @property {() => boolean} [enabled] Optional dynamic gate.
13
+ * @property {string[]} [keywords] Extra palette search terms.
14
+ */
15
+
16
+ const MOD_ORDER = ['ctrl', 'meta', 'alt', 'shift']
17
+
18
+ const MODIFIER_ALIAS = { cmd: 'meta', meta: 'meta', ctrl: 'ctrl', control: 'ctrl', shift: 'shift', alt: 'alt', option: 'alt' }
19
+
20
+ function isMac() {
21
+ if (typeof navigator === 'undefined') return false
22
+ return /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent || '')
23
+ }
24
+
25
+ /** Canonicalize a shortcut string: resolve `mod`, sort modifiers, lowercase the key. */
26
+ export function normalizeShortcut(shortcut) {
27
+ const mods = new Set()
28
+ let key = ''
29
+ for (const raw of String(shortcut).toLowerCase().split('+')) {
30
+ const p = raw.trim()
31
+ if (!p) continue
32
+ if (p === 'mod') mods.add(isMac() ? 'meta' : 'ctrl')
33
+ else if (MODIFIER_ALIAS[p]) mods.add(MODIFIER_ALIAS[p])
34
+ else key = p
35
+ }
36
+ return [...MOD_ORDER.filter((m) => mods.has(m)), key].join('+')
37
+ }
38
+
39
+ /** Build the canonical shortcut string from a KeyboardEvent. */
40
+ export function eventToShortcut(event) {
41
+ const mods = []
42
+ if (event.ctrlKey) mods.push('ctrl')
43
+ if (event.metaKey) mods.push('meta')
44
+ if (event.altKey) mods.push('alt')
45
+ if (event.shiftKey) mods.push('shift')
46
+ return [...MOD_ORDER.filter((m) => mods.includes(m)), String(event.key).toLowerCase()].join('+')
47
+ }
48
+
49
+ class CommandRegistry {
50
+ #commands = $state(/** @type {Command[]} */ ([]))
51
+ #byShortcut = new Map() // normalized shortcut → id (first registrant wins)
52
+
53
+ /** @returns {Command[]} */
54
+ get all() {
55
+ return this.#commands
56
+ }
57
+
58
+ /** @param {Command} cmd @returns {() => void} unregister */
59
+ register(cmd) {
60
+ const idx = this.#commands.findIndex((c) => c.id === cmd.id)
61
+ if (idx >= 0) {
62
+ // eslint-disable-next-line no-console
63
+ console.warn(`[commands] duplicate id "${cmd.id}" — replacing existing registration`)
64
+ this.#commands[idx] = cmd
65
+ // drop any shortcut(s) previously indexed to this id (avoid stale bindings)
66
+ for (const [sc, owner] of this.#byShortcut) {
67
+ if (owner === cmd.id) this.#byShortcut.delete(sc)
68
+ }
69
+ } else {
70
+ this.#commands = [...this.#commands, cmd]
71
+ }
72
+ if (cmd.shortcut) {
73
+ const norm = normalizeShortcut(cmd.shortcut)
74
+ const owner = this.#byShortcut.get(norm)
75
+ if (owner && owner !== cmd.id) {
76
+ // eslint-disable-next-line no-console
77
+ console.warn(
78
+ `[commands] shortcut "${cmd.shortcut}" already bound to "${owner}"; "${cmd.id}" will not receive it`
79
+ )
80
+ } else {
81
+ this.#byShortcut.set(norm, cmd.id)
82
+ }
83
+ }
84
+ return () => this.unregister(cmd.id)
85
+ }
86
+
87
+ /** @param {Command[]} cmds @returns {() => void} */
88
+ registerMany(cmds) {
89
+ const offs = cmds.map((c) => this.register(c))
90
+ return () => offs.forEach((off) => off())
91
+ }
92
+
93
+ /** @param {string} id */
94
+ unregister(id) {
95
+ this.#commands = this.#commands.filter((c) => c.id !== id)
96
+ for (const [shortcut, owner] of this.#byShortcut) {
97
+ if (owner === id) this.#byShortcut.delete(shortcut)
98
+ }
99
+ }
100
+
101
+ /** @param {string} id */
102
+ execute(id) {
103
+ const cmd = this.#commands.find((c) => c.id === id)
104
+ if (!cmd) {
105
+ // eslint-disable-next-line no-console
106
+ console.warn(`[commands] unknown command "${id}"`)
107
+ return
108
+ }
109
+ if (cmd.enabled && !cmd.enabled()) return
110
+ cmd.run()
111
+ }
112
+
113
+ /** @param {KeyboardEvent} event @returns {Command | null} */
114
+ resolve(event) {
115
+ const id = this.#byShortcut.get(eventToShortcut(event))
116
+ if (!id) return null
117
+ const cmd = this.#commands.find((c) => c.id === id)
118
+ if (!cmd || (cmd.enabled && !cmd.enabled())) return null
119
+ return cmd
120
+ }
121
+ }
122
+
123
+ export const commands = new CommandRegistry()
package/src/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { alerts } from './alerts.svelte.js'
2
2
  export { vibe } from './vibe.svelte.js'
3
3
  export { messages } from './messages.svelte.js'
4
+ export { commands, normalizeShortcut, eventToShortcut } from './commands.svelte.js'
4
5
  export { ProxyItem, LazyProxyItem, BASE_FIELDS } from './proxy-item.svelte.js'
5
6
  export { ProxyTree } from './proxy-tree.svelte.js'
6
7
  export { ProxyTable } from './proxy-table.svelte.js'
@@ -44,7 +44,12 @@ const defaultMessages = {
44
44
  remove: 'Remove'
45
45
  },
46
46
  floatingNav: { label: 'Page navigation', pin: 'Pin navigation', unpin: 'Unpin navigation' },
47
- mode: { system: 'System', light: 'Light', dark: 'Dark' }
47
+ mode: { system: 'System', light: 'Light', dark: 'Dark' },
48
+ command: {
49
+ placeholder: 'Run a command…',
50
+ noResults: 'No commands found',
51
+ label: 'Command palette'
52
+ },
48
53
  }
49
54
 
50
55
  /**
@@ -99,6 +104,7 @@ class MessagesStore {
99
104
  uploadProgress = $state({ ...defaultMessages.uploadProgress })
100
105
  floatingNav = $state({ ...defaultMessages.floatingNav })
101
106
  mode = $state({ ...defaultMessages.mode })
107
+ command = $state({ ...defaultMessages.command })
102
108
 
103
109
  // ─── Active locale ─────────────────────────────────────────────────────────
104
110
 
@@ -22,8 +22,10 @@ function isAllowedValue(value, allowed, current) {
22
22
  */
23
23
  class Vibe {
24
24
  #allowedStyles = $state(DEFAULT_STYLES)
25
+ #allowedSkins = $state(['default'])
25
26
  #mode = $state('dark')
26
27
  #style = $state('rokkit')
28
+ #skin = $state('default')
27
29
  #colors = $state(defaultColors)
28
30
  #density = $state('comfortable')
29
31
  #direction = $state(detectDirection())
@@ -53,6 +55,23 @@ class Vibe {
53
55
  }
54
56
  }
55
57
 
58
+ get allowedSkins() {
59
+ return this.#allowedSkins
60
+ }
61
+
62
+ set allowedSkins(input) {
63
+ const skins = (Array.isArray(input) ? input : [input]).filter(Boolean)
64
+ if (skins.length > 0) this.#allowedSkins = skins
65
+ }
66
+
67
+ get skin() {
68
+ return this.#skin
69
+ }
70
+
71
+ set skin(value) {
72
+ if (isAllowedValue(value, this.#allowedSkins, this.#skin)) this.#skin = value
73
+ }
74
+
56
75
  get colorMap() {
57
76
  return this.#colorMap
58
77
  }
@@ -158,7 +177,8 @@ class Vibe {
158
177
  style: this.#style,
159
178
  mode: this.#mode,
160
179
  density: this.#density,
161
- direction: this.#direction
180
+ direction: this.#direction,
181
+ skin: this.#skin
162
182
  }
163
183
  localStorage.setItem(key, JSON.stringify(config))
164
184
  } catch (e) {
@@ -176,6 +196,7 @@ class Vibe {
176
196
  this.mode = value.mode
177
197
  this.density = value.density
178
198
  this.direction = value.direction
199
+ if (value.skin) this.skin = value.skin
179
200
  }
180
201
 
181
202
  /**