face-up 0.0.0 → 0.0.2

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.
Files changed (46) hide show
  1. package/.gitmodules +3 -0
  2. package/.kiro/steering/project-context.md +17 -0
  3. package/.vscode/settings.json +3 -0
  4. package/FaceUp.js +305 -0
  5. package/README.md +162 -2
  6. package/imports.html +8 -0
  7. package/package.json +30 -20
  8. package/tests/test1.html +115 -0
  9. package/types/.kiro/specs/conversion-template/README.md +128 -0
  10. package/types/.kiro/specs/conversion-template/design.md +360 -0
  11. package/types/.kiro/specs/conversion-template/requirements.md +191 -0
  12. package/types/.kiro/specs/conversion-template/tasks.md +174 -0
  13. package/types/.kiro/steering/coding-standards.md +17 -0
  14. package/types/.kiro/steering/conversion-guide.md +103 -0
  15. package/types/.kiro/steering/declarative-configuration.md +108 -0
  16. package/types/.kiro/steering/emc-json-serializability.md +306 -0
  17. package/types/EnhancementConversionInstructions.md +1626 -0
  18. package/types/LICENSE +21 -0
  19. package/types/NewCustomElementFeature.md +673 -0
  20. package/types/NewEnhancementInstructions.md +395 -0
  21. package/types/README.md +2 -0
  22. package/types/agrace/types.d.ts +11 -0
  23. package/types/assign-gingerly/types.d.ts +328 -0
  24. package/types/be-a-beacon/types.d.ts +17 -0
  25. package/types/be-bound/types.d.ts +61 -0
  26. package/types/be-buttoned-up/types.d.ts +19 -0
  27. package/types/be-clonable/types.d.ts +36 -0
  28. package/types/be-committed/types.d.ts +22 -0
  29. package/types/be-decked-with/types.d.ts +26 -0
  30. package/types/be-delible/types.d.ts +25 -0
  31. package/types/be-reflective/types.d.ts +80 -0
  32. package/types/be-render-neutral/types.d.ts +29 -0
  33. package/types/be-typed/types.d.ts +31 -0
  34. package/types/do-inc/types.d.ts +56 -0
  35. package/types/do-invoke/types.d.ts +38 -0
  36. package/types/do-merge/types.d.ts +28 -0
  37. package/types/do-toggle/types.d.ts +31 -0
  38. package/types/face-up/types.d.ts +104 -0
  39. package/types/global.d.ts +29 -0
  40. package/types/id-generation/types.d.ts +26 -0
  41. package/types/inferencer/types.d.ts +46 -0
  42. package/types/mount-observer/types.d.ts +363 -0
  43. package/types/nested-regex-groups/types.d.ts +101 -0
  44. package/types/roundabout/types.d.ts +255 -0
  45. package/types/time-ticker/types.d.ts +66 -0
  46. package/types/truth-sourcer/types.d.ts +46 -0
