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