@skirbi/sugar 0.0.6

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/Changes ADDED
@@ -0,0 +1,77 @@
1
+ Revision history for @skirbi/sugar
2
+
3
+ 0.0.6 2026-02-20 04:30:44Z
4
+
5
+ * Add a way to actually add aliases. It seems between time of initial
6
+ development and now my alias approach didn't work. Add a way to register a
7
+ non-valid spec breaking HTML tag which gets upgraded `OnDocumentLoad`. That
8
+ way you can still write `<book>` but have to target `<my-book>` in CSS.
9
+ Sad, but true.
10
+ * Yay renames. Since I'm building a whole suite or family of suites that deal
11
+ with webcomponents and HTML authoring I've decided to move everything to
12
+ the skirbi org on npm. So this package will from now onwards be called
13
+ @skirbi/sugar. Why? Well, because we will introduce @skirbi/semtic and
14
+ @skirbi/theme(s) in the near future. I feel this shouldn't be under my
15
+ companies name. Eventho that is a major sponsor of the project.
16
+
17
+ @skirbi/sugar meet world
18
+
19
+ No more renames, I promise :)
20
+
21
+ 0.0.5 2026-02-17 17:05:05Z
22
+
23
+ * Fix small CI glitch for tag-builds
24
+
25
+ * For another project I wanted to have selectboxes that implement the
26
+ following API:
27
+
28
+ <my-select-box options='{"json": "here"}' jpath-label="foo"
29
+ jpath-value="bar" endpoint="https://foo.example.com"></my-select-box>
30
+
31
+ Instead of having to reinvent the wheel everywhere I've decided to
32
+ implement this in skirbi so this pattern can be reused by downstream
33
+ consumers.
34
+
35
+ * Fix bug to allow inheritence by removing hasOwnProperty()
36
+
37
+ Now subclasses can be “tag-only” building blocks and still inherit the
38
+ config contract from HTMLElementSugarSelect (HTMLElementSugar* in
39
+ general).
40
+
41
+ This also makes skirbi behave more like people expect:
42
+ base classes can define defaults + observed attrs, and subclasses can just
43
+ set tag.
44
+
45
+ 0.0.4 2026-02-17 02:47:33Z
46
+
47
+ * Update README.md
48
+ * Rename module from @opndev/webcomponents-core to @opndev/skirbi
49
+ I'll leave the old module in place, so you stay on version 0.0.3 forever,
50
+ but going forward this is the new name.
51
+
52
+ 0.0.3 2026-02-16 22:14:34Z
53
+
54
+ * jsdom got added as a runtime dep. This isn't the case. It is a test
55
+ dependency which you need if you use things from
56
+ @opndev/webcomponents-core/testing. Fixed the glitch.
57
+
58
+ * Add HTMLElementSugarInput for a base class that can be used for input
59
+ parameters. It adds logic so input types can be used in for example
60
+ livewire projects. Which was the main reason for the creation of the class.
61
+ Livewire isn't the only intended audience tho, feel free to use it in other
62
+ frameworks.
63
+
64
+ 0.0.2 2026-02-16 01:50:02Z
65
+
66
+ * Add new HTMLTemplateSpec so you as webcomponent author can set a default
67
+ template but also allow consumers to use a html template found in HTML.
68
+ This is mainly intended for theme authors.
69
+ * Add tpl() static function for easier dealing with templates
70
+ * Derive the tag from the ClassName: class-name. You can now omit a tag and
71
+ it's derived from the class name. If you want a different class name and a
72
+ different tag, just set the tag to what-ever-you-want.
73
+ * rzilla release
74
+
75
+ 0.0.1 2025-10-17T23:00:00-0400
76
+
77
+ * First release
package/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ MIT License
2
+
3
+ Copyright (c) <year> <copyright holders>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6
+ associated documentation files (the "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
9
+ following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial
12
+ portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
16
+ EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
18
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,454 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2025, 2026 Wesley Schwengle <wesleys@opperschaap.net>
3
+
4
+ SPDX-License-Identifier: MIT
5
+ -->
6
+
7
+ # @skirbi/sugar
8
+
9
+ A lightweight base layer for building Web Components.
10
+
11
+ It provides:
12
+
13
+ - Declarative attribute → config mapping
14
+ - Template helpers
15
+ - Alias utilities
16
+ - Form-control wrapping primitives
17
+
18
+ ## TL;DR
19
+
20
+ You can create Web Components like so:
21
+
22
+ ```javascript
23
+ class MyNewElement extends HTMLElementSugar {
24
+ static tag = 'my-new-element';
25
+ static attributeDefs = {
26
+ foo: { default: 'bar' },
27
+ bar: { parser: parseBoolean },
28
+ };
29
+ }
30
+ ```
31
+
32
+ You can also create aliases:
33
+
34
+ ```javascript
35
+ TrackItem.alias('some-song', { type: 'song' });
36
+ YourBook.alias('a-book');
37
+ ```
38
+
39
+ And to register them:
40
+
41
+ ```javascript
42
+ TrackItem.register();
43
+ YourBook.register();
44
+ ```
45
+
46
+ ## Observable patterns
47
+
48
+ You can define attributes to be observable:
49
+
50
+ ```javascript
51
+ class MyNewElement extends HTMLElementSugar {
52
+ static tag = 'my-new-element';
53
+ static attributeDefs = {
54
+ 'foo': { default: 'bar' },
55
+ 'bar': { parser: parseBoolean },
56
+ 'baz': null,
57
+ 'qux': {},
58
+ 'toto': { observed: "" }, // not observed
59
+ 'titi': { observed: 0 }, // not observed
60
+ 'tata': { observed: false }, // not observed
61
+ 'tete': { observed: null }, // not observed
62
+ 'yes': { observed: true },
63
+ 'yez': { observed: 1 },
64
+ 'yep': { observed: "here be dragons" },
65
+ };
66
+ }
67
+ ```
68
+
69
+ ## Templates
70
+
71
+ ### Template via HTML template reference
72
+
73
+ ```javascript
74
+ class HTMLTemplate extends HTMLElementSugar {
75
+ static tag = 'html-template';
76
+ static HtmlTemplate = 'track-item-template'; // find in html
77
+ }
78
+ ```
79
+
80
+ HTMLElementSugar asserts that it can find the template on registration. Which
81
+ means you need to have the template ready in HTML. It is therefore essential
82
+ that you register your component in window.addEventListener('DOMContentLoaded',
83
+ () => { }.
84
+
85
+ ### Template via javascript element reference
86
+
87
+ ```javascript
88
+ const tpl = document.createElement('template');
89
+ tpl.id = 'inline';
90
+ tpl.innerHTML =
91
+ `<div class="track-row"><span class="song">song</span></div>`;
92
+ document.body.appendChild(tpl);
93
+
94
+ class JSElement extends HTMLElementSugar {
95
+ static tag = 'js-element';
96
+ static HtmlTemplate = tpl;
97
+ }
98
+ ```
99
+
100
+ ### Template via javascript function
101
+
102
+ ```javascript
103
+ class TemplateFunction extends HTMLElementSugar {
104
+ static tag = 'template-function';
105
+
106
+ static HtmlTemplate() {
107
+ const t = document.createElement('template');
108
+ t.innerHTML =
109
+ `<div class="track-row"><div class="track-info">from fn</div></div>`;
110
+ return t;
111
+ }
112
+ }
113
+ ```
114
+
115
+ We recently added a helper you can now do this too:
116
+
117
+ ```javascript
118
+ class TemplateFunction extends HTMLElementSugar {
119
+ static tag = 'template-function';
120
+
121
+ static HtmlTemplate = this.tpl(`
122
+ <div class="track-row">
123
+ <div class="track-info">from tpl</div>
124
+ </div>
125
+ `);
126
+ }
127
+ ```
128
+
129
+ ### Custom template with fallback
130
+
131
+ ```javascript
132
+ class TemplateFunction extends HTMLElementSugar {
133
+ static tag = 'template-function';
134
+
135
+ static HtmlTemplate = [
136
+ 'some-id-in-html',
137
+ () => {
138
+ const t = document.createElement('template');
139
+ t.innerHTML =
140
+ `<div class="track-row"><div class="track-info">from fn</div></div>`;
141
+ return t;
142
+ }
143
+ ];
144
+ }
145
+ ```
146
+
147
+ ### No template (the ultimate minimalism)
148
+
149
+ ```javascript
150
+ class MinimalistComponent extends HTMLElementSugar {
151
+ static tag = 'ultimate-minimalist';
152
+ // Look at all this template code I'm not writing
153
+ }
154
+ ```
155
+
156
+ You can register your Web Components by running:
157
+
158
+ ```
159
+ MyComponent.register();
160
+ ```
161
+
162
+ # HTMLElementSugarInput
163
+
164
+ `HTMLElementSugarInput` extends `HTMLElementSugar` and is designed for
165
+ components that wrap a real form control in the light DOM.
166
+
167
+ It provides:
168
+
169
+ - Attribute forwarding to the real control
170
+ - Native `input` and `change` re-emission
171
+ - Optional attribute mirroring via `data-sync`
172
+ - Proper `value` property handling
173
+
174
+ ## Contract
175
+
176
+ A subclass must:
177
+
178
+ - Extend `HTMLElementSugarInput`
179
+ - Render exactly one element matching `[wc-control]`
180
+ (or override `static controlSelector`)
181
+
182
+ Example:
183
+
184
+ ```javascript
185
+ class MyInput extends HTMLElementSugarInput {
186
+ static tag = 'my-input';
187
+
188
+ static attributeDefs = {
189
+ label: { default: '' },
190
+ value: { default: '' },
191
+ };
192
+
193
+ static HtmlTemplate = this.tpl(`
194
+ <div>
195
+ <label wc-label></label>
196
+ <input wc-control type="text">
197
+ </div>
198
+ `);
199
+
200
+ connectedCallback() {
201
+ super.connectedCallback();
202
+
203
+ const frag = this.renderFromTemplate();
204
+ const control = this.enhanceControl(frag);
205
+
206
+ const { label, value } = this.getConfig();
207
+
208
+ frag.querySelector('[wc-label]').textContent = label;
209
+ control.value = value ?? '';
210
+
211
+ this.replaceChildren(frag);
212
+ }
213
+ }
214
+
215
+ MyInput.register();
216
+ ```
217
+
218
+ ## Attribute Forwarding
219
+
220
+ All host attributes that are **not defined in `attributeDefs`**
221
+ are forwarded to the real control element.
222
+
223
+ Example:
224
+
225
+ ```html
226
+ <my-input
227
+ label="Name"
228
+ wire:model.defer="name"
229
+ aria-label="Name"
230
+ ></my-input>
231
+ ```
232
+
233
+ Results in:
234
+
235
+ ```html
236
+ <input
237
+ wire:model.defer="name"
238
+ aria-label="Name"
239
+ >
240
+ ```
241
+
242
+ ### Not forwarded
243
+
244
+ - Attributes defined in `attributeDefs`
245
+ - `id`, `class`, `style` (kept on the host)
246
+
247
+ This keeps components framework-agnostic:
248
+
249
+ - Livewire (`wire:*`)
250
+ - Alpine (`x-*`)
251
+ - HTMX (`hx-*`)
252
+ - `aria-*`
253
+ - `data-*`
254
+
255
+ ## Event Re-Emission
256
+
257
+ Native events from the control are re-emitted from the host:
258
+
259
+ - `input`
260
+ - `change`
261
+
262
+ Listeners may bind to either the host or the internal control.
263
+
264
+ ## data-sync (Optional Attribute Mirroring)
265
+
266
+ By default, attributes are forwarded once during connect.
267
+
268
+ If you enable `data-sync`, subsequent attribute changes on the host are
269
+ mirrored to the control using a MutationObserver.
270
+
271
+ Example:
272
+
273
+ ```html
274
+ <my-input wire:model.defer="name" data-sync></my-input>
275
+ ```
276
+
277
+ When `data-sync` is enabled:
278
+
279
+ - Host attribute changes are mirrored to the control
280
+ - `value` is mirrored as a property (not an attribute)
281
+ - Removing attributes removes them from the control
282
+
283
+ This is particularly useful in reactive environments such as Livewire.
284
+
285
+ ## HTMLElementSugarSelect
286
+
287
+ HTMLElementSugarSelect extends HTMLElementSugarInput and provides a
288
+ structured way to author `<select>`-based components.
289
+
290
+ It supports:
291
+ * Static option lists (via JSON)
292
+ * Remote option loading (via endpoint)
293
+ * Flexible data mapping via jpath
294
+ * Label/value templates
295
+ * Optional search input
296
+ * Attribute forwarding to the underlying `<select>`
297
+
298
+ It always renders a real `<select>` in the light DOM.
299
+
300
+ ### Contract
301
+
302
+ A subclass must:
303
+ * Extend HTMLElementSugarSelect
304
+ * Render exactly one `<select wc-control>`
305
+ * Optionally include a search input if searchable is enabled
306
+
307
+ Example:
308
+
309
+ ```javascript
310
+ class MySelect extends HTMLElementSugarSelect {
311
+ static tag = 'my-select';
312
+
313
+ static HtmlTemplate = this.tpl(`
314
+ <select wc-control></select>
315
+ `);
316
+ }
317
+
318
+ MySelect.register();
319
+ ```
320
+
321
+ ### Static Options
322
+
323
+ You may provide options via the options attribute as a JSON array:
324
+
325
+ ```html
326
+ <my-select
327
+ options='[
328
+ { "id": 1, "name": "Alice" },
329
+ { "id": 2, "name": "Bob" }
330
+ ]'
331
+ jpath-label="name"
332
+ jpath-value="id"
333
+ ></my-select>
334
+ ```
335
+
336
+ ### Mapping Options
337
+
338
+ Data mapping is controlled via:
339
+ * `jpath`: path to the array in a nested JSON response
340
+ * `jpath-label`: path for the option label
341
+ * `jpath-value`: path for the option value
342
+ * `jpath-label-template`: template string for label
343
+ * `jpath-value-template`: template string for value
344
+
345
+ Templates take precedence over path mappings.
346
+
347
+ Example:
348
+
349
+ ```html
350
+ <my-select
351
+ options='[
352
+ { "id": 1, "name": "Alice", "email": "a@example.com" }
353
+ ]'
354
+ jpath-label-template="{name} ({email})"
355
+ jpath-value-template="user:{id}"
356
+ ></my-select>
357
+ ```
358
+
359
+ Missing template fields resolve to empty strings.
360
+
361
+ ### Remote Options
362
+
363
+ Options can be loaded from a remote endpoint:
364
+
365
+ ```html
366
+ <my-select
367
+ endpoint="/api/users"
368
+ method="GET"
369
+ param="q"
370
+ searchable
371
+ min-chars="2"
372
+ debounce="300"
373
+ nosearch-initial
374
+ ></my-select>
375
+ ```
376
+
377
+ Behavior:
378
+ * Fetches JSON from endpoint
379
+ * Adds search query via param
380
+ * Applies mapping rules
381
+ * Replaces options on each fetch
382
+
383
+ If `searchable` is enabled:
384
+ * A search input is rendered
385
+ * Input is debounced
386
+ * Fetch is triggered after min-chars is reached
387
+ * By default, a remote endpoint triggers an initial fetch with an empty query
388
+ on connect. If `nosearch-initial` is enabled this behavior is negated.
389
+
390
+
391
+ * No initial searches are
392
+
393
+ ### Search Behavior (Static Mode)
394
+
395
+ When using static options, enabling searchable:
396
+ * Filters `<option>` elements in place
397
+ * Uses case-insensitive substring matching
398
+ * Does not modify underlying data
399
+
400
+ ### Value Handling
401
+
402
+ The value attribute behaves consistently with `HTMLElementSugarInput`:
403
+ * Initial value is applied after options render
404
+ * With data-sync, updates are mirrored to the `<select>` element
405
+ * Native input and change events are re-emitted from the host
406
+
407
+ ### Framework Compatibility
408
+
409
+ Because it renders a real `<select>` in the light DOM, it works naturally with:
410
+ * Livewire (`wire:*`)
411
+ * Alpine (`x-*`)
412
+ * HTMX (`hx-*`)
413
+ * `aria-*`
414
+ * `data-*`
415
+
416
+ Attributes not defined in attributeDefs are forwarded to the `<select>`
417
+ element.
418
+
419
+ ### Why This Works
420
+ * No shadow DOM
421
+ * No custom dropdown UI
422
+ * No CSS lock-in
423
+ * Real `<option>` elements
424
+ * Progressive enhancement friendly
425
+
426
+ ## Code of Conduct
427
+
428
+ Be human.
429
+
430
+ ## Developer notes about this package
431
+
432
+ ### Versioning
433
+
434
+ This project does not follow semver. It follows a Perl-style release philosophy
435
+ centered on backward compatibility. This translates to the following hard
436
+ guarantee: We do not intentionally break working code. If a release causes
437
+ breakage, it will be addressed accordingly.
438
+
439
+ The `x.y.z` version number should not be used to infer stability.
440
+ Consult the Changes file for important updates, deprecations,
441
+ and breaking changes.
442
+
443
+ The current `0.x.z` range does not imply alpha, beta, or instability.
444
+ It is simply the starting point of the project.
445
+
446
+ In case we foresee breaking changes we'll add deprecation warnings. Giving you
447
+ ample time to fix things before a breaking change will be introduced. When a
448
+ change will be introduced is communicated in the Changes file. Security fixes
449
+ may cause breakage at any given time without notice.
450
+
451
+ This package is released by `@opndev/rzilla`, changes to `package.json` will be
452
+ overridden. In addition to a little bit of promotion, this also means that
453
+ version numbers are autoincremented at release time and bumped in all relevant
454
+ files: Versioning for humans, not machines.
@@ -0,0 +1,21 @@
1
+ // SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+
5
+ import { applyDevAliases } from './aliases.mjs';
6
+
7
+ export function registerAliases(root = document) {
8
+ applyDevAliases(root);
9
+ }
10
+
11
+ // Run immediately if DOM is ready, otherwise on DOMContentLoaded.
12
+ if (typeof document !== 'undefined') {
13
+ if (document.readyState === 'loading') {
14
+ document.addEventListener('DOMContentLoaded', () => registerAliases(), {
15
+ once: true,
16
+ });
17
+ } else {
18
+ registerAliases();
19
+ }
20
+ }
21
+
@@ -0,0 +1,49 @@
1
+ // SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+
5
+ const KEY = '__skirbi_dev_aliases__';
6
+
7
+ function getMap() {
8
+ globalThis[KEY] ??= new Map();
9
+ return globalThis[KEY];
10
+ }
11
+
12
+ // Register a dev alias (authoring sugar): <from> -> <to>
13
+ export function registerDevAlias(from, to, defaultAttributes = {}) {
14
+ if (!from || !to) throw new Error('registerDevAlias(from, to): missing args');
15
+
16
+ getMap().set(String(from).toLowerCase(), {
17
+ to: String(to).toLowerCase(),
18
+ defaultAttributes: defaultAttributes ?? {},
19
+ });
20
+ }
21
+
22
+ // Rewrite all dev aliases under root.
23
+ export function applyDevAliases(root = document) {
24
+ const map = getMap();
25
+
26
+ for (const [from, spec] of map.entries()) {
27
+ const nodes = root.querySelectorAll(from);
28
+
29
+ for (const oldEl of nodes) {
30
+ const neu = document.createElement(spec.to);
31
+
32
+ // Copy attributes
33
+ for (const { name, value } of Array.from(oldEl.attributes)) {
34
+ neu.setAttribute(name, value);
35
+ }
36
+
37
+ // Apply default attributes if missing
38
+ for (const [k, v] of Object.entries(spec.defaultAttributes)) {
39
+ if (!neu.hasAttribute(k)) neu.setAttribute(k, v);
40
+ }
41
+
42
+ // Move children
43
+ while (oldEl.firstChild) neu.appendChild(oldEl.firstChild);
44
+
45
+ oldEl.replaceWith(neu);
46
+ }
47
+ }
48
+ }
49
+
@@ -0,0 +1,22 @@
1
+ // SPDX-FileCopyrightText: 2025 Wesley Schwengle <wesleys@opperschaap.net>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+
5
+ /**
6
+ * Normalize an HTML attribute value to a boolean.
7
+ *
8
+ * This function interprets a variety of string-like inputs as either `true` or `false`:
9
+ * - Empty string ("") becomes true (e.g. <input disabled>)
10
+ * - "false" and "0" become false
11
+ * - All other defined values become true
12
+ *
13
+ * @param {string | null | undefined} value - The attribute value to parse
14
+ * @returns {boolean} The normalized boolean result
15
+ */
16
+ export function parseBoolean(value) {
17
+ if (value === null || value === undefined) return false;
18
+
19
+ const val = String(value).toLowerCase().trim();
20
+
21
+ return val !== 'false' && val !== '0';
22
+ }