@bquery/bquery 1.2.0 → 1.3.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 (305) hide show
  1. package/README.md +501 -427
  2. package/dist/batch-4LAvfLE7.js +13 -0
  3. package/dist/batch-4LAvfLE7.js.map +1 -0
  4. package/dist/component/component.d.ts +69 -0
  5. package/dist/component/component.d.ts.map +1 -0
  6. package/dist/component/html.d.ts +35 -0
  7. package/dist/component/html.d.ts.map +1 -0
  8. package/dist/component/index.d.ts +3 -126
  9. package/dist/component/index.d.ts.map +1 -1
  10. package/dist/component/props.d.ts +18 -0
  11. package/dist/component/props.d.ts.map +1 -0
  12. package/dist/component/types.d.ts +77 -0
  13. package/dist/component/types.d.ts.map +1 -0
  14. package/dist/component.es.mjs +90 -59
  15. package/dist/component.es.mjs.map +1 -1
  16. package/dist/core/collection.d.ts +36 -0
  17. package/dist/core/collection.d.ts.map +1 -1
  18. package/dist/core/dom.d.ts +6 -0
  19. package/dist/core/dom.d.ts.map +1 -0
  20. package/dist/core/element.d.ts +8 -0
  21. package/dist/core/element.d.ts.map +1 -1
  22. package/dist/core/index.d.ts +1 -0
  23. package/dist/core/index.d.ts.map +1 -1
  24. package/dist/core/utils/array.d.ts +74 -0
  25. package/dist/core/utils/array.d.ts.map +1 -0
  26. package/dist/core/utils/function.d.ts +70 -0
  27. package/dist/core/utils/function.d.ts.map +1 -0
  28. package/dist/core/utils/index.d.ts +70 -0
  29. package/dist/core/utils/index.d.ts.map +1 -0
  30. package/dist/core/utils/misc.d.ts +63 -0
  31. package/dist/core/utils/misc.d.ts.map +1 -0
  32. package/dist/core/utils/number.d.ts +65 -0
  33. package/dist/core/utils/number.d.ts.map +1 -0
  34. package/dist/core/utils/object.d.ts +133 -0
  35. package/dist/core/utils/object.d.ts.map +1 -0
  36. package/dist/core/utils/string.d.ts +80 -0
  37. package/dist/core/utils/string.d.ts.map +1 -0
  38. package/dist/core/utils/type-guards.d.ts +79 -0
  39. package/dist/core/utils/type-guards.d.ts.map +1 -0
  40. package/dist/core-COenAZjD.js +145 -0
  41. package/dist/core-COenAZjD.js.map +1 -0
  42. package/dist/core.es.mjs +411 -448
  43. package/dist/core.es.mjs.map +1 -1
  44. package/dist/full.d.ts +2 -2
  45. package/dist/full.d.ts.map +1 -1
  46. package/dist/full.es.mjs +87 -64
  47. package/dist/full.es.mjs.map +1 -1
  48. package/dist/full.iife.js +2 -2
  49. package/dist/full.iife.js.map +1 -1
  50. package/dist/full.umd.js +2 -2
  51. package/dist/full.umd.js.map +1 -1
  52. package/dist/index.es.mjs +138 -68
  53. package/dist/index.es.mjs.map +1 -1
  54. package/dist/motion/animate.d.ts +25 -0
  55. package/dist/motion/animate.d.ts.map +1 -0
  56. package/dist/motion/easing.d.ts +30 -0
  57. package/dist/motion/easing.d.ts.map +1 -0
  58. package/dist/motion/flip.d.ts +55 -0
  59. package/dist/motion/flip.d.ts.map +1 -0
  60. package/dist/motion/index.d.ts +11 -138
  61. package/dist/motion/index.d.ts.map +1 -1
  62. package/dist/motion/keyframes.d.ts +21 -0
  63. package/dist/motion/keyframes.d.ts.map +1 -0
  64. package/dist/motion/reduced-motion.d.ts +12 -0
  65. package/dist/motion/reduced-motion.d.ts.map +1 -0
  66. package/dist/motion/scroll.d.ts +15 -0
  67. package/dist/motion/scroll.d.ts.map +1 -0
  68. package/dist/motion/spring.d.ts +42 -0
  69. package/dist/motion/spring.d.ts.map +1 -0
  70. package/dist/motion/stagger.d.ts +22 -0
  71. package/dist/motion/stagger.d.ts.map +1 -0
  72. package/dist/motion/timeline.d.ts +21 -0
  73. package/dist/motion/timeline.d.ts.map +1 -0
  74. package/dist/motion/transition.d.ts +22 -0
  75. package/dist/motion/transition.d.ts.map +1 -0
  76. package/dist/motion/types.d.ts +182 -0
  77. package/dist/motion/types.d.ts.map +1 -0
  78. package/dist/motion.es.mjs +320 -61
  79. package/dist/motion.es.mjs.map +1 -1
  80. package/dist/persisted-Dz_ryNuC.js +278 -0
  81. package/dist/persisted-Dz_ryNuC.js.map +1 -0
  82. package/dist/reactive/batch.d.ts +13 -0
  83. package/dist/reactive/batch.d.ts.map +1 -0
  84. package/dist/reactive/computed.d.ts +50 -0
  85. package/dist/reactive/computed.d.ts.map +1 -0
  86. package/dist/reactive/core.d.ts +60 -0
  87. package/dist/reactive/core.d.ts.map +1 -0
  88. package/dist/reactive/effect.d.ts +15 -0
  89. package/dist/reactive/effect.d.ts.map +1 -0
  90. package/dist/reactive/index.d.ts +2 -2
  91. package/dist/reactive/index.d.ts.map +1 -1
  92. package/dist/reactive/internals.d.ts +36 -0
  93. package/dist/reactive/internals.d.ts.map +1 -0
  94. package/dist/reactive/linked.d.ts +36 -0
  95. package/dist/reactive/linked.d.ts.map +1 -0
  96. package/dist/reactive/persisted.d.ts +14 -0
  97. package/dist/reactive/persisted.d.ts.map +1 -0
  98. package/dist/reactive/readonly.d.ts +26 -0
  99. package/dist/reactive/readonly.d.ts.map +1 -0
  100. package/dist/reactive/signal.d.ts +13 -312
  101. package/dist/reactive/signal.d.ts.map +1 -1
  102. package/dist/reactive/type-guards.d.ts +20 -0
  103. package/dist/reactive/type-guards.d.ts.map +1 -0
  104. package/dist/reactive/untrack.d.ts +29 -0
  105. package/dist/reactive/untrack.d.ts.map +1 -0
  106. package/dist/reactive/watch.d.ts +42 -0
  107. package/dist/reactive/watch.d.ts.map +1 -0
  108. package/dist/reactive.es.mjs +30 -163
  109. package/dist/reactive.es.mjs.map +1 -1
  110. package/dist/router/index.d.ts +6 -252
  111. package/dist/router/index.d.ts.map +1 -1
  112. package/dist/router/links.d.ts +44 -0
  113. package/dist/router/links.d.ts.map +1 -0
  114. package/dist/router/match.d.ts +20 -0
  115. package/dist/router/match.d.ts.map +1 -0
  116. package/dist/router/navigation.d.ts +45 -0
  117. package/dist/router/navigation.d.ts.map +1 -0
  118. package/dist/router/query.d.ts +16 -0
  119. package/dist/router/query.d.ts.map +1 -0
  120. package/dist/router/router.d.ts +34 -0
  121. package/dist/router/router.d.ts.map +1 -0
  122. package/dist/router/state.d.ts +27 -0
  123. package/dist/router/state.d.ts.map +1 -0
  124. package/dist/router/types.d.ts +88 -0
  125. package/dist/router/types.d.ts.map +1 -0
  126. package/dist/router/utils.d.ts +65 -0
  127. package/dist/router/utils.d.ts.map +1 -0
  128. package/dist/router.es.mjs +168 -132
  129. package/dist/router.es.mjs.map +1 -1
  130. package/dist/sanitize-1FBEPAFH.js +272 -0
  131. package/dist/sanitize-1FBEPAFH.js.map +1 -0
  132. package/dist/security/constants.d.ts +42 -0
  133. package/dist/security/constants.d.ts.map +1 -0
  134. package/dist/security/csp.d.ts +24 -0
  135. package/dist/security/csp.d.ts.map +1 -0
  136. package/dist/security/index.d.ts +4 -2
  137. package/dist/security/index.d.ts.map +1 -1
  138. package/dist/security/sanitize-core.d.ts +13 -0
  139. package/dist/security/sanitize-core.d.ts.map +1 -0
  140. package/dist/security/sanitize.d.ts +5 -57
  141. package/dist/security/sanitize.d.ts.map +1 -1
  142. package/dist/security/trusted-types.d.ts +25 -0
  143. package/dist/security/trusted-types.d.ts.map +1 -0
  144. package/dist/security/types.d.ts +36 -0
  145. package/dist/security/types.d.ts.map +1 -0
  146. package/dist/security.es.mjs +50 -277
  147. package/dist/security.es.mjs.map +1 -1
  148. package/dist/store/create-store.d.ts +15 -0
  149. package/dist/store/create-store.d.ts.map +1 -0
  150. package/dist/store/define-store.d.ts +28 -0
  151. package/dist/store/define-store.d.ts.map +1 -0
  152. package/dist/store/devtools.d.ts +22 -0
  153. package/dist/store/devtools.d.ts.map +1 -0
  154. package/dist/store/index.d.ts +10 -286
  155. package/dist/store/index.d.ts.map +1 -1
  156. package/dist/store/mapping.d.ts +28 -0
  157. package/dist/store/mapping.d.ts.map +1 -0
  158. package/dist/store/persisted.d.ts +13 -0
  159. package/dist/store/persisted.d.ts.map +1 -0
  160. package/dist/store/plugins.d.ts +13 -0
  161. package/dist/store/plugins.d.ts.map +1 -0
  162. package/dist/store/registry.d.ts +28 -0
  163. package/dist/store/registry.d.ts.map +1 -0
  164. package/dist/store/types.d.ts +71 -0
  165. package/dist/store/types.d.ts.map +1 -0
  166. package/dist/store/utils.d.ts +28 -0
  167. package/dist/store/utils.d.ts.map +1 -0
  168. package/dist/store/watch.d.ts +23 -0
  169. package/dist/store/watch.d.ts.map +1 -0
  170. package/dist/store.es.mjs +22 -224
  171. package/dist/store.es.mjs.map +1 -1
  172. package/dist/type-guards-DRma3-Kc.js +16 -0
  173. package/dist/type-guards-DRma3-Kc.js.map +1 -0
  174. package/dist/untrack-BuEQKH7_.js +6 -0
  175. package/dist/untrack-BuEQKH7_.js.map +1 -0
  176. package/dist/view/directives/bind.d.ts +7 -0
  177. package/dist/view/directives/bind.d.ts.map +1 -0
  178. package/dist/view/directives/class.d.ts +8 -0
  179. package/dist/view/directives/class.d.ts.map +1 -0
  180. package/dist/view/directives/for.d.ts +23 -0
  181. package/dist/view/directives/for.d.ts.map +1 -0
  182. package/dist/view/directives/html.d.ts +7 -0
  183. package/dist/view/directives/html.d.ts.map +1 -0
  184. package/dist/view/directives/if.d.ts +7 -0
  185. package/dist/view/directives/if.d.ts.map +1 -0
  186. package/dist/view/directives/index.d.ts +12 -0
  187. package/dist/view/directives/index.d.ts.map +1 -0
  188. package/dist/view/directives/model.d.ts +7 -0
  189. package/dist/view/directives/model.d.ts.map +1 -0
  190. package/dist/view/directives/on.d.ts +7 -0
  191. package/dist/view/directives/on.d.ts.map +1 -0
  192. package/dist/view/directives/ref.d.ts +7 -0
  193. package/dist/view/directives/ref.d.ts.map +1 -0
  194. package/dist/view/directives/show.d.ts +7 -0
  195. package/dist/view/directives/show.d.ts.map +1 -0
  196. package/dist/view/directives/style.d.ts +7 -0
  197. package/dist/view/directives/style.d.ts.map +1 -0
  198. package/dist/view/directives/text.d.ts +7 -0
  199. package/dist/view/directives/text.d.ts.map +1 -0
  200. package/dist/view/evaluate.d.ts +43 -0
  201. package/dist/view/evaluate.d.ts.map +1 -0
  202. package/dist/view/index.d.ts +3 -93
  203. package/dist/view/index.d.ts.map +1 -1
  204. package/dist/view/mount.d.ts +69 -0
  205. package/dist/view/mount.d.ts.map +1 -0
  206. package/dist/view/process.d.ts +26 -0
  207. package/dist/view/process.d.ts.map +1 -0
  208. package/dist/view/types.d.ts +36 -0
  209. package/dist/view/types.d.ts.map +1 -0
  210. package/dist/view.es.mjs +368 -267
  211. package/dist/view.es.mjs.map +1 -1
  212. package/dist/watch-CXyaBC_9.js +58 -0
  213. package/dist/watch-CXyaBC_9.js.map +1 -0
  214. package/package.json +132 -132
  215. package/src/component/component.ts +289 -0
  216. package/src/component/html.ts +53 -0
  217. package/src/component/index.ts +40 -414
  218. package/src/component/props.ts +116 -0
  219. package/src/component/types.ts +85 -0
  220. package/src/core/collection.ts +588 -454
  221. package/src/core/dom.ts +38 -0
  222. package/src/core/element.ts +746 -740
  223. package/src/core/index.ts +43 -0
  224. package/src/core/utils/array.ts +102 -0
  225. package/src/core/utils/function.ts +110 -0
  226. package/src/core/utils/index.ts +83 -0
  227. package/src/core/utils/misc.ts +82 -0
  228. package/src/core/utils/number.ts +78 -0
  229. package/src/core/utils/object.ts +206 -0
  230. package/src/core/utils/string.ts +112 -0
  231. package/src/core/utils/type-guards.ts +112 -0
  232. package/src/full.ts +187 -150
  233. package/src/index.ts +36 -36
  234. package/src/motion/animate.ts +113 -0
  235. package/src/motion/easing.ts +40 -0
  236. package/src/motion/flip.ts +176 -0
  237. package/src/motion/index.ts +41 -358
  238. package/src/motion/keyframes.ts +46 -0
  239. package/src/motion/reduced-motion.ts +17 -0
  240. package/src/motion/scroll.ts +57 -0
  241. package/src/motion/spring.ts +150 -0
  242. package/src/motion/stagger.ts +43 -0
  243. package/src/motion/timeline.ts +246 -0
  244. package/src/motion/transition.ts +51 -0
  245. package/src/motion/types.ts +198 -0
  246. package/src/reactive/batch.ts +22 -0
  247. package/src/reactive/computed.ts +92 -0
  248. package/src/reactive/core.ts +93 -0
  249. package/src/reactive/effect.ts +43 -0
  250. package/src/reactive/index.ts +23 -22
  251. package/src/reactive/internals.ts +105 -0
  252. package/src/reactive/linked.ts +56 -0
  253. package/src/reactive/persisted.ts +74 -0
  254. package/src/reactive/readonly.ts +35 -0
  255. package/src/reactive/signal.ts +20 -520
  256. package/src/reactive/type-guards.ts +22 -0
  257. package/src/reactive/untrack.ts +31 -0
  258. package/src/reactive/watch.ts +73 -0
  259. package/src/router/index.ts +41 -718
  260. package/src/router/links.ts +130 -0
  261. package/src/router/match.ts +106 -0
  262. package/src/router/navigation.ts +71 -0
  263. package/src/router/query.ts +35 -0
  264. package/src/router/router.ts +211 -0
  265. package/src/router/state.ts +46 -0
  266. package/src/router/types.ts +93 -0
  267. package/src/router/utils.ts +116 -0
  268. package/src/security/constants.ts +209 -0
  269. package/src/security/csp.ts +77 -0
  270. package/src/security/index.ts +4 -12
  271. package/src/security/sanitize-core.ts +343 -0
  272. package/src/security/sanitize.ts +66 -625
  273. package/src/security/trusted-types.ts +69 -0
  274. package/src/security/types.ts +40 -0
  275. package/src/store/create-store.ts +329 -0
  276. package/src/store/define-store.ts +48 -0
  277. package/src/store/devtools.ts +45 -0
  278. package/src/store/index.ts +22 -848
  279. package/src/store/mapping.ts +73 -0
  280. package/src/store/persisted.ts +61 -0
  281. package/src/store/plugins.ts +32 -0
  282. package/src/store/registry.ts +51 -0
  283. package/src/store/types.ts +94 -0
  284. package/src/store/utils.ts +141 -0
  285. package/src/store/watch.ts +52 -0
  286. package/src/view/directives/bind.ts +23 -0
  287. package/src/view/directives/class.ts +70 -0
  288. package/src/view/directives/for.ts +275 -0
  289. package/src/view/directives/html.ts +19 -0
  290. package/src/view/directives/if.ts +30 -0
  291. package/src/view/directives/index.ts +11 -0
  292. package/src/view/directives/model.ts +56 -0
  293. package/src/view/directives/on.ts +41 -0
  294. package/src/view/directives/ref.ts +41 -0
  295. package/src/view/directives/show.ts +26 -0
  296. package/src/view/directives/style.ts +47 -0
  297. package/src/view/directives/text.ts +15 -0
  298. package/src/view/evaluate.ts +274 -0
  299. package/src/view/index.ts +112 -1041
  300. package/src/view/mount.ts +200 -0
  301. package/src/view/process.ts +92 -0
  302. package/src/view/types.ts +44 -0
  303. package/dist/core/utils.d.ts +0 -313
  304. package/dist/core/utils.d.ts.map +0 -1
  305. package/src/core/utils.ts +0 -444
