@bquery/bquery 1.4.0 → 1.5.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.
Files changed (127) hide show
  1. package/README.md +139 -120
  2. package/dist/component/component.d.ts.map +1 -1
  3. package/dist/component/index.d.ts +2 -0
  4. package/dist/component/index.d.ts.map +1 -1
  5. package/dist/component/library.d.ts +34 -0
  6. package/dist/component/library.d.ts.map +1 -0
  7. package/dist/component/types.d.ts +10 -6
  8. package/dist/component/types.d.ts.map +1 -1
  9. package/dist/component-CY5MVoYN.js +531 -0
  10. package/dist/component-CY5MVoYN.js.map +1 -0
  11. package/dist/component.es.mjs +6 -184
  12. package/dist/config-DRmZZno3.js +40 -0
  13. package/dist/config-DRmZZno3.js.map +1 -0
  14. package/dist/core-CK2Mfpf4.js +648 -0
  15. package/dist/core-CK2Mfpf4.js.map +1 -0
  16. package/dist/core-DPdbItcq.js +112 -0
  17. package/dist/core-DPdbItcq.js.map +1 -0
  18. package/dist/core.es.mjs +45 -1261
  19. package/dist/full.d.ts +6 -6
  20. package/dist/full.d.ts.map +1 -1
  21. package/dist/full.es.mjs +98 -92
  22. package/dist/full.iife.js +173 -3
  23. package/dist/full.iife.js.map +1 -1
  24. package/dist/full.umd.js +173 -3
  25. package/dist/full.umd.js.map +1 -1
  26. package/dist/index.es.mjs +143 -139
  27. package/dist/motion/transition.d.ts +1 -1
  28. package/dist/motion/transition.d.ts.map +1 -1
  29. package/dist/motion/types.d.ts +11 -1
  30. package/dist/motion/types.d.ts.map +1 -1
  31. package/dist/motion-C5DRdPnO.js +415 -0
  32. package/dist/motion-C5DRdPnO.js.map +1 -0
  33. package/dist/motion.es.mjs +25 -361
  34. package/dist/object-qGpWr6-J.js +38 -0
  35. package/dist/object-qGpWr6-J.js.map +1 -0
  36. package/dist/platform/announcer.d.ts +59 -0
  37. package/dist/platform/announcer.d.ts.map +1 -0
  38. package/dist/platform/config.d.ts +92 -0
  39. package/dist/platform/config.d.ts.map +1 -0
  40. package/dist/platform/cookies.d.ts +45 -0
  41. package/dist/platform/cookies.d.ts.map +1 -0
  42. package/dist/platform/index.d.ts +8 -0
  43. package/dist/platform/index.d.ts.map +1 -1
  44. package/dist/platform/meta.d.ts +62 -0
  45. package/dist/platform/meta.d.ts.map +1 -0
  46. package/dist/platform-B7JhGBc7.js +361 -0
  47. package/dist/platform-B7JhGBc7.js.map +1 -0
  48. package/dist/platform.es.mjs +11 -248
  49. package/dist/reactive/async-data.d.ts +114 -0
  50. package/dist/reactive/async-data.d.ts.map +1 -0
  51. package/dist/reactive/index.d.ts +2 -2
  52. package/dist/reactive/index.d.ts.map +1 -1
  53. package/dist/reactive/signal.d.ts +2 -0
  54. package/dist/reactive/signal.d.ts.map +1 -1
  55. package/dist/reactive-BDya-ia8.js +253 -0
  56. package/dist/reactive-BDya-ia8.js.map +1 -0
  57. package/dist/reactive.es.mjs +18 -34
  58. package/dist/router-CijiICxt.js +188 -0
  59. package/dist/router-CijiICxt.js.map +1 -0
  60. package/dist/router.es.mjs +11 -200
  61. package/dist/sanitize-jyJ2ryE2.js +302 -0
  62. package/dist/sanitize-jyJ2ryE2.js.map +1 -0
  63. package/dist/security/constants.d.ts.map +1 -1
  64. package/dist/security.es.mjs +10 -56
  65. package/dist/store-CPK9E62U.js +262 -0
  66. package/dist/store-CPK9E62U.js.map +1 -0
  67. package/dist/store.es.mjs +12 -25
  68. package/dist/view-Cdi0g-qo.js +396 -0
  69. package/dist/view-Cdi0g-qo.js.map +1 -0
  70. package/dist/view.es.mjs +10 -430
  71. package/package.json +15 -11
  72. package/src/component/component.ts +319 -289
  73. package/src/component/index.ts +42 -40
  74. package/src/component/library.ts +504 -0
  75. package/src/component/types.ts +91 -85
  76. package/src/core/collection.ts +628 -628
  77. package/src/core/element.ts +774 -774
  78. package/src/core/index.ts +48 -48
  79. package/src/core/utils/function.ts +151 -151
  80. package/src/full.ts +223 -187
  81. package/src/motion/animate.ts +113 -113
  82. package/src/motion/flip.ts +176 -176
  83. package/src/motion/scroll.ts +57 -57
  84. package/src/motion/spring.ts +150 -150
  85. package/src/motion/timeline.ts +246 -246
  86. package/src/motion/transition.ts +53 -7
  87. package/src/motion/types.ts +208 -198
  88. package/src/platform/announcer.ts +208 -0
  89. package/src/platform/config.ts +163 -0
  90. package/src/platform/cookies.ts +165 -0
  91. package/src/platform/index.ts +39 -18
  92. package/src/platform/meta.ts +168 -0
  93. package/src/platform/storage.ts +215 -215
  94. package/src/reactive/async-data.ts +486 -0
  95. package/src/reactive/core.ts +114 -114
  96. package/src/reactive/effect.ts +54 -54
  97. package/src/reactive/index.ts +37 -23
  98. package/src/reactive/internals.ts +122 -122
  99. package/src/reactive/signal.ts +29 -20
  100. package/src/security/constants.ts +211 -209
  101. package/src/security/sanitize-core.ts +364 -364
  102. package/src/view/evaluate.ts +290 -290
  103. package/dist/batch-x7b2eZST.js +0 -13
  104. package/dist/batch-x7b2eZST.js.map +0 -1
  105. package/dist/component.es.mjs.map +0 -1
  106. package/dist/core-BhpuvPhy.js +0 -170
  107. package/dist/core-BhpuvPhy.js.map +0 -1
  108. package/dist/core.es.mjs.map +0 -1
  109. package/dist/full.es.mjs.map +0 -1
  110. package/dist/index.es.mjs.map +0 -1
  111. package/dist/motion.es.mjs.map +0 -1
  112. package/dist/persisted-DHoi3uEs.js +0 -278
  113. package/dist/persisted-DHoi3uEs.js.map +0 -1
  114. package/dist/platform.es.mjs.map +0 -1
  115. package/dist/reactive.es.mjs.map +0 -1
  116. package/dist/router.es.mjs.map +0 -1
  117. package/dist/sanitize-Cxvxa-DX.js +0 -283
  118. package/dist/sanitize-Cxvxa-DX.js.map +0 -1
  119. package/dist/security.es.mjs.map +0 -1
  120. package/dist/store.es.mjs.map +0 -1
  121. package/dist/type-guards-BdKlYYlS.js +0 -32
  122. package/dist/type-guards-BdKlYYlS.js.map +0 -1
  123. package/dist/untrack-DNnnqdlR.js +0 -6
  124. package/dist/untrack-DNnnqdlR.js.map +0 -1
  125. package/dist/view.es.mjs.map +0 -1
  126. package/dist/watch-DXXv3iAI.js +0 -58
  127. package/dist/watch-DXXv3iAI.js.map +0 -1
