@bquery/bquery 1.3.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 (138) hide show
  1. package/README.md +546 -501
  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/collection.d.ts +19 -3
  15. package/dist/core/collection.d.ts.map +1 -1
  16. package/dist/core/element.d.ts +23 -4
  17. package/dist/core/element.d.ts.map +1 -1
  18. package/dist/core/index.d.ts +1 -0
  19. package/dist/core/index.d.ts.map +1 -1
  20. package/dist/core/utils/function.d.ts +21 -4
  21. package/dist/core/utils/function.d.ts.map +1 -1
  22. package/dist/core-CK2Mfpf4.js +648 -0
  23. package/dist/core-CK2Mfpf4.js.map +1 -0
  24. package/dist/core-DPdbItcq.js +112 -0
  25. package/dist/core-DPdbItcq.js.map +1 -0
  26. package/dist/core.es.mjs +45 -1218
  27. package/dist/full.d.ts +6 -6
  28. package/dist/full.d.ts.map +1 -1
  29. package/dist/full.es.mjs +98 -92
  30. package/dist/full.iife.js +173 -3
  31. package/dist/full.iife.js.map +1 -1
  32. package/dist/full.umd.js +173 -3
  33. package/dist/full.umd.js.map +1 -1
  34. package/dist/index.es.mjs +143 -139
  35. package/dist/motion/transition.d.ts +1 -1
  36. package/dist/motion/transition.d.ts.map +1 -1
  37. package/dist/motion/types.d.ts +11 -1
  38. package/dist/motion/types.d.ts.map +1 -1
  39. package/dist/motion-C5DRdPnO.js +415 -0
  40. package/dist/motion-C5DRdPnO.js.map +1 -0
  41. package/dist/motion.es.mjs +25 -361
  42. package/dist/object-qGpWr6-J.js +38 -0
  43. package/dist/object-qGpWr6-J.js.map +1 -0
  44. package/dist/platform/announcer.d.ts +59 -0
  45. package/dist/platform/announcer.d.ts.map +1 -0
  46. package/dist/platform/config.d.ts +92 -0
  47. package/dist/platform/config.d.ts.map +1 -0
  48. package/dist/platform/cookies.d.ts +45 -0
  49. package/dist/platform/cookies.d.ts.map +1 -0
  50. package/dist/platform/index.d.ts +8 -0
  51. package/dist/platform/index.d.ts.map +1 -1
  52. package/dist/platform/meta.d.ts +62 -0
  53. package/dist/platform/meta.d.ts.map +1 -0
  54. package/dist/platform/storage.d.ts.map +1 -1
  55. package/dist/platform-B7JhGBc7.js +361 -0
  56. package/dist/platform-B7JhGBc7.js.map +1 -0
  57. package/dist/platform.es.mjs +11 -243
  58. package/dist/reactive/async-data.d.ts +114 -0
  59. package/dist/reactive/async-data.d.ts.map +1 -0
  60. package/dist/reactive/core.d.ts +12 -0
  61. package/dist/reactive/core.d.ts.map +1 -1
  62. package/dist/reactive/effect.d.ts.map +1 -1
  63. package/dist/reactive/index.d.ts +2 -2
  64. package/dist/reactive/index.d.ts.map +1 -1
  65. package/dist/reactive/internals.d.ts +6 -0
  66. package/dist/reactive/internals.d.ts.map +1 -1
  67. package/dist/reactive/signal.d.ts +2 -0
  68. package/dist/reactive/signal.d.ts.map +1 -1
  69. package/dist/reactive-BDya-ia8.js +253 -0
  70. package/dist/reactive-BDya-ia8.js.map +1 -0
  71. package/dist/reactive.es.mjs +18 -34
  72. package/dist/router-CijiICxt.js +188 -0
  73. package/dist/router-CijiICxt.js.map +1 -0
  74. package/dist/router.es.mjs +11 -200
  75. package/dist/sanitize-jyJ2ryE2.js +302 -0
  76. package/dist/sanitize-jyJ2ryE2.js.map +1 -0
  77. package/dist/security/constants.d.ts.map +1 -1
  78. package/dist/security/sanitize-core.d.ts.map +1 -1
  79. package/dist/security.es.mjs +10 -56
  80. package/dist/store-CPK9E62U.js +262 -0
  81. package/dist/store-CPK9E62U.js.map +1 -0
  82. package/dist/store.es.mjs +12 -25
  83. package/dist/view/evaluate.d.ts.map +1 -1
  84. package/dist/view-Cdi0g-qo.js +396 -0
  85. package/dist/view-Cdi0g-qo.js.map +1 -0
  86. package/dist/view.es.mjs +10 -424
  87. package/package.json +136 -132
  88. package/src/component/component.ts +319 -289
  89. package/src/component/index.ts +42 -40
  90. package/src/component/library.ts +504 -0
  91. package/src/component/types.ts +91 -85
  92. package/src/core/collection.ts +44 -4
  93. package/src/core/element.ts +33 -5
  94. package/src/core/index.ts +1 -0
  95. package/src/core/utils/function.ts +56 -15
  96. package/src/full.ts +223 -187
  97. package/src/motion/transition.ts +97 -51
  98. package/src/motion/types.ts +208 -198
  99. package/src/platform/announcer.ts +208 -0
  100. package/src/platform/config.ts +163 -0
  101. package/src/platform/cookies.ts +165 -0
  102. package/src/platform/index.ts +39 -18
  103. package/src/platform/meta.ts +168 -0
  104. package/src/platform/storage.ts +8 -1
  105. package/src/reactive/async-data.ts +486 -0
  106. package/src/reactive/core.ts +21 -0
  107. package/src/reactive/effect.ts +18 -7
  108. package/src/reactive/index.ts +37 -23
  109. package/src/reactive/internals.ts +18 -1
  110. package/src/reactive/signal.ts +29 -20
  111. package/src/security/constants.ts +211 -209
  112. package/src/security/sanitize-core.ts +22 -1
  113. package/src/view/evaluate.ts +29 -13
  114. package/dist/batch-4LAvfLE7.js +0 -13
  115. package/dist/batch-4LAvfLE7.js.map +0 -1
  116. package/dist/component.es.mjs.map +0 -1
  117. package/dist/core-COenAZjD.js +0 -145
  118. package/dist/core-COenAZjD.js.map +0 -1
  119. package/dist/core.es.mjs.map +0 -1
  120. package/dist/full.es.mjs.map +0 -1
  121. package/dist/index.es.mjs.map +0 -1
  122. package/dist/motion.es.mjs.map +0 -1
  123. package/dist/persisted-Dz_ryNuC.js +0 -278
  124. package/dist/persisted-Dz_ryNuC.js.map +0 -1
  125. package/dist/platform.es.mjs.map +0 -1
  126. package/dist/reactive.es.mjs.map +0 -1
  127. package/dist/router.es.mjs.map +0 -1
  128. package/dist/sanitize-1FBEPAFH.js +0 -272
  129. package/dist/sanitize-1FBEPAFH.js.map +0 -1
  130. package/dist/security.es.mjs.map +0 -1
  131. package/dist/store.es.mjs.map +0 -1
  132. package/dist/type-guards-DRma3-Kc.js +0 -16
  133. package/dist/type-guards-DRma3-Kc.js.map +0 -1
  134. package/dist/untrack-BuEQKH7_.js +0 -6
  135. package/dist/untrack-BuEQKH7_.js.map +0 -1
  136. package/dist/view.es.mjs.map +0 -1
  137. package/dist/watch-CXyaBC_9.js +0 -58
  138. package/dist/watch-CXyaBC_9.js.map +0 -1
