@context-action/lit-ui 0.0.0-stage → 0.1.0

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/dist/index.cjs ADDED
@@ -0,0 +1,1636 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ // \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ let lit = require("lit");
24
+ let lit_decorators_js = require("lit/decorators.js");
25
+ let _lit_context = require("@lit/context");
26
+ let react = require("react");
27
+ react = __toESM(react, 1);
28
+ let _context_action_lit = require("@context-action/lit");
29
+ // \0@oxc-project+runtime@0.155.0/helpers/esm/decorate.js
30
+ function __decorate(decorators, target, key, desc) {
31
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
32
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
33
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
34
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
35
+ }
36
+ // src/form-associated-element.ts
37
+ /**
38
+ * @fileoverview Abstract Form-Associated Custom Element (FACE) base class extending LitElement.
39
+ * Provides ElementInternals integration, standard constraint validation, form value management,
40
+ * and robust form reset lifecycle hooks across browser and headless (JSDOM/SSR) environments.
41
+ *
42
+ * @implements view-layer
43
+ * @implements web-component-primitive
44
+ * @memberof core-concepts
45
+ */
46
+ /**
47
+ * Synthesizes an immutable ValidityState object from a set of ValidityStateFlags.
48
+ * Used as a high-fidelity fallback when ElementInternals.validity is unavailable in the environment.
49
+ */
50
+ function createSyntheticValidityState(flags) {
51
+ const badInput = !!flags.badInput;
52
+ const customError = !!flags.customError;
53
+ const patternMismatch = !!flags.patternMismatch;
54
+ const rangeOverflow = !!flags.rangeOverflow;
55
+ const rangeUnderflow = !!flags.rangeUnderflow;
56
+ const stepMismatch = !!flags.stepMismatch;
57
+ const tooLong = !!flags.tooLong;
58
+ const tooShort = !!flags.tooShort;
59
+ const typeMismatch = !!flags.typeMismatch;
60
+ const valueMissing = !!flags.valueMissing;
61
+ return {
62
+ badInput,
63
+ customError,
64
+ patternMismatch,
65
+ rangeOverflow,
66
+ rangeUnderflow,
67
+ stepMismatch,
68
+ tooLong,
69
+ tooShort,
70
+ typeMismatch,
71
+ valid: !(badInput || customError || patternMismatch || rangeOverflow || rangeUnderflow || stepMismatch || tooLong || tooShort || typeMismatch || valueMissing),
72
+ valueMissing
73
+ };
74
+ }
75
+ /**
76
+ * Abstract Form-Associated Custom Element (FACE) base class extending LitElement.
77
+ *
78
+ * Provides:
79
+ * - Native form participation via `ElementInternals` and `static formAssociated = true`
80
+ * - Defensive fallbacks for server-side rendering (SSR) and headless test environments (JSDOM)
81
+ * - Complete Form value management (`setFormValue`, `getFormValue`, `formValue`)
82
+ * - Constraint validation APIs (`setValidity`, `validity`, `validationMessage`, `checkValidity`, `reportValidity`, `setCustomValidity`)
83
+ * - Lifecycle callbacks (`formResetCallback`, `formDisabledCallback`, `formStateRestoreCallback`, `formAssociatedCallback`)
84
+ * - Standard overridable hooks (`onFormReset`, `onFormDisabled`, `onFormStateRestore`, `onFormAssociated`)
85
+ * - Automatic form reset event synchronization and leak-free unbinding on disconnect
86
+ */
87
+ var FormAssociatedLitElement = class extends lit.LitElement {
88
+ static {
89
+ this.formAssociated = true;
90
+ }
91
+ #formValue;
92
+ #formState;
93
+ #validityFlags;
94
+ #validationMessage;
95
+ #customError;
96
+ #isResetting;
97
+ #associatedForm;
98
+ constructor() {
99
+ super();
100
+ this.name = "";
101
+ this.type = "custom-field";
102
+ this.disabled = false;
103
+ this.required = false;
104
+ this.#formValue = null;
105
+ this.#formState = null;
106
+ this.#validityFlags = {};
107
+ this.#validationMessage = "";
108
+ this.#customError = false;
109
+ this.#isResetting = false;
110
+ this.#associatedForm = null;
111
+ this.#handleFormReset = (event) => {
112
+ if (this.#isResetting || event.defaultPrevented) return;
113
+ queueMicrotask(() => {
114
+ if (this.#isResetting || event.defaultPrevented) return;
115
+ this.formResetCallback();
116
+ });
117
+ };
118
+ if (typeof this.attachInternals === "function") try {
119
+ this.internals = this.attachInternals();
120
+ } catch {}
121
+ }
122
+ /**
123
+ * The associated HTMLFormElement, if any.
124
+ * Resolves first via ElementInternals.form, then checks the 'form' attribute by ID,
125
+ * and finally falls back to the closest ancestor <form>.
126
+ */
127
+ get form() {
128
+ if (this.internals && "form" in this.internals && this.internals.form !== void 0) return this.internals.form;
129
+ const formAttr = this.getAttribute("form");
130
+ if (formAttr && this.ownerDocument) {
131
+ const el = this.ownerDocument.getElementById(formAttr);
132
+ if (el instanceof HTMLFormElement) return el;
133
+ }
134
+ return this.closest("form");
135
+ }
136
+ /**
137
+ * Sets the element's form submission value and optional state restoration value.
138
+ *
139
+ * @param value The value to submit with the form: string, File, FormData, or null (clears submission).
140
+ * @param state Optional state object used for session restoration / autocomplete.
141
+ */
142
+ setFormValue(value, state) {
143
+ this.#formValue = value;
144
+ this.#formState = state;
145
+ if (this.internals && typeof this.internals.setFormValue === "function") try {
146
+ this.internals.setFormValue(value, state);
147
+ } catch {}
148
+ }
149
+ /**
150
+ * Returns the current form submission value.
151
+ */
152
+ getFormValue() {
153
+ return this.#formValue;
154
+ }
155
+ /**
156
+ * Getter alias for getFormValue() for uniform property access.
157
+ */
158
+ get formValue() {
159
+ return this.#formValue;
160
+ }
161
+ /**
162
+ * Returns the current state restoration object.
163
+ */
164
+ get formState() {
165
+ return this.#formState;
166
+ }
167
+ /**
168
+ * Sets the validity flags, validation error message, and optional anchor element.
169
+ *
170
+ * @param flags A map of ValidityStateFlags indicating constraint violations.
171
+ * @param message A human-readable validation error message (required if any flag is true).
172
+ * @param anchor An optional HTMLElement inside the element's ShadowRoot used to anchor browser tooltips.
173
+ */
174
+ setValidity(flags = {}, message, anchor) {
175
+ this.#validityFlags = { ...flags };
176
+ this.#validationMessage = message ?? "";
177
+ this.#customError = !!flags.customError;
178
+ if (this.internals && typeof this.internals.setValidity === "function") try {
179
+ this.internals.setValidity(flags, message, anchor);
180
+ } catch {}
181
+ }
182
+ /**
183
+ * Returns the current ValidityState object representing constraint validation status.
184
+ */
185
+ get validity() {
186
+ if (this.internals && "validity" in this.internals && this.internals.validity) return this.internals.validity;
187
+ return createSyntheticValidityState({
188
+ ...this.#validityFlags,
189
+ customError: this.#validityFlags.customError || this.#customError
190
+ });
191
+ }
192
+ /**
193
+ * Returns the current validation error message, or an empty string if valid.
194
+ */
195
+ get validationMessage() {
196
+ if (this.internals && "validationMessage" in this.internals && typeof this.internals.validationMessage === "string") return this.internals.validationMessage;
197
+ return this.validity.valid ? "" : this.#validationMessage;
198
+ }
199
+ /**
200
+ * Returns true if the element participates in constraint validation and is not disabled.
201
+ */
202
+ get willValidate() {
203
+ if (this.internals && "willValidate" in this.internals && typeof this.internals.willValidate === "boolean") return this.internals.willValidate;
204
+ return !this.disabled;
205
+ }
206
+ /**
207
+ * Checks whether the element satisfies all validation constraints.
208
+ * If invalid, fires a cancelable 'invalid' event on the element and returns false.
209
+ * If valid (or validation is disabled), returns true.
210
+ */
211
+ checkValidity() {
212
+ if (this.internals && typeof this.internals.checkValidity === "function") try {
213
+ return this.internals.checkValidity();
214
+ } catch {}
215
+ if (!this.willValidate) return true;
216
+ if (!this.validity.valid) {
217
+ const event = new Event("invalid", {
218
+ bubbles: false,
219
+ cancelable: true
220
+ });
221
+ this.dispatchEvent(event);
222
+ return false;
223
+ }
224
+ return true;
225
+ }
226
+ /**
227
+ * Checks validity, reports errors to the user (e.g. browser popup), and returns validity status.
228
+ */
229
+ reportValidity() {
230
+ if (this.internals && typeof this.internals.reportValidity === "function") try {
231
+ return this.internals.reportValidity();
232
+ } catch {}
233
+ return this.checkValidity();
234
+ }
235
+ /**
236
+ * Sets a custom validation error message. If message is non-empty, sets customError to true.
237
+ * If message is empty, clears custom validity errors.
238
+ */
239
+ setCustomValidity(message) {
240
+ if (message) this.setValidity({ customError: true }, message);
241
+ else this.setValidity({});
242
+ }
243
+ /**
244
+ * Native FACE callback invoked by the browser engine when the associated form is reset.
245
+ * Dispatches to the overridable onFormReset() hook.
246
+ */
247
+ formResetCallback() {
248
+ if (this.#isResetting) return;
249
+ this.#isResetting = true;
250
+ try {
251
+ this.onFormReset();
252
+ } finally {
253
+ queueMicrotask(() => {
254
+ this.#isResetting = false;
255
+ });
256
+ }
257
+ }
258
+ /**
259
+ * Overridable lifecycle hook called when the associated form is reset.
260
+ * Subclasses should restore default values, re-evaluate validation, and request updates.
261
+ */
262
+ onFormReset() {}
263
+ /**
264
+ * Native FACE callback invoked when the element or parent fieldset is disabled/enabled.
265
+ */
266
+ formDisabledCallback(disabled) {
267
+ this.disabled = disabled;
268
+ this.onFormDisabled(disabled);
269
+ this.requestUpdate();
270
+ }
271
+ /**
272
+ * Overridable lifecycle hook called when the disabled state changes.
273
+ */
274
+ onFormDisabled(_disabled) {}
275
+ /**
276
+ * Native FACE callback invoked when the browser restores form state (history navigation / autofill).
277
+ */
278
+ formStateRestoreCallback(state, mode) {
279
+ this.#formState = state;
280
+ this.onFormStateRestore(state, mode);
281
+ }
282
+ /**
283
+ * Overridable lifecycle hook called when the browser restores form state.
284
+ */
285
+ onFormStateRestore(_state, _mode) {}
286
+ /**
287
+ * Native FACE callback invoked when the element is associated or disassociated with a form.
288
+ */
289
+ formAssociatedCallback(form) {
290
+ this.#syncFormListener(form);
291
+ this.onFormAssociated(form);
292
+ }
293
+ /**
294
+ * Overridable lifecycle hook called when the element's form association changes.
295
+ */
296
+ onFormAssociated(_form) {}
297
+ #handleFormReset;
298
+ #syncFormListener(newForm) {
299
+ if (this.#associatedForm === newForm) return;
300
+ if (this.#associatedForm) this.#associatedForm.removeEventListener("reset", this.#handleFormReset);
301
+ this.#associatedForm = newForm;
302
+ if (this.#associatedForm) this.#associatedForm.addEventListener("reset", this.#handleFormReset);
303
+ }
304
+ connectedCallback() {
305
+ super.connectedCallback();
306
+ this.#syncFormListener(this.form);
307
+ }
308
+ disconnectedCallback() {
309
+ this.#syncFormListener(null);
310
+ super.disconnectedCallback();
311
+ }
312
+ };
313
+ __decorate([(0, lit_decorators_js.property)({
314
+ type: String,
315
+ reflect: true
316
+ })], FormAssociatedLitElement.prototype, "name", void 0);
317
+ __decorate([(0, lit_decorators_js.property)({
318
+ type: String,
319
+ reflect: true
320
+ })], FormAssociatedLitElement.prototype, "type", void 0);
321
+ __decorate([(0, lit_decorators_js.property)({
322
+ type: Boolean,
323
+ reflect: true
324
+ })], FormAssociatedLitElement.prototype, "disabled", void 0);
325
+ __decorate([(0, lit_decorators_js.property)({
326
+ type: Boolean,
327
+ reflect: true
328
+ })], FormAssociatedLitElement.prototype, "required", void 0);
329
+ // src/context.ts
330
+ /**
331
+ * @fileoverview W3C Context Protocol (@lit/context) tokens and dependency injection utilities
332
+ * for Context-Action stores, action registers, and dispatchers across Shadow DOM boundaries.
333
+ *
334
+ * @implements context-protocol
335
+ * @implements dependency-injection
336
+ * @implements store-integration-pattern
337
+ * @implements action-pipeline-system
338
+ * @memberof core-concepts
339
+ */
340
+ /**
341
+ * Standard W3C Context token for an ActionRegister.
342
+ * Uses Symbol.for to ensure singleton identity across independent bundle chunks and micro-frontends.
343
+ *
344
+ * @implements action-pipeline-system
345
+ * @memberof api-terms
346
+ */
347
+ const actionRegisterContext = (0, _lit_context.createContext)(Symbol.for("context-action.register"));
348
+ /**
349
+ * Standard W3C Context token for an ActionDispatcher function.
350
+ * Uses Symbol.for to ensure singleton identity across independent bundle chunks.
351
+ *
352
+ * @implements action-pipeline-system
353
+ * @memberof api-terms
354
+ */
355
+ const actionDispatcherContext = (0, _lit_context.createContext)(Symbol.for("context-action.dispatcher"));
356
+ /**
357
+ * Factory function creating a strongly-typed W3C Context token for a named Context-Action store.
358
+ *
359
+ * Distinct store names yield isolated contexts. Identical store names across different
360
+ * modules or micro-frontend bundles share the same underlying global Symbol.
361
+ *
362
+ * @template T State type managed by the store.
363
+ * @param name Unique name of the store domain (e.g. 'cart', 'auth', 'theme').
364
+ * @returns A Context token for the named ReadableStore.
365
+ *
366
+ * @example
367
+ * ```typescript
368
+ * export const cartStoreContext = createStoreContext<CartState>('cart');
369
+ * ```
370
+ *
371
+ * @implements store-integration-pattern
372
+ * @memberof api-terms
373
+ */
374
+ function createStoreContext(name) {
375
+ if (!name || typeof name !== "string") throw new TypeError("createStoreContext: \"name\" must be a non-empty string.");
376
+ return (0, _lit_context.createContext)(Symbol.for(`context-action.store.${name}`));
377
+ }
378
+ /**
379
+ * Factory function creating a strongly-typed ActionRegister context token for a specific action map.
380
+ *
381
+ * @template A Action payload map.
382
+ * @param name Optional domain qualifier. Defaults to the root action register.
383
+ * @returns A typed Context token for ActionRegister<A>.
384
+ */
385
+ function createActionRegisterContext(name = "default") {
386
+ return (0, _lit_context.createContext)(Symbol.for(`context-action.register.${name}`));
387
+ }
388
+ /**
389
+ * Factory function creating a strongly-typed ActionDispatcher context token for a specific action map.
390
+ *
391
+ * @template A Action payload map.
392
+ * @param name Optional domain qualifier. Defaults to the root action dispatcher.
393
+ * @returns A typed Context token for ActionDispatcher<A>.
394
+ */
395
+ function createActionDispatcherContext(name = "default") {
396
+ return (0, _lit_context.createContext)(Symbol.for(`context-action.dispatcher.${name}`));
397
+ }
398
+ /**
399
+ * Imperatively attaches an ActionRegister ContextProvider to a host element.
400
+ *
401
+ * @template A Action payload map.
402
+ * @param host The hosting LitElement or ReactiveElement.
403
+ * @param register The ActionRegister instance to provide.
404
+ * @param context Optional custom context token (defaults to actionRegisterContext).
405
+ * @returns The initialized ContextProvider instance.
406
+ */
407
+ function provideActionRegister(host, register, context = actionRegisterContext) {
408
+ return new _lit_context.ContextProvider(host, {
409
+ context,
410
+ initialValue: register
411
+ });
412
+ }
413
+ /**
414
+ * Imperatively attaches an ActionDispatcher ContextProvider to a host element.
415
+ *
416
+ * @template A Action payload map.
417
+ * @param host The hosting LitElement or ReactiveElement.
418
+ * @param dispatcher The ActionDispatcher function to provide.
419
+ * @param context Optional custom context token (defaults to actionDispatcherContext).
420
+ * @returns The initialized ContextProvider instance.
421
+ */
422
+ function provideActionDispatcher(host, dispatcher, context = actionDispatcherContext) {
423
+ return new _lit_context.ContextProvider(host, {
424
+ context,
425
+ initialValue: dispatcher
426
+ });
427
+ }
428
+ /**
429
+ * Imperatively attaches a ReadableStore ContextProvider to a host element.
430
+ *
431
+ * @template T Store state type.
432
+ * @param host The hosting LitElement or ReactiveElement.
433
+ * @param context The store Context token created via `createStoreContext`.
434
+ * @param store The ReadableStore instance to provide.
435
+ * @returns The initialized ContextProvider instance.
436
+ */
437
+ function provideStore(host, context, store) {
438
+ return new _lit_context.ContextProvider(host, {
439
+ context,
440
+ initialValue: store
441
+ });
442
+ }
443
+ /**
444
+ * ReactiveController that consumes a Context-Action ReadableStore from the W3C Context
445
+ * hierarchy across Shadow DOM boundaries, subscribes to state updates, and triggers host re-renders.
446
+ *
447
+ * Key guarantees:
448
+ * 1. Attaches to host via `host.addController(this)`.
449
+ * 2. Uses `@lit/context` ContextConsumer to dynamically resolve store instances across Shadow DOM boundaries.
450
+ * 3. Supports selective projections with memoization (`equalityFn`).
451
+ * 4. Ensures ZERO memory leaks: completely terminates store subscription when `hostDisconnected()` runs.
452
+ * 5. Re-evaluates store state and resubscribes on `hostConnected()`.
453
+ *
454
+ * @template T Source store state type.
455
+ * @template Selected Derived slice type.
456
+ *
457
+ * @implements store-integration-pattern
458
+ * @implements reactive-state-management
459
+ * @memberof core-concepts
460
+ */
461
+ var ContextStoreController = class {
462
+ #host;
463
+ #selector;
464
+ #equalityFn;
465
+ #currentStore = void 0;
466
+ #unsubscribeStore = null;
467
+ #selectedValue = void 0;
468
+ #isResolved = false;
469
+ constructor(host, options) {
470
+ if (!host || typeof host.addController !== "function") throw new TypeError("ContextStoreController requires a valid host implementing ReactiveControllerHost and HTMLElement.");
471
+ if (!options?.context) throw new TypeError("ContextStoreController requires a valid Context token.");
472
+ this.#host = host;
473
+ this.#selector = options.selector ?? ((s) => s);
474
+ this.#equalityFn = options.equalityFn ?? Object.is;
475
+ new _lit_context.ContextConsumer(host, {
476
+ context: options.context,
477
+ callback: (store) => this.#handleStoreResolved(store),
478
+ subscribe: true
479
+ });
480
+ host.addController(this);
481
+ }
482
+ /** Current projected state derived from the injected store, or undefined if not yet provided. */
483
+ get value() {
484
+ return this.#selectedValue;
485
+ }
486
+ /** The resolved ReadableStore instance currently bound to this controller. */
487
+ get store() {
488
+ return this.#currentStore;
489
+ }
490
+ /** Indicates whether the context request has been satisfied by an ancestor provider. */
491
+ get isResolved() {
492
+ return this.#isResolved;
493
+ }
494
+ #extractRawValue(store) {
495
+ if (typeof store.getValue === "function") return store.getValue();
496
+ if (typeof store.getSnapshot === "function") return store.getSnapshot();
497
+ throw new TypeError("ContextStoreController: Provided store must implement getValue() or getSnapshot().");
498
+ }
499
+ #handleStoreResolved(nextStore) {
500
+ if (this.#currentStore === nextStore && this.#isResolved) return;
501
+ this.#cleanupStoreSubscription();
502
+ this.#currentStore = nextStore;
503
+ if (!nextStore) {
504
+ this.#isResolved = false;
505
+ this.#selectedValue = void 0;
506
+ this.#host.requestUpdate();
507
+ return;
508
+ }
509
+ this.#isResolved = true;
510
+ this.#syncStateAndSubscribe();
511
+ this.#host.requestUpdate();
512
+ }
513
+ #syncStateAndSubscribe() {
514
+ if (!this.#currentStore) return;
515
+ const raw = this.#extractRawValue(this.#currentStore);
516
+ this.#selectedValue = this.#selector(raw);
517
+ this.#unsubscribeStore = this.#currentStore.subscribe(() => {
518
+ if (!this.#currentStore) return;
519
+ const nextRaw = this.#extractRawValue(this.#currentStore);
520
+ const nextSelected = this.#selector(nextRaw);
521
+ if (!this.#equalityFn(this.#selectedValue, nextSelected)) {
522
+ this.#selectedValue = nextSelected;
523
+ this.#host.requestUpdate();
524
+ }
525
+ });
526
+ }
527
+ #cleanupStoreSubscription() {
528
+ if (this.#unsubscribeStore) {
529
+ this.#unsubscribeStore();
530
+ this.#unsubscribeStore = null;
531
+ }
532
+ }
533
+ hostConnected() {
534
+ if (this.#currentStore && !this.#unsubscribeStore) {
535
+ this.#syncStateAndSubscribe();
536
+ this.#host.requestUpdate();
537
+ }
538
+ }
539
+ hostDisconnected() {
540
+ this.#cleanupStoreSubscription();
541
+ }
542
+ };
543
+ /**
544
+ * ReactiveController that consumes an ActionRegister from the W3C Context hierarchy
545
+ * across Shadow DOM boundaries and exposes typed dispatch methods.
546
+ *
547
+ * @template A Action payload map.
548
+ *
549
+ * @implements action-pipeline-system
550
+ * @implements action-controller
551
+ * @memberof core-concepts
552
+ */
553
+ var ContextActionController = class {
554
+ #host;
555
+ #register = void 0;
556
+ #isPending = false;
557
+ #lastError = void 0;
558
+ constructor(host, options) {
559
+ if (!host || typeof host.addController !== "function") throw new TypeError("ContextActionController requires a valid host implementing ReactiveControllerHost and HTMLElement.");
560
+ this.#host = host;
561
+ const ctx = options?.context ?? actionRegisterContext;
562
+ new _lit_context.ContextConsumer(host, {
563
+ context: ctx,
564
+ callback: (reg) => {
565
+ this.#register = reg;
566
+ this.#host.requestUpdate();
567
+ },
568
+ subscribe: true
569
+ });
570
+ host.addController(this);
571
+ }
572
+ /** The resolved ActionRegister instance, or undefined if no provider is present. */
573
+ get register() {
574
+ return this.#register;
575
+ }
576
+ /** Whether an action dispatch is currently in progress. */
577
+ get isPending() {
578
+ return this.#isPending;
579
+ }
580
+ /** The last error thrown during an action dispatch, if any. */
581
+ get lastError() {
582
+ return this.#lastError;
583
+ }
584
+ /**
585
+ * Dispatches an action through the context-injected ActionRegister pipeline.
586
+ *
587
+ * @template K Action name.
588
+ * @param action Name of the registered action.
589
+ * @param args Action payload arguments.
590
+ */
591
+ async dispatch(action, ...args) {
592
+ if (!this.#register) throw new Error(`ContextActionController: Cannot dispatch "${String(action)}". No ActionRegister found in context hierarchy.`);
593
+ this.#isPending = true;
594
+ this.#lastError = void 0;
595
+ this.#host.requestUpdate();
596
+ try {
597
+ await this.#register.dispatch(action, ...args);
598
+ } catch (err) {
599
+ const errorObj = err instanceof Error ? err : new Error(String(err));
600
+ this.#lastError = errorObj;
601
+ throw errorObj;
602
+ } finally {
603
+ this.#isPending = false;
604
+ this.#host.requestUpdate();
605
+ }
606
+ }
607
+ /**
608
+ * Dispatches an action and returns the pipeline execution result.
609
+ *
610
+ * @template K Action name.
611
+ * @template R Expected return value.
612
+ * @param action Name of the registered action.
613
+ * @param args Action payload arguments.
614
+ * @returns Pipeline result promise.
615
+ */
616
+ async dispatchWithResult(action, ...args) {
617
+ if (!this.#register) throw new Error(`ContextActionController: Cannot dispatch "${String(action)}". No ActionRegister found in context hierarchy.`);
618
+ this.#isPending = true;
619
+ this.#lastError = void 0;
620
+ this.#host.requestUpdate();
621
+ try {
622
+ if (typeof this.#register.dispatchWithResult === "function") return await this.#register.dispatchWithResult(action, ...args);
623
+ return await this.#register.dispatch(action, ...args);
624
+ } catch (err) {
625
+ const errorObj = err instanceof Error ? err : new Error(String(err));
626
+ this.#lastError = errorObj;
627
+ throw errorObj;
628
+ } finally {
629
+ this.#isPending = false;
630
+ this.#host.requestUpdate();
631
+ }
632
+ }
633
+ hostConnected() {}
634
+ hostDisconnected() {}
635
+ };
636
+ // src/facade.ts
637
+ function findPropertyDescriptor(target, key) {
638
+ let current = target;
639
+ while (current) {
640
+ const descriptor = Object.getOwnPropertyDescriptor(current, key);
641
+ if (descriptor) return descriptor;
642
+ current = Object.getPrototypeOf(current);
643
+ }
644
+ }
645
+ /**
646
+ * Creates a reusable host-agnostic handle factory that exposes only the specified
647
+ * properties and methods from a Custom Element, hiding raw `HTMLElement` APIs.
648
+ * Can be used directly with Vanilla DOM, Lit, or
649
+ * `createLitElementBridge({ createHandle })`.
650
+ *
651
+ * Methods are bound to the current element and cached per element, so repeated
652
+ * reads return the same function identity until the element or the underlying
653
+ * method changes. The cache is keyed weakly and never retains a detached element.
654
+ *
655
+ * @example
656
+ * ```ts
657
+ * const TimelineElement = createLitElementBridge<TimelineProps, TimelineElement, TimelineHandle>({
658
+ * tagName: 'st-timeline',
659
+ * properties: ['input', 'set'],
660
+ * createHandle: createElementFacade(['focusItem', 'scrollTo', 'commitSelection']),
661
+ * });
662
+ * ```
663
+ */
664
+ function createElementFacade(keys) {
665
+ const uniqueKeys = Array.from(new Set(keys));
666
+ return (getElement) => {
667
+ const requireElement = () => {
668
+ const element = getElement();
669
+ if (!element) throw new Error("Custom element handle is not mounted");
670
+ return element;
671
+ };
672
+ const boundMethods = /* @__PURE__ */ new WeakMap();
673
+ const facade = {};
674
+ for (const key of uniqueKeys) Object.defineProperty(facade, key, {
675
+ configurable: false,
676
+ enumerable: true,
677
+ get() {
678
+ const element = requireElement();
679
+ const value = Reflect.get(element, key, element);
680
+ if (typeof value !== "function") return value;
681
+ let cache = boundMethods.get(element);
682
+ if (!cache) {
683
+ cache = /* @__PURE__ */ new Map();
684
+ boundMethods.set(element, cache);
685
+ }
686
+ const cached = cache.get(key);
687
+ if (cached && cached.source === value) return cached.bound;
688
+ const bound = value.bind(element);
689
+ cache.set(key, {
690
+ source: value,
691
+ bound
692
+ });
693
+ return bound;
694
+ },
695
+ set(nextValue) {
696
+ const element = requireElement();
697
+ const descriptor = findPropertyDescriptor(element, key);
698
+ if (descriptor && !descriptor.set && !descriptor.writable) throw new TypeError(`Cannot assign to read-only property '${String(key)}'`);
699
+ if (!Reflect.set(element, key, nextValue, element)) throw new TypeError(`Cannot assign to read-only property '${String(key)}'`);
700
+ }
701
+ });
702
+ return Object.freeze(facade);
703
+ };
704
+ }
705
+ // src/react-bridge.ts
706
+ /**
707
+ * @fileoverview React 18/19 bridge helper for consuming Lit Custom Elements.
708
+ * Provides direct property assignment to bypass attribute stringification,
709
+ * stable custom event listener mapping with deterministic cleanup, and
710
+ * OOP Facade handle forwarding via `createHandle`.
711
+ *
712
+ * @implements view-layer
713
+ * @implements web-component-primitive
714
+ * @memberof core-concepts
715
+ */
716
+ const useIsomorphicLayoutEffect = typeof window === "undefined" ? react.useEffect : react.useLayoutEffect;
717
+ function isEventHandler(value) {
718
+ return typeof value === "function" || typeof value === "object" && value !== null && typeof value.handleEvent === "function";
719
+ }
720
+ /**
721
+ * Creates a type-safe React bridge component for consuming any Lit Custom Element
722
+ * with seamless property synchronization, custom event listeners, and ref forwarding.
723
+ *
724
+ * Solves standard React ↔ Web Component pain points:
725
+ * 1. Direct Property Assignment: Directly assigns objects, arrays, and stores to the DOM
726
+ * instance without stringifying to `"[object Object]"` in React 18/19.
727
+ * 2. Property Removal: Clears keys removed from `properties` on later renders.
728
+ * 3. Stable Custom Events: Attaches one listener per event and reads the latest
729
+ * handler, so ordinary re-renders never re-register listeners.
730
+ * 4. Ref Forwarding: Forwards an OOP Facade handle (`createHandle`) or the raw
731
+ * Custom Element instance when no factory is provided.
732
+ */
733
+ function createLitElementBridge(tagOrOptions, maybeConfig) {
734
+ const { tagName, properties = [], events = {}, createHandle } = typeof tagOrOptions === "string" ? {
735
+ tagName: tagOrOptions,
736
+ ...maybeConfig
737
+ } : tagOrOptions;
738
+ const propertyKeys = Array.from(properties);
739
+ const propertyKeySet = new Set(propertyKeys);
740
+ const eventEntries = Object.entries(events);
741
+ const Bridge = react.forwardRef(function LitCustomElementBridge(props, forwardedRef) {
742
+ const elementRef = react.useRef(null);
743
+ const [element, setElement] = react.useState(null);
744
+ const appliedPropertiesRef = react.useRef(/* @__PURE__ */ new Set());
745
+ const appliedElementRef = react.useRef(null);
746
+ const eventBindingsRef = react.useRef([]);
747
+ const handleRef = react.useCallback((node) => {
748
+ elementRef.current = node;
749
+ setElement(node);
750
+ }, []);
751
+ react.useImperativeHandle(forwardedRef, () => {
752
+ if (createHandle) return createHandle(() => elementRef.current);
753
+ return elementRef.current;
754
+ }, [createHandle]);
755
+ useIsomorphicLayoutEffect(() => {
756
+ if (!element) return;
757
+ if (appliedElementRef.current !== element) {
758
+ appliedElementRef.current = element;
759
+ appliedPropertiesRef.current = /* @__PURE__ */ new Set();
760
+ }
761
+ const host = element;
762
+ const propsRecord = props;
763
+ const appliedProperties = appliedPropertiesRef.current;
764
+ for (const propKey of propertyKeys) if (propKey in propsRecord) {
765
+ appliedProperties.add(propKey);
766
+ try {
767
+ host[propKey] = propsRecord[propKey];
768
+ } catch {}
769
+ } else if (appliedProperties.has(propKey)) try {
770
+ host[propKey] = void 0;
771
+ } catch {} finally {
772
+ appliedProperties.delete(propKey);
773
+ }
774
+ }, [
775
+ element,
776
+ props,
777
+ propertyKeys
778
+ ]);
779
+ react.useEffect(() => {
780
+ if (!element) return;
781
+ const previousBindings = eventBindingsRef.current;
782
+ const nextBindings = [];
783
+ const propsRecord = props;
784
+ for (let index = 0; index < eventEntries.length; index++) {
785
+ const [propHandlerName, customEventName] = eventEntries[index];
786
+ const handler = propsRecord[propHandlerName];
787
+ const previous = previousBindings[index];
788
+ if (previous && previous.element === element && previous.eventName === customEventName && previous.handler === handler) {
789
+ nextBindings.push(previous);
790
+ continue;
791
+ }
792
+ if (previous?.listener) previous.element.removeEventListener(previous.eventName, previous.listener);
793
+ if (isEventHandler(handler)) {
794
+ const listener = (event) => {
795
+ if (typeof handler === "function") handler(event);
796
+ else handler.handleEvent(event);
797
+ };
798
+ element.addEventListener(customEventName, listener);
799
+ nextBindings.push({
800
+ element,
801
+ eventName: customEventName,
802
+ handler,
803
+ listener
804
+ });
805
+ } else nextBindings.push({
806
+ element,
807
+ eventName: customEventName,
808
+ handler
809
+ });
810
+ }
811
+ for (let index = eventEntries.length; index < previousBindings.length; index++) {
812
+ const stale = previousBindings[index];
813
+ if (stale.listener) stale.element.removeEventListener(stale.eventName, stale.listener);
814
+ }
815
+ eventBindingsRef.current = nextBindings;
816
+ });
817
+ react.useEffect(() => {
818
+ return () => {
819
+ for (const binding of eventBindingsRef.current) if (binding.listener) binding.element.removeEventListener(binding.eventName, binding.listener);
820
+ eventBindingsRef.current = [];
821
+ };
822
+ }, []);
823
+ const domProps = { ref: handleRef };
824
+ for (const [key, value] of Object.entries(props)) if (key === "children") domProps[key] = value;
825
+ else if (key === "className") domProps.class = value;
826
+ else if (key === "htmlFor") domProps.for = value;
827
+ else if (!propertyKeySet.has(key) && !(key in events)) domProps[key] = value;
828
+ return react.createElement(tagName, domProps);
829
+ });
830
+ Bridge.displayName = `LitBridge(${tagName})`;
831
+ return Bridge;
832
+ }
833
+ // src/define-once.ts
834
+ /**
835
+ * @fileoverview Idempotent custom element registration for the reference
836
+ * components. ESM and CJS builds can both be loaded in one process, so an
837
+ * unconditional `customElements.define()` would throw on the second load.
838
+ *
839
+ * @implements web-component-primitive
840
+ * @memberof core-concepts
841
+ */
842
+ function defineCustomElement(tagName, elementConstructor) {
843
+ if (typeof customElements === "undefined") return;
844
+ if (customElements.get(tagName)) return;
845
+ customElements.define(tagName, elementConstructor);
846
+ }
847
+ // src/components/quantity-stepper.ts
848
+ /**
849
+ * @fileoverview Reference implementation of <lit-quantity-stepper> custom element.
850
+ * Extends FormAssociatedLitElement for full native <form> and FormData participation,
851
+ * constraint validation, form reset lifecycle, and Shadow DOM encapsulation.
852
+ *
853
+ * @implements view-layer
854
+ * @implements web-component-primitive
855
+ * @memberof core-concepts
856
+ */
857
+ /**
858
+ * Reference Form-Associated Custom Element providing a numerical stepper control.
859
+ *
860
+ * Features:
861
+ * - Native <form> submission and `new FormData()` integration via `ElementInternals`.
862
+ * - HTML5 constraint validation (`rangeUnderflow`, `rangeOverflow`, `stepMismatch`, `badInput`).
863
+ * - Form reset lifecycle restoration via `onFormReset()`.
864
+ * - Disabled and readonly state propagation.
865
+ * - Increment / Decrement methods with step precision and boundary clamping.
866
+ * - Standard CustomEvent dispatching (`change`, `input`, `quantity-change`) with `{ bubbles: true, composed: true }`.
867
+ * - Fully encapsulated Shadow DOM styling with `::part(button)` and `::part(input)` styling hooks.
868
+ */
869
+ var LitQuantityStepper = class extends FormAssociatedLitElement {
870
+ constructor(..._args) {
871
+ super(..._args);
872
+ this.value = 1;
873
+ this.min = 1;
874
+ this.max = 99;
875
+ this.step = 1;
876
+ this.readonly = false;
877
+ this.focused = false;
878
+ this.#initialValue = 1;
879
+ this.#handleInput = (e) => {
880
+ e.stopPropagation();
881
+ const target = e.target;
882
+ const parsed = parseFloat(target.value);
883
+ this.value = Number.isNaN(parsed) ? target.value : parsed;
884
+ this.#syncFormState();
885
+ this.#emitEvents("input");
886
+ };
887
+ this.#handleChange = (e) => {
888
+ e.stopPropagation();
889
+ const target = e.target;
890
+ const parsed = parseFloat(target.value);
891
+ this.value = Number.isNaN(parsed) ? target.value : parsed;
892
+ this.#syncFormState();
893
+ this.#emitEvents("change");
894
+ };
895
+ }
896
+ static {
897
+ this.styles = lit.css`
898
+ :host {
899
+ display: inline-flex;
900
+ vertical-align: middle;
901
+ font-family: var(
902
+ --stepper-font-family,
903
+ system-ui,
904
+ -apple-system,
905
+ BlinkMacSystemFont,
906
+ 'Segoe UI',
907
+ Roboto,
908
+ sans-serif
909
+ );
910
+ --_border-color: var(--stepper-border-color, #cbd5e1);
911
+ --_focus-border-color: var(--stepper-focus-border-color, #3b82f6);
912
+ --_invalid-border-color: var(--stepper-invalid-border-color, #ef4444);
913
+ --_bg: var(--stepper-bg, #ffffff);
914
+ --_btn-bg: var(--stepper-button-bg, #f8fafc);
915
+ --_btn-hover-bg: var(--stepper-button-hover-bg, #e2e8f0);
916
+ --_btn-active-bg: var(--stepper-button-active-bg, #cbd5e1);
917
+ --_btn-color: var(--stepper-button-color, #334155);
918
+ --_radius: var(--stepper-radius, 8px);
919
+ --_font-size: var(--stepper-font-size, 14px);
920
+ --_height: var(--stepper-height, 36px);
921
+ }
922
+
923
+ :host([disabled]) {
924
+ opacity: 0.5;
925
+ pointer-events: none;
926
+ cursor: not-allowed;
927
+ }
928
+
929
+ :host([hidden]) {
930
+ display: none !important;
931
+ }
932
+
933
+ .stepper {
934
+ display: inline-flex;
935
+ align-items: stretch;
936
+ height: var(--_height);
937
+ background: var(--_bg);
938
+ border: 1px solid var(--_border-color);
939
+ border-radius: var(--_radius);
940
+ overflow: hidden;
941
+ transition: border-color 0.15s ease, box-shadow 0.15s ease;
942
+ box-sizing: border-box;
943
+ }
944
+
945
+ .stepper.focused {
946
+ border-color: var(--_focus-border-color);
947
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
948
+ }
949
+
950
+ :host(:invalid) .stepper,
951
+ .stepper.invalid {
952
+ border-color: var(--_invalid-border-color);
953
+ }
954
+
955
+ :host(:invalid) .stepper.focused,
956
+ .stepper.invalid.focused {
957
+ box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2);
958
+ }
959
+
960
+ button {
961
+ display: inline-flex;
962
+ align-items: center;
963
+ justify-content: center;
964
+ width: var(--_height);
965
+ padding: 0;
966
+ margin: 0;
967
+ border: none;
968
+ background: var(--_btn-bg);
969
+ color: var(--_btn-color);
970
+ font-size: calc(var(--_font-size) + 2px);
971
+ font-weight: 600;
972
+ cursor: pointer;
973
+ user-select: none;
974
+ transition: background-color 0.15s ease, color 0.15s ease;
975
+ box-sizing: border-box;
976
+ }
977
+
978
+ button:hover:not(:disabled) {
979
+ background: var(--_btn-hover-bg);
980
+ }
981
+
982
+ button:active:not(:disabled) {
983
+ background: var(--_btn-active-bg);
984
+ }
985
+
986
+ button:disabled {
987
+ cursor: not-allowed;
988
+ opacity: 0.4;
989
+ }
990
+
991
+ .btn-decrement {
992
+ border-right: 1px solid var(--_border-color);
993
+ }
994
+
995
+ .btn-increment {
996
+ border-left: 1px solid var(--_border-color);
997
+ }
998
+
999
+ input {
1000
+ width: 48px;
1001
+ padding: 0 6px;
1002
+ margin: 0;
1003
+ border: none;
1004
+ background: transparent;
1005
+ text-align: center;
1006
+ font-size: var(--_font-size);
1007
+ font-weight: 600;
1008
+ color: var(--stepper-text-color, #0f172a);
1009
+ font-family: inherit;
1010
+ box-sizing: border-box;
1011
+ -moz-appearance: textfield;
1012
+ }
1013
+
1014
+ input::-webkit-outer-spin-button,
1015
+ input::-webkit-inner-spin-button {
1016
+ -webkit-appearance: none;
1017
+ margin: 0;
1018
+ }
1019
+
1020
+ input:focus {
1021
+ outline: none;
1022
+ }
1023
+ `;
1024
+ }
1025
+ #initialValue;
1026
+ connectedCallback() {
1027
+ super.connectedCallback();
1028
+ this.#initialValue = this.defaultValue ?? this.value;
1029
+ this.#syncFormState();
1030
+ }
1031
+ updated(changedProperties) {
1032
+ super.updated(changedProperties);
1033
+ if (changedProperties.has("defaultValue") && this.defaultValue !== void 0) this.#initialValue = this.defaultValue;
1034
+ if (changedProperties.has("value") || changedProperties.has("min") || changedProperties.has("max") || changedProperties.has("step") || changedProperties.has("disabled")) this.#syncFormState();
1035
+ }
1036
+ /**
1037
+ * Evaluates step alignment using floating-point safe division.
1038
+ */
1039
+ #checkStepMismatch(val, min, step) {
1040
+ if (step <= 0 || Number.isNaN(val)) return false;
1041
+ const quotient = (val - min) / step;
1042
+ return Math.abs(quotient - Math.round(quotient)) > 1e-7;
1043
+ }
1044
+ /**
1045
+ * Formats a value for form submission and HTML input display,
1046
+ * preventing scientific notation drift.
1047
+ */
1048
+ #formatValue(val) {
1049
+ if (typeof val === "number") {
1050
+ if (Number.isNaN(val)) return "NaN";
1051
+ if (Math.abs(val) < 1e-9) return "0";
1052
+ const str = String(val);
1053
+ if (str.includes("e-")) return val.toFixed(10).replace(/\.?0+$/, "");
1054
+ return str;
1055
+ }
1056
+ return String(val ?? "");
1057
+ }
1058
+ /**
1059
+ * Derives decimal precision from numbers to avoid IEEE 754 precision drift.
1060
+ */
1061
+ #getPrecision(...numbers) {
1062
+ let maxDecimals = 0;
1063
+ for (const n of numbers) {
1064
+ if (!Number.isFinite(n)) continue;
1065
+ const str = n.toString();
1066
+ const dot = str.indexOf(".");
1067
+ if (dot !== -1) maxDecimals = Math.max(maxDecimals, str.length - dot - 1);
1068
+ }
1069
+ return Math.min(12, maxDecimals);
1070
+ }
1071
+ /**
1072
+ * Synchronizes internal value with ElementInternals form value and constraint validation.
1073
+ */
1074
+ #syncFormState() {
1075
+ this.setFormValue(this.#formatValue(this.value));
1076
+ const flags = {};
1077
+ let message = "";
1078
+ const rawVal = this.value;
1079
+ const isBlank = typeof rawVal === "string" && rawVal.trim() === "";
1080
+ const num = isBlank ? NaN : Number(this.value);
1081
+ if (isBlank || Number.isNaN(num)) {
1082
+ flags.badInput = true;
1083
+ message = "Please enter a valid number.";
1084
+ } else if (num < this.min) {
1085
+ flags.rangeUnderflow = true;
1086
+ message = `Value must be greater than or equal to ${this.min}.`;
1087
+ } else if (num > this.max) {
1088
+ flags.rangeOverflow = true;
1089
+ message = `Value must be less than or equal to ${this.max}.`;
1090
+ } else if (this.#checkStepMismatch(num, this.min, this.step)) {
1091
+ flags.stepMismatch = true;
1092
+ message = `Value must be an increment of ${this.step} from ${this.min}.`;
1093
+ }
1094
+ const anchor = this.inputEl ?? this.renderRoot?.querySelector("input");
1095
+ this.setValidity(flags, message, anchor);
1096
+ }
1097
+ /**
1098
+ * Emits input, change, and quantity-change CustomEvents across Shadow DOM boundaries.
1099
+ */
1100
+ #emitEvents(triggerType) {
1101
+ const detail = { value: this.value };
1102
+ if (triggerType === "input" || triggerType === "both") this.dispatchEvent(new CustomEvent("input", {
1103
+ bubbles: true,
1104
+ composed: true,
1105
+ detail
1106
+ }));
1107
+ if (triggerType === "change" || triggerType === "both") {
1108
+ this.dispatchEvent(new CustomEvent("change", {
1109
+ bubbles: true,
1110
+ composed: true,
1111
+ detail
1112
+ }));
1113
+ this.dispatchEvent(new CustomEvent("quantity-change", {
1114
+ bubbles: true,
1115
+ composed: true,
1116
+ detail
1117
+ }));
1118
+ }
1119
+ }
1120
+ /**
1121
+ * Increments the current value by the configured step.
1122
+ * If at or above max, or if disabled/readonly, this operation is a no-op.
1123
+ */
1124
+ increment() {
1125
+ if (this.disabled || this.readonly) return;
1126
+ const current = Number(this.value);
1127
+ if (Number.isNaN(current)) return;
1128
+ const precision = this.#getPrecision(this.step, current, this.max);
1129
+ const next = Number((current + this.step).toFixed(precision));
1130
+ if (next > this.max + 1e-9) return;
1131
+ const clamped = Math.abs(next - this.max) < 1e-9 ? this.max : Math.min(next, this.max);
1132
+ this.value = Math.abs(clamped) < 1e-9 ? 0 : clamped;
1133
+ this.#syncFormState();
1134
+ this.requestUpdate();
1135
+ this.#emitEvents("both");
1136
+ }
1137
+ /**
1138
+ * Decrements the current value by the configured step.
1139
+ * If at or below min, or if disabled/readonly, this operation is a no-op.
1140
+ */
1141
+ decrement() {
1142
+ if (this.disabled || this.readonly) return;
1143
+ const current = Number(this.value);
1144
+ if (Number.isNaN(current)) return;
1145
+ const precision = this.#getPrecision(this.step, current, this.min);
1146
+ const next = Number((current - this.step).toFixed(precision));
1147
+ if (next < this.min - 1e-9) return;
1148
+ const clamped = Math.abs(next - this.min) < 1e-9 ? this.min : Math.max(next, this.min);
1149
+ this.value = Math.abs(clamped) < 1e-9 ? 0 : clamped;
1150
+ this.#syncFormState();
1151
+ this.requestUpdate();
1152
+ this.#emitEvents("both");
1153
+ }
1154
+ /**
1155
+ * Sets the value imperatively with optional event dispatching.
1156
+ *
1157
+ * @param val New numerical value.
1158
+ * @param options.dispatchEvents If true, emits `input` and `change` events.
1159
+ */
1160
+ setValue(val, options) {
1161
+ const num = typeof val === "number" ? val : Number(val);
1162
+ this.value = num;
1163
+ this.#syncFormState();
1164
+ this.requestUpdate();
1165
+ if (options?.dispatchEvents) this.#emitEvents("both");
1166
+ }
1167
+ /**
1168
+ * Handles user input from the internal number input field.
1169
+ */
1170
+ #handleInput;
1171
+ /**
1172
+ * Handles commit / change from the internal number input field.
1173
+ */
1174
+ #handleChange;
1175
+ /**
1176
+ * Invoked by FormAssociatedLitElement when the parent form executes a reset.
1177
+ * Restores initial value, clears validation, and synchronizes form value.
1178
+ */
1179
+ onFormReset() {
1180
+ this.value = this.defaultValue ?? this.#initialValue;
1181
+ this.#syncFormState();
1182
+ this.requestUpdate();
1183
+ }
1184
+ render() {
1185
+ const isInvalid = !this.validity.valid;
1186
+ const current = Number(this.value);
1187
+ const isDecDisabled = this.disabled || this.readonly || Number.isNaN(current) || current <= this.min + 1e-9;
1188
+ const isIncDisabled = this.disabled || this.readonly || Number.isNaN(current) || current >= this.max - 1e-9;
1189
+ return lit.html`
1190
+ <div
1191
+ class="stepper ${this.focused ? "focused" : ""} ${isInvalid ? "invalid" : ""}"
1192
+ role="group"
1193
+ aria-label=${this.getAttribute("aria-label") || "Quantity Stepper"}
1194
+ >
1195
+ <button
1196
+ type="button"
1197
+ part="button button-decrement"
1198
+ class="btn-decrement"
1199
+ ?disabled=${isDecDisabled}
1200
+ @click=${this.decrement}
1201
+ tabindex="-1"
1202
+ aria-label="Decrease quantity"
1203
+ >−</button>
1204
+ <input
1205
+ type="number"
1206
+ part="input"
1207
+ .value=${this.#formatValue(this.value)}
1208
+ min=${this.min}
1209
+ max=${this.max}
1210
+ step=${this.step}
1211
+ ?disabled=${this.disabled}
1212
+ ?readonly=${this.readonly}
1213
+ @input=${this.#handleInput}
1214
+ @change=${this.#handleChange}
1215
+ @focus=${() => {
1216
+ this.focused = true;
1217
+ }}
1218
+ @blur=${() => {
1219
+ this.focused = false;
1220
+ }}
1221
+ aria-label="Quantity"
1222
+ />
1223
+ <button
1224
+ type="button"
1225
+ part="button button-increment"
1226
+ class="btn-increment"
1227
+ ?disabled=${isIncDisabled}
1228
+ @click=${this.increment}
1229
+ tabindex="-1"
1230
+ aria-label="Increase quantity"
1231
+ >+</button>
1232
+ </div>
1233
+ `;
1234
+ }
1235
+ };
1236
+ __decorate([(0, lit_decorators_js.property)({ type: Number })], LitQuantityStepper.prototype, "value", void 0);
1237
+ __decorate([(0, lit_decorators_js.property)({ type: Number })], LitQuantityStepper.prototype, "min", void 0);
1238
+ __decorate([(0, lit_decorators_js.property)({ type: Number })], LitQuantityStepper.prototype, "max", void 0);
1239
+ __decorate([(0, lit_decorators_js.property)({ type: Number })], LitQuantityStepper.prototype, "step", void 0);
1240
+ __decorate([(0, lit_decorators_js.property)({
1241
+ type: Boolean,
1242
+ reflect: true
1243
+ })], LitQuantityStepper.prototype, "readonly", void 0);
1244
+ __decorate([(0, lit_decorators_js.property)({
1245
+ type: Number,
1246
+ attribute: "default-value"
1247
+ })], LitQuantityStepper.prototype, "defaultValue", void 0);
1248
+ __decorate([(0, lit_decorators_js.state)()], LitQuantityStepper.prototype, "focused", void 0);
1249
+ __decorate([(0, lit_decorators_js.query)("input")], LitQuantityStepper.prototype, "inputEl", void 0);
1250
+ defineCustomElement("lit-quantity-stepper", LitQuantityStepper);
1251
+ // src/components/cart-badge.ts
1252
+ /**
1253
+ * @fileoverview Reference implementation of <lit-cart-badge> custom element.
1254
+ * Demonstrates dual-mode store subscription (direct property vs W3C Context Protocol),
1255
+ * selective projection for total count, coalesced pending state tracking,
1256
+ * and zero-memory-leak unmounting.
1257
+ *
1258
+ * @implements view-layer
1259
+ * @implements store-integration-pattern
1260
+ * @implements action-pipeline-system
1261
+ * @memberof core-concepts
1262
+ */
1263
+ /**
1264
+ * Standard W3C Context token for the shopping cart domain store.
1265
+ * Uses Symbol.for to guarantee singleton identity across independent bundle chunks.
1266
+ */
1267
+ const cartStoreContext = createStoreContext("cart");
1268
+ /**
1269
+ * Default projection selector that extracts total quantity from diverse cart store state structures:
1270
+ * - Primitive numbers (e.g. `5`)
1271
+ * - State objects with `totalCount`, `count`, or `total` numerical fields
1272
+ * - State objects with `items` arrays containing quantity numbers or objects (`{ quantity: n }`)
1273
+ */
1274
+ function defaultCartCountSelector(state) {
1275
+ if (typeof state === "number") return Number.isFinite(state) ? Math.max(0, Math.floor(state)) : 0;
1276
+ if (state && typeof state === "object") {
1277
+ const obj = state;
1278
+ if (typeof obj.totalCount === "number" && Number.isFinite(obj.totalCount)) return Math.max(0, Math.floor(obj.totalCount));
1279
+ if (typeof obj.count === "number" && Number.isFinite(obj.count)) return Math.max(0, Math.floor(obj.count));
1280
+ if (typeof obj.total === "number" && Number.isFinite(obj.total)) return Math.max(0, Math.floor(obj.total));
1281
+ if (Array.isArray(obj.items)) return obj.items.reduce((sum, item) => {
1282
+ const qty = typeof item === "number" ? item : typeof item?.quantity === "number" ? item.quantity : 1;
1283
+ return sum + (Number.isFinite(qty) ? Math.max(0, qty) : 0);
1284
+ }, 0);
1285
+ }
1286
+ return 0;
1287
+ }
1288
+ /**
1289
+ * Reference Custom Element for displaying a dynamic shopping cart badge.
1290
+ *
1291
+ * Supports two consumption paradigms:
1292
+ * 1. Direct property assignment: `.store=${cartStore}` and `.register=${actionRegister}`.
1293
+ * 2. W3C Context Protocol: automatically injects `cartStoreContext` and `actionRegisterContext`
1294
+ * from ancestor providers across Shadow DOM boundaries.
1295
+ */
1296
+ var LitCartBadge = class extends lit.LitElement {
1297
+ static {
1298
+ this.styles = lit.css`
1299
+ :host {
1300
+ display: inline-flex;
1301
+ vertical-align: middle;
1302
+ font-family: var(
1303
+ --cart-badge-font-family,
1304
+ system-ui,
1305
+ -apple-system,
1306
+ BlinkMacSystemFont,
1307
+ 'Segoe UI',
1308
+ Roboto,
1309
+ sans-serif
1310
+ );
1311
+ --_bg: var(--cart-badge-bg, #2563eb);
1312
+ --_color: var(--cart-badge-color, #ffffff);
1313
+ --_hover-bg: var(--cart-badge-hover-bg, #1d4ed8);
1314
+ --_active-bg: var(--cart-badge-active-bg, #1e40af);
1315
+ --_radius: var(--cart-badge-radius, 9999px);
1316
+ --_font-size: var(--cart-badge-font-size, 0.875rem);
1317
+ --_font-weight: var(--cart-badge-font-weight, 600);
1318
+ --_padding: var(--cart-badge-padding, 4px 10px);
1319
+ --_pulse-scale: var(--cart-badge-pulse-scale, 1.2);
1320
+ }
1321
+
1322
+ :host([variant='secondary']) {
1323
+ --_bg: var(--cart-badge-bg, #475569);
1324
+ --_hover-bg: var(--cart-badge-hover-bg, #334155);
1325
+ --_active-bg: var(--cart-badge-active-bg, #1e293b);
1326
+ }
1327
+
1328
+ :host([variant='accent']) {
1329
+ --_bg: var(--cart-badge-bg, #7c3aed);
1330
+ --_hover-bg: var(--cart-badge-hover-bg, #6d28d9);
1331
+ --_active-bg: var(--cart-badge-active-bg, #5b21b6);
1332
+ }
1333
+
1334
+ :host([variant='danger']) {
1335
+ --_bg: var(--cart-badge-bg, #dc2626);
1336
+ --_hover-bg: var(--cart-badge-hover-bg, #b91c1c);
1337
+ --_active-bg: var(--cart-badge-active-bg, #991b1b);
1338
+ }
1339
+
1340
+ :host([hidden]) {
1341
+ display: none !important;
1342
+ }
1343
+
1344
+ :host([hide-zero][count-zero]) {
1345
+ display: none !important;
1346
+ }
1347
+
1348
+ :host([disabled]) {
1349
+ opacity: 0.6;
1350
+ pointer-events: none;
1351
+ cursor: not-allowed;
1352
+ }
1353
+
1354
+ .badge {
1355
+ display: inline-flex;
1356
+ align-items: center;
1357
+ gap: 6px;
1358
+ padding: var(--_padding);
1359
+ background-color: var(--_bg);
1360
+ color: var(--_color);
1361
+ border-radius: var(--_radius);
1362
+ font-size: var(--_font-size);
1363
+ font-weight: var(--_font-weight);
1364
+ border: none;
1365
+ cursor: pointer;
1366
+ user-select: none;
1367
+ outline: none;
1368
+ transition: background-color 0.15s ease, transform 0.15s ease;
1369
+ box-sizing: border-box;
1370
+ line-height: 1;
1371
+ }
1372
+
1373
+ .badge:hover:not(:disabled) {
1374
+ background-color: var(--_hover-bg);
1375
+ }
1376
+
1377
+ .badge:active:not(:disabled) {
1378
+ background-color: var(--_active-bg);
1379
+ }
1380
+
1381
+ .badge:focus-visible {
1382
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35);
1383
+ }
1384
+
1385
+ .badge:disabled {
1386
+ cursor: not-allowed;
1387
+ opacity: 0.6;
1388
+ }
1389
+
1390
+ .count {
1391
+ display: inline-flex;
1392
+ align-items: center;
1393
+ justify-content: center;
1394
+ min-width: 1.25em;
1395
+ font-variant-numeric: tabular-nums;
1396
+ }
1397
+
1398
+ .pulse {
1399
+ animation: cart-badge-pulse 300ms cubic-bezier(0.4, 0, 0.2, 1);
1400
+ }
1401
+
1402
+ @keyframes cart-badge-pulse {
1403
+ 0% {
1404
+ transform: scale(1);
1405
+ }
1406
+ 50% {
1407
+ transform: scale(var(--_pulse-scale));
1408
+ }
1409
+ 100% {
1410
+ transform: scale(1);
1411
+ }
1412
+ }
1413
+
1414
+ .spinner {
1415
+ display: inline-block;
1416
+ width: 12px;
1417
+ height: 12px;
1418
+ border: 2px solid currentColor;
1419
+ border-top-color: transparent;
1420
+ border-radius: 50%;
1421
+ animation: cart-badge-spin 0.8s linear infinite;
1422
+ box-sizing: border-box;
1423
+ }
1424
+
1425
+ @keyframes cart-badge-spin {
1426
+ to {
1427
+ transform: rotate(360deg);
1428
+ }
1429
+ }
1430
+ `;
1431
+ }
1432
+ #contextStoreController;
1433
+ #contextActionController;
1434
+ #directStoreController;
1435
+ #directActionController;
1436
+ #previousCount;
1437
+ constructor() {
1438
+ super();
1439
+ this.value = void 0;
1440
+ this.maxCount = 99;
1441
+ this.hideZero = false;
1442
+ this.label = "Cart";
1443
+ this.disabled = false;
1444
+ this.variant = "primary";
1445
+ this.isPulsing = false;
1446
+ this.#directStoreController = null;
1447
+ this.#directActionController = null;
1448
+ this.#handleClick = async () => {
1449
+ if (this.disabled || this.isPending) return;
1450
+ this.dispatchEvent(new CustomEvent("cart-badge-click", {
1451
+ bubbles: true,
1452
+ composed: true,
1453
+ detail: {
1454
+ count: this.count,
1455
+ isPending: this.isPending
1456
+ }
1457
+ }));
1458
+ if (this.actionName) try {
1459
+ if (this.#directActionController) await this.#directActionController.dispatch(this.actionName, this.actionPayload);
1460
+ else if (this.#contextActionController.register) await this.#contextActionController.dispatch(this.actionName, this.actionPayload);
1461
+ } catch (error) {
1462
+ this.dispatchEvent(new CustomEvent("cart-action-error", {
1463
+ bubbles: true,
1464
+ composed: true,
1465
+ detail: { error }
1466
+ }));
1467
+ }
1468
+ };
1469
+ this.#contextStoreController = new ContextStoreController(this, {
1470
+ context: cartStoreContext,
1471
+ selector: (state) => this.selector ? this.selector(state) : defaultCartCountSelector(state),
1472
+ equalityFn: (a, b) => this.equalityFn ? this.equalityFn(a, b) : Object.is(a, b)
1473
+ });
1474
+ this.#contextActionController = new ContextActionController(this, { context: actionRegisterContext });
1475
+ }
1476
+ willUpdate(changedProperties) {
1477
+ super.willUpdate(changedProperties);
1478
+ if (changedProperties.has("store") || changedProperties.has("selector") || changedProperties.has("equalityFn")) {
1479
+ if (this.#directStoreController) {
1480
+ this.#directStoreController.dispose();
1481
+ this.#directStoreController = null;
1482
+ }
1483
+ if (this.store) this.#directStoreController = new _context_action_lit.StoreController(this, this.store, {
1484
+ selector: (state) => this.selector ? this.selector(state) : defaultCartCountSelector(state),
1485
+ equalityFn: this.equalityFn ?? Object.is
1486
+ });
1487
+ }
1488
+ if (changedProperties.has("register")) {
1489
+ if (this.register) this.#directActionController = new _context_action_lit.ActionController(this, this.register);
1490
+ else this.#directActionController = null;
1491
+ }
1492
+ const currentCount = this.count;
1493
+ if (this.#previousCount !== void 0 && currentCount > this.#previousCount) this.isPulsing = true;
1494
+ this.#previousCount = currentCount;
1495
+ }
1496
+ /**
1497
+ * Derives the current total count from direct override, direct store, or context store.
1498
+ */
1499
+ get count() {
1500
+ if (this.value !== void 0) return this.value;
1501
+ if (this.#directStoreController) return this.#directStoreController.value;
1502
+ if (this.store) {
1503
+ const raw = typeof this.store.getValue === "function" ? this.store.getValue() : typeof this.store.getSnapshot === "function" ? this.store.getSnapshot() : void 0;
1504
+ return this.selector ? this.selector(raw) : defaultCartCountSelector(raw);
1505
+ }
1506
+ if (this.#contextStoreController.isResolved && this.#contextStoreController.value !== void 0) return this.#contextStoreController.value;
1507
+ return 0;
1508
+ }
1509
+ set count(val) {
1510
+ const oldVal = this.count;
1511
+ if (val === void 0 || val === null) this.value = void 0;
1512
+ else this.value = Number.isFinite(val) ? Math.max(0, Math.floor(val)) : 0;
1513
+ this.requestUpdate("count", oldVal);
1514
+ }
1515
+ /**
1516
+ * Indicates whether an action dispatch is currently in flight.
1517
+ */
1518
+ get isPending() {
1519
+ if (this.#directActionController) return this.#directActionController.isPending;
1520
+ return this.#contextActionController.isPending;
1521
+ }
1522
+ updated(changedProperties) {
1523
+ super.updated(changedProperties);
1524
+ const currentCount = this.count;
1525
+ this.toggleAttribute("has-items", currentCount > 0);
1526
+ this.toggleAttribute("count-zero", currentCount === 0);
1527
+ }
1528
+ #handleClick;
1529
+ render() {
1530
+ const current = this.count;
1531
+ if (this.hideZero && current <= 0) return lit.nothing;
1532
+ const pending = this.isPending;
1533
+ const displayCount = current > this.maxCount ? `${this.maxCount}+` : String(current);
1534
+ return lit.html`
1535
+ <button
1536
+ type="button"
1537
+ part="button badge"
1538
+ class="badge ${this.isPulsing ? "pulse" : ""}"
1539
+ ?disabled=${this.disabled || pending}
1540
+ @click=${this.#handleClick}
1541
+ @animationend=${() => {
1542
+ this.isPulsing = false;
1543
+ }}
1544
+ aria-label=${this.getAttribute("aria-label") || `${this.label}: ${displayCount} items`}
1545
+ >
1546
+ <slot name="icon">
1547
+ <span part="icon" class="icon" aria-hidden="true">🛒</span>
1548
+ </slot>
1549
+ <span part="label" class="label"><slot>${this.label}</slot></span>
1550
+ <span part="count" class="count">${displayCount}</span>
1551
+ ${pending ? lit.html`<span
1552
+ part="spinner"
1553
+ class="spinner"
1554
+ aria-label="Loading"
1555
+ ></span>` : lit.nothing}
1556
+ </button>
1557
+ `;
1558
+ }
1559
+ };
1560
+ __decorate([(0, lit_decorators_js.property)({ attribute: false })], LitCartBadge.prototype, "store", void 0);
1561
+ __decorate([(0, lit_decorators_js.property)({ attribute: false })], LitCartBadge.prototype, "register", void 0);
1562
+ __decorate([(0, lit_decorators_js.property)({ attribute: false })], LitCartBadge.prototype, "selector", void 0);
1563
+ __decorate([(0, lit_decorators_js.property)({ attribute: false })], LitCartBadge.prototype, "equalityFn", void 0);
1564
+ __decorate([(0, lit_decorators_js.property)({ attribute: false })], LitCartBadge.prototype, "storeContext", void 0);
1565
+ __decorate([(0, lit_decorators_js.property)({ attribute: false })], LitCartBadge.prototype, "actionContext", void 0);
1566
+ __decorate([(0, lit_decorators_js.property)({ type: Number })], LitCartBadge.prototype, "value", void 0);
1567
+ __decorate([(0, lit_decorators_js.property)({
1568
+ type: Number,
1569
+ attribute: "max-count"
1570
+ })], LitCartBadge.prototype, "maxCount", void 0);
1571
+ __decorate([(0, lit_decorators_js.property)({
1572
+ type: Boolean,
1573
+ reflect: true,
1574
+ attribute: "hide-zero"
1575
+ })], LitCartBadge.prototype, "hideZero", void 0);
1576
+ __decorate([(0, lit_decorators_js.property)({ type: String })], LitCartBadge.prototype, "label", void 0);
1577
+ __decorate([(0, lit_decorators_js.property)({
1578
+ type: String,
1579
+ attribute: "action-name"
1580
+ })], LitCartBadge.prototype, "actionName", void 0);
1581
+ __decorate([(0, lit_decorators_js.property)({ attribute: false })], LitCartBadge.prototype, "actionPayload", void 0);
1582
+ __decorate([(0, lit_decorators_js.property)({
1583
+ type: Boolean,
1584
+ reflect: true
1585
+ })], LitCartBadge.prototype, "disabled", void 0);
1586
+ __decorate([(0, lit_decorators_js.property)({
1587
+ type: String,
1588
+ reflect: true
1589
+ })], LitCartBadge.prototype, "variant", void 0);
1590
+ __decorate([(0, lit_decorators_js.state)()], LitCartBadge.prototype, "isPulsing", void 0);
1591
+ defineCustomElement("lit-cart-badge", LitCartBadge);
1592
+ exports.ContextActionController = ContextActionController;
1593
+ Object.defineProperty(exports, "ContextConsumer", {
1594
+ enumerable: true,
1595
+ get: function() {
1596
+ return _lit_context.ContextConsumer;
1597
+ }
1598
+ });
1599
+ Object.defineProperty(exports, "ContextProvider", {
1600
+ enumerable: true,
1601
+ get: function() {
1602
+ return _lit_context.ContextProvider;
1603
+ }
1604
+ });
1605
+ exports.ContextStoreController = ContextStoreController;
1606
+ Object.defineProperty(exports, "ControllerScope", {
1607
+ enumerable: true,
1608
+ get: function() {
1609
+ return _context_action_lit.ControllerScope;
1610
+ }
1611
+ });
1612
+ exports.FormAssociatedLitElement = FormAssociatedLitElement;
1613
+ exports.LitCartBadge = LitCartBadge;
1614
+ exports.LitQuantityStepper = LitQuantityStepper;
1615
+ exports.actionDispatcherContext = actionDispatcherContext;
1616
+ exports.actionRegisterContext = actionRegisterContext;
1617
+ exports.cartStoreContext = cartStoreContext;
1618
+ exports.createActionDispatcherContext = createActionDispatcherContext;
1619
+ exports.createActionRegisterContext = createActionRegisterContext;
1620
+ Object.defineProperty(exports, "createContext", {
1621
+ enumerable: true,
1622
+ get: function() {
1623
+ return _lit_context.createContext;
1624
+ }
1625
+ });
1626
+ exports.createCustomElementBridge = createLitElementBridge;
1627
+ exports.createElementFacade = createElementFacade;
1628
+ exports.createLitBridge = createLitElementBridge;
1629
+ exports.createLitElementBridge = createLitElementBridge;
1630
+ exports.createStoreContext = createStoreContext;
1631
+ exports.defaultCartCountSelector = defaultCartCountSelector;
1632
+ exports.provideActionDispatcher = provideActionDispatcher;
1633
+ exports.provideActionRegister = provideActionRegister;
1634
+ exports.provideStore = provideStore;
1635
+
1636
+ //# sourceMappingURL=index.cjs.map