@@ -1,289 +1,319 @@
1
- /**
2
- * Web Component factory and registry.
3
- *
4
- * @module bquery/component
5
- */
6
-
7
- import { sanitizeHtml } from '../security/sanitize';
8
- import { coercePropValue } from './props';
9
- import type { ComponentDefinition, PropDefinition } from './types';
10
-
11
- /**
12
- * Creates a custom element class for a component definition.
13
- *
14
- * This is useful when you want to extend or register the class manually
15
- * (e.g. with different tag names in tests or custom registries).
16
- *
17
- * @template TProps - Type of the component's props
18
- * @param tagName - The custom element tag name (used for diagnostics)
19
- * @param definition - The component configuration
20
- */
21
- export const defineComponent = <TProps extends Record<string, unknown>>(
22
- tagName: string,
23
- definition: ComponentDefinition<TProps>
24
- ): typeof HTMLElement => {
25
- class BQueryComponent extends HTMLElement {
26
- /** Internal state object for the component */
27
- private readonly state = { ...(definition.state ?? {}) };
28
- /** Typed props object populated from attributes */
29
- private props = {} as TProps;
30
- /** Tracks missing required props for validation during connectedCallback */
31
- private missingRequiredProps = new Set<string>();
32
- /** Tracks whether the component has completed its initial mount */
33
- private hasMounted = false;
34
-
35
- constructor() {
36
- super();
37
- this.attachShadow({ mode: 'open' });
38
- this.syncProps();
39
- }
40
-
41
- /**
42
- * Returns the list of attributes to observe for changes.
43
- */
44
- static get observedAttributes(): string[] {
45
- return Object.keys(definition.props ?? {});
46
- }
47
-
48
- /**
49
- * Called when the element is added to the DOM.
50
- */
51
- connectedCallback(): void {
52
- try {
53
- // Defer initial render until all required props are present
54
- // This allows attributes to be set after element creation
55
- if (this.missingRequiredProps.size > 0) {
56
- // Component will mount once all required props are satisfied
57
- // via attributeChangedCallback
58
- return;
59
- }
60
- this.mount();
61
- } catch (error) {
62
- this.handleError(error as Error);
63
- }
64
- }
65
-
66
- /**
67
- * Performs the initial mount of the component.
68
- * Called when the element is connected and all required props are present.
69
- * @internal
70
- */
71
- private mount(): void {
72
- if (this.hasMounted) return;
73
- definition.beforeMount?.call(this);
74
- definition.connected?.call(this);
75
- this.render();
76
- this.hasMounted = true;
77
- }
78
-
79
- /**
80
- * Called when the element is removed from the DOM.
81
- */
82
- disconnectedCallback(): void {
83
- try {
84
- definition.disconnected?.call(this);
85
- } catch (error) {
86
- this.handleError(error as Error);
87
- }
88
- }
89
-
90
- /**
91
- * Called when an observed attribute changes.
92
- */
93
- attributeChangedCallback(
94
- _name: string,
95
- _oldValue: string | null,
96
- _newValue: string | null
97
- ): void {
98
- try {
99
- this.syncProps();
100
-
101
- if (this.hasMounted) {
102
- // Component already mounted - trigger update render
103
- this.render(true);
104
- } else if (this.isConnected && this.missingRequiredProps.size === 0) {
105
- // All required props are now satisfied and element is connected
106
- // Trigger the deferred initial mount
107
- this.mount();
108
- }
109
- } catch (error) {
110
- this.handleError(error as Error);
111
- }
112
- }
113
-
114
- /**
115
- * Handles errors during component lifecycle.
116
- * @internal
117
- */
118
- private handleError(error: Error): void {
119
- if (definition.onError) {
120
- definition.onError.call(this, error);
121
- } else {
122
- console.error(`bQuery component error in <${tagName}>:`, error);
123
- }
124
- }
125
-
126
- /**
127
- * Updates a state property and triggers a re-render.
128
- *
129
- * @param key - The state property key
130
- * @param value - The new value
131
- */
132
- setState(key: string, value: unknown): void {
133
- this.state[key] = value;
134
- this.render(true);
135
- }
136
-
137
- /**
138
- * Gets a state property value.
139
- *
140
- * @param key - The state property key
141
- * @returns The current value
142
- */
143
- getState<T = unknown>(key: string): T {
144
- return this.state[key] as T;
145
- }
146
-
147
- /**
148
- * Synchronizes props from attributes.
149
- * @internal
150
- */
151
- private syncProps(): void {
152
- const props = definition.props ?? {};
153
- for (const [key, config] of Object.entries(props) as [string, PropDefinition][]) {
154
- const attrValue = this.getAttribute(key);
155
- let value: unknown;
156
-
157
- if (attrValue == null) {
158
- if (config.required && config.default === undefined) {
159
- // Mark as missing instead of throwing - validate during connectedCallback
160
- this.missingRequiredProps.add(key);
161
- value = undefined;
162
- } else {
163
- value = config.default ?? undefined;
164
- }
165
- } else {
166
- // Attribute is present, remove from missing set if it was there
167
- if (this.missingRequiredProps.has(key)) {
168
- this.missingRequiredProps.delete(key);
169
- }
170
- value = coercePropValue(attrValue, config);
171
- }
172
-
173
- if (config.validator && value !== undefined) {
174
- const isValid = config.validator(value);
175
- if (!isValid) {
176
- throw new Error(
177
- `bQuery component: validation failed for prop "${key}" with value ${JSON.stringify(value)}`
178
- );
179
- }
180
- }
181
-
182
- (this.props as Record<string, unknown>)[key] = value;
183
- }
184
- }
185
-
186
- /**
187
- * Renders the component to its shadow root.
188
- * @internal
189
- */
190
- private render(triggerUpdated = false): void {
191
- try {
192
- if (triggerUpdated && definition.beforeUpdate) {
193
- const shouldUpdate = definition.beforeUpdate.call(this, this.props);
194
- if (shouldUpdate === false) return;
195
- }
196
-
197
- const emit = (event: string, detail?: unknown): void => {
198
- this.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, composed: true }));
199
- };
200
-
201
- if (!this.shadowRoot) return;
202
-
203
- const markup = definition.render({
204
- props: this.props,
205
- state: this.state,
206
- emit,
207
- });
208
-
209
- const sanitizedMarkup = sanitizeHtml(markup);
210
- this.shadowRoot.innerHTML = sanitizedMarkup;
211
-
212
- if (definition.styles) {
213
- const styleElement = document.createElement('style');
214
- styleElement.textContent = definition.styles;
215
- this.shadowRoot.prepend(styleElement);
216
- }
217
-
218
- if (triggerUpdated) {
219
- definition.updated?.call(this);
220
- }
221
- } catch (error) {
222
- this.handleError(error as Error);
223
- }
224
- }
225
- }
226
-
227
- return BQueryComponent;
228
- };
229
-
230
- /**
231
- * Defines and registers a custom Web Component.
232
- *
233
- * This function creates a new custom element with the given tag name
234
- * and configuration. The component uses Shadow DOM for encapsulation
235
- * and automatically re-renders when observed attributes change.
236
- *
237
- * @template TProps - Type of the component's props
238
- * @param tagName - The custom element tag name (must contain a hyphen)
239
- * @param definition - The component configuration
240
- *
241
- * @example
242
- * ```ts
243
- * component('counter-button', {
244
- * props: {
245
- * start: { type: Number, default: 0 },
246
- * },
247
- * state: { count: 0 },
248
- * styles: `
249
- * button { padding: 0.5rem 1rem; }
250
- * `,
251
- * connected() {
252
- * // Use event delegation on shadow root so handler survives re-renders
253
- * const handleClick = (event: Event) => {
254
- * const target = event.target as HTMLElement | null;
255
- * if (target?.matches('button')) {
256
- * this.setState('count', (this.getState('count') as number) + 1);
257
- * }
258
- * };
259
- * this.shadowRoot?.addEventListener('click', handleClick);
260
- * // Store handler for cleanup
261
- * (this as any)._handleClick = handleClick;
262
- * },
263
- * disconnected() {
264
- * // Clean up event listener to prevent memory leaks
265
- * const handleClick = (this as any)._handleClick;
266
- * if (handleClick) {
267
- * this.shadowRoot?.removeEventListener('click', handleClick);
268
- * }
269
- * },
270
- * render({ props, state }) {
271
- * return html`
272
- * <button>
273
- * Count: ${state.count}
274
- * </button>
275
- * `;
276
- * },
277
- * });
278
- * ```
279
- */
280
- export const component = <TProps extends Record<string, unknown>>(
281
- tagName: string,
282
- definition: ComponentDefinition<TProps>
283
- ): void => {
284
- const elementClass = defineComponent(tagName, definition);
285
-
286
- if (!customElements.get(tagName)) {
287
- customElements.define(tagName, elementClass);
288
- }
289
- };
1
+ /**
2
+ * Web Component factory and registry.
3
+ *
4
+ * @module bquery/component
5
+ */
6
+
7
+ import { sanitizeHtml } from '../security/sanitize';
8
+ import { coercePropValue } from './props';
9
+ import type { ComponentDefinition, PropDefinition } from './types';
10
+
11
+ /**
12
+ * Creates a custom element class for a component definition.
13
+ *
14
+ * This is useful when you want to extend or register the class manually
15
+ * (e.g. with different tag names in tests or custom registries).
16
+ *
17
+ * @template TProps - Type of the component's props
18
+ * @param tagName - The custom element tag name (used for diagnostics)
19
+ * @param definition - The component configuration
20
+ */
21
+ export const defineComponent = <TProps extends Record<string, unknown>>(
22
+ tagName: string,
23
+ definition: ComponentDefinition<TProps>
24
+ ): typeof HTMLElement => {
25
+ class BQueryComponent extends HTMLElement {
26
+ /** Internal state object for the component */
27
+ private readonly state = { ...(definition.state ?? {}) };
28
+ /** Typed props object populated from attributes */
29
+ private props = {} as TProps;
30
+ /** Tracks missing required props for validation during connectedCallback */
31
+ private missingRequiredProps = new Set<string>();
32
+ /** Tracks whether the component has completed its initial mount */
33
+ private hasMounted = false;
34
+
35
+ constructor() {
36
+ super();
37
+ this.attachShadow({ mode: 'open' });
38
+ this.syncProps();
39
+ }
40
+
41
+ /**
42
+ * Returns the list of attributes to observe for changes.
43
+ */
44
+ static get observedAttributes(): string[] {
45
+ return Object.keys(definition.props ?? {});
46
+ }
47
+
48
+ /**
49
+ * Called when the element is added to the DOM.
50
+ */
51
+ connectedCallback(): void {
52
+ try {
53
+ // Defer initial render until all required props are present
54
+ // This allows attributes to be set after element creation
55
+ if (this.missingRequiredProps.size > 0) {
56
+ // Component will mount once all required props are satisfied
57
+ // via attributeChangedCallback
58
+ return;
59
+ }
60
+ this.mount();
61
+ } catch (error) {
62
+ this.handleError(error as Error);
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Performs the initial mount of the component.
68
+ * Called when the element is connected and all required props are present.
69
+ * @internal
70
+ */
71
+ private mount(): void {
72
+ if (this.hasMounted) return;
73
+ definition.beforeMount?.call(this);
74
+ definition.connected?.call(this);
75
+ this.render();
76
+ this.hasMounted = true;
77
+ }
78
+
79
+ /**
80
+ * Called when the element is removed from the DOM.
81
+ */
82
+ disconnectedCallback(): void {
83
+ try {
84
+ definition.disconnected?.call(this);
85
+ } catch (error) {
86
+ this.handleError(error as Error);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Called when an observed attribute changes.
92
+ */
93
+ attributeChangedCallback(
94
+ _name: string,
95
+ _oldValue: string | null,
96
+ _newValue: string | null
97
+ ): void {
98
+ try {
99
+ this.syncProps();
100
+
101
+ if (this.hasMounted) {
102
+ // Component already mounted - trigger update render
103
+ this.render(true);
104
+ } else if (this.isConnected && this.missingRequiredProps.size === 0) {
105
+ // All required props are now satisfied and element is connected
106
+ // Trigger the deferred initial mount
107
+ this.mount();
108
+ }
109
+ } catch (error) {
110
+ this.handleError(error as Error);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Handles errors during component lifecycle.
116
+ * @internal
117
+ */
118
+ private handleError(error: Error): void {
119
+ if (definition.onError) {
120
+ definition.onError.call(this, error);
121
+ } else {
122
+ console.error(`bQuery component error in <${tagName}>:`, error);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Updates a state property and triggers a re-render.
128
+ *
129
+ * @param key - The state property key
130
+ * @param value - The new value
131
+ */
132
+ setState(key: string, value: unknown): void {
133
+ this.state[key] = value;
134
+ this.render(true);
135
+ }
136
+
137
+ /**
138
+ * Gets a state property value.
139
+ *
140
+ * @param key - The state property key
141
+ * @returns The current value
142
+ */
143
+ getState<T = unknown>(key: string): T {
144
+ return this.state[key] as T;
145
+ }
146
+
147
+ /**
148
+ * Synchronizes props from attributes.
149
+ * @internal
150
+ */
151
+ private syncProps(): void {
152
+ const props = definition.props ?? {};
153
+ for (const [key, config] of Object.entries(props) as [string, PropDefinition][]) {
154
+ const attrValue = this.getAttribute(key);
155
+ let value: unknown;
156
+
157
+ if (attrValue == null) {
158
+ if (config.required && config.default === undefined) {
159
+ // Mark as missing instead of throwing - validate during connectedCallback
160
+ this.missingRequiredProps.add(key);
161
+ value = undefined;
162
+ } else {
163
+ value = config.default ?? undefined;
164
+ }
165
+ } else {
166
+ // Attribute is present, remove from missing set if it was there
167
+ if (this.missingRequiredProps.has(key)) {
168
+ this.missingRequiredProps.delete(key);
169
+ }
170
+ value = coercePropValue(attrValue, config);
171
+ }
172
+
173
+ if (config.validator && value !== undefined) {
174
+ const isValid = config.validator(value);
175
+ if (!isValid) {
176
+ throw new Error(
177
+ `bQuery component: validation failed for prop "${key}" with value ${JSON.stringify(value)}`
178
+ );
179
+ }
180
+ }
181
+
182
+ (this.props as Record<string, unknown>)[key] = value;
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Renders the component to its shadow root.
188
+ * @internal
189
+ */
190
+ private render(triggerUpdated = false): void {
191
+ try {
192
+ if (triggerUpdated && definition.beforeUpdate) {
193
+ const shouldUpdate = definition.beforeUpdate.call(this, this.props);
194
+ if (shouldUpdate === false) return;
195
+ }
196
+
197
+ const emit = (event: string, detail?: unknown): void => {
198
+ this.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, composed: true }));
199
+ };
200
+
201
+ if (!this.shadowRoot) return;
202
+
203
+ const markup = definition.render({
204
+ props: this.props,
205
+ state: this.state,
206
+ emit,
207
+ });
208
+
209
+ // Component render output is authored by the component definition itself,
210
+ // so we can explicitly preserve shadow-DOM-specific markup such as <slot>,
211
+ // the stylistic `part` attribute, and standard form/input attributes without
212
+ // relaxing the global DOM sanitization rules.
213
+ const sanitizedMarkup = sanitizeHtml(markup, {
214
+ allowTags: ['slot'],
215
+ allowAttributes: [
216
+ 'part',
217
+ // Standard form attributes required by interactive shadow DOM content
218
+ 'disabled',
219
+ 'checked',
220
+ 'placeholder',
221
+ 'value',
222
+ 'rows',
223
+ 'cols',
224
+ 'readonly',
225
+ 'required',
226
+ 'maxlength',
227
+ 'minlength',
228
+ 'max',
229
+ 'min',
230
+ 'step',
231
+ 'pattern',
232
+ 'autocomplete',
233
+ 'autofocus',
234
+ 'for',
235
+ 'multiple',
236
+ 'selected',
237
+ 'wrap',
238
+ ],
239
+ });
240
+ this.shadowRoot.innerHTML = sanitizedMarkup;
241
+
242
+ if (definition.styles) {
243
+ const styleElement = document.createElement('style');
244
+ styleElement.textContent = definition.styles;
245
+ this.shadowRoot.prepend(styleElement);
246
+ }
247
+
248
+ if (triggerUpdated) {
249
+ definition.updated?.call(this);
250
+ }
251
+ } catch (error) {
252
+ this.handleError(error as Error);
253
+ }
254
+ }
255
+ }
256
+
257
+ return BQueryComponent;
258
+ };
259
+
260
+ /**
261
+ * Defines and registers a custom Web Component.
262
+ *
263
+ * This function creates a new custom element with the given tag name
264
+ * and configuration. The component uses Shadow DOM for encapsulation
265
+ * and automatically re-renders when observed attributes change.
266
+ *
267
+ * @template TProps - Type of the component's props
268
+ * @param tagName - The custom element tag name (must contain a hyphen)
269
+ * @param definition - The component configuration
270
+ *
271
+ * @example
272
+ * ```ts
273
+ * component('counter-button', {
274
+ * props: {
275
+ * start: { type: Number, default: 0 },
276
+ * },
277
+ * state: { count: 0 },
278
+ * styles: `
279
+ * button { padding: 0.5rem 1rem; }
280
+ * `,
281
+ * connected() {
282
+ * // Use event delegation on shadow root so handler survives re-renders
283
+ * const handleClick = (event: Event) => {
284
+ * const target = event.target as HTMLElement | null;
285
+ * if (target?.matches('button')) {
286
+ * this.setState('count', (this.getState('count') as number) + 1);
287
+ * }
288
+ * };
289
+ * this.shadowRoot?.addEventListener('click', handleClick);
290
+ * // Store handler for cleanup
291
+ * (this as any)._handleClick = handleClick;
292
+ * },
293
+ * disconnected() {
294
+ * // Clean up event listener to prevent memory leaks
295
+ * const handleClick = (this as any)._handleClick;
296
+ * if (handleClick) {
297
+ * this.shadowRoot?.removeEventListener('click', handleClick);
298
+ * }
299
+ * },
300
+ * render({ props, state }) {
301
+ * return html`
302
+ * <button>
303
+ * Count: ${state.count}
304
+ * </button>
305
+ * `;
306
+ * },
307
+ * });
308
+ * ```
309
+ */
310
+ export const component = <TProps extends Record<string, unknown>>(
311
+ tagName: string,
312
+ definition: ComponentDefinition<TProps>
313
+ ): void => {
314
+ const elementClass = defineComponent(tagName, definition);
315
+
316
+ if (!customElements.get(tagName)) {
317
+ customElements.define(tagName, elementClass);
318
+ }
319
+ };