@@ -1,40 +1,42 @@
1
- /**
2
- * Minimal Web Component helper for building custom elements.
3
- *
4
- * This module provides a declarative API for defining Web Components
5
- * without complex build steps. Features include:
6
- * - Type-safe props with automatic attribute coercion
7
- * - Reactive state management
8
- * - Shadow DOM encapsulation with scoped styles
9
- * - Lifecycle hooks (connected, disconnected)
10
- * - Event emission helpers
11
- *
12
- * @module bquery/component
13
- *
14
- * @example
15
- * ```ts
16
- * import { component, html } from 'bquery/component';
17
- *
18
- * component('user-card', {
19
- * props: {
20
- * username: { type: String, required: true },
21
- * avatar: { type: String, default: '/default-avatar.png' },
22
- * },
23
- * styles: `
24
- * .card { padding: 1rem; border: 1px solid #ccc; }
25
- * `,
26
- * render({ props }) {
27
- * return html`
28
- * <div class="card">
29
- * <img src="${props.avatar}" alt="${props.username}" />
30
- * <h3>${props.username}</h3>
31
- * </div>
32
- * `;
33
- * },
34
- * });
35
- * ```
36
- */
37
-
38
- export { component, defineComponent } from './component';
39
- export { html, safeHtml } from './html';
40
- export type { ComponentDefinition, ComponentRenderContext, PropDefinition } from './types';
1
+ /**
2
+ * Minimal Web Component helper for building custom elements.
3
+ *
4
+ * This module provides a declarative API for defining Web Components
5
+ * without complex build steps. Features include:
6
+ * - Type-safe props with automatic attribute coercion
7
+ * - Reactive state management
8
+ * - Shadow DOM encapsulation with scoped styles
9
+ * - Lifecycle hooks (connected, disconnected)
10
+ * - Event emission helpers
11
+ *
12
+ * @module bquery/component
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { component, html } from 'bquery/component';
17
+ *
18
+ * component('user-card', {
19
+ * props: {
20
+ * username: { type: String, required: true },
21
+ * avatar: { type: String, default: '/default-avatar.png' },
22
+ * },
23
+ * styles: `
24
+ * .card { padding: 1rem; border: 1px solid #ccc; }
25
+ * `,
26
+ * render({ props }) {
27
+ * return html`
28
+ * <div class="card">
29
+ * <img src="${props.avatar}" alt="${props.username}" />
30
+ * <h3>${props.username}</h3>
31
+ * </div>
32
+ * `;
33
+ * },
34
+ * });
35
+ * ```
36
+ */
37
+
38
+ export { component, defineComponent } from './component';
39
+ export { html, safeHtml } from './html';
40
+ export { registerDefaultComponents } from './library';
41
+ export type { DefaultComponentLibraryOptions, RegisteredDefaultComponents } from './library';
42
+ export type { ComponentDefinition, ComponentRenderContext, PropDefinition } from './types';
@@ -0,0 +1,504 @@
1
+ /**
2
+ * Default component library based on native Web Components.
3
+ *
4
+ * @module bquery/component
5
+ */
6
+
7
+ import { getBqueryConfig } from '../platform/config';
8
+ import { escapeHtml } from '../security';
9
+ import { component } from './component';
10
+ import { html } from './html';
11
+
12
+ /** Options for registering the default component library. */
13
+ export interface DefaultComponentLibraryOptions {
14
+ /** Prefix used for all registered component tags. Defaults to `bq`. */
15
+ prefix?: string;
16
+ }
17
+
18
+ /** Tag names returned by registerDefaultComponents(). */
19
+ export interface RegisteredDefaultComponents {
20
+ /** Button component tag name. */
21
+ button: string;
22
+ /** Card component tag name. */
23
+ card: string;
24
+ /** Input component tag name. */
25
+ input: string;
26
+ /** Textarea component tag name. */
27
+ textarea: string;
28
+ /** Checkbox component tag name. */
29
+ checkbox: string;
30
+ }
31
+
32
+ const baseStyles = `
33
+ :host {
34
+ color: inherit;
35
+ font: inherit;
36
+ }
37
+ `;
38
+
39
+ const controlStyles = `
40
+ ${baseStyles}
41
+ .field {
42
+ display: inline-flex;
43
+ flex-direction: column;
44
+ gap: 0.375rem;
45
+ width: 100%;
46
+ }
47
+ .label {
48
+ color: #334155;
49
+ font-size: 0.875rem;
50
+ font-weight: 600;
51
+ }
52
+ .control {
53
+ border: 1px solid #cbd5e1;
54
+ border-radius: 0.75rem;
55
+ box-sizing: border-box;
56
+ font: inherit;
57
+ min-height: 2.75rem;
58
+ outline: none;
59
+ padding: 0.75rem 0.875rem;
60
+ width: 100%;
61
+ background: #fff;
62
+ color: #0f172a;
63
+ transition: border-color 160ms ease, box-shadow 160ms ease;
64
+ }
65
+ .control:focus {
66
+ border-color: #2563eb;
67
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
68
+ }
69
+ .control:disabled {
70
+ background: #f8fafc;
71
+ color: #94a3b8;
72
+ cursor: not-allowed;
73
+ }
74
+ `;
75
+
76
+ const escapeProp = (value: string): string => escapeHtml(value);
77
+
78
+ const handlerStore = new WeakMap<HTMLElement, Record<string, EventListener>>();
79
+
80
+ const readHandler = (element: HTMLElement, key: string): EventListener | undefined => {
81
+ return handlerStore.get(element)?.[key];
82
+ };
83
+
84
+ const storeHandler = (element: HTMLElement, key: string, value: EventListener): void => {
85
+ const handlers = handlerStore.get(element) ?? {};
86
+ handlers[key] = value;
87
+ handlerStore.set(element, handlers);
88
+ };
89
+
90
+ const getShadowLabelText = (element: HTMLElement): string => {
91
+ return element.shadowRoot?.querySelector('.label')?.textContent ?? '';
92
+ };
93
+
94
+ /**
95
+ * Detect a value-only input update, patch the live control in place, and
96
+ * return whether the component can skip a full shadow DOM re-render.
97
+ *
98
+ * @param element - The host custom element whose shadow DOM is being updated
99
+ * @param props - The next reflected input props for the pending update
100
+ */
101
+ const canSkipInputRender = (
102
+ element: HTMLElement,
103
+ props: {
104
+ label: string;
105
+ type: string;
106
+ value: string;
107
+ placeholder: string;
108
+ name: string;
109
+ disabled: boolean;
110
+ }
111
+ ): boolean => {
112
+ const control = element.shadowRoot?.querySelector('input.control') as HTMLInputElement | null;
113
+ if (!control) return false;
114
+
115
+ if (getShadowLabelText(element) !== props.label) return false;
116
+ if ((control.getAttribute('type') ?? 'text') !== props.type) return false;
117
+ if ((control.getAttribute('placeholder') ?? '') !== props.placeholder) return false;
118
+ if ((control.getAttribute('name') ?? '') !== props.name) return false;
119
+ if (control.disabled !== props.disabled) return false;
120
+
121
+ if (control.value !== props.value) {
122
+ control.value = props.value;
123
+ }
124
+
125
+ return true;
126
+ };
127
+
128
+ /**
129
+ * Detect a value-only textarea update, patch the live control in place, and
130
+ * return whether the component can skip a full shadow DOM re-render.
131
+ *
132
+ * @param element - The host custom element whose shadow DOM is being updated
133
+ * @param props - The next reflected textarea props for the pending update
134
+ */
135
+ const canSkipTextareaRender = (
136
+ element: HTMLElement,
137
+ props: {
138
+ label: string;
139
+ value: string;
140
+ placeholder: string;
141
+ name: string;
142
+ rows: number;
143
+ disabled: boolean;
144
+ }
145
+ ): boolean => {
146
+ const control = element.shadowRoot?.querySelector(
147
+ 'textarea.control'
148
+ ) as HTMLTextAreaElement | null;
149
+ if (!control) return false;
150
+
151
+ if (getShadowLabelText(element) !== props.label) return false;
152
+ if ((control.getAttribute('placeholder') ?? '') !== props.placeholder) return false;
153
+ if ((control.getAttribute('name') ?? '') !== props.name) return false;
154
+ if (control.getAttribute('rows') !== String(props.rows)) return false;
155
+ if (control.disabled !== props.disabled) return false;
156
+
157
+ if (control.value !== props.value) {
158
+ control.value = props.value;
159
+ }
160
+ return true;
161
+ };
162
+
163
+ const renderTextareaControl = (props: {
164
+ value: string;
165
+ placeholder: string;
166
+ name: string;
167
+ rows: number;
168
+ disabled: boolean;
169
+ }): string => {
170
+ return [
171
+ '<textarea',
172
+ ' part="control"',
173
+ ' class="control"',
174
+ ` placeholder="${escapeProp(props.placeholder)}"`,
175
+ ` name="${escapeProp(props.name)}"`,
176
+ ` rows="${props.rows}"`,
177
+ props.disabled ? ' disabled' : '',
178
+ `>${escapeProp(props.value)}</textarea>`,
179
+ ].join('');
180
+ };
181
+
182
+ /**
183
+ * Register a default set of foundational UI components.
184
+ *
185
+ * The library is intentionally small and dependency-free, providing common
186
+ * primitives that can be themed via shadow parts and CSS custom properties.
187
+ *
188
+ * @param options - Optional registration settings such as a custom tag prefix
189
+ * @returns The registered tag names for each component
190
+ */
191
+ export const registerDefaultComponents = (
192
+ options: DefaultComponentLibraryOptions = {}
193
+ ): RegisteredDefaultComponents => {
194
+ const prefix = options.prefix ?? getBqueryConfig().components?.prefix ?? 'bq';
195
+ const tags: RegisteredDefaultComponents = {
196
+ button: `${prefix}-button`,
197
+ card: `${prefix}-card`,
198
+ input: `${prefix}-input`,
199
+ textarea: `${prefix}-textarea`,
200
+ checkbox: `${prefix}-checkbox`,
201
+ };
202
+
203
+ component<{
204
+ label: string;
205
+ variant: string;
206
+ size: string;
207
+ type: string;
208
+ disabled: boolean;
209
+ }>(tags.button, {
210
+ props: {
211
+ label: { type: String, default: '' },
212
+ variant: { type: String, default: 'primary' },
213
+ size: { type: String, default: 'md' },
214
+ type: { type: String, default: 'button' },
215
+ disabled: { type: Boolean, default: false },
216
+ },
217
+ styles: `
218
+ ${baseStyles}
219
+ button {
220
+ appearance: none;
221
+ border: 0;
222
+ border-radius: 999px;
223
+ cursor: pointer;
224
+ display: inline-flex;
225
+ align-items: center;
226
+ justify-content: center;
227
+ font: inherit;
228
+ font-weight: 600;
229
+ gap: 0.5rem;
230
+ min-height: 2.5rem;
231
+ padding: 0.65rem 1rem;
232
+ transition: transform 160ms ease, opacity 160ms ease, background 160ms ease;
233
+ background: #2563eb;
234
+ color: #fff;
235
+ }
236
+ button[data-variant='secondary'] {
237
+ background: #e2e8f0;
238
+ color: #0f172a;
239
+ }
240
+ button[data-size='sm'] {
241
+ min-height: 2.125rem;
242
+ padding: 0.5rem 0.875rem;
243
+ }
244
+ button[data-size='lg'] {
245
+ min-height: 3rem;
246
+ padding: 0.875rem 1.25rem;
247
+ }
248
+ button:hover:not(:disabled) {
249
+ transform: translateY(-1px);
250
+ }
251
+ button:disabled {
252
+ cursor: not-allowed;
253
+ opacity: 0.6;
254
+ }
255
+ `,
256
+ render: ({ props }) => html`
257
+ <button
258
+ part="button"
259
+ type="${escapeProp(props.type)}"
260
+ data-variant="${escapeProp(props.variant)}"
261
+ data-size="${escapeProp(props.size)}"
262
+ ${props.disabled ? 'disabled' : ''}
263
+ >
264
+ <slot>${escapeProp(props.label)}</slot>
265
+ </button>
266
+ `,
267
+ });
268
+
269
+ component<{ title: string; footer: string; elevated: boolean }>(tags.card, {
270
+ props: {
271
+ title: { type: String, default: '' },
272
+ footer: { type: String, default: '' },
273
+ elevated: { type: Boolean, default: true },
274
+ },
275
+ styles: `
276
+ ${baseStyles}
277
+ article {
278
+ background: #fff;
279
+ border: 1px solid #e2e8f0;
280
+ border-radius: 1rem;
281
+ box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
282
+ color: #0f172a;
283
+ display: block;
284
+ padding: 1rem;
285
+ }
286
+ article[data-elevated='false'] {
287
+ box-shadow: none;
288
+ }
289
+ header, footer {
290
+ color: #475569;
291
+ font-size: 0.95rem;
292
+ font-weight: 600;
293
+ }
294
+ header {
295
+ margin-bottom: 0.75rem;
296
+ }
297
+ footer {
298
+ margin-top: 0.75rem;
299
+ }
300
+ `,
301
+ render: ({ props }) => html`
302
+ <article part="card" data-elevated="${String(props.elevated)}">
303
+ ${props.title ? `<header part="header">${escapeProp(props.title)}</header>` : ''}
304
+ <section part="body"><slot></slot></section>
305
+ ${props.footer ? `<footer part="footer">${escapeProp(props.footer)}</footer>` : ''}
306
+ </article>
307
+ `,
308
+ });
309
+
310
+ component<{
311
+ label: string;
312
+ type: string;
313
+ value: string;
314
+ placeholder: string;
315
+ name: string;
316
+ disabled: boolean;
317
+ }>(tags.input, {
318
+ props: {
319
+ label: { type: String, default: '' },
320
+ type: { type: String, default: 'text' },
321
+ value: { type: String, default: '' },
322
+ placeholder: { type: String, default: '' },
323
+ name: { type: String, default: '' },
324
+ disabled: { type: Boolean, default: false },
325
+ },
326
+ styles: controlStyles,
327
+ /**
328
+ * Skip the full shadow DOM re-render when only the reflected input value
329
+ * changed, because the live control has already been patched in place.
330
+ */
331
+ beforeUpdate(props) {
332
+ if (canSkipInputRender(this, props)) {
333
+ return false;
334
+ }
335
+ return true;
336
+ },
337
+ connected() {
338
+ const handleInput = (event: Event) => {
339
+ const target = event.target as HTMLInputElement | null;
340
+ if (!target?.matches('input')) return;
341
+ event.stopPropagation();
342
+ this.setAttribute('value', target.value);
343
+ this.dispatchEvent(
344
+ new CustomEvent('input', {
345
+ detail: { value: target.value },
346
+ bubbles: true,
347
+ composed: true,
348
+ })
349
+ );
350
+ };
351
+ storeHandler(this, '__bqueryInputHandler', handleInput);
352
+ this.shadowRoot?.addEventListener('input', handleInput);
353
+ },
354
+ disconnected() {
355
+ const handleInput = readHandler(this, '__bqueryInputHandler');
356
+ if (handleInput) {
357
+ this.shadowRoot?.removeEventListener('input', handleInput);
358
+ }
359
+ },
360
+ render: ({ props }) => html`
361
+ <label part="field" class="field">
362
+ ${props.label ? `<span part="label" class="label">${escapeProp(props.label)}</span>` : ''}
363
+ <input
364
+ part="control"
365
+ class="control"
366
+ type="${escapeProp(props.type)}"
367
+ value="${escapeProp(props.value)}"
368
+ placeholder="${escapeProp(props.placeholder)}"
369
+ name="${escapeProp(props.name)}"
370
+ ${props.disabled ? 'disabled' : ''}
371
+ />
372
+ </label>
373
+ `,
374
+ });
375
+
376
+ component<{
377
+ label: string;
378
+ value: string;
379
+ placeholder: string;
380
+ name: string;
381
+ rows: number;
382
+ disabled: boolean;
383
+ }>(tags.textarea, {
384
+ props: {
385
+ label: { type: String, default: '' },
386
+ value: { type: String, default: '' },
387
+ placeholder: { type: String, default: '' },
388
+ name: { type: String, default: '' },
389
+ rows: { type: Number, default: 4 },
390
+ disabled: { type: Boolean, default: false },
391
+ },
392
+ styles: `${controlStyles}
393
+ textarea.control {
394
+ min-height: 6rem;
395
+ resize: vertical;
396
+ }
397
+ `,
398
+ /**
399
+ * Skip the full shadow DOM re-render when only the reflected textarea value
400
+ * changed, because the live control has already been patched in place.
401
+ */
402
+ beforeUpdate(props) {
403
+ if (canSkipTextareaRender(this, props)) {
404
+ return false;
405
+ }
406
+ return true;
407
+ },
408
+ connected() {
409
+ const handleInput = (event: Event) => {
410
+ const target = event.target as HTMLTextAreaElement | null;
411
+ if (!target?.matches('textarea')) return;
412
+ event.stopPropagation();
413
+ this.setAttribute('value', target.value);
414
+ this.dispatchEvent(
415
+ new CustomEvent('input', {
416
+ detail: { value: target.value },
417
+ bubbles: true,
418
+ composed: true,
419
+ })
420
+ );
421
+ };
422
+ storeHandler(this, '__bqueryTextareaHandler', handleInput);
423
+ this.shadowRoot?.addEventListener('input', handleInput);
424
+ },
425
+ disconnected() {
426
+ const handleInput = readHandler(this, '__bqueryTextareaHandler');
427
+ if (handleInput) {
428
+ this.shadowRoot?.removeEventListener('input', handleInput);
429
+ }
430
+ },
431
+ render: ({ props }) => html`
432
+ <label part="field" class="field">
433
+ ${props.label ? `<span part="label" class="label">${escapeProp(props.label)}</span>` : ''}
434
+ ${renderTextareaControl(props)}
435
+ </label>
436
+ `,
437
+ });
438
+
439
+ component<{ label: string; checked: boolean; disabled: boolean }>(tags.checkbox, {
440
+ props: {
441
+ label: { type: String, default: '' },
442
+ checked: { type: Boolean, default: false },
443
+ disabled: { type: Boolean, default: false },
444
+ },
445
+ styles: `
446
+ ${baseStyles}
447
+ label {
448
+ align-items: center;
449
+ color: #0f172a;
450
+ cursor: pointer;
451
+ display: inline-flex;
452
+ gap: 0.625rem;
453
+ }
454
+ input {
455
+ accent-color: #2563eb;
456
+ block-size: 1rem;
457
+ inline-size: 1rem;
458
+ }
459
+ input:disabled {
460
+ cursor: not-allowed;
461
+ }
462
+ `,
463
+ connected() {
464
+ const handleChange = (event: Event) => {
465
+ const target = event.target as HTMLInputElement | null;
466
+ if (!target?.matches('input[type="checkbox"]')) return;
467
+ event.stopPropagation();
468
+ if (target.checked) {
469
+ this.setAttribute('checked', 'true');
470
+ } else {
471
+ this.removeAttribute('checked');
472
+ }
473
+ this.dispatchEvent(
474
+ new CustomEvent('change', {
475
+ detail: { checked: target.checked },
476
+ bubbles: true,
477
+ composed: true,
478
+ })
479
+ );
480
+ };
481
+ storeHandler(this, '__bqueryCheckboxHandler', handleChange);
482
+ this.shadowRoot?.addEventListener('change', handleChange);
483
+ },
484
+ disconnected() {
485
+ const handleChange = readHandler(this, '__bqueryCheckboxHandler');
486
+ if (handleChange) {
487
+ this.shadowRoot?.removeEventListener('change', handleChange);
488
+ }
489
+ },
490
+ render: ({ props }) => html`
491
+ <label part="label">
492
+ <input
493
+ part="control"
494
+ type="checkbox"
495
+ ${props.checked ? 'checked' : ''}
496
+ ${props.disabled ? 'disabled' : ''}
497
+ />
498
+ <span part="text"><slot>${escapeProp(props.label)}</slot></span>
499
+ </label>
500
+ `,
501
+ });
502
+
503
+ return tags;
504
+ };