thunderous 0.0.1

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Jonathan DeWitt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs ADDED
@@ -0,0 +1,329 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/index.ts
20
+ var src_exports = {};
21
+ __export(src_exports, {
22
+ createEffect: () => createEffect,
23
+ createRegistry: () => createRegistry,
24
+ createSignal: () => createSignal,
25
+ css: () => css,
26
+ customElement: () => customElement,
27
+ derived: () => derived,
28
+ html: () => html
29
+ });
30
+ module.exports = __toCommonJS(src_exports);
31
+
32
+ // src/html-helpers.ts
33
+ var clearHTML = (element) => {
34
+ while (element.childNodes.length > 0) {
35
+ element.childNodes[0].remove();
36
+ }
37
+ };
38
+ var parseFragment = (htmlStr) => {
39
+ const range = document.createRange();
40
+ range.selectNode(document.body);
41
+ return range.createContextualFragment(htmlStr);
42
+ };
43
+ var setInnerHTML = (element, html2) => {
44
+ clearHTML(element);
45
+ const fragment = typeof html2 === "string" ? parseFragment(html2) : html2;
46
+ element.append(fragment);
47
+ };
48
+
49
+ // src/signals.ts
50
+ var subscriber = null;
51
+ var createSignal = (initVal) => {
52
+ const subscribers = /* @__PURE__ */ new Set();
53
+ let value = initVal;
54
+ const getter = () => {
55
+ if (subscriber !== null) {
56
+ subscribers.add(subscriber);
57
+ }
58
+ return value;
59
+ };
60
+ const setter = (newValue) => {
61
+ value = newValue;
62
+ for (const fn of subscribers) {
63
+ fn();
64
+ }
65
+ };
66
+ return [getter, setter];
67
+ };
68
+ var derived = (fn) => {
69
+ const [getter, setter] = createSignal();
70
+ createEffect(() => {
71
+ setter(fn());
72
+ });
73
+ return getter;
74
+ };
75
+ var createEffect = (fn) => {
76
+ subscriber = fn;
77
+ fn();
78
+ subscriber = null;
79
+ };
80
+
81
+ // src/custom-element.ts
82
+ var DEFAULT_RENDER_OPTIONS = {
83
+ formAssociated: false
84
+ };
85
+ var customElement = (render, options) => {
86
+ const { formAssociated } = { ...DEFAULT_RENDER_OPTIONS, ...options };
87
+ class CustomElement extends HTMLElement {
88
+ #attrSignals = {};
89
+ #attributeChangedFns = /* @__PURE__ */ new Set();
90
+ #connectedFns = /* @__PURE__ */ new Set();
91
+ #disconnectedFns = /* @__PURE__ */ new Set();
92
+ #adoptedCallbackFns = /* @__PURE__ */ new Set();
93
+ #formDisabledCallbackFns = /* @__PURE__ */ new Set();
94
+ #formResetCallbackFns = /* @__PURE__ */ new Set();
95
+ #formStateRestoreCallbackFns = /* @__PURE__ */ new Set();
96
+ #shadowRoot = this.attachShadow({ mode: "closed" });
97
+ #internals = this.attachInternals();
98
+ #observer = new MutationObserver((mutations) => {
99
+ for (const mutation of mutations) {
100
+ const attrName = mutation.attributeName;
101
+ if (mutation.type !== "attributes" || attrName === null) continue;
102
+ const [value, setValue] = this.#attrSignals[attrName];
103
+ const _oldValue = value();
104
+ const oldValue = _oldValue === null ? null : _oldValue;
105
+ const newValue = this.getAttribute(attrName);
106
+ setValue(newValue);
107
+ for (const fn of this.#attributeChangedFns) {
108
+ fn(attrName, oldValue, newValue);
109
+ }
110
+ }
111
+ });
112
+ #render() {
113
+ const fragment = render({
114
+ elementRef: this,
115
+ root: this.#shadowRoot,
116
+ internals: this.#internals,
117
+ attributeChangedCallback: (fn) => this.#attributeChangedFns.add(fn),
118
+ connectedCallback: (fn) => this.#connectedFns.add(fn),
119
+ disconnectedCallback: (fn) => this.#disconnectedFns.add(fn),
120
+ adoptedCallback: (fn) => this.#adoptedCallbackFns.add(fn),
121
+ formDisabledCallback: (fn) => this.#formDisabledCallbackFns.add(fn),
122
+ formResetCallback: (fn) => this.#formResetCallbackFns.add(fn),
123
+ formStateRestoreCallback: (fn) => this.#formStateRestoreCallbackFns.add(fn),
124
+ attrSignals: new Proxy(
125
+ {},
126
+ {
127
+ get: (_, prop) => {
128
+ if (!(prop in this.#attrSignals)) this.#attrSignals[prop] = createSignal(null);
129
+ const [getter] = this.#attrSignals[prop];
130
+ const setter = (newValue) => this.setAttribute(prop, newValue);
131
+ return [getter, setter];
132
+ },
133
+ set: () => {
134
+ console.error("Signals must be assigned via setters.");
135
+ return false;
136
+ }
137
+ }
138
+ ),
139
+ refs: new Proxy(
140
+ {},
141
+ {
142
+ get: (_, prop) => this.#shadowRoot.querySelector(`[ref=${prop}]`),
143
+ set: () => {
144
+ console.error("Refs are readonly and cannot be assigned.");
145
+ return false;
146
+ }
147
+ }
148
+ ),
149
+ adoptStyleSheet: (stylesheet) => {
150
+ this.#shadowRoot.adoptedStyleSheets.push(stylesheet);
151
+ }
152
+ });
153
+ setInnerHTML(this.#shadowRoot, fragment);
154
+ }
155
+ static get formAssociated() {
156
+ return formAssociated;
157
+ }
158
+ constructor() {
159
+ super();
160
+ for (const attr of this.attributes) {
161
+ this.#attrSignals[attr.name] = createSignal(attr.value);
162
+ }
163
+ this.#render();
164
+ }
165
+ connectedCallback() {
166
+ this.#observer.observe(this, { attributes: true });
167
+ for (const fn of this.#connectedFns) {
168
+ fn();
169
+ }
170
+ }
171
+ disconnectedCallback() {
172
+ this.#observer.disconnect();
173
+ for (const fn of this.#disconnectedFns) {
174
+ fn();
175
+ }
176
+ }
177
+ adoptedCallback() {
178
+ for (const fn of this.#adoptedCallbackFns) {
179
+ fn();
180
+ }
181
+ }
182
+ formDisabledCallback() {
183
+ for (const fn of this.#formDisabledCallbackFns) {
184
+ fn();
185
+ }
186
+ }
187
+ formResetCallback() {
188
+ for (const fn of this.#formResetCallbackFns) {
189
+ fn();
190
+ }
191
+ }
192
+ formStateRestoreCallback() {
193
+ for (const fn of this.#formStateRestoreCallbackFns) {
194
+ fn();
195
+ }
196
+ }
197
+ }
198
+ let _tagname = null;
199
+ return {
200
+ define(tagname) {
201
+ if (customElements.get(tagname) !== void 0) {
202
+ console.warn(`Custom element "${tagname}" was already defined. Skipping...`);
203
+ return this;
204
+ }
205
+ customElements.define(tagname, CustomElement);
206
+ _tagname = tagname;
207
+ return this;
208
+ },
209
+ register(registry) {
210
+ if (_tagname === null) {
211
+ console.error("Custom element must be defined before registering.");
212
+ return this;
213
+ }
214
+ registry.register(_tagname, CustomElement);
215
+ return this;
216
+ },
217
+ eject: () => CustomElement
218
+ };
219
+ };
220
+ var createRegistry = () => {
221
+ const registry = /* @__PURE__ */ new Map();
222
+ return {
223
+ register: (tagName, element) => {
224
+ if (registry.has(element)) {
225
+ console.warn(`Custom element class "${element.constructor.name}" was already registered. Skipping...`);
226
+ return;
227
+ }
228
+ registry.set(element, tagName.toUpperCase());
229
+ },
230
+ getTagName: (element) => registry.get(element)
231
+ };
232
+ };
233
+
234
+ // src/render.ts
235
+ var html = (strings, ...values) => {
236
+ let innerHTML = "";
237
+ const signalMap = /* @__PURE__ */ new Map();
238
+ strings.forEach((string, i) => {
239
+ let value = values[i] ?? "";
240
+ if (typeof value === "function") {
241
+ const uniqueKey = crypto.randomUUID();
242
+ signalMap.set(uniqueKey, value);
243
+ value = `{{signal:${uniqueKey}}}`;
244
+ }
245
+ innerHTML += string + String(value);
246
+ });
247
+ const fragment = parseFragment(innerHTML);
248
+ const signalBindingRegex = /(\{\{signal:.+\}\})/;
249
+ const parseChildren = (element) => {
250
+ for (const child of element.childNodes) {
251
+ if (child instanceof Text && signalBindingRegex.test(child.data)) {
252
+ const textList = child.data.split(signalBindingRegex);
253
+ textList.forEach((text, i) => {
254
+ const uniqueKey = text.replace(/\{\{signal:(.+)\}\}/, "$1");
255
+ const signal = uniqueKey !== text ? signalMap.get(uniqueKey) : null;
256
+ const newText = signal !== null ? signal() : text;
257
+ const newNode = new Text(newText);
258
+ if (i === 0) {
259
+ child.replaceWith(newNode);
260
+ } else {
261
+ element.insertBefore(newNode, child.nextSibling);
262
+ }
263
+ if (signal !== null) {
264
+ createEffect(() => {
265
+ newNode.data = signal();
266
+ });
267
+ }
268
+ });
269
+ }
270
+ if (child instanceof Element) {
271
+ for (const attr of child.attributes) {
272
+ if (signalBindingRegex.test(attr.value)) {
273
+ const textList = attr.value.split(signalBindingRegex);
274
+ createEffect(() => {
275
+ let newText = "";
276
+ for (const text of textList) {
277
+ const uniqueKey = text.replace(/\{\{signal:(.+)\}\}/, "$1");
278
+ const signal = uniqueKey !== text ? signalMap.get(uniqueKey) : null;
279
+ newText += signal !== null ? signal() : text;
280
+ }
281
+ child.setAttribute(attr.name, newText);
282
+ });
283
+ }
284
+ }
285
+ parseChildren(child);
286
+ }
287
+ }
288
+ };
289
+ parseChildren(fragment);
290
+ return fragment;
291
+ };
292
+ var css = (strings, ...values) => {
293
+ let cssText = "";
294
+ const signalMap = /* @__PURE__ */ new Map();
295
+ const signalBindingRegex = /(\{\{signal:.+\}\})/;
296
+ strings.forEach((string, i) => {
297
+ let value = values[i] ?? "";
298
+ if (typeof value === "function") {
299
+ const uniqueKey = crypto.randomUUID();
300
+ signalMap.set(uniqueKey, value);
301
+ value = `{{signal:${uniqueKey}}}`;
302
+ }
303
+ cssText += string + String(value);
304
+ });
305
+ const stylesheet = new CSSStyleSheet();
306
+ const textList = cssText.split(signalBindingRegex);
307
+ createEffect(() => {
308
+ const newCSSTextList = [];
309
+ for (const text of textList) {
310
+ const uniqueKey = text.replace(/\{\{signal:(.+)\}\}/, "$1");
311
+ const signal = uniqueKey !== text ? signalMap.get(uniqueKey) : null;
312
+ const newText = signal !== null ? signal() : text;
313
+ newCSSTextList.push(newText);
314
+ }
315
+ const newCSSText = newCSSTextList.join("");
316
+ stylesheet.replace(newCSSText);
317
+ });
318
+ return stylesheet;
319
+ };
320
+ // Annotate the CommonJS export names for ESM import in node:
321
+ 0 && (module.exports = {
322
+ createEffect,
323
+ createRegistry,
324
+ createSignal,
325
+ css,
326
+ customElement,
327
+ derived,
328
+ html
329
+ });
@@ -0,0 +1,43 @@
1
+ type SignalGetter<T> = () => T;
2
+ type SignalSetter<T> = (newValue: T) => void;
3
+ type Signal<T = unknown> = [SignalGetter<T>, SignalSetter<T>];
4
+ declare const createSignal: <T = undefined>(initVal?: T) => Signal<T>;
5
+ declare const derived: <T>(fn: () => T) => SignalGetter<T>;
6
+ declare const createEffect: (fn: () => void) => void;
7
+
8
+ type ElementResult = {
9
+ define: (tagname: `${string}-${string}`) => ElementResult;
10
+ register: (registry: Registry) => ElementResult;
11
+ eject: () => CustomElementConstructor;
12
+ };
13
+ type AttributeChangedCallback = (name: string, oldValue: string | null, newValue: string | null) => void;
14
+ type RenderProps = {
15
+ elementRef: HTMLElement;
16
+ root: ShadowRoot;
17
+ internals: ElementInternals;
18
+ attributeChangedCallback: (fn: AttributeChangedCallback) => void;
19
+ connectedCallback: (fn: () => void) => void;
20
+ disconnectedCallback: (fn: () => void) => void;
21
+ adoptedCallback: (fn: () => void) => void;
22
+ formDisabledCallback: (fn: () => void) => void;
23
+ formResetCallback: (fn: () => void) => void;
24
+ formStateRestoreCallback: (fn: () => void) => void;
25
+ attrSignals: Record<string, Signal<string | null>>;
26
+ refs: Record<string, HTMLElement | null>;
27
+ adoptStyleSheet: (stylesheet: CSSStyleSheet) => void;
28
+ };
29
+ type RenderOptions = {
30
+ formAssociated?: boolean;
31
+ };
32
+ type RenderFunction = (props: RenderProps) => DocumentFragment;
33
+ declare const customElement: (render: RenderFunction, options?: RenderOptions) => ElementResult;
34
+ type Registry = {
35
+ register: (tagName: string, element: CustomElementConstructor) => void;
36
+ getTagName: (element: CustomElementConstructor) => string | undefined;
37
+ };
38
+ declare const createRegistry: () => Registry;
39
+
40
+ declare const html: (strings: TemplateStringsArray, ...values: unknown[]) => DocumentFragment;
41
+ declare const css: (strings: TemplateStringsArray, ...values: unknown[]) => CSSStyleSheet;
42
+
43
+ export { type RenderFunction, type RenderProps, type Signal, type SignalGetter, type SignalSetter, createEffect, createRegistry, createSignal, css, customElement, derived, html };
@@ -0,0 +1,43 @@
1
+ type SignalGetter<T> = () => T;
2
+ type SignalSetter<T> = (newValue: T) => void;
3
+ type Signal<T = unknown> = [SignalGetter<T>, SignalSetter<T>];
4
+ declare const createSignal: <T = undefined>(initVal?: T) => Signal<T>;
5
+ declare const derived: <T>(fn: () => T) => SignalGetter<T>;
6
+ declare const createEffect: (fn: () => void) => void;
7
+
8
+ type ElementResult = {
9
+ define: (tagname: `${string}-${string}`) => ElementResult;
10
+ register: (registry: Registry) => ElementResult;
11
+ eject: () => CustomElementConstructor;
12
+ };
13
+ type AttributeChangedCallback = (name: string, oldValue: string | null, newValue: string | null) => void;
14
+ type RenderProps = {
15
+ elementRef: HTMLElement;
16
+ root: ShadowRoot;
17
+ internals: ElementInternals;
18
+ attributeChangedCallback: (fn: AttributeChangedCallback) => void;
19
+ connectedCallback: (fn: () => void) => void;
20
+ disconnectedCallback: (fn: () => void) => void;
21
+ adoptedCallback: (fn: () => void) => void;
22
+ formDisabledCallback: (fn: () => void) => void;
23
+ formResetCallback: (fn: () => void) => void;
24
+ formStateRestoreCallback: (fn: () => void) => void;
25
+ attrSignals: Record<string, Signal<string | null>>;
26
+ refs: Record<string, HTMLElement | null>;
27
+ adoptStyleSheet: (stylesheet: CSSStyleSheet) => void;
28
+ };
29
+ type RenderOptions = {
30
+ formAssociated?: boolean;
31
+ };
32
+ type RenderFunction = (props: RenderProps) => DocumentFragment;
33
+ declare const customElement: (render: RenderFunction, options?: RenderOptions) => ElementResult;
34
+ type Registry = {
35
+ register: (tagName: string, element: CustomElementConstructor) => void;
36
+ getTagName: (element: CustomElementConstructor) => string | undefined;
37
+ };
38
+ declare const createRegistry: () => Registry;
39
+
40
+ declare const html: (strings: TemplateStringsArray, ...values: unknown[]) => DocumentFragment;
41
+ declare const css: (strings: TemplateStringsArray, ...values: unknown[]) => CSSStyleSheet;
42
+
43
+ export { type RenderFunction, type RenderProps, type Signal, type SignalGetter, type SignalSetter, createEffect, createRegistry, createSignal, css, customElement, derived, html };
package/dist/index.js ADDED
@@ -0,0 +1,297 @@
1
+ // src/html-helpers.ts
2
+ var clearHTML = (element) => {
3
+ while (element.childNodes.length > 0) {
4
+ element.childNodes[0].remove();
5
+ }
6
+ };
7
+ var parseFragment = (htmlStr) => {
8
+ const range = document.createRange();
9
+ range.selectNode(document.body);
10
+ return range.createContextualFragment(htmlStr);
11
+ };
12
+ var setInnerHTML = (element, html2) => {
13
+ clearHTML(element);
14
+ const fragment = typeof html2 === "string" ? parseFragment(html2) : html2;
15
+ element.append(fragment);
16
+ };
17
+
18
+ // src/signals.ts
19
+ var subscriber = null;
20
+ var createSignal = (initVal) => {
21
+ const subscribers = /* @__PURE__ */ new Set();
22
+ let value = initVal;
23
+ const getter = () => {
24
+ if (subscriber !== null) {
25
+ subscribers.add(subscriber);
26
+ }
27
+ return value;
28
+ };
29
+ const setter = (newValue) => {
30
+ value = newValue;
31
+ for (const fn of subscribers) {
32
+ fn();
33
+ }
34
+ };
35
+ return [getter, setter];
36
+ };
37
+ var derived = (fn) => {
38
+ const [getter, setter] = createSignal();
39
+ createEffect(() => {
40
+ setter(fn());
41
+ });
42
+ return getter;
43
+ };
44
+ var createEffect = (fn) => {
45
+ subscriber = fn;
46
+ fn();
47
+ subscriber = null;
48
+ };
49
+
50
+ // src/custom-element.ts
51
+ var DEFAULT_RENDER_OPTIONS = {
52
+ formAssociated: false
53
+ };
54
+ var customElement = (render, options) => {
55
+ const { formAssociated } = { ...DEFAULT_RENDER_OPTIONS, ...options };
56
+ class CustomElement extends HTMLElement {
57
+ #attrSignals = {};
58
+ #attributeChangedFns = /* @__PURE__ */ new Set();
59
+ #connectedFns = /* @__PURE__ */ new Set();
60
+ #disconnectedFns = /* @__PURE__ */ new Set();
61
+ #adoptedCallbackFns = /* @__PURE__ */ new Set();
62
+ #formDisabledCallbackFns = /* @__PURE__ */ new Set();
63
+ #formResetCallbackFns = /* @__PURE__ */ new Set();
64
+ #formStateRestoreCallbackFns = /* @__PURE__ */ new Set();
65
+ #shadowRoot = this.attachShadow({ mode: "closed" });
66
+ #internals = this.attachInternals();
67
+ #observer = new MutationObserver((mutations) => {
68
+ for (const mutation of mutations) {
69
+ const attrName = mutation.attributeName;
70
+ if (mutation.type !== "attributes" || attrName === null) continue;
71
+ const [value, setValue] = this.#attrSignals[attrName];
72
+ const _oldValue = value();
73
+ const oldValue = _oldValue === null ? null : _oldValue;
74
+ const newValue = this.getAttribute(attrName);
75
+ setValue(newValue);
76
+ for (const fn of this.#attributeChangedFns) {
77
+ fn(attrName, oldValue, newValue);
78
+ }
79
+ }
80
+ });
81
+ #render() {
82
+ const fragment = render({
83
+ elementRef: this,
84
+ root: this.#shadowRoot,
85
+ internals: this.#internals,
86
+ attributeChangedCallback: (fn) => this.#attributeChangedFns.add(fn),
87
+ connectedCallback: (fn) => this.#connectedFns.add(fn),
88
+ disconnectedCallback: (fn) => this.#disconnectedFns.add(fn),
89
+ adoptedCallback: (fn) => this.#adoptedCallbackFns.add(fn),
90
+ formDisabledCallback: (fn) => this.#formDisabledCallbackFns.add(fn),
91
+ formResetCallback: (fn) => this.#formResetCallbackFns.add(fn),
92
+ formStateRestoreCallback: (fn) => this.#formStateRestoreCallbackFns.add(fn),
93
+ attrSignals: new Proxy(
94
+ {},
95
+ {
96
+ get: (_, prop) => {
97
+ if (!(prop in this.#attrSignals)) this.#attrSignals[prop] = createSignal(null);
98
+ const [getter] = this.#attrSignals[prop];
99
+ const setter = (newValue) => this.setAttribute(prop, newValue);
100
+ return [getter, setter];
101
+ },
102
+ set: () => {
103
+ console.error("Signals must be assigned via setters.");
104
+ return false;
105
+ }
106
+ }
107
+ ),
108
+ refs: new Proxy(
109
+ {},
110
+ {
111
+ get: (_, prop) => this.#shadowRoot.querySelector(`[ref=${prop}]`),
112
+ set: () => {
113
+ console.error("Refs are readonly and cannot be assigned.");
114
+ return false;
115
+ }
116
+ }
117
+ ),
118
+ adoptStyleSheet: (stylesheet) => {
119
+ this.#shadowRoot.adoptedStyleSheets.push(stylesheet);
120
+ }
121
+ });
122
+ setInnerHTML(this.#shadowRoot, fragment);
123
+ }
124
+ static get formAssociated() {
125
+ return formAssociated;
126
+ }
127
+ constructor() {
128
+ super();
129
+ for (const attr of this.attributes) {
130
+ this.#attrSignals[attr.name] = createSignal(attr.value);
131
+ }
132
+ this.#render();
133
+ }
134
+ connectedCallback() {
135
+ this.#observer.observe(this, { attributes: true });
136
+ for (const fn of this.#connectedFns) {
137
+ fn();
138
+ }
139
+ }
140
+ disconnectedCallback() {
141
+ this.#observer.disconnect();
142
+ for (const fn of this.#disconnectedFns) {
143
+ fn();
144
+ }
145
+ }
146
+ adoptedCallback() {
147
+ for (const fn of this.#adoptedCallbackFns) {
148
+ fn();
149
+ }
150
+ }
151
+ formDisabledCallback() {
152
+ for (const fn of this.#formDisabledCallbackFns) {
153
+ fn();
154
+ }
155
+ }
156
+ formResetCallback() {
157
+ for (const fn of this.#formResetCallbackFns) {
158
+ fn();
159
+ }
160
+ }
161
+ formStateRestoreCallback() {
162
+ for (const fn of this.#formStateRestoreCallbackFns) {
163
+ fn();
164
+ }
165
+ }
166
+ }
167
+ let _tagname = null;
168
+ return {
169
+ define(tagname) {
170
+ if (customElements.get(tagname) !== void 0) {
171
+ console.warn(`Custom element "${tagname}" was already defined. Skipping...`);
172
+ return this;
173
+ }
174
+ customElements.define(tagname, CustomElement);
175
+ _tagname = tagname;
176
+ return this;
177
+ },
178
+ register(registry) {
179
+ if (_tagname === null) {
180
+ console.error("Custom element must be defined before registering.");
181
+ return this;
182
+ }
183
+ registry.register(_tagname, CustomElement);
184
+ return this;
185
+ },
186
+ eject: () => CustomElement
187
+ };
188
+ };
189
+ var createRegistry = () => {
190
+ const registry = /* @__PURE__ */ new Map();
191
+ return {
192
+ register: (tagName, element) => {
193
+ if (registry.has(element)) {
194
+ console.warn(`Custom element class "${element.constructor.name}" was already registered. Skipping...`);
195
+ return;
196
+ }
197
+ registry.set(element, tagName.toUpperCase());
198
+ },
199
+ getTagName: (element) => registry.get(element)
200
+ };
201
+ };
202
+
203
+ // src/render.ts
204
+ var html = (strings, ...values) => {
205
+ let innerHTML = "";
206
+ const signalMap = /* @__PURE__ */ new Map();
207
+ strings.forEach((string, i) => {
208
+ let value = values[i] ?? "";
209
+ if (typeof value === "function") {
210
+ const uniqueKey = crypto.randomUUID();
211
+ signalMap.set(uniqueKey, value);
212
+ value = `{{signal:${uniqueKey}}}`;
213
+ }
214
+ innerHTML += string + String(value);
215
+ });
216
+ const fragment = parseFragment(innerHTML);
217
+ const signalBindingRegex = /(\{\{signal:.+\}\})/;
218
+ const parseChildren = (element) => {
219
+ for (const child of element.childNodes) {
220
+ if (child instanceof Text && signalBindingRegex.test(child.data)) {
221
+ const textList = child.data.split(signalBindingRegex);
222
+ textList.forEach((text, i) => {
223
+ const uniqueKey = text.replace(/\{\{signal:(.+)\}\}/, "$1");
224
+ const signal = uniqueKey !== text ? signalMap.get(uniqueKey) : null;
225
+ const newText = signal !== null ? signal() : text;
226
+ const newNode = new Text(newText);
227
+ if (i === 0) {
228
+ child.replaceWith(newNode);
229
+ } else {
230
+ element.insertBefore(newNode, child.nextSibling);
231
+ }
232
+ if (signal !== null) {
233
+ createEffect(() => {
234
+ newNode.data = signal();
235
+ });
236
+ }
237
+ });
238
+ }
239
+ if (child instanceof Element) {
240
+ for (const attr of child.attributes) {
241
+ if (signalBindingRegex.test(attr.value)) {
242
+ const textList = attr.value.split(signalBindingRegex);
243
+ createEffect(() => {
244
+ let newText = "";
245
+ for (const text of textList) {
246
+ const uniqueKey = text.replace(/\{\{signal:(.+)\}\}/, "$1");
247
+ const signal = uniqueKey !== text ? signalMap.get(uniqueKey) : null;
248
+ newText += signal !== null ? signal() : text;
249
+ }
250
+ child.setAttribute(attr.name, newText);
251
+ });
252
+ }
253
+ }
254
+ parseChildren(child);
255
+ }
256
+ }
257
+ };
258
+ parseChildren(fragment);
259
+ return fragment;
260
+ };
261
+ var css = (strings, ...values) => {
262
+ let cssText = "";
263
+ const signalMap = /* @__PURE__ */ new Map();
264
+ const signalBindingRegex = /(\{\{signal:.+\}\})/;
265
+ strings.forEach((string, i) => {
266
+ let value = values[i] ?? "";
267
+ if (typeof value === "function") {
268
+ const uniqueKey = crypto.randomUUID();
269
+ signalMap.set(uniqueKey, value);
270
+ value = `{{signal:${uniqueKey}}}`;
271
+ }
272
+ cssText += string + String(value);
273
+ });
274
+ const stylesheet = new CSSStyleSheet();
275
+ const textList = cssText.split(signalBindingRegex);
276
+ createEffect(() => {
277
+ const newCSSTextList = [];
278
+ for (const text of textList) {
279
+ const uniqueKey = text.replace(/\{\{signal:(.+)\}\}/, "$1");
280
+ const signal = uniqueKey !== text ? signalMap.get(uniqueKey) : null;
281
+ const newText = signal !== null ? signal() : text;
282
+ newCSSTextList.push(newText);
283
+ }
284
+ const newCSSText = newCSSTextList.join("");
285
+ stylesheet.replace(newCSSText);
286
+ });
287
+ return stylesheet;
288
+ };
289
+ export {
290
+ createEffect,
291
+ createRegistry,
292
+ createSignal,
293
+ css,
294
+ customElement,
295
+ derived,
296
+ html
297
+ };
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "thunderous",
3
+ "version": "0.0.1",
4
+ "description": "",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "/dist",
11
+ "/package.json",
12
+ "/README.md",
13
+ "/LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "demo": "cd demo && rm -rf node_modules && npm i && npm start",
17
+ "build": "tsup src/index.ts --format cjs,esm --dts --no-clean"
18
+ },
19
+ "author": "",
20
+ "license": "MIT",
21
+ "devDependencies": {
22
+ "@types/eslint": "^8.56.10",
23
+ "@typescript-eslint/eslint-plugin": "^7.1.1",
24
+ "@typescript-eslint/parser": "^7.1.1",
25
+ "eslint": "^8.57.0",
26
+ "eslint-plugin-import": "^2.31.0",
27
+ "eslint-plugin-node": "^11.1.0",
28
+ "eslint-plugin-promise": "^7.1.0",
29
+ "express": "^4.17.1",
30
+ "prettier": "^3.3.3",
31
+ "typescript": "^5.6.3",
32
+ "tsup": "^8.3.0"
33
+ }
34
+ }
package/readme.md ADDED
@@ -0,0 +1,73 @@
1
+ # Thunderous
2
+
3
+ Thunderous is a library for writing web components in a functional style, reducing the boilerplate, while signals make it better for managing and sharing state.
4
+
5
+ Each component renders only once, then binds signals to DOM nodes for direct updates with _thunderous_ efficiency.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [Usage](#usage)
11
+ - [Development](#development)
12
+ - [License](#license)
13
+
14
+ ## Installation
15
+
16
+ Install Thunderous via npm:
17
+
18
+ ```sh
19
+ npm install thunderous
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ Below is a basic usage of the functions available.
25
+
26
+ ```ts
27
+ import { customElement, html, css, createSignal } from 'thunderous';
28
+
29
+ const MyElement = customElement(({ attrSignals, connectedCallback, refs, adoptStyleSheet }) => {
30
+ const [heading] = attrSignals.heading;
31
+ const [count, setCount] = createSignal(0);
32
+ connectedCallback(() => {
33
+ refs.increment.addEventListener('click', () => {
34
+ setCount(count() + 1);
35
+ });
36
+ });
37
+ adoptStyleSheet(css`
38
+ :host {
39
+ display: block;
40
+ font-family: sans-serif;
41
+ }
42
+ `);
43
+ return html`
44
+ <h2>${heading}</h2>
45
+ <button ref="increment">Increment</button>
46
+ <output>${count}</output>
47
+ `;
48
+ });
49
+
50
+ MyElement.define('my-element');
51
+ ```
52
+
53
+ If you should need to access the class directly for some reason, you can use the `eject` method.
54
+
55
+ ```ts
56
+ const MyElementClass = MyElement.eject();
57
+ ```
58
+
59
+ [more examples to be updated...]
60
+
61
+ ## Development
62
+
63
+ ### Local Server
64
+
65
+ To see it in action, start the demo server with:
66
+
67
+ ```sh
68
+ npm run demo
69
+ ```
70
+
71
+ ## License
72
+
73
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.