package/types/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bruce B. Anderson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,673 @@
1
+ # New Custom Element Feature Instructions
2
+
3
+ ## Introduction
4
+
5
+ This document provides step-by-step instructions for creating a **brand new** custom element feature project. Custom Element Features provide dependency injection of composable feature classes onto custom element prototypes via lazy getters. They allow decomposing large components into smaller, testable units without mixins or subclassing.
6
+
7
+ **Note:** This guide is specifically for **custom element features** (composable behavior injected into custom elements via `assignFeatures`). It does NOT apply to enhancements (which use `be-hive` and `mount-observer` to attach behavior to existing elements via attributes). For enhancements, see [NewEnhancementInstructions.md](./NewEnhancementInstructions.md).
8
+
9
+ ## What is a Custom Element Feature?
10
+
11
+ A custom element feature is a class that:
12
+
13
+ 1. Gets lazily instantiated as a getter-only property on a custom element's prototype.
14
+ 2. Receives the host element, a spawn context, and optional initial values in its constructor.
15
+ 3. Can be swapped out for mocks in tests without subclassing.
16
+ 4. Integrates with `assignGingerly` automatically — because the property is getter-only, `assignGingerly` merges into the spawned instance.
17
+ 5. Supports async lazy-loading of the implementation.
18
+ 6. Can parse element attributes into initial values via `withAttrs`.
19
+
20
+ ## Reference Implementation
21
+
22
+ - **[truth-sourcer](https://github.com/bahrus/truth-sourcer)** — The world's first custom element feature
23
+ - **[be-reflective](https://github.com/bahrus/be-reflective)** — Demonstrates `callbackForwarding` and `getSharedContext` for features that need DOM context (computed styles)
24
+
25
+ ## Prerequisites
26
+
27
+ - Node.js installed
28
+ - npm installed
29
+ - `ncu` (npm-check-updates) installed globally: `npm install -g npm-check-updates`
30
+ - Chrome 146+ for testing (scoped custom element registry support required)
31
+
32
+ ## Step 1: Initialize the Project
33
+
34
+ 1. Create a new repository (e.g., `truth-sourcer` or `my-feature`)
35
+ 2. Run `npm init` or create a `package.json` manually
36
+ 3. Add the `types` submodule:
37
+ ```bash
38
+ git submodule add https://github.com/bahrus/types.git types
39
+ ```
40
+
41
+ ## Step 2: Configure package.json
42
+
43
+ ```json
44
+ {
45
+ "name": "my-feature",
46
+ "version": "0.0.0",
47
+ "description": "Description of what the feature does",
48
+ "type": "module",
49
+ "main": "MyFeature.js",
50
+ "scripts": {
51
+ "serve": "node ./node_modules/spa-ssi/serve.js",
52
+ "test": "playwright test",
53
+ "update": "ncu -u && npm install",
54
+ "safari": "npx playwright wk http://localhost:8000",
55
+ "chrome": "npx playwright cr http://localhost:8000"
56
+ },
57
+ "devDependencies": {
58
+ "assign-gingerly": "0.0.39",
59
+ "@playwright/test": "1.60.0",
60
+ "spa-ssi": "0.0.27"
61
+ },
62
+ "dependencies": {
63
+ }
64
+ }
65
+ ```
66
+
67
+ **Notes:**
68
+ - Use exact versions, not ranges (no `^` or `~`)
69
+ - `assign-gingerly` is a devDependency — the consuming custom element project brings it in
70
+ - Run `npm run update` after creating package.json to install dependencies
71
+
72
+ ## Step 3: Create Type Definitions
73
+
74
+ Create `types/[project-name]/types.d.ts` with the feature structure:
75
+
76
+ ```typescript
77
+ import { SpawnContext } from "../assign-gingerly/types";
78
+
79
+ /**
80
+ * Configuration/properties that the feature exposes
81
+ */
82
+ export interface FeatureProps {
83
+ // Properties the feature manages
84
+ myProp: string;
85
+ anotherProp: number;
86
+ }
87
+
88
+ /**
89
+ * Internal state (not exposed to consumers)
90
+ */
91
+ export interface AllProps extends FeatureProps {
92
+ host: WeakRef<Element>;
93
+ }
94
+
95
+ export type AP = AllProps;
96
+ export type PAP = Partial<AP>;
97
+
98
+ /**
99
+ * Context passed to the feature constructor
100
+ */
101
+ export interface FeatureSpawnContext extends SpawnContext {
102
+ key: string;
103
+ optIn: any;
104
+ injection: any;
105
+ featuresRegistry: any;
106
+ shared?: any;
107
+ }
108
+ ```
109
+
110
+ **Key points:**
111
+ - `FeatureProps` — the public API of the feature
112
+ - `AllProps` — includes internal state like a WeakRef to the host element
113
+ - The feature class does NOT need to extend any base class
114
+
115
+ ## Step 4: Create the Feature Class
116
+
117
+ Create `[FeatureName].js` (e.g., `AttrMgr.js`):
118
+
119
+ ```javascript
120
+ // @ts-check
121
+ /** @import {FeatureProps, AllProps, FeatureSpawnContext} from './types/[project-name]/types' */
122
+
123
+ /**
124
+ * @implements {FeatureProps}
125
+ */
126
+ class MyFeature {
127
+ /** @type {WeakRef<Element> | undefined} */
128
+ #host;
129
+
130
+ /**
131
+ * @param {Element} hostElement
132
+ * @param {FeatureSpawnContext} ctx
133
+ * @param {Partial<FeatureProps>} [initVals]
134
+ */
135
+ constructor(hostElement, ctx, initVals) {
136
+ this.#host = new WeakRef(hostElement);
137
+ if (initVals) {
138
+ Object.assign(this, initVals);
139
+ }
140
+ }
141
+
142
+ // Feature methods and properties here
143
+ }
144
+
145
+ export { MyFeature };
146
+ ```
147
+
148
+ **Key patterns:**
149
+ - No base class — plain JavaScript class
150
+ - Constructor signature: `(hostElement, ctx, initVals)`
151
+ - Store host as a `WeakRef` to avoid preventing garbage collection
152
+ - Apply `initVals` via `Object.assign` in the constructor
153
+ - Use `@ts-check` with JSDoc type imports from the `types/` folder
154
+ - No compiled TypeScript — ship raw `.js` files
155
+
156
+ ## Step 5: Create imports.html
157
+
158
+ ```html
159
+ <script type=importmap>
160
+ {
161
+ "imports": {
162
+ "assign-gingerly/": "/node_modules/assign-gingerly/",
163
+ "[project-name]/": "/"
164
+ }
165
+ }
166
+ </script>
167
+ ```
168
+
169
+ **Notes:**
170
+ - Import maps use trailing slashes for package-style resolution
171
+ - The feature project maps to `/` (root) for local development
172
+ - Only include packages actually needed
173
+
174
+ ## Step 6: Create Test HTML
175
+
176
+ Create a test file (e.g., `tests/test1.html`):
177
+
178
+ ```html
179
+ <!DOCTYPE html>
180
+ <html lang="en">
181
+ <head>
182
+ <meta charset="UTF-8">
183
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
184
+ <title>Test - My Feature</title>
185
+ <!-- #include virtual="/imports.html" -->
186
+ <script type=module>
187
+ import 'assign-gingerly/assignFeatures.js';
188
+ import {MyFeature} from '[project-name]/MyFeature.js';
189
+
190
+ // Define a test custom element that uses the feature
191
+ class TestElement extends HTMLElement {
192
+ static supportedFeatures = {
193
+ myFeature: {
194
+ fallbackSpawn: MyFeature
195
+ }
196
+ };
197
+
198
+ constructor() {
199
+ super();
200
+ }
201
+ }
202
+
203
+ // Inject the feature
204
+ customElements.assignFeatures(TestElement, {
205
+ myFeature: { spawn: MyFeature }
206
+ });
207
+
208
+ customElements.define('test-element', TestElement);
209
+ </script>
210
+ </head>
211
+ <body>
212
+ <test-element></test-element>
213
+ </body>
214
+ </html>
215
+ ```
216
+
217
+ ## Step 7: Configure VS Code
218
+
219
+ Create `.vscode/settings.json`:
220
+
221
+ ```json
222
+ {
223
+ "explorer.fileNesting.enabled": true
224
+ }
225
+ ```
226
+
227
+ ## Step 8: Set Up .kiro Directory
228
+
229
+ Create `.kiro/steering/project-context.md` to reference the shared types documentation:
230
+
231
+ ```markdown
232
+ ---
233
+ inclusion: auto
234
+ ---
235
+
236
+ # Project Context
237
+
238
+ This project uses shared type definitions and documentation from the `types` submodule.
239
+
240
+ ## Key References
241
+
242
+ #[[file:types/NewCustomElementFeature.md]]
243
+
244
+ ## Coding Standards
245
+
246
+ #[[file:types/.kiro/steering/coding-standards.md]]
247
+ ```
248
+
249
+ ## Architecture Overview
250
+
251
+ ```
252
+ [project-name]/
253
+ ├── .kiro/
254
+ │ └── steering/
255
+ │ └── project-context.md
256
+ ├── .vscode/
257
+ │ └── settings.json
258
+ ├── types/ (git submodule)
259
+ │ └── [project-name]/
260
+ │ └── types.d.ts
261
+ ├── [FeatureName].js (feature class - browser code)
262
+ ├── imports.html (import map for browser)
263
+ ├── package.json
264
+ ├── tests/
265
+ │ └── test1.html
266
+ └── README.md
267
+ ```
268
+
269
+ ## How Custom Element Features Work
270
+
271
+ ### The Consumer Side (Custom Element Author)
272
+
273
+ A custom element declares which feature "slots" it supports:
274
+
275
+ ```javascript
276
+ // @ts-check
277
+ import 'assign-gingerly/assignFeatures.js';
278
+ import {MyFeature} from 'my-feature/MyFeature.js';
279
+
280
+ class MyElement extends HTMLElement {
281
+ static supportedFeatures = {
282
+ myFeature: {
283
+ fallbackSpawn: MyFeature,
284
+ // Optional: validate the spawned instance
285
+ validateShape(instance) {
286
+ return typeof instance.myMethod === 'function';
287
+ }
288
+ }
289
+ };
290
+
291
+ constructor() {
292
+ super();
293
+ }
294
+ }
295
+
296
+ // Inject feature implementations
297
+ customElements.assignFeatures(MyElement, {
298
+ myFeature: { spawn: MyFeature }
299
+ });
300
+
301
+ customElements.define('my-element', MyElement);
302
+ ```
303
+
304
+ ### Lazy Instantiation
305
+
306
+ The feature is NOT instantiated until first property access:
307
+
308
+ ```javascript
309
+ const el = document.createElement('my-element');
310
+ // Feature not yet instantiated
311
+
312
+ console.log(el.myFeature.myProp);
313
+ // NOW the feature is instantiated (lazy getter fires)
314
+ ```
315
+
316
+ ### Integration with assignGingerly
317
+
318
+ Because `assignFeatures` installs getter-only properties (no setter), `assignGingerly` automatically merges into the feature instance:
319
+
320
+ ```javascript
321
+ import 'assign-gingerly/object-extension.js';
322
+
323
+ el.assignGingerly({
324
+ myFeature: { myProp: 'updated value' }
325
+ });
326
+ // Merges into the existing feature instance
327
+ ```
328
+
329
+ ### Attribute Parsing with withAttrs
330
+
331
+ Features can declare attribute patterns to parse element attributes into `initVals`:
332
+
333
+ ```javascript
334
+ customElements.assignFeatures(MyElement, {
335
+ myFeature: {
336
+ spawn: MyFeature,
337
+ withAttrs: {
338
+ base: 'my-feature',
339
+ myProp: '${base}-my-prop',
340
+ count: '${base}-count',
341
+ _count: { instanceOf: 'Number' }
342
+ }
343
+ }
344
+ });
345
+ ```
346
+
347
+ ```html
348
+ <my-element my-feature-my-prop="hello" my-feature-count="42"></my-element>
349
+ ```
350
+
351
+ The parsed attributes (`{ myProp: 'hello', count: 42 }`) are passed as `initVals` to the constructor.
352
+
353
+ ### Async Spawn (Lazy Loading)
354
+
355
+ Feature implementations can be loaded asynchronously:
356
+
357
+ ```javascript
358
+ customElements.assignFeatures(MyElement, {
359
+ myFeature: {
360
+ spawn: () => import('my-feature/MyFeature.js').then(m => m.MyFeature)
361
+ }
362
+ });
363
+ ```
364
+
365
+ During the loading window, a placeholder `{}` is returned. Once the async import resolves, the real instance replaces it (with any accumulated properties passed as `initVals`).
366
+
367
+ ### Shared Context (Access to Private Fields)
368
+
369
+ Features can receive private data from the host element via `getSharedContext`:
370
+
371
+ ```javascript
372
+ class MyElement extends HTMLElement {
373
+ #internals;
374
+
375
+ static supportedFeatures = {
376
+ myFeature: {
377
+ fallbackSpawn: MyFeature,
378
+ getSharedContext(instance) {
379
+ return { internals: instance.#internals };
380
+ }
381
+ }
382
+ };
383
+
384
+ constructor() {
385
+ super();
386
+ this.#internals = this.attachInternals();
387
+ }
388
+ }
389
+ ```
390
+
391
+ The feature receives this via `ctx.shared`:
392
+
393
+ ```javascript
394
+ class MyFeature {
395
+ constructor(hostElement, ctx, initVals) {
396
+ this.internals = ctx.shared?.internals;
397
+ }
398
+ }
399
+ ```
400
+
401
+ ### Lifecycle Callback Forwarding with `callbackForwarding`
402
+
403
+ Features that need DOM context (computed styles, layout info) or cleanup on disconnect can declare `callbackForwarding` in `static supportedFeatures` to receive the host element's lifecycle callbacks automatically:
404
+
405
+ ```javascript
406
+ class MyElement extends HTMLElement {
407
+ static supportedFeatures = {
408
+ myFeature: {
409
+ fallbackSpawn: MyFeature,
410
+ callbackForwarding: ['connectedCallback', 'disconnectedCallback']
411
+ }
412
+ };
413
+ }
414
+
415
+ customElements.assignFeatures(MyElement, {
416
+ myFeature: { spawn: MyFeature }
417
+ });
418
+ ```
419
+
420
+ `callbackForwarding` can also be specified in the injection config passed to `assignFeatures`. A union is taken between both — so the feature author can guarantee the callbacks they need in `supportedFeatures`, and the injector can add additional ones if needed.
421
+
422
+ **How it works:**
423
+
424
+ 1. `assignFeatures` patches the custom element's lifecycle callbacks on the prototype (once per callback type).
425
+ 2. The original callback runs first, then all registered features are forwarded.
426
+ 3. On first `connectedCallback`, the lazy getter is triggered — spawning the feature at the correct lifecycle moment (when the element is in the DOM and computed styles are available).
427
+ 4. For async features, forwarding is skipped until the real instance is available.
428
+
429
+ **Supported callbacks:** `connectedCallback`, `disconnectedCallback`, `attributeChangedCallback`, `adoptedCallback`, `formAssociatedCallback`, `formDisabledCallback`, `formResetCallback`, `formStateRestoreCallback`
430
+
431
+ **When to use it:**
432
+
433
+ - The feature needs `attributeChangedCallback` forwarded (e.g., truth-sourcer)
434
+ - The feature needs `getComputedStyle` (which requires the element to be in the DOM)
435
+ - The feature sets up event listeners that should be cleaned up on disconnect
436
+ - The feature needs to handle elements created via cloned templates (where the constructor fires before DOM insertion)
437
+ - The feature manages form-associated behavior (`formDisabledCallback`, `formResetCallback`, `formStateRestoreCallback`) — e.g., face-up
438
+
439
+ **Avoiding double-connect on initial spawn:**
440
+
441
+ Since the feature is *spawned* during the first `connectedCallback` (the getter fires), the constructor already has the opportunity to self-connect. When `callbackForwarding` then immediately forwards `connectedCallback` to the freshly-spawned instance, you need to guard against double-initialization. The standard pattern is a `#hasDisconnected` flag:
442
+
443
+ ```javascript
444
+ class MyFeature {
445
+ #hasDisconnected = false;
446
+
447
+ constructor(hostElement, ctx, initVals) {
448
+ // Self-connect on construction (we know we're in the DOM
449
+ // because connectedCallback triggered our spawn)
450
+ this.#connect();
451
+ }
452
+
453
+ connectedCallback() {
454
+ // Only re-connect after a prior disconnection
455
+ if (this.#hasDisconnected) {
456
+ this.#hasDisconnected = false;
457
+ this.#connect();
458
+ }
459
+ }
460
+
461
+ disconnectedCallback() {
462
+ this.#hasDisconnected = true;
463
+ this.#cleanup();
464
+ }
465
+
466
+ #connect() {
467
+ // Safe to call getComputedStyle here — element is in the DOM
468
+ const styles = getComputedStyle(this.#hostRef.deref());
469
+ // ... wire up listeners, parse CSS, etc.
470
+ }
471
+
472
+ #cleanup() {
473
+ // Abort listeners, clear state
474
+ }
475
+ }
476
+ ```
477
+
478
+ **Complete example with `getSharedContext` + `callbackForwarding`:**
479
+
480
+ This pattern eliminates all manual wiring in the consumer's constructor — the feature self-activates at the correct lifecycle moment with all dependencies provided declaratively:
481
+
482
+ ```javascript
483
+ class MyElement extends HTMLElement {
484
+ propagator = new EventTarget();
485
+ #internals;
486
+
487
+ static supportedFeatures = {
488
+ myFeature: {
489
+ fallbackSpawn: MyFeature,
490
+ callbackForwarding: ['connectedCallback', 'disconnectedCallback'],
491
+ getSharedContext(instance) {
492
+ return {
493
+ internals: instance.#internals,
494
+ hostPropagator: instance.propagator
495
+ };
496
+ }
497
+ }
498
+ };
499
+
500
+ constructor() {
501
+ super();
502
+ this.#internals = this.attachInternals();
503
+ // No manual feature activation needed!
504
+ }
505
+ }
506
+
507
+ customElements.assignFeatures(MyElement, {
508
+ myFeature: { spawn: MyFeature }
509
+ });
510
+
511
+ customElements.define('my-element', MyElement);
512
+ ```
513
+
514
+ **Async features and `callbackForwarding`:**
515
+
516
+ For async features (where `spawn` is an async function), the feature is instantiated when the async import resolves. Since the constructor already handles initial connection, `callbackForwarding` only needs to forward *subsequent* lifecycle events. If you wrap an async feature around a sync one (lazy-loading pattern), delegate lifecycle calls to the inner feature once it's loaded:
517
+
518
+ ```javascript
519
+ class MyFeatureLazy {
520
+ #delegate = null;
521
+ #hasDisconnected = false;
522
+
523
+ constructor(hostElement, ctx, initVals) {
524
+ this.#maybeActivate(hostElement, ctx);
525
+ }
526
+
527
+ connectedCallback() {
528
+ if (this.#hasDisconnected) {
529
+ this.#hasDisconnected = false;
530
+ this.#delegate?.connectedCallback();
531
+ }
532
+ }
533
+
534
+ disconnectedCallback() {
535
+ this.#hasDisconnected = true;
536
+ this.#delegate?.disconnectedCallback();
537
+ }
538
+
539
+ async #maybeActivate(hostElement, ctx) {
540
+ // Guard: only load if actually needed
541
+ const computed = getComputedStyle(hostElement);
542
+ if (!computed.getPropertyValue('--my-config').trim()) return;
543
+
544
+ const { MyFeature } = await import('./MyFeature.js');
545
+ this.#delegate = new MyFeature(hostElement, ctx);
546
+ }
547
+ }
548
+ ```
549
+
550
+ ### Class-Level Setup with `static onAssigned`
551
+
552
+ Features that need one-time class-level setup before any instances are created can define a `static onAssigned` method. This is called by `assignFeatures` immediately after registration, receiving the host constructor and the feature config:
553
+
554
+ ```javascript
555
+ class MyFeature {
556
+ /**
557
+ * Called once when assignFeatures processes this feature.
558
+ * Use for one-time class-level setup: installing prototype properties,
559
+ * setting static flags, or pre-loading modules.
560
+ */
561
+ static onAssigned(ctr, featureConfig) {
562
+ // Set static properties on the host constructor
563
+ ctr.formAssociated = true;
564
+ // Or install prototype getter/setters, pre-load modules, etc.
565
+ }
566
+
567
+ constructor(hostElement, ctx, initVals) {
568
+ // Instance-level setup (runs on first getter access)
569
+ }
570
+ }
571
+ ```
572
+
573
+ **Usage:**
574
+
575
+ ```javascript
576
+ // await is safe — returns undefined if no async onAssigned hooks exist
577
+ await customElements.assignFeatures(MyElement, {
578
+ myFeature: { spawn: MyFeature }
579
+ });
580
+
581
+ // Now define — class is fully set up
582
+ customElements.define('my-element', MyElement);
583
+ ```
584
+
585
+ **How it works:**
586
+
587
+ - `assignFeatures` checks if the spawn class defines `static onAssigned` (via `Object.hasOwn`).
588
+ - If found, calls `SpawnClass.onAssigned(ctr, featureConfig)` after installing the getter.
589
+ - If `onAssigned` returns a Promise, `assignFeatures` returns a `Promise<void>` that resolves when all async hooks complete.
590
+ - If no `onAssigned` hooks are async (or none exist), `assignFeatures` returns `undefined` (backward compatible).
591
+ - Only applies to synchronous spawners (the class must be available at registration time).
592
+
593
+ **When to use it:**
594
+
595
+ - Setting `static formAssociated = true` on the host (for form-associated custom elements)
596
+ - Installing prototype getter/setters that the feature depends on
597
+ - Pre-loading modules or resources needed at spawn time
598
+ - Any setup that must happen once per class, not once per instance
599
+
600
+ **`await` is always safe:**
601
+
602
+ ```javascript
603
+ // These are equivalent for features without onAssigned:
604
+ customElements.assignFeatures(MyElement, { feature: { spawn: SyncFeature } });
605
+ await customElements.assignFeatures(MyElement, { feature: { spawn: SyncFeature } });
606
+ // Both work — await on undefined is a no-op
607
+ ```
608
+
609
+ ### Pre-upgrade Property Capture
610
+
611
+ If properties are set on an element before it upgrades, use `captureFeatureInitVals`:
612
+
613
+ ```javascript
614
+ import { captureFeatureInitVals } from 'assign-gingerly/assignFeatures.js';
615
+
616
+ class MyElement extends HTMLElement {
617
+ static supportedFeatures = {
618
+ myFeature: { fallbackSpawn: MyFeature }
619
+ };
620
+
621
+ constructor() {
622
+ super();
623
+ captureFeatureInitVals(this);
624
+ }
625
+ }
626
+ ```
627
+
628
+ ### Property Forwarding with installForwarding
629
+
630
+ To expose nested feature properties at the top level of the custom element:
631
+
632
+ ```javascript
633
+ import { installForwarding } from 'assign-gingerly/installForwarding.js';
634
+
635
+ class MyElement extends HTMLElement {
636
+ static supportedFeatures = {
637
+ myFeature: { fallbackSpawn: MyFeature }
638
+ };
639
+
640
+ static propLinks = {
641
+ 'myProp': '?.myFeature?.myProp'
642
+ };
643
+ }
644
+
645
+ customElements.assignFeatures(MyElement, {
646
+ myFeature: { spawn: MyFeature }
647
+ });
648
+ installForwarding(MyElement);
649
+ customElements.define('my-element', MyElement);
650
+
651
+ // Now el.myProp delegates to el.myFeature.myProp
652
+ ```
653
+
654
+ ## Key Differences from Enhancements
655
+
656
+ | Aspect | Enhancement | Custom Element Feature |
657
+ |--------|-------------|----------------------|
658
+ | Target | Any existing HTML element | Custom element prototypes |
659
+ | Attachment | Via attributes + mount-observer | Via `assignFeatures` + lazy getters |
660
+ | Registration | `enhancementRegistry` | `featuresRegistry` |
661
+ | Discovery | DOM observation (be-hive) | Explicit injection before `define()` |
662
+ | Dependencies | mount-observer, be-hive, roundabout | assign-gingerly only |
663
+ | Build step | emc.mjs → emc.json | None required |
664
+ | Attribute prefix | `enh-` for isolation | Unprefixed (features own their element) |
665
+
666
+ ## Tips
667
+
668
+ - **Call `assignFeatures` before `customElements.define()`** — getters must be on the prototype before instances exist
669
+ - **Use `@ts-check`** — catches type errors early in `.js` files
670
+ - **Store host as WeakRef** — prevents memory leaks
671
+ - **Keep features focused** — one responsibility per feature class
672
+ - **Use `validateShape`** — catches injection errors early in development
673
+ - **Test with mocks** — swap real implementations for test doubles without subclassing