@ozio/spicerack 0.1.0-beta.7
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 +449 -0
- package/dist/index.js +4682 -0
- package/dist/types/index.d.ts +84 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
# Spicerack
|
|
2
|
+
|
|
3
|
+
Spicerack is a small, dat.GUI-inspired controls panel for inspecting and changing values in a web application.
|
|
4
|
+
|
|
5
|
+
Spicerack uses Vue internally, but its consumer API is ordinary JavaScript. Applications do not need to create Vue components, install the Vue Vite plugin, or mount a Vue app.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import { createSpicerack } from '@ozio/spicerack';
|
|
9
|
+
|
|
10
|
+
const gui = createSpicerack([
|
|
11
|
+
{ key: 'name', value: 'Nicholas', label: 'Name' },
|
|
12
|
+
{ key: 'points', value: 32, label: 'Points' },
|
|
13
|
+
{ key: 'enabled', value: true, label: 'Enabled' }
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
gui.mount('#spicerack');
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
### npm
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npm install @ozio/spicerack
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import { createSpicerack } from '@ozio/spicerack';
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Vue and the built-in controls are bundled implementation details of `@ozio/spicerack`; consumers do not install or import them directly.
|
|
32
|
+
|
|
33
|
+
### jsDelivr (ESM)
|
|
34
|
+
|
|
35
|
+
```html
|
|
36
|
+
<div id="spicerack"></div>
|
|
37
|
+
|
|
38
|
+
<script type="module">
|
|
39
|
+
import { createSpicerack } from 'https://cdn.jsdelivr.net/npm/@ozio/spicerack/+esm';
|
|
40
|
+
|
|
41
|
+
const gui = createSpicerack([
|
|
42
|
+
{ key: 'message', value: 'Hello', label: 'Message' }
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
gui.mount('#spicerack');
|
|
46
|
+
</script>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Pin a package version in production if reproducible builds are important:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
import { createSpicerack } from 'https://cdn.jsdelivr.net/npm/@ozio/spicerack@0.1.0-beta.7/+esm';
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Sample usage
|
|
56
|
+
|
|
57
|
+
### Defining a config
|
|
58
|
+
|
|
59
|
+
Pass a config array and optional settings to `createSpicerack(config, options)`. A control's `type` can be omitted for string, number, and boolean values.
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import { createSpicerack } from '@ozio/spicerack';
|
|
63
|
+
|
|
64
|
+
const gui = createSpicerack([
|
|
65
|
+
{
|
|
66
|
+
type: 'folder',
|
|
67
|
+
label: 'Player',
|
|
68
|
+
open: true,
|
|
69
|
+
config: [
|
|
70
|
+
{
|
|
71
|
+
key: 'name',
|
|
72
|
+
value: 'LeBron James',
|
|
73
|
+
label: 'Name',
|
|
74
|
+
maxlength: 40
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: 'points',
|
|
78
|
+
type: 'range',
|
|
79
|
+
value: 32,
|
|
80
|
+
label: 'Points',
|
|
81
|
+
min: 0,
|
|
82
|
+
max: 100,
|
|
83
|
+
step: 1
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
key: 'conference',
|
|
89
|
+
type: 'list',
|
|
90
|
+
value: '',
|
|
91
|
+
label: 'Conference',
|
|
92
|
+
options: [
|
|
93
|
+
{ value: '', label: 'All Conferences' },
|
|
94
|
+
{ value: 'east', label: 'Eastern' },
|
|
95
|
+
{ value: 'west', label: 'Western' }
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: 'button',
|
|
100
|
+
label: 'Reset',
|
|
101
|
+
onClick(event) {
|
|
102
|
+
console.log('Reset clicked', event);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
], {
|
|
106
|
+
title: 'Player Settings',
|
|
107
|
+
theme: 'dark',
|
|
108
|
+
position: 'top-right',
|
|
109
|
+
open: true
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Spicerack currently supports one folder level. Do not put a folder inside another folder.
|
|
114
|
+
|
|
115
|
+
### Mounting to the DOM
|
|
116
|
+
|
|
117
|
+
Construction and mounting are separate operations:
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
const gui = createSpicerack(config);
|
|
121
|
+
|
|
122
|
+
gui.mount('#spicerack');
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`mount()` accepts a CSS selector or an `Element`. If no target is supplied, it mounts to `document.body`.
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
gui.mount(document.querySelector('#spicerack'));
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
An instance currently supports one application mount in normal use. An explicit public `unmount()` API is not yet available.
|
|
132
|
+
|
|
133
|
+
### Reading and changing values
|
|
134
|
+
|
|
135
|
+
Each keyed value control is available through `gui.model`:
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
console.log(gui.model.points.value);
|
|
139
|
+
|
|
140
|
+
gui.model.points.value = 50;
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Calling `gui.json()` returns a plain snapshot of all model values:
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
console.log(gui.json());
|
|
147
|
+
// { name: 'LeBron James', points: 50, conference: '' }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Folders and buttons are structural or action controls and are not added to the model. During the current beta, a divider still requires a key and creates an `undefined` model field; this is tracked as a structural-control issue.
|
|
151
|
+
|
|
152
|
+
### Adding event handlers
|
|
153
|
+
|
|
154
|
+
Subscribe to model changes with `on('change', callback)`. This observes both user input and programmatic assignments.
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
const stop = gui.model.points.on('change', (value, previousValue) => {
|
|
158
|
+
console.log({ value, previousValue });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
gui.model.points.value = 51;
|
|
162
|
+
|
|
163
|
+
stop();
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Control-level listeners can also be supplied with Vue-style listener prop names. Value controls emit the new domain value followed by the original DOM event:
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
const config = [
|
|
170
|
+
{
|
|
171
|
+
key: 'name',
|
|
172
|
+
value: 'Nicholas',
|
|
173
|
+
label: 'Name',
|
|
174
|
+
onChange(value, event) {
|
|
175
|
+
console.log(value, event);
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
type: 'button',
|
|
180
|
+
label: 'Save',
|
|
181
|
+
onClick(event) {
|
|
182
|
+
console.log('Save', event);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
];
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Prefer model subscriptions when application behavior should also react to programmatic value changes.
|
|
189
|
+
|
|
190
|
+
## API
|
|
191
|
+
|
|
192
|
+
### `createSpicerack(config, options?)`
|
|
193
|
+
|
|
194
|
+
Creates and returns a Spicerack instance. It does not mount anything during construction.
|
|
195
|
+
|
|
196
|
+
### Config
|
|
197
|
+
|
|
198
|
+
The first argument is an array of config records. Spicerack reserves `key`, `type`, `value`, and `config`; other properties are forwarded to the selected control as props or event listeners.
|
|
199
|
+
|
|
200
|
+
| Property | Type | Description |
|
|
201
|
+
| --- | --- | --- |
|
|
202
|
+
| `key` | `string` | Unique model key. Required for value controls. |
|
|
203
|
+
| `type` | `string` | Registered control type. Optional when inferred from a string, number, or boolean value. |
|
|
204
|
+
| `value` | `unknown` | Initial value for a value control. |
|
|
205
|
+
| `label` | `string` | Visible control label. |
|
|
206
|
+
| `config` | `SpicerackConfig[]` | Child controls for a folder. One folder level is supported. |
|
|
207
|
+
| Additional properties | `unknown` | Forwarded to the selected control, such as `min`, `max`, `step`, `options`, or `onChange`. |
|
|
208
|
+
|
|
209
|
+
Keys must be non-empty strings and unique across the entire config, including controls inside folders. Buttons do not require a key or value.
|
|
210
|
+
|
|
211
|
+
### Options
|
|
212
|
+
|
|
213
|
+
| Option | Type | Default | Description |
|
|
214
|
+
| --- | --- | --- | --- |
|
|
215
|
+
| `controls` | `Record<string, SpicerackControl>` | `{}` | Custom controls added to the registry. A matching name can override a built-in control. |
|
|
216
|
+
| `tokens` | `SpicerackTokens` | Built-in theme | Style-token overrides. |
|
|
217
|
+
| `open` | `boolean` | Persisted value or `true` | Initial expanded state. |
|
|
218
|
+
| `theme` | `'light' \| 'dark'` | Persisted value or `'dark'` | Initial color theme. |
|
|
219
|
+
| `position` | `'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right'` | Persisted value or `'top-right'` | Initial panel corner. |
|
|
220
|
+
| `title` | `string` | `''` | Header title. |
|
|
221
|
+
|
|
222
|
+
An explicit option takes precedence over a persisted value. Panel state is currently stored in `localStorage` with the keys `sr:open`, `sr:theme`, and `sr:corner`.
|
|
223
|
+
|
|
224
|
+
### Return object
|
|
225
|
+
|
|
226
|
+
#### `mount(target?)`
|
|
227
|
+
|
|
228
|
+
Mounts the panel to a selector or `Element`. The default target is `document.body`.
|
|
229
|
+
|
|
230
|
+
#### `model`
|
|
231
|
+
|
|
232
|
+
A null-prototype object containing one field per keyed value control. Each field exposes:
|
|
233
|
+
|
|
234
|
+
- `value`: get or set the current value.
|
|
235
|
+
- `on('change', callback)`: observe value changes and receive an unsubscribe function.
|
|
236
|
+
|
|
237
|
+
#### `json()`
|
|
238
|
+
|
|
239
|
+
Returns a plain object containing the current value of every model field.
|
|
240
|
+
|
|
241
|
+
#### `version`
|
|
242
|
+
|
|
243
|
+
The installed Spicerack package version as a string.
|
|
244
|
+
|
|
245
|
+
## Built-in controls
|
|
246
|
+
|
|
247
|
+
| Type | Value | Additional props | Events |
|
|
248
|
+
| --- | --- | --- | --- |
|
|
249
|
+
| `boolean` | `boolean` | `label` | `change(value, event)` |
|
|
250
|
+
| `button` | None | `label` | `click(event)` |
|
|
251
|
+
| `divider` | None | None | None |
|
|
252
|
+
| `folder` | None | `label`, `open`, `config` | `click(event)` |
|
|
253
|
+
| `list` | Any option value | `label`, `options` | `change(value, event)` |
|
|
254
|
+
| `number` | `number` | `label`, `min`, `max` | `change(value, event)` |
|
|
255
|
+
| `range` | `number` | `label`, `min`, `max`, `step` | `input(value, event)`, `change(value, event)` |
|
|
256
|
+
| `switch` | `boolean` | `label` | `change(value, event)` |
|
|
257
|
+
| `text` | `string` | `label`, `minlength`, `maxlength` | `change(value, event)` |
|
|
258
|
+
| `textarea` | `string` | `label`, `minlength`, `maxlength`, `rows`, `resize` | `change(value, event)` |
|
|
259
|
+
| `toggle` | `boolean` | `label` | `change(value, event)` |
|
|
260
|
+
|
|
261
|
+
The shared control contract is still evolving. In particular, a common `disabled` prop is not yet implemented across every interactive control.
|
|
262
|
+
|
|
263
|
+
During the current beta, divider config records require a unique `key` even though dividers have no value. This requirement is expected to be removed.
|
|
264
|
+
|
|
265
|
+
## Custom controls
|
|
266
|
+
|
|
267
|
+
Custom controls are compiled Vue components registered through the framework-neutral Spicerack options object. Consumers of your control package still interact with Spicerack through ordinary JavaScript.
|
|
268
|
+
|
|
269
|
+
```js
|
|
270
|
+
import { createSpicerack } from '@ozio/spicerack';
|
|
271
|
+
import { FancyControl } from 'fancy-spicerack-controls';
|
|
272
|
+
|
|
273
|
+
const gui = createSpicerack([
|
|
274
|
+
{
|
|
275
|
+
key: 'nickname',
|
|
276
|
+
type: 'fancy',
|
|
277
|
+
value: 'King',
|
|
278
|
+
label: 'Nickname',
|
|
279
|
+
emphasis: 'strong'
|
|
280
|
+
}
|
|
281
|
+
], {
|
|
282
|
+
controls: {
|
|
283
|
+
fancy: FancyControl
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
gui.mount('#spicerack');
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
See the [built-in controls source](https://github.com/ortenzio/spicerack/tree/main/packages/controls/src) for working component examples.
|
|
291
|
+
|
|
292
|
+
### Package dependencies
|
|
293
|
+
|
|
294
|
+
A third-party control package should treat Vue as a peer dependency and include the build tooling it uses as development dependencies:
|
|
295
|
+
|
|
296
|
+
```json
|
|
297
|
+
{
|
|
298
|
+
"peerDependencies": {
|
|
299
|
+
"vue": "^3.5.40"
|
|
300
|
+
},
|
|
301
|
+
"devDependencies": {
|
|
302
|
+
"@vitejs/plugin-vue": "^6.0.0",
|
|
303
|
+
"vite": "^8.2.0",
|
|
304
|
+
"vite-plugin-css-injected-by-js": "^5.0.2",
|
|
305
|
+
"vue": "^3.5.40"
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Vite config
|
|
311
|
+
|
|
312
|
+
This configuration matches the current built-in control packaging strategy. Vue is externalized so the control package uses Spicerack's runtime:
|
|
313
|
+
|
|
314
|
+
```js
|
|
315
|
+
import { resolve } from 'node:path';
|
|
316
|
+
import { defineConfig } from 'vite';
|
|
317
|
+
import vue from '@vitejs/plugin-vue';
|
|
318
|
+
import cssInjectedByJs from 'vite-plugin-css-injected-by-js';
|
|
319
|
+
|
|
320
|
+
export default defineConfig({
|
|
321
|
+
plugins: [
|
|
322
|
+
vue(),
|
|
323
|
+
cssInjectedByJs()
|
|
324
|
+
],
|
|
325
|
+
build: {
|
|
326
|
+
lib: {
|
|
327
|
+
entry: resolve(import.meta.dirname, 'index.js'),
|
|
328
|
+
formats: ['es'],
|
|
329
|
+
fileName: 'index'
|
|
330
|
+
},
|
|
331
|
+
rolldownOptions: {
|
|
332
|
+
external: ['vue']
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
If you extract CSS instead, document the stylesheet import that consumers must add.
|
|
339
|
+
|
|
340
|
+
### Vue SFC contract
|
|
341
|
+
|
|
342
|
+
Value controls receive `modelValue` and should emit `update:modelValue`. Vue's `defineModel()` macro handles both:
|
|
343
|
+
|
|
344
|
+
```vue
|
|
345
|
+
<template>
|
|
346
|
+
<label data-sr-control="fancy">
|
|
347
|
+
<span v-if="label" data-sr-label>{{ label }}</span>
|
|
348
|
+
<input
|
|
349
|
+
v-model="model"
|
|
350
|
+
:disabled="disabled"
|
|
351
|
+
@change="handleChange"
|
|
352
|
+
/>
|
|
353
|
+
</label>
|
|
354
|
+
</template>
|
|
355
|
+
|
|
356
|
+
<script setup>
|
|
357
|
+
defineProps({
|
|
358
|
+
label: { type: String, default: null },
|
|
359
|
+
disabled: { type: Boolean, default: false }
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const model = defineModel();
|
|
363
|
+
const emit = defineEmits(['change']);
|
|
364
|
+
|
|
365
|
+
function handleChange (event) {
|
|
366
|
+
emit('change', model.value, event);
|
|
367
|
+
}
|
|
368
|
+
</script>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Action controls should emit `click` and do not need to define a model.
|
|
372
|
+
|
|
373
|
+
### Style directives
|
|
374
|
+
|
|
375
|
+
Spicerack provides low-specificity structural styles for these attributes:
|
|
376
|
+
|
|
377
|
+
- `[data-sr-control]`: root element for a control.
|
|
378
|
+
- `[data-sr-label]`: shared label typography.
|
|
379
|
+
- `[data-sr-spacer]`: flexible spacer used in horizontal controls.
|
|
380
|
+
|
|
381
|
+
Controls can use Spicerack's CSS custom properties, including:
|
|
382
|
+
|
|
383
|
+
- `--sr-bg-app`, `--sr-bg-control`, `--sr-bg-accent`
|
|
384
|
+
- `--sr-fg-app`, `--sr-fg-elm`, `--sr-fg-muted`
|
|
385
|
+
- `--sr-border-app`, `--sr-border-active`
|
|
386
|
+
- `--sr-fs-control`, `--sr-fs-input`
|
|
387
|
+
- `--sr-control-radius`, `--sr-control-gap`
|
|
388
|
+
|
|
389
|
+
Treat these as part of the current authoring environment, but expect the design-token surface to evolve during beta releases.
|
|
390
|
+
|
|
391
|
+
## Style tokens
|
|
392
|
+
|
|
393
|
+
Pass token overrides through `options.tokens`:
|
|
394
|
+
|
|
395
|
+
```js
|
|
396
|
+
const gui = createSpicerack(config, {
|
|
397
|
+
tokens: {
|
|
398
|
+
appWidth: '20rem',
|
|
399
|
+
appZIndex: 1000,
|
|
400
|
+
controlRadius: '0.5rem',
|
|
401
|
+
bgColorAccent: 'rebeccapurple'
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Available keys:
|
|
407
|
+
|
|
408
|
+
- Layout: `appEdgeSpacing`, `appPadding`, `appRadius`, `appWidth`, `appZIndex`
|
|
409
|
+
- Controls: `controlRadius`, `controlGap`
|
|
410
|
+
- Typography: `fontSizeControl`, `fontSizeInput`, `fontSizeTitle`
|
|
411
|
+
- Backgrounds: `bgColorAccent`, `bgColorAccentHover`, `bgColorActive`, `bgColorAlt`, `bgColorApp`, `bgColorCard`, `bgColorControl`, `bgColorElm`, `bgColorMuted`
|
|
412
|
+
- Foregrounds: `fgColorAccent`, `fgColorActive`, `fgColorAlt`, `fgColorApp`, `fgColorCard`, `fgColorElm`, `fgColorMuted`
|
|
413
|
+
- Borders: `borderColorActive`, `borderColorAlt`, `borderColorApp`, `borderColorCard`, `borderColorElm`, `borderColorMuted`
|
|
414
|
+
|
|
415
|
+
CSS length and color tokens accept CSS strings. `appZIndex` accepts a number.
|
|
416
|
+
|
|
417
|
+
## Types
|
|
418
|
+
|
|
419
|
+
The package includes declarations for `createSpicerack` and exports the public API types:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
import {
|
|
423
|
+
createSpicerack,
|
|
424
|
+
type SpicerackConfig,
|
|
425
|
+
type SpicerackInstance,
|
|
426
|
+
type SpicerackOptions,
|
|
427
|
+
type SpicerackTokens
|
|
428
|
+
} from '@ozio/spicerack';
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
The maintained declaration file is emitted as part of the Vite library build. TypeScript is not used to compile the application.
|
|
432
|
+
|
|
433
|
+
## Contact
|
|
434
|
+
|
|
435
|
+
- [GitHub repository](https://github.com/ortenzio/spicerack)
|
|
436
|
+
- [Issue tracker](https://github.com/ortenzio/spicerack/issues)
|
|
437
|
+
|
|
438
|
+
## Contributing
|
|
439
|
+
|
|
440
|
+
Install dependencies and run the workspace checks from the repository root:
|
|
441
|
+
|
|
442
|
+
```sh
|
|
443
|
+
npm install
|
|
444
|
+
npm run build
|
|
445
|
+
npm run lint -w @ozio/spicerack
|
|
446
|
+
npm run lint -w @ozio/spicerack-controls
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Keep the demo framework-agnostic: it should consume `@ozio/spicerack` as plain JavaScript and should not import Vue or use the Vue Vite plugin.
|