sibujs 3.4.0 → 4.0.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 (98) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +118 -15
  3. package/dist/browser.cjs +410 -101
  4. package/dist/browser.d.cts +50 -5
  5. package/dist/browser.d.ts +50 -5
  6. package/dist/browser.js +8 -6
  7. package/dist/build.cjs +777 -385
  8. package/dist/build.js +15 -15
  9. package/dist/cdn.global.js +11 -7
  10. package/dist/{chunk-JCO7AHNU.js → chunk-3EKJK5FZ.js} +73 -34
  11. package/dist/chunk-4MFZMLUU.js +147 -0
  12. package/dist/chunk-52XFPGSN.js +123 -0
  13. package/dist/chunk-5INI7D2L.js +194 -0
  14. package/dist/{chunk-OAVXAFAY.js → chunk-6LG2T7LO.js} +5 -5
  15. package/dist/{chunk-OOUQUCNN.js → chunk-ACBT6UZS.js} +57 -22
  16. package/dist/{chunk-F7HW2NI5.js → chunk-AW6HPJ3L.js} +1 -1
  17. package/dist/chunk-DHDZ7IVN.js +109 -0
  18. package/dist/{chunk-JNGGM2XE.js → chunk-DQVUBVNX.js} +16 -25
  19. package/dist/{chunk-AXFWGY32.js → chunk-DR7H6UGM.js} +232 -67
  20. package/dist/{chunk-WSGLJYFI.js → chunk-E62UADKL.js} +9 -3
  21. package/dist/{chunk-3Z2NGWQI.js → chunk-ES2GERY2.js} +1 -1
  22. package/dist/{chunk-S3V6ZXPI.js → chunk-EWEE7NBF.js} +115 -41
  23. package/dist/{chunk-3DVGCW7O.js → chunk-F64ZPCFE.js} +8 -8
  24. package/dist/{chunk-APFRCIRI.js → chunk-GCSGGA3Q.js} +295 -198
  25. package/dist/{chunk-EX77FXTT.js → chunk-HYCCIYNS.js} +228 -51
  26. package/dist/{chunk-ELXMRSVN.js → chunk-NF3LYCQH.js} +259 -124
  27. package/dist/chunk-O3QQ3XOL.js +61 -0
  28. package/dist/{chunk-BUKYCWHX.js → chunk-PBQEW5VW.js} +14 -10
  29. package/dist/{chunk-H5FI5WDP.js → chunk-RRIDJQ5C.js} +3 -3
  30. package/dist/{chunk-S3NFJO6L.js → chunk-TIRZCERI.js} +36 -5
  31. package/dist/{chunk-ODXVQ3CS.js → chunk-VFHIVEPF.js} +309 -188
  32. package/dist/chunk-VPP2FONR.js +142 -0
  33. package/dist/{chunk-KZBEVFXZ.js → chunk-W55YHPEP.js} +5 -3
  34. package/dist/{chunk-FKQG7FTY.js → chunk-WN6WDFCB.js} +4 -4
  35. package/dist/{chunk-CEBWDD63.js → chunk-WOOMUJFT.js} +228 -86
  36. package/dist/{chunk-ITWCTZYH.js → chunk-Z2CE42DB.js} +19 -25
  37. package/dist/{contracts-ey_Qh8ef.d.cts → contracts-Hg6ITAEF.d.cts} +6 -6
  38. package/dist/{contracts-ey_Qh8ef.d.ts → contracts-Hg6ITAEF.d.ts} +6 -6
  39. package/dist/{customElement-CPfIrbvg.d.ts → customElement-CKLoO63w.d.cts} +73 -11
  40. package/dist/{customElement-CPfIrbvg.d.cts → customElement-CKLoO63w.d.ts} +73 -11
  41. package/dist/data.cjs +483 -205
  42. package/dist/data.d.cts +139 -7
  43. package/dist/data.d.ts +139 -7
  44. package/dist/data.js +9 -9
  45. package/dist/devtools.cjs +222 -87
  46. package/dist/devtools.js +8 -8
  47. package/dist/dispose-4FZ6OFXR.js +19 -0
  48. package/dist/ecosystem.cjs +419 -175
  49. package/dist/ecosystem.js +11 -11
  50. package/dist/extras.cjs +1981 -765
  51. package/dist/extras.d.cts +7 -7
  52. package/dist/extras.d.ts +7 -7
  53. package/dist/extras.js +29 -26
  54. package/dist/index.cjs +784 -385
  55. package/dist/index.d.cts +299 -28
  56. package/dist/index.d.ts +299 -28
  57. package/dist/index.js +33 -18
  58. package/dist/motion.cjs +180 -50
  59. package/dist/motion.d.cts +26 -0
  60. package/dist/motion.d.ts +26 -0
  61. package/dist/motion.js +5 -5
  62. package/dist/patterns.cjs +437 -170
  63. package/dist/patterns.d.cts +60 -13
  64. package/dist/patterns.d.ts +60 -13
  65. package/dist/patterns.js +7 -7
  66. package/dist/performance.cjs +333 -148
  67. package/dist/performance.d.cts +55 -7
  68. package/dist/performance.d.ts +55 -7
  69. package/dist/performance.js +9 -9
  70. package/dist/plugins.cjs +1374 -576
  71. package/dist/plugins.d.cts +227 -9
  72. package/dist/plugins.d.ts +227 -9
  73. package/dist/plugins.js +613 -321
  74. package/dist/{ssr-D62yFwuw.d.cts → ssr-BiPRdZ6n.d.cts} +44 -15
  75. package/dist/{ssr-D62yFwuw.d.ts → ssr-BiPRdZ6n.d.ts} +44 -15
  76. package/dist/{ssr-2PPULEK2.js → ssr-JFP5535P.js} +5 -4
  77. package/dist/ssr.cjs +1104 -443
  78. package/dist/ssr.d.cts +230 -20
  79. package/dist/ssr.d.ts +230 -20
  80. package/dist/ssr.js +15 -12
  81. package/dist/{startup-0Qv6aosO.d.cts → startup-BMpaiMhP.d.cts} +1 -1
  82. package/dist/{startup-0Qv6aosO.d.ts → startup-BMpaiMhP.d.ts} +1 -1
  83. package/dist/testing.cjs +205 -63
  84. package/dist/testing.d.cts +1 -1
  85. package/dist/testing.d.ts +1 -1
  86. package/dist/testing.js +8 -10
  87. package/dist/ui.cjs +640 -199
  88. package/dist/ui.d.cts +2 -2
  89. package/dist/ui.d.ts +2 -2
  90. package/dist/ui.js +10 -10
  91. package/dist/widgets.cjs +207 -82
  92. package/dist/widgets.js +9 -9
  93. package/package.json +12 -7
  94. package/dist/chunk-5VGSK6D2.js +0 -80
  95. package/dist/chunk-COY6PUD2.js +0 -34
  96. package/dist/chunk-K35BAR4P.js +0 -96
  97. package/dist/chunk-L3GAGWCC.js +0 -111
  98. package/dist/chunk-RT3IYFN2.js +0 -38