@@ -1,414 +1,40 @@
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
- /**
39
- * Defines a single prop's type and configuration.
40
- *
41
- * @template T - The TypeScript type of the prop value
42
- *
43
- * @example
44
- * ```ts
45
- * const myProp: PropDefinition<number> = {
46
- * type: Number,
47
- * required: false,
48
- * default: 0,
49
- * };
50
- * ```
51
- */
52
- export type PropDefinition<T = unknown> = {
53
- /** Constructor or converter function for the prop type */
54
- type:
55
- | StringConstructor
56
- | NumberConstructor
57
- | BooleanConstructor
58
- | ObjectConstructor
59
- | ArrayConstructor
60
- | { new (value: unknown): T }
61
- | ((value: unknown) => T);
62
- /** Whether the prop must be provided */
63
- required?: boolean;
64
- /** Default value when prop is not provided */
65
- default?: T;
66
- /** Optional validator function to validate prop values */
67
- validator?: (value: T) => boolean;
68
- };
69
-
70
- /**
71
- * Complete component definition including props, state, styles, and lifecycle.
72
- *
73
- * @template TProps - Type of the component's props
74
- */
75
- export type ComponentDefinition<TProps extends Record<string, unknown> = Record<string, unknown>> =
76
- {
77
- /** Prop definitions with types and defaults */
78
- props?: Record<keyof TProps, PropDefinition>;
79
- /** Initial internal state */
80
- state?: Record<string, unknown>;
81
- /** CSS styles scoped to the component's shadow DOM */
82
- styles?: string;
83
- /** Lifecycle hook called before the component mounts (before first render) */
84
- beforeMount?: () => void;
85
- /** Lifecycle hook called when component is added to DOM */
86
- connected?: () => void;
87
- /** Lifecycle hook called when component is removed from DOM */
88
- disconnected?: () => void;
89
- /** Lifecycle hook called before an update render; return false to prevent */
90
- beforeUpdate?: (props: TProps) => boolean | void;
91
- /** Lifecycle hook called after reactive updates trigger a render */
92
- updated?: () => void;
93
- /** Error handler for errors during rendering or lifecycle */
94
- onError?: (error: Error) => void;
95
- /** Render function returning HTML string */
96
- render: (context: {
97
- props: TProps;
98
- state: Record<string, unknown>;
99
- emit: (event: string, detail?: unknown) => void;
100
- }) => string;
101
- };
102
-
103
- /**
104
- * Coerces a string attribute value into a typed prop value.
105
- * Supports String, Number, Boolean, Object, Array, and custom converters.
106
- *
107
- * @internal
108
- * @template T - The target type
109
- * @param rawValue - The raw string value from the attribute
110
- * @param config - The prop definition with type information
111
- * @returns The coerced value of type T
112
- */
113
- const coercePropValue = <T>(rawValue: string, config: PropDefinition<T>): T => {
114
- const { type } = config;
115
-
116
- if (type === String) return rawValue as T;
117
-
118
- if (type === Number) {
119
- const parsed = Number(rawValue);
120
- return (Number.isNaN(parsed) ? rawValue : parsed) as T;
121
- }
122
-
123
- if (type === Boolean) {
124
- const normalized = rawValue.trim().toLowerCase();
125
- if (normalized === '' || normalized === 'true' || normalized === '1') {
126
- return true as T;
127
- }
128
- if (normalized === 'false' || normalized === '0') {
129
- return false as T;
130
- }
131
- return Boolean(rawValue) as T;
132
- }
133
-
134
- if (type === Object || type === Array) {
135
- try {
136
- return JSON.parse(rawValue) as T;
137
- } catch {
138
- return rawValue as T;
139
- }
140
- }
141
-
142
- if (typeof type === 'function') {
143
- const callable = type as (value: unknown) => T;
144
- const constructable = type as new (value: unknown) => T;
145
- try {
146
- return callable(rawValue);
147
- } catch {
148
- return new constructable(rawValue);
149
- }
150
- }
151
-
152
- return rawValue as T;
153
- };
154
-
155
- /**
156
- * Tagged template literal for creating HTML strings.
157
- *
158
- * This function handles interpolation of values into HTML templates,
159
- * converting null/undefined to empty strings.
160
- *
161
- * @param strings - Template literal string parts
162
- * @param values - Interpolated values
163
- * @returns Combined HTML string
164
- *
165
- * @example
166
- * ```ts
167
- * const name = 'World';
168
- * const greeting = html`<h1>Hello, ${name}!</h1>`;
169
- * // Result: '<h1>Hello, World!</h1>'
170
- * ```
171
- */
172
- export const html = (strings: TemplateStringsArray, ...values: unknown[]): string => {
173
- return strings.reduce((acc, part, index) => `${acc}${part}${values[index] ?? ''}`, '');
174
- };
175
-
176
- /**
177
- * Escapes HTML entities in interpolated values for XSS prevention.
178
- * Use this when you need to safely embed user content in templates.
179
- *
180
- * @param strings - Template literal string parts
181
- * @param values - Interpolated values to escape
182
- * @returns Combined HTML string with escaped values
183
- *
184
- * @example
185
- * ```ts
186
- * const userInput = '<script>alert("xss")</script>';
187
- * const safe = safeHtml`<div>${userInput}</div>`;
188
- * // Result: '<div>&lt;script&gt;alert("xss")&lt;/script&gt;</div>'
189
- * ```
190
- */
191
- export const safeHtml = (strings: TemplateStringsArray, ...values: unknown[]): string => {
192
- const escapeMap: Record<string, string> = {
193
- '&': '&amp;',
194
- '<': '&lt;',
195
- '>': '&gt;',
196
- '"': '&quot;',
197
- "'": '&#x27;',
198
- '`': '&#x60;',
199
- };
200
-
201
- const escape = (value: unknown): string => {
202
- const str = String(value ?? '');
203
- return str.replace(/[&<>"'`]/g, (char) => escapeMap[char]);
204
- };
205
-
206
- return strings.reduce((acc, part, index) => `${acc}${part}${escape(values[index])}`, '');
207
- };
208
-
209
- /**
210
- * Defines and registers a custom Web Component.
211
- *
212
- * This function creates a new custom element with the given tag name
213
- * and configuration. The component uses Shadow DOM for encapsulation
214
- * and automatically re-renders when observed attributes change.
215
- *
216
- * @template TProps - Type of the component's props
217
- * @param tagName - The custom element tag name (must contain a hyphen)
218
- * @param definition - The component configuration
219
- *
220
- * @example
221
- * ```ts
222
- * component('counter-button', {
223
- * props: {
224
- * start: { type: Number, default: 0 },
225
- * },
226
- * state: { count: 0 },
227
- * styles: `
228
- * button { padding: 0.5rem 1rem; }
229
- * `,
230
- * connected() {
231
- * console.log('Counter mounted');
232
- * },
233
- * render({ props, state, emit }) {
234
- * return html`
235
- * <button onclick="this.getRootNode().host.increment()">
236
- * Count: ${state.count}
237
- * </button>
238
- * `;
239
- * },
240
- * });
241
- * ```
242
- */
243
- export const component = <TProps extends Record<string, unknown>>(
244
- tagName: string,
245
- definition: ComponentDefinition<TProps>
246
- ): void => {
247
- /**
248
- * Internal Web Component class created for each component definition.
249
- * @internal
250
- */
251
- class BQueryComponent extends HTMLElement {
252
- /** Internal state object for the component */
253
- private readonly state = { ...(definition.state ?? {}) };
254
- /** Typed props object populated from attributes */
255
- private props = {} as TProps;
256
-
257
- constructor() {
258
- super();
259
- this.attachShadow({ mode: 'open' });
260
- this.syncProps();
261
- }
262
-
263
- /**
264
- * Returns the list of attributes to observe for changes.
265
- */
266
- static get observedAttributes(): string[] {
267
- return Object.keys(definition.props ?? {});
268
- }
269
-
270
- /**
271
- * Called when the element is added to the DOM.
272
- */
273
- connectedCallback(): void {
274
- try {
275
- definition.beforeMount?.call(this);
276
- definition.connected?.call(this);
277
- this.render();
278
- } catch (error) {
279
- this.handleError(error as Error);
280
- }
281
- }
282
-
283
- /**
284
- * Called when the element is removed from the DOM.
285
- */
286
- disconnectedCallback(): void {
287
- try {
288
- definition.disconnected?.call(this);
289
- } catch (error) {
290
- this.handleError(error as Error);
291
- }
292
- }
293
-
294
- /**
295
- * Called when an observed attribute changes.
296
- */
297
- attributeChangedCallback(): void {
298
- try {
299
- this.syncProps();
300
- this.render(true);
301
- } catch (error) {
302
- this.handleError(error as Error);
303
- }
304
- }
305
-
306
- /**
307
- * Handles errors during component lifecycle.
308
- * @internal
309
- */
310
- private handleError(error: Error): void {
311
- if (definition.onError) {
312
- definition.onError.call(this, error);
313
- } else {
314
- console.error(`bQuery component error in <${tagName}>:`, error);
315
- }
316
- }
317
-
318
- /**
319
- * Updates a state property and triggers a re-render.
320
- *
321
- * @param key - The state property key
322
- * @param value - The new value
323
- */
324
- setState(key: string, value: unknown): void {
325
- this.state[key] = value;
326
- this.render(true);
327
- }
328
-
329
- /**
330
- * Gets a state property value.
331
- *
332
- * @param key - The state property key
333
- * @returns The current value
334
- */
335
- getState<T = unknown>(key: string): T {
336
- return this.state[key] as T;
337
- }
338
-
339
- /**
340
- * Synchronizes props from attributes.
341
- * @internal
342
- */
343
- private syncProps(): void {
344
- const props = definition.props ?? {};
345
- for (const [key, config] of Object.entries(props) as [string, PropDefinition][]) {
346
- const attrValue = this.getAttribute(key);
347
- let value: unknown;
348
-
349
- if (attrValue == null) {
350
- if (config.required && config.default === undefined) {
351
- throw new Error(`bQuery component: missing required prop "${key}"`);
352
- }
353
- value = config.default ?? undefined;
354
- } else {
355
- value = coercePropValue(attrValue, config);
356
- }
357
-
358
- // Validate the prop value if a validator is provided
359
- if (config.validator && value !== undefined) {
360
- const isValid = config.validator(value);
361
- if (!isValid) {
362
- throw new Error(
363
- `bQuery component: validation failed for prop "${key}" with value ${JSON.stringify(value)}`
364
- );
365
- }
366
- }
367
-
368
- (this.props as Record<string, unknown>)[key] = value;
369
- }
370
- }
371
-
372
- /**
373
- * Renders the component to its shadow root.
374
- * @internal
375
- */
376
- private render(triggerUpdated = false): void {
377
- try {
378
- // Check beforeUpdate hook if this is an update
379
- if (triggerUpdated && definition.beforeUpdate) {
380
- const shouldUpdate = definition.beforeUpdate.call(this, this.props);
381
- if (shouldUpdate === false) return;
382
- }
383
-
384
- /**
385
- * Emits a custom event from the component.
386
- */
387
- const emit = (event: string, detail?: unknown): void => {
388
- this.dispatchEvent(new CustomEvent(event, { detail, bubbles: true, composed: true }));
389
- };
390
-
391
- if (!this.shadowRoot) return;
392
-
393
- const markup = definition.render({
394
- props: this.props,
395
- state: this.state,
396
- emit,
397
- });
398
-
399
- const styles = definition.styles ? `<style>${definition.styles}</style>` : '';
400
- this.shadowRoot.innerHTML = `${styles}${markup}`;
401
-
402
- if (triggerUpdated) {
403
- definition.updated?.call(this);
404
- }
405
- } catch (error) {
406
- this.handleError(error as Error);
407
- }
408
- }
409
- }
410
-
411
- if (!customElements.get(tagName)) {
412
- customElements.define(tagName, BQueryComponent);
413
- }
414
- };
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';
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Prop coercion utilities.
3
+ *
4
+ * @module bquery/component
5
+ */
6
+
7
+ import type { PropDefinition } from './types';
8
+
9
+ /**
10
+ * Coerces a string attribute value into a typed prop value.
11
+ * Supports String, Number, Boolean, Object, Array, and custom converters.
12
+ *
13
+ * @internal
14
+ * @template T - The target type
15
+ * @param rawValue - The raw string value from the attribute
16
+ * @param config - The prop definition with type information
17
+ * @returns The coerced value of type T
18
+ */
19
+ export const coercePropValue = <T>(rawValue: string, config: PropDefinition<T>): T => {
20
+ const { type } = config;
21
+
22
+ if (type === String) return rawValue as T;
23
+
24
+ if (type === Number) {
25
+ return Number(rawValue) as T;
26
+ }
27
+
28
+ if (type === Boolean) {
29
+ const normalized = rawValue.trim().toLowerCase();
30
+ if (normalized === '' || normalized === 'true' || normalized === '1') {
31
+ return true as T;
32
+ }
33
+ if (normalized === 'false' || normalized === '0') {
34
+ return false as T;
35
+ }
36
+ return Boolean(rawValue) as T;
37
+ }
38
+
39
+ if (type === Object || type === Array) {
40
+ try {
41
+ return JSON.parse(rawValue) as T;
42
+ } catch {
43
+ return rawValue as T;
44
+ }
45
+ }
46
+
47
+ if (typeof type === 'function') {
48
+ const callable = type as (value: unknown) => T;
49
+ const constructable = type as new (value: unknown) => T;
50
+
51
+ // Explicit construct mode takes precedence
52
+ if (config.construct === true) {
53
+ return Reflect.construct(constructable, [rawValue]) as T;
54
+ }
55
+ if (config.construct === false) {
56
+ return callable(rawValue);
57
+ }
58
+
59
+ // Auto-detect: Check if type is constructable
60
+ // A function is considered constructable if:
61
+ // 1. It has a prototype with properties beyond just constructor, OR
62
+ // 2. Its prototype.constructor is not itself (inherited), OR
63
+ // 3. It's a class (toString starts with "class")
64
+ const hasPrototype = type.prototype !== undefined && type.prototype !== null;
65
+ const prototypeProps = hasPrototype ? Object.getOwnPropertyNames(type.prototype) : [];
66
+ const hasPrototypeMethods = prototypeProps.length > 1;
67
+ const hasInheritedConstructor = hasPrototype && type.prototype.constructor !== type;
68
+ const isClassSyntax = /^class\s/.test(Function.prototype.toString.call(type));
69
+
70
+ const isConstructable = hasPrototypeMethods || hasInheritedConstructor || isClassSyntax;
71
+
72
+ // For constructable types (e.g. Date, custom classes), prefer `new` to avoid
73
+ // silent wrong-type returns (Date() returns string, new Date() returns Date)
74
+ if (isConstructable) {
75
+ try {
76
+ return Reflect.construct(constructable, [rawValue]) as T;
77
+ } catch {
78
+ // Fall back to calling as function if construction fails
79
+ return callable(rawValue);
80
+ }
81
+ }
82
+
83
+ // For non-constructable types (arrow functions, plain functions), call directly
84
+ // but fall back to constructor if result is undefined (common for function constructors)
85
+ try {
86
+ const result = callable(rawValue);
87
+
88
+ // If calling without `new` returned undefined and the function has a prototype,
89
+ // it's likely a function constructor that should be called with `new`
90
+ if (result === undefined && hasPrototype) {
91
+ try {
92
+ return Reflect.construct(constructable, [rawValue]) as T;
93
+ } catch {
94
+ // Construction also failed, return the undefined
95
+ return result as T;
96
+ }
97
+ }
98
+
99
+ return result as T;
100
+ } catch (error) {
101
+ // Fall back to constructor if error indicates 'new' is required
102
+ const isNewRequired =
103
+ error instanceof TypeError &&
104
+ /cannot be invoked without 'new'|is not a function/i.test(error.message);
105
+
106
+ if (isNewRequired) {
107
+ return Reflect.construct(constructable, [rawValue]) as T;
108
+ }
109
+
110
+ // Rethrow original error for non-constructable converters
111
+ throw error;
112
+ }
113
+ }
114
+
115
+ return rawValue as T;
116
+ };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Component types and render context definitions.
3
+ *
4
+ * @module bquery/component
5
+ */
6
+
7
+ /**
8
+ * Defines a single prop's type and configuration.
9
+ *
10
+ * @template T - The TypeScript type of the prop value
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const myProp: PropDefinition<number> = {
15
+ * type: Number,
16
+ * required: false,
17
+ * default: 0,
18
+ * };
19
+ * ```
20
+ */
21
+ export type PropDefinition<T = unknown> = {
22
+ /** Constructor or converter function for the prop type */
23
+ type:
24
+ | StringConstructor
25
+ | NumberConstructor
26
+ | BooleanConstructor
27
+ | ObjectConstructor
28
+ | ArrayConstructor
29
+ | { new (value: unknown): T }
30
+ | ((value: unknown) => T);
31
+ /** Whether the prop must be provided */
32
+ required?: boolean;
33
+ /** Default value when prop is not provided */
34
+ default?: T;
35
+ /** Optional validator function to validate prop values */
36
+ validator?: (value: T) => boolean;
37
+ /**
38
+ * Explicitly control whether to invoke `type` with `new` (constructor) or as a plain function.
39
+ * - `true`: Always use `new type(value)` (for class constructors, Date, etc.)
40
+ * - `false`: Always call `type(value)` (for converter functions)
41
+ * - `undefined` (default): Auto-detect based on heuristics with fallback
42
+ */
43
+ construct?: boolean;
44
+ };
45
+
46
+ /**
47
+ * Render context passed into a component render function.
48
+ */
49
+ export type ComponentRenderContext<TProps extends Record<string, unknown>> = {
50
+ /** Typed props object populated from attributes */
51
+ props: TProps;
52
+ /** Internal mutable state object */
53
+ state: Record<string, unknown>;
54
+ /** Emit a custom event from the component */
55
+ emit: (event: string, detail?: unknown) => void;
56
+ };
57
+
58
+ /**
59
+ * Complete component definition including props, state, styles, and lifecycle.
60
+ *
61
+ * @template TProps - Type of the component's props
62
+ */
63
+ export type ComponentDefinition<TProps extends Record<string, unknown> = Record<string, unknown>> =
64
+ {
65
+ /** Prop definitions with types and defaults */
66
+ props?: Record<keyof TProps, PropDefinition>;
67
+ /** Initial internal state */
68
+ state?: Record<string, unknown>;
69
+ /** CSS styles scoped to the component's shadow DOM */
70
+ styles?: string;
71
+ /** Lifecycle hook called before the component mounts (before first render) */
72
+ beforeMount?: () => void;
73
+ /** Lifecycle hook called when component is added to DOM */
74
+ connected?: () => void;
75
+ /** Lifecycle hook called when component is removed from DOM */
76
+ disconnected?: () => void;
77
+ /** Lifecycle hook called before an update render; return false to prevent */
78
+ beforeUpdate?: (props: TProps) => boolean | void;
79
+ /** Lifecycle hook called after reactive updates trigger a render */
80
+ updated?: () => void;
81
+ /** Error handler for errors during rendering or lifecycle */
82
+ onError?: (error: Error) => void;
83
+ /** Render function returning HTML string */
84
+ render: (context: ComponentRenderContext<TProps>) => string;
85
+ };