@@ -32,7 +32,7 @@ declare function withWrapper<P>(WrappedComponent: Component<P>, wrapper: (compon
32
32
  * Button("Click"); // type="button", disabled=false automatically
33
33
  * ```
34
34
  */
35
- declare function withDefaults<P extends Record<string, unknown>>(component: Component<P>, defaults: Partial<P>): Component<Partial<P>>;
35
+ declare function withDefaults<P extends object>(component: Component<P>, defaults: Partial<P>): Component<Partial<P>>;
36
36
  /**
37
37
  * Composes multiple HOC wrappers into a single wrapper.
38
38
  * Applied from right to left (like function composition).
@@ -117,7 +117,7 @@ type WithNodes<Props> = Props & {
117
117
  * // Usage: Button({ label: 'Click me' }) — TypeScript infers props
118
118
  * ```
119
119
  */
120
- declare function defineComponent<Props extends Record<string, unknown>>(config: {
120
+ declare function defineComponent<Props extends object>(config: {
121
121
  defaults?: Partial<Props>;
122
122
  setup: (props: Props) => HTMLElement;
123
123
  }): (props: Props) => HTMLElement;
@@ -148,7 +148,7 @@ declare function defineComponent<Props extends Record<string, unknown>>(config:
148
148
  * // Usage: Card({ title: 'Hello', nodes: p('World') })
149
149
  * ```
150
150
  */
151
- declare function defineSlottedComponent<Props extends Record<string, unknown>>(config: {
151
+ declare function defineSlottedComponent<Props extends object>(config: {
152
152
  defaults?: Partial<Props>;
153
153
  setup: (props: WithNodes<Props>) => HTMLElement;
154
154
  }): (props: WithNodes<Props>) => HTMLElement;
@@ -177,7 +177,7 @@ declare function defineSlottedComponent<Props extends Record<string, unknown>>(c
177
177
  * // Usage: SmallIconButton({ icon: 'star', label: 'Favorite' })
178
178
  * ```
179
179
  */
180
- declare function withProps<OuterProps extends Record<string, unknown>, InnerProps extends Record<string, unknown>>(component: (props: InnerProps) => HTMLElement, mapProps: (outer: OuterProps) => InnerProps): (props: OuterProps) => HTMLElement;
180
+ declare function withProps<OuterProps extends object, InnerProps extends object>(component: (props: InnerProps) => HTMLElement, mapProps: (outer: OuterProps) => InnerProps): (props: OuterProps) => HTMLElement;
181
181
 
182
182
  /**
183
183
  * Runtime prop validation and strict typing contracts for SibuJS.
@@ -216,12 +216,12 @@ type PropSchema<Props> = {
216
216
  * In production mode (process.env.NODE_ENV === 'production'), validation is skipped
217
217
  * and only defaults are applied for performance.
218
218
  */
219
- declare function validateProps<Props extends Record<string, unknown>>(props: Partial<Props>, schema: PropSchema<Props>): Props;
219
+ declare function validateProps<Props extends object>(props: Partial<Props>, schema: PropSchema<Props>): Props;
220
220
  /**
221
221
  * Define a component with runtime prop validation.
222
222
  * Validates props in development mode, applies defaults, then calls setup.
223
223
  */
224
- declare function defineStrictComponent<Props extends Record<string, unknown>>(config: {
224
+ declare function defineStrictComponent<Props extends object>(config: {
225
225
  name: string;
226
226
  props: PropSchema<Props>;
227
227
  setup: (props: Props) => HTMLElement;
@@ -3,7 +3,7 @@ interface FieldConfig<T = unknown> {
3
3
  initial: T;
4
4
  validators?: ValidatorFn<T>[];
5
5
  }
6
- type FormConfig<T extends Record<string, unknown>> = {
6
+ type FormConfig<T extends object> = {
7
7
  [K in keyof T]: FieldConfig<T[K]>;
8
8
  };
9
9
  interface FormField<T = unknown> {
@@ -14,7 +14,7 @@ interface FormField<T = unknown> {
14
14
  touch: () => void;
15
15
  reset: () => void;
16
16
  }
17
- interface FormReturn<T extends Record<string, unknown>> {
17
+ interface FormReturn<T extends object> {
18
18
  fields: {
19
19
  [K in keyof T]: FormField<T[K]>;
20
20
  };
@@ -40,8 +40,18 @@ declare function custom<T>(fn: (value: T) => boolean, message: string): Validato
40
40
  /**
41
41
  * Props returned by bindField, ready to spread into an input tag factory.
42
42
  */
43
- interface BoundFieldProps {
44
- value: () => unknown;
43
+ /**
44
+ * Props produced by {@link bindField}, ready to spread onto a tag factory.
45
+ *
46
+ * Generic over the field's value type. `value: () => unknown` forced every
47
+ * consumer to cast, because `unknown` does not satisfy the typed tag factories'
48
+ * `reactive<string>` — so the declaration contradicted this helper's own
49
+ * documented purpose ("props object ready to pass directly to a tag factory").
50
+ * The parameter defaults to `unknown`, so existing annotations of the bare
51
+ * `BoundFieldProps` keep compiling. (TYPE-010)
52
+ */
53
+ interface BoundFieldProps<T = unknown> {
54
+ value: () => T;
45
55
  on: {
46
56
  input: (e: Event) => void;
47
57
  change: (e: Event) => void;
@@ -69,8 +79,8 @@ interface BoundFieldProps {
69
79
  * input(bindField(f.fields.email, { type: "email", placeholder: "Email" }))
70
80
  * ```
71
81
  */
72
- declare function bindField<T>(field: FormField<T>, extras?: Record<string, unknown>): BoundFieldProps;
73
- declare function form<T extends Record<string, unknown>>(config: FormConfig<T>): FormReturn<T>;
82
+ declare function bindField<T>(field: FormField<T>, extras?: Record<string, unknown>): BoundFieldProps<T>;
83
+ declare function form<T extends object>(config: FormConfig<T>): FormReturn<T>;
74
84
 
75
85
  interface VirtualListProps<T> {
76
86
  items: () => T[];
@@ -195,25 +205,58 @@ declare function withScopedStyle<P>(css: string, component: (props: P) => HTMLEl
195
205
  */
196
206
  declare function removeScopedStyle(scopeId: string): void;
197
207
 
208
+ /**
209
+ * Every value the shared attribute primitive accepts, and nothing more.
210
+ *
211
+ * `null` / `undefined` are part of the contract, not an oversight: they REMOVE
212
+ * the attribute. That is what makes the ordinary optional-value getter —
213
+ * `() => user?.label` typed `string | null | undefined` — expressible without a
214
+ * cast. The declared type used to exclude both while the runtime handled them,
215
+ * so the documented behaviour was unreachable from type-checked code.
216
+ *
217
+ * Deliberately NOT widened to `unknown`: the runtime only defines behaviour for
218
+ * these, and a public signature should describe the runtime exactly rather than
219
+ * inviting values whose handling is incidental.
220
+ */
221
+ type AttributeValue = string | number | boolean | null | undefined;
222
+ /** An attribute value, or a reactive getter producing one. */
223
+ type AttributeSource = AttributeValue | (() => AttributeValue);
198
224
  /**
199
225
  * Bind multiple reactive attributes to an element.
200
226
  * Each attribute value can be a static value or a reactive getter.
201
227
  * Returns a single teardown function that stops all bindings.
228
+ *
229
+ * Static and reactive values share ONE security policy. They previously did
230
+ * not: a getter went through `bindAttribute` (which refuses `on*` and sanitizes
231
+ * URL/style/srcset), while a plain string went straight to `setAttribute`. So
232
+ * `bindAttrs(a, { href: "javascript:…" })` was live XSS while the identical
233
+ * `bindAttrs(a, { href: () => "javascript:…" })` was blocked — the same
234
+ * authoring intent with two different verdicts. Both now commit through
235
+ * `setSafeAttribute`.
202
236
  */
203
- declare function bindAttrs(el: HTMLElement, attrs: Record<string, string | number | boolean | (() => string | number | boolean)>): () => void;
237
+ declare function bindAttrs(el: HTMLElement, attrs: Record<string, AttributeSource>): () => void;
204
238
  /**
205
239
  * Reactively toggle a boolean attribute (like disabled, readonly, hidden).
206
240
  * When the value is truthy the attribute is present (set to ""),
207
241
  * when falsy the attribute is removed entirely.
208
242
  * Returns a teardown function to stop reactive tracking.
243
+ *
244
+ * The attribute NAME is policed too: `bindBoolAttr(el, "onclick", true)` would
245
+ * otherwise create a live `onclick=""` handler slot through a helper whose
246
+ * whole purpose is inert presence toggling.
209
247
  */
210
248
  declare function bindBoolAttr(el: HTMLElement, attr: string, getter: boolean | (() => boolean)): () => void;
211
249
  /**
212
250
  * Bind a data-* attribute reactively.
213
251
  * Shorthand for `bindAttribute(el, "data-<key>", getter)`.
214
252
  * Returns a teardown function to stop reactive tracking.
253
+ *
254
+ * Shares the removal semantics of every other attribute writer: `null` /
255
+ * `undefined` remove `data-<key>` rather than writing the text "null". The
256
+ * value is handed to the primitive unstringified so that decision is made in
257
+ * one place.
215
258
  */
216
- declare function bindData(el: HTMLElement, key: string, getter: string | (() => string)): () => void;
259
+ declare function bindData(el: HTMLElement, key: string, getter: AttributeValue | (() => AttributeValue)): () => void;
217
260
 
218
261
  /**
219
262
  * Test-only helper to reset the module-level stack between specs. Client-only:
@@ -261,6 +304,15 @@ declare function toast(options?: {
261
304
  /**
262
305
  * infiniteScroll combines IntersectionObserver with a data-fetching trigger
263
306
  * to implement infinite scroll behavior.
307
+ *
308
+ * ERROR CONTRACT: `onLoadMore` is user code and may reject. The observer
309
+ * callback cannot `await` it, so the promise it starts is floating — and a
310
+ * floating rejection is an unhandled rejection, which crashes a Node SSR
311
+ * process and fires `window.onunhandledrejection` in a browser, for what is
312
+ * really just a failed page of data. `loadMore()` therefore CONTAINS its own
313
+ * failure: it reports through the framework's central error pipeline
314
+ * (`reportError`, phase `"async"`) and never rejects to its caller. The
315
+ * `loading` flag is always cleared, so a failed page does not wedge the list.
264
316
  */
265
317
  declare function infiniteScroll(options: {
266
318
  onLoadMore: () => Promise<void>;
@@ -296,7 +348,7 @@ declare function pagination(options: {
296
348
  * eventBus creates a typed publish/subscribe event system.
297
349
  * No reactive state needed -- pure event dispatching.
298
350
  */
299
- declare function eventBus<T extends Record<string, unknown>>(): {
351
+ declare function eventBus<T extends object>(): {
300
352
  on: <K extends keyof T>(event: K, handler: (data: T[K]) => void) => () => void;
301
353
  emit: <K extends keyof T>(event: K, data: T[K]) => void;
302
354
  off: <K extends keyof T>(event: K, handler: (data: T[K]) => void) => void;
@@ -308,7 +360,6 @@ interface CustomElementOptions {
308
360
  mode?: "open" | "closed";
309
361
  styles?: string;
310
362
  observedAttributes?: string[];
311
- extends?: string;
312
363
  }
313
364
  /**
314
365
  * defineElement creates a Web Component wrapping a SibuJS component function.
@@ -316,7 +367,18 @@ interface CustomElementOptions {
316
367
  declare function defineElement(name: string, component: (props: Record<string, unknown>, element: HTMLElement) => HTMLElement, options?: CustomElementOptions): void;
317
368
  /**
318
369
  * Creates an SVG element with proper namespace.
370
+ *
371
+ * Attribute writes go through the framework's shared commit primitive, so the
372
+ * SVG helper enforces exactly the policy the HTML tag factory and the reactive
373
+ * bindings enforce: `on*` strings are refused, `href` / `xlink:href` go through
374
+ * the URL allowlist, and `style` goes through the declaration-list sanitizer.
375
+ * Previously every non-function prop was a raw `setAttribute`, so
376
+ * `svgElement("svg", { onload: "alert(1)" })` installed a live handler that the
377
+ * equivalent HTML call had always refused.
378
+ *
379
+ * Function-valued `on*` props keep their existing meaning — `addEventListener`,
380
+ * never an attribute.
319
381
  */
320
382
  declare function svgElement(tag: string, props?: Record<string, unknown>, ...nodes: (SVGElement | string)[]): SVGElement;
321
383
 
322
- export { lazyLoad as A, type BoundFieldProps as B, type CustomElementOptions as C, matchesPattern as D, max as E, type FieldConfig as F, maxLength as G, min as H, type IntersectionResult as I, minLength as J, pagination as K, phoneMask as L, type MaskOptions as M, removeScopedStyle as N, required as O, scopedStyle as P, ssnMask as Q, svgElement as R, timeMask as S, type Toast as T, toast as U, type ValidatorFn as V, withScopedStyle as W, zipMask as X, __resetDialogStack as _, FocusTrap as a, type FormConfig as b, type FormField as c, type FormReturn as d, type ToastInstance as e, VirtualList as f, type VirtualListProps as g, announce as h, aria as i, bindAttrs as j, bindBoolAttr as k, bindData as l, bindField as m, creditCardMask as n, custom as o, dateMask as p, defineElement as q, dialog as r, email as s, eventBus as t, focus as u, form as v, hotkey as w, infiniteScroll as x, inputMask as y, intersection as z };
384
+ export { type AttributeSource as A, type BoundFieldProps as B, type CustomElementOptions as C, intersection as D, lazyLoad as E, type FieldConfig as F, matchesPattern as G, max as H, type IntersectionResult as I, maxLength as J, min as K, minLength as L, type MaskOptions as M, pagination as N, phoneMask as O, removeScopedStyle as P, required as Q, scopedStyle as R, ssnMask as S, type Toast as T, svgElement as U, type ValidatorFn as V, timeMask as W, toast as X, withScopedStyle as Y, zipMask as Z, __resetDialogStack as _, type AttributeValue as a, FocusTrap as b, type FormConfig as c, type FormField as d, type FormReturn as e, type ToastInstance as f, VirtualList as g, type VirtualListProps as h, announce as i, aria as j, bindAttrs as k, bindBoolAttr as l, bindData as m, bindField as n, creditCardMask as o, custom as p, dateMask as q, defineElement as r, dialog as s, email as t, eventBus as u, focus as v, form as w, hotkey as x, infiniteScroll as y, inputMask as z };
@@ -3,7 +3,7 @@ interface FieldConfig<T = unknown> {
3
3
  initial: T;
4
4
  validators?: ValidatorFn<T>[];
5
5
  }
6
- type FormConfig<T extends Record<string, unknown>> = {
6
+ type FormConfig<T extends object> = {
7
7
  [K in keyof T]: FieldConfig<T[K]>;
8
8
  };
9
9
  interface FormField<T = unknown> {
@@ -14,7 +14,7 @@ interface FormField<T = unknown> {
14
14
  touch: () => void;
15
15
  reset: () => void;
16
16
  }
17
- interface FormReturn<T extends Record<string, unknown>> {
17
+ interface FormReturn<T extends object> {
18
18
  fields: {
19
19
  [K in keyof T]: FormField<T[K]>;
20
20
  };
@@ -40,8 +40,18 @@ declare function custom<T>(fn: (value: T) => boolean, message: string): Validato
40
40
  /**
41
41
  * Props returned by bindField, ready to spread into an input tag factory.
42
42
  */
43
- interface BoundFieldProps {
44
- value: () => unknown;
43
+ /**
44
+ * Props produced by {@link bindField}, ready to spread onto a tag factory.
45
+ *
46
+ * Generic over the field's value type. `value: () => unknown` forced every
47
+ * consumer to cast, because `unknown` does not satisfy the typed tag factories'
48
+ * `reactive<string>` — so the declaration contradicted this helper's own
49
+ * documented purpose ("props object ready to pass directly to a tag factory").
50
+ * The parameter defaults to `unknown`, so existing annotations of the bare
51
+ * `BoundFieldProps` keep compiling. (TYPE-010)
52
+ */
53
+ interface BoundFieldProps<T = unknown> {
54
+ value: () => T;
45
55
  on: {
46
56
  input: (e: Event) => void;
47
57
  change: (e: Event) => void;
@@ -69,8 +79,8 @@ interface BoundFieldProps {
69
79
  * input(bindField(f.fields.email, { type: "email", placeholder: "Email" }))
70
80
  * ```
71
81
  */
72
- declare function bindField<T>(field: FormField<T>, extras?: Record<string, unknown>): BoundFieldProps;
73
- declare function form<T extends Record<string, unknown>>(config: FormConfig<T>): FormReturn<T>;
82
+ declare function bindField<T>(field: FormField<T>, extras?: Record<string, unknown>): BoundFieldProps<T>;
83
+ declare function form<T extends object>(config: FormConfig<T>): FormReturn<T>;
74
84
 
75
85
  interface VirtualListProps<T> {
76
86
  items: () => T[];
@@ -195,25 +205,58 @@ declare function withScopedStyle<P>(css: string, component: (props: P) => HTMLEl
195
205
  */
196
206
  declare function removeScopedStyle(scopeId: string): void;
197
207
 
208
+ /**
209
+ * Every value the shared attribute primitive accepts, and nothing more.
210
+ *
211
+ * `null` / `undefined` are part of the contract, not an oversight: they REMOVE
212
+ * the attribute. That is what makes the ordinary optional-value getter —
213
+ * `() => user?.label` typed `string | null | undefined` — expressible without a
214
+ * cast. The declared type used to exclude both while the runtime handled them,
215
+ * so the documented behaviour was unreachable from type-checked code.
216
+ *
217
+ * Deliberately NOT widened to `unknown`: the runtime only defines behaviour for
218
+ * these, and a public signature should describe the runtime exactly rather than
219
+ * inviting values whose handling is incidental.
220
+ */
221
+ type AttributeValue = string | number | boolean | null | undefined;
222
+ /** An attribute value, or a reactive getter producing one. */
223
+ type AttributeSource = AttributeValue | (() => AttributeValue);
198
224
  /**
199
225
  * Bind multiple reactive attributes to an element.
200
226
  * Each attribute value can be a static value or a reactive getter.
201
227
  * Returns a single teardown function that stops all bindings.
228
+ *
229
+ * Static and reactive values share ONE security policy. They previously did
230
+ * not: a getter went through `bindAttribute` (which refuses `on*` and sanitizes
231
+ * URL/style/srcset), while a plain string went straight to `setAttribute`. So
232
+ * `bindAttrs(a, { href: "javascript:…" })` was live XSS while the identical
233
+ * `bindAttrs(a, { href: () => "javascript:…" })` was blocked — the same
234
+ * authoring intent with two different verdicts. Both now commit through
235
+ * `setSafeAttribute`.
202
236
  */
203
- declare function bindAttrs(el: HTMLElement, attrs: Record<string, string | number | boolean | (() => string | number | boolean)>): () => void;
237
+ declare function bindAttrs(el: HTMLElement, attrs: Record<string, AttributeSource>): () => void;
204
238
  /**
205
239
  * Reactively toggle a boolean attribute (like disabled, readonly, hidden).
206
240
  * When the value is truthy the attribute is present (set to ""),
207
241
  * when falsy the attribute is removed entirely.
208
242
  * Returns a teardown function to stop reactive tracking.
243
+ *
244
+ * The attribute NAME is policed too: `bindBoolAttr(el, "onclick", true)` would
245
+ * otherwise create a live `onclick=""` handler slot through a helper whose
246
+ * whole purpose is inert presence toggling.
209
247
  */
210
248
  declare function bindBoolAttr(el: HTMLElement, attr: string, getter: boolean | (() => boolean)): () => void;
211
249
  /**
212
250
  * Bind a data-* attribute reactively.
213
251
  * Shorthand for `bindAttribute(el, "data-<key>", getter)`.
214
252
  * Returns a teardown function to stop reactive tracking.
253
+ *
254
+ * Shares the removal semantics of every other attribute writer: `null` /
255
+ * `undefined` remove `data-<key>` rather than writing the text "null". The
256
+ * value is handed to the primitive unstringified so that decision is made in
257
+ * one place.
215
258
  */
216
- declare function bindData(el: HTMLElement, key: string, getter: string | (() => string)): () => void;
259
+ declare function bindData(el: HTMLElement, key: string, getter: AttributeValue | (() => AttributeValue)): () => void;
217
260
 
218
261
  /**
219
262
  * Test-only helper to reset the module-level stack between specs. Client-only:
@@ -261,6 +304,15 @@ declare function toast(options?: {
261
304
  /**
262
305
  * infiniteScroll combines IntersectionObserver with a data-fetching trigger
263
306
  * to implement infinite scroll behavior.
307
+ *
308
+ * ERROR CONTRACT: `onLoadMore` is user code and may reject. The observer
309
+ * callback cannot `await` it, so the promise it starts is floating — and a
310
+ * floating rejection is an unhandled rejection, which crashes a Node SSR
311
+ * process and fires `window.onunhandledrejection` in a browser, for what is
312
+ * really just a failed page of data. `loadMore()` therefore CONTAINS its own
313
+ * failure: it reports through the framework's central error pipeline
314
+ * (`reportError`, phase `"async"`) and never rejects to its caller. The
315
+ * `loading` flag is always cleared, so a failed page does not wedge the list.
264
316
  */
265
317
  declare function infiniteScroll(options: {
266
318
  onLoadMore: () => Promise<void>;
@@ -296,7 +348,7 @@ declare function pagination(options: {
296
348
  * eventBus creates a typed publish/subscribe event system.
297
349
  * No reactive state needed -- pure event dispatching.
298
350
  */
299
- declare function eventBus<T extends Record<string, unknown>>(): {
351
+ declare function eventBus<T extends object>(): {
300
352
  on: <K extends keyof T>(event: K, handler: (data: T[K]) => void) => () => void;
301
353
  emit: <K extends keyof T>(event: K, data: T[K]) => void;
302
354
  off: <K extends keyof T>(event: K, handler: (data: T[K]) => void) => void;
@@ -308,7 +360,6 @@ interface CustomElementOptions {
308
360
  mode?: "open" | "closed";
309
361
  styles?: string;
310
362
  observedAttributes?: string[];
311
- extends?: string;
312
363
  }
313
364
  /**
314
365
  * defineElement creates a Web Component wrapping a SibuJS component function.
@@ -316,7 +367,18 @@ interface CustomElementOptions {
316
367
  declare function defineElement(name: string, component: (props: Record<string, unknown>, element: HTMLElement) => HTMLElement, options?: CustomElementOptions): void;
317
368
  /**
318
369
  * Creates an SVG element with proper namespace.
370
+ *
371
+ * Attribute writes go through the framework's shared commit primitive, so the
372
+ * SVG helper enforces exactly the policy the HTML tag factory and the reactive
373
+ * bindings enforce: `on*` strings are refused, `href` / `xlink:href` go through
374
+ * the URL allowlist, and `style` goes through the declaration-list sanitizer.
375
+ * Previously every non-function prop was a raw `setAttribute`, so
376
+ * `svgElement("svg", { onload: "alert(1)" })` installed a live handler that the
377
+ * equivalent HTML call had always refused.
378
+ *
379
+ * Function-valued `on*` props keep their existing meaning — `addEventListener`,
380
+ * never an attribute.
319
381
  */
320
382
  declare function svgElement(tag: string, props?: Record<string, unknown>, ...nodes: (SVGElement | string)[]): SVGElement;
321
383
 
322
- export { lazyLoad as A, type BoundFieldProps as B, type CustomElementOptions as C, matchesPattern as D, max as E, type FieldConfig as F, maxLength as G, min as H, type IntersectionResult as I, minLength as J, pagination as K, phoneMask as L, type MaskOptions as M, removeScopedStyle as N, required as O, scopedStyle as P, ssnMask as Q, svgElement as R, timeMask as S, type Toast as T, toast as U, type ValidatorFn as V, withScopedStyle as W, zipMask as X, __resetDialogStack as _, FocusTrap as a, type FormConfig as b, type FormField as c, type FormReturn as d, type ToastInstance as e, VirtualList as f, type VirtualListProps as g, announce as h, aria as i, bindAttrs as j, bindBoolAttr as k, bindData as l, bindField as m, creditCardMask as n, custom as o, dateMask as p, defineElement as q, dialog as r, email as s, eventBus as t, focus as u, form as v, hotkey as w, infiniteScroll as x, inputMask as y, intersection as z };
384
+ export { type AttributeSource as A, type BoundFieldProps as B, type CustomElementOptions as C, intersection as D, lazyLoad as E, type FieldConfig as F, matchesPattern as G, max as H, type IntersectionResult as I, maxLength as J, min as K, minLength as L, type MaskOptions as M, pagination as N, phoneMask as O, removeScopedStyle as P, required as Q, scopedStyle as R, ssnMask as S, type Toast as T, svgElement as U, type ValidatorFn as V, timeMask as W, toast as X, withScopedStyle as Y, zipMask as Z, __resetDialogStack as _, type AttributeValue as a, FocusTrap as b, type FormConfig as c, type FormField as d, type FormReturn as e, type ToastInstance as f, VirtualList as g, type VirtualListProps as h, announce as i, aria as j, bindAttrs as k, bindBoolAttr as l, bindData as m, bindField as n, creditCardMask as o, custom as p, dateMask as q, defineElement as r, dialog as s, email as t, eventBus as u, focus as v, form as w, hotkey as x, infiniteScroll as y, inputMask as z };