@fun-land/fun-web 0.2.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 (71) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +515 -0
  3. package/coverage/clover.xml +136 -0
  4. package/coverage/coverage-final.json +4 -0
  5. package/coverage/lcov-report/base.css +224 -0
  6. package/coverage/lcov-report/block-navigation.js +87 -0
  7. package/coverage/lcov-report/dom.ts.html +961 -0
  8. package/coverage/lcov-report/favicon.png +0 -0
  9. package/coverage/lcov-report/index.html +146 -0
  10. package/coverage/lcov-report/mount.ts.html +202 -0
  11. package/coverage/lcov-report/prettify.css +1 -0
  12. package/coverage/lcov-report/prettify.js +2 -0
  13. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  14. package/coverage/lcov-report/sorter.js +196 -0
  15. package/coverage/lcov-report/state.ts.html +91 -0
  16. package/coverage/lcov.info +260 -0
  17. package/dist/esm/src/dom.d.ts +85 -0
  18. package/dist/esm/src/dom.js +207 -0
  19. package/dist/esm/src/dom.js.map +1 -0
  20. package/dist/esm/src/index.d.ts +7 -0
  21. package/dist/esm/src/index.js +5 -0
  22. package/dist/esm/src/index.js.map +1 -0
  23. package/dist/esm/src/mount.d.ts +21 -0
  24. package/dist/esm/src/mount.js +27 -0
  25. package/dist/esm/src/mount.js.map +1 -0
  26. package/dist/esm/src/state.d.ts +2 -0
  27. package/dist/esm/src/state.js +3 -0
  28. package/dist/esm/src/state.js.map +1 -0
  29. package/dist/esm/src/types.d.ts +3 -0
  30. package/dist/esm/src/types.js +3 -0
  31. package/dist/esm/src/types.js.map +1 -0
  32. package/dist/esm/tsconfig.publish.tsbuildinfo +1 -0
  33. package/dist/src/dom.d.ts +85 -0
  34. package/dist/src/dom.js +224 -0
  35. package/dist/src/dom.js.map +1 -0
  36. package/dist/src/index.d.ts +7 -0
  37. package/dist/src/index.js +24 -0
  38. package/dist/src/index.js.map +1 -0
  39. package/dist/src/mount.d.ts +21 -0
  40. package/dist/src/mount.js +31 -0
  41. package/dist/src/mount.js.map +1 -0
  42. package/dist/src/state.d.ts +2 -0
  43. package/dist/src/state.js +7 -0
  44. package/dist/src/state.js.map +1 -0
  45. package/dist/src/types.d.ts +3 -0
  46. package/dist/src/types.js +4 -0
  47. package/dist/src/types.js.map +1 -0
  48. package/dist/tsconfig.publish.tsbuildinfo +1 -0
  49. package/eslint.config.js +54 -0
  50. package/examples/README.md +67 -0
  51. package/examples/counter/bundle.js +219 -0
  52. package/examples/counter/counter.ts +112 -0
  53. package/examples/counter/index.html +44 -0
  54. package/examples/todo-app/Todo.ts +79 -0
  55. package/examples/todo-app/index.html +142 -0
  56. package/examples/todo-app/todo-app.ts +120 -0
  57. package/examples/todo-app/todo-bundle.js +410 -0
  58. package/jest.config.js +5 -0
  59. package/package.json +49 -0
  60. package/src/dom.test.ts +768 -0
  61. package/src/dom.ts +296 -0
  62. package/src/index.ts +25 -0
  63. package/src/mount.test.ts +220 -0
  64. package/src/mount.ts +39 -0
  65. package/src/state.test.ts +225 -0
  66. package/src/state.ts +2 -0
  67. package/src/types.ts +9 -0
  68. package/tsconfig.json +16 -0
  69. package/tsconfig.publish.json +6 -0
  70. package/wip/hx-magic-properties-plan.md +575 -0
  71. package/wip/next.md +22 -0
@@ -0,0 +1,575 @@
1
+ # Magic Properties for fun-web Element Constructor
2
+
3
+ ## Overview
4
+ Add new `hx()` helper function with magic properties `$bind`, `$on`, and `$model` to enable inline reactive bindings, reducing boilerplate while maintaining type safety and explicit signal management. The `$model` property provides automatic two-way binding sugar.
5
+
6
+ ## User Decisions (Confirmed)
7
+ - ✅ **Approach**: Non-curried `hx(signal, tag, attrs)` - signal as first parameter
8
+ - ✅ **Naming**: Use `$` prefix (`$bind`, `$on`, `$model`)
9
+ - ✅ **Initial scope**: Include `$bind`, `$on`, and `$model` (not `$class`/`$style` yet)
10
+ - ✅ **Two-way binding**: Include `$model` sugar for automatic two-way binding
11
+ - ✅ **File structure**: Keep hx() in separate files (`hx.ts`, `hx.test.ts`) since it's experimental
12
+
13
+ ## Current State
14
+ Users must chain `bindProperty()` and `on()` calls after creating elements:
15
+
16
+ ```typescript
17
+ const input = h("input", { type: "text" });
18
+ bindProperty(input, "value", state.prop("name"), signal);
19
+ on(input, "input", (e) => state.prop("name").set(e.currentTarget.value), signal);
20
+ ```
21
+
22
+ ## Proposed API
23
+
24
+ ### New hx() Function
25
+ ```typescript
26
+ // Signal is explicit as first parameter
27
+ const input = hx(signal, "input", {
28
+ type: "text",
29
+ $model: { value: state.prop("name") }
30
+ });
31
+
32
+ // Can mix with regular h() for static elements
33
+ const heading = h("h1", {}, "Login Form");
34
+ ```
35
+
36
+ ### Real-world Example
37
+ ```typescript
38
+ const LoginForm: Component<{}, [FunWebState<LoginState>]> = (signal, props, state) => {
39
+ return h("form", { className: "login" }, [
40
+ h("h2", {}, "Login"),
41
+
42
+ // Email input with automatic two-way binding using $model
43
+ hx(signal, "input", {
44
+ type: "email",
45
+ placeholder: "Email",
46
+ $model: { value: state.prop("email") }
47
+ }),
48
+
49
+ // Password input with automatic two-way binding
50
+ hx(signal, "input", {
51
+ type: "password",
52
+ placeholder: "Password",
53
+ $model: { value: state.prop("password") }
54
+ }),
55
+
56
+ // Submit button with custom handler
57
+ hx(signal, "button", {
58
+ type: "submit",
59
+ $on: { click: handleSubmit }
60
+ })
61
+ ]);
62
+ };
63
+ ```
64
+
65
+ ## Implementation Plan
66
+
67
+ ### Phase 1: Core Implementation
68
+
69
+ **File: `/packages/fun-web/src/hx.ts`** (new file - keeping experimental features separate)
70
+
71
+ 1. **Create new hx.ts file** with hx() function (non-curried, signal as first param):
72
+ ```typescript
73
+ // src/hx.ts - Experimental magic properties for fun-web
74
+ import { bindProperty, on, appendChildren } from "./dom";
75
+ import type { ElementChild } from "./types";
76
+ import type { FunWebState } from "./state";
77
+
78
+ export interface MagicAttrs<E extends Element> {
79
+ $bind?: Partial<{
80
+ [K in keyof E]: FunWebState<E[K]>
81
+ }>;
82
+ $on?: Partial<{
83
+ [K in keyof HTMLElementEventMap]: (
84
+ ev: HTMLElementEventMap[K] & { currentTarget: E }
85
+ ) => void
86
+ }>;
87
+ $model?: Partial<{
88
+ [K in keyof E]: FunWebState<E[K]>
89
+ }>;
90
+ }
91
+
92
+ export const hx = <Tag extends keyof HTMLElementTagNameMap>(
93
+ signal: AbortSignal,
94
+ tag: Tag,
95
+ attrs?: (Record<string, any> & MagicAttrs<HTMLElementTagNameMap[Tag]>) | null,
96
+ children?: ElementChild | ElementChild[]
97
+ ): HTMLElementTagNameMap[Tag] => {
98
+ const element = document.createElement(tag);
99
+
100
+ if (attrs) {
101
+ const { $bind, $on, $model, ...regularAttrs } = attrs;
102
+
103
+ // Process regular attributes (reuse h() logic)
104
+ for (const [key, value] of Object.entries(regularAttrs)) {
105
+ if (value == null) continue;
106
+
107
+ if (key.startsWith("on") && typeof value === "function") {
108
+ const eventName = key.slice(2).toLowerCase();
109
+ element.addEventListener(eventName, value);
110
+ } else if (key.includes("-") || key === "role") {
111
+ element.setAttribute(key, String(value));
112
+ } else {
113
+ (element as any)[key] = value;
114
+ }
115
+ }
116
+
117
+ // Process $model magic property (two-way binding sugar)
118
+ if ($model) {
119
+ for (const [key, state] of Object.entries($model)) {
120
+ // Bind property (state → element)
121
+ bindProperty(element, key as any, state as any, signal);
122
+
123
+ // Auto-detect event and setup listener (element → state)
124
+ const eventType = getEventForProperty(key, element.tagName);
125
+ on(element, eventType as any, (e: any) => {
126
+ (state as any).set(e.currentTarget[key]);
127
+ }, signal);
128
+ }
129
+ }
130
+
131
+ // Process $bind magic property
132
+ if ($bind) {
133
+ for (const [key, state] of Object.entries($bind)) {
134
+ bindProperty(element, key as any, state as any, signal);
135
+ }
136
+ }
137
+
138
+ // Process $on magic property
139
+ if ($on) {
140
+ for (const [type, handler] of Object.entries($on)) {
141
+ on(element, type as any, handler as any, signal);
142
+ }
143
+ }
144
+ }
145
+
146
+ // Append children (reuse appendChildren from h())
147
+ if (children != null) {
148
+ appendChildren(element, children);
149
+ }
150
+
151
+ return element;
152
+ };
153
+
154
+ // Helper to auto-detect the correct event type for a property
155
+ function getEventForProperty(prop: string, tagName: string): string {
156
+ if (prop === "checked") return "change";
157
+ if (prop === "value" && tagName === "SELECT") return "change";
158
+ if (prop === "value") return "input";
159
+ return "change"; // default
160
+ }
161
+ ```
162
+
163
+ **Note**: hx() will duplicate some attribute processing logic from h(), but keeps experimental features isolated.
164
+
165
+ **File: `/packages/fun-web/src/index.ts`**
166
+
167
+ 2. **Export hx() and types from the new file**:
168
+ ```typescript
169
+ export { hx, type MagicAttrs } from "./hx";
170
+ ```
171
+
172
+ ### Phase 2: Testing
173
+
174
+ **File: `/packages/fun-web/src/hx.test.ts`** (new file - separate tests for experimental feature)
175
+
176
+ Create comprehensive test suite:
177
+
178
+ ```typescript
179
+ import { hx } from "./hx";
180
+ import { useFunWebState } from "./state";
181
+
182
+ describe("hx() magic properties", () => {
183
+ let signal: AbortSignal;
184
+
185
+ beforeEach(() => {
186
+ const controller = new AbortController();
187
+ signal = controller.signal;
188
+ });
189
+
190
+ describe("$bind", () => {
191
+ it("should set initial property value from state", () => {
192
+ const state = useFunWebState("hello");
193
+ const input = hx(signal, "input", {
194
+ $bind: { value: state }
195
+ }) as HTMLInputElement;
196
+ expect(input.value).toBe("hello");
197
+ });
198
+
199
+ it("should update property when state changes", () => {
200
+ const state = useFunWebState("hello");
201
+ const input = hx(signal, "input", {
202
+ $bind: { value: state }
203
+ }) as HTMLInputElement;
204
+
205
+ state.set("world");
206
+ expect(input.value).toBe("world");
207
+ });
208
+
209
+ it("should stop updating after signal aborts", () => {
210
+ const controller = new AbortController();
211
+ const state = useFunWebState("hello");
212
+ const input = hx(controller.signal, "input", {
213
+ $bind: { value: state }
214
+ }) as HTMLInputElement;
215
+
216
+ controller.abort();
217
+ state.set("world");
218
+ expect(input.value).toBe("hello");
219
+ });
220
+
221
+ it("should support binding multiple properties", () => {
222
+ const valueState = useFunWebState("test");
223
+ const disabledState = useFunWebState(true);
224
+ const input = hx(signal, "input", {
225
+ $bind: {
226
+ value: valueState,
227
+ disabled: disabledState
228
+ }
229
+ }) as HTMLInputElement;
230
+
231
+ expect(input.value).toBe("test");
232
+ expect(input.disabled).toBe(true);
233
+ });
234
+ });
235
+
236
+ describe("$on", () => {
237
+ it("should attach event listeners", () => {
238
+ const handler = jest.fn();
239
+ const button = hx(signal, "button", {
240
+ $on: { click: handler }
241
+ });
242
+
243
+ button.click();
244
+ expect(handler).toHaveBeenCalled();
245
+ });
246
+
247
+ it("should support multiple event listeners", () => {
248
+ const clickHandler = jest.fn();
249
+ const mouseoverHandler = jest.fn();
250
+ const button = hx(signal, "button", {
251
+ $on: {
252
+ click: clickHandler,
253
+ mouseover: mouseoverHandler
254
+ }
255
+ });
256
+
257
+ button.click();
258
+ expect(clickHandler).toHaveBeenCalled();
259
+
260
+ button.dispatchEvent(new MouseEvent("mouseover"));
261
+ expect(mouseoverHandler).toHaveBeenCalled();
262
+ });
263
+
264
+ it("should cleanup listeners on signal abort", () => {
265
+ const controller = new AbortController();
266
+ const handler = jest.fn();
267
+ const button = hx(controller.signal, "button", {
268
+ $on: { click: handler }
269
+ });
270
+
271
+ controller.abort();
272
+ button.click();
273
+ expect(handler).not.toHaveBeenCalled();
274
+ });
275
+ });
276
+
277
+ describe("$bind and $on together", () => {
278
+ it("should support both in same element (two-way binding)", () => {
279
+ const state = useFunWebState("hello");
280
+ const input = hx(signal, "input", {
281
+ $bind: { value: state },
282
+ $on: {
283
+ input: (e) => state.set(e.currentTarget.value)
284
+ }
285
+ }) as HTMLInputElement;
286
+
287
+ expect(input.value).toBe("hello");
288
+
289
+ // Simulate user typing
290
+ input.value = "world";
291
+ input.dispatchEvent(new Event("input"));
292
+
293
+ expect(state.get()).toBe("world");
294
+ });
295
+ });
296
+
297
+ describe("$model", () => {
298
+ it("should create automatic two-way binding for input value", () => {
299
+ const state = useFunWebState("hello");
300
+ const input = hx(signal, "input", {
301
+ type: "text",
302
+ $model: { value: state }
303
+ }) as HTMLInputElement;
304
+
305
+ // Initial binding (state → element)
306
+ expect(input.value).toBe("hello");
307
+
308
+ // Simulate user typing (element → state)
309
+ input.value = "world";
310
+ input.dispatchEvent(new Event("input"));
311
+ expect(state.get()).toBe("world");
312
+
313
+ // State changes should update element
314
+ state.set("foo");
315
+ expect(input.value).toBe("foo");
316
+ });
317
+
318
+ it("should create automatic two-way binding for checkbox checked", () => {
319
+ const state = useFunWebState(false);
320
+ const checkbox = hx(signal, "input", {
321
+ type: "checkbox",
322
+ $model: { checked: state }
323
+ }) as HTMLInputElement;
324
+
325
+ expect(checkbox.checked).toBe(false);
326
+
327
+ checkbox.checked = true;
328
+ checkbox.dispatchEvent(new Event("change"));
329
+ expect(state.get()).toBe(true);
330
+
331
+ state.set(false);
332
+ expect(checkbox.checked).toBe(false);
333
+ });
334
+
335
+ it("should use 'change' event for select elements", () => {
336
+ const state = useFunWebState("option1");
337
+ const select = hx(signal, "select", {
338
+ $model: { value: state }
339
+ }) as HTMLSelectElement;
340
+
341
+ expect(select.value).toBe("option1");
342
+
343
+ select.value = "option2";
344
+ select.dispatchEvent(new Event("change"));
345
+ expect(state.get()).toBe("option2");
346
+ });
347
+
348
+ it("should cleanup on signal abort", () => {
349
+ const controller = new AbortController();
350
+ const state = useFunWebState("hello");
351
+ const input = hx(controller.signal, "input", {
352
+ $model: { value: state }
353
+ }) as HTMLInputElement;
354
+
355
+ controller.abort();
356
+
357
+ // After abort, element changes shouldn't update state
358
+ input.value = "world";
359
+ input.dispatchEvent(new Event("input"));
360
+ expect(state.get()).toBe("hello");
361
+
362
+ // And state changes shouldn't update element
363
+ state.set("foo");
364
+ expect(input.value).toBe("world");
365
+ });
366
+
367
+ it("should be simpler than explicit $bind + $on", () => {
368
+ // This is a documentation test showing the benefit
369
+ const state = useFunWebState("test");
370
+
371
+ // Explicit way (verbose)
372
+ const input1 = hx(signal, "input", {
373
+ $bind: { value: state },
374
+ $on: { input: (e) => state.set(e.currentTarget.value) }
375
+ });
376
+
377
+ // Sugar way (concise)
378
+ const input2 = hx(signal, "input", {
379
+ $model: { value: state }
380
+ });
381
+
382
+ // Both should behave identically
383
+ expect(input1.value).toBe("test");
384
+ expect(input2.value).toBe("test");
385
+ });
386
+ });
387
+
388
+ describe("regular attributes", () => {
389
+ it("should process regular attributes alongside magic properties", () => {
390
+ const state = useFunWebState("test");
391
+ const input = hx(signal, "input", {
392
+ type: "text",
393
+ className: "form-input",
394
+ placeholder: "Enter name",
395
+ $bind: { value: state }
396
+ }) as HTMLInputElement;
397
+
398
+ expect(input.type).toBe("text");
399
+ expect(input.className).toBe("form-input");
400
+ expect(input.placeholder).toBe("Enter name");
401
+ expect(input.value).toBe("test");
402
+ });
403
+ });
404
+
405
+ describe("type inference", () => {
406
+ it("should infer correct element type from tag", () => {
407
+ // This is a compile-time test - if it compiles, it passes
408
+ const input = hx(signal, "input", {});
409
+ const _value: string = input.value; // HTMLInputElement has value
410
+
411
+ const div = hx(signal, "div", {});
412
+ // @ts-expect-error - HTMLDivElement doesn't have value property
413
+ const _noValue: string = div.value;
414
+ });
415
+ });
416
+ });
417
+ ```
418
+
419
+ ### Phase 3: Documentation
420
+
421
+ **File: `/packages/fun-web/README.md`**
422
+
423
+ Add new section after "Best Practices":
424
+
425
+ ```markdown
426
+ ## Magic Properties with hx()
427
+
428
+ For more concise inline bindings, use the `hx()` helper:
429
+
430
+ ### Basic Usage
431
+
432
+ ```typescript
433
+ import { hx } from "@fun-land/fun-web";
434
+
435
+ const MyComponent: Component<{}, [FunWebState<State>]> = (signal, props, state) => {
436
+ // Use hx() with $model for simple two-way binding
437
+ const input = hx(signal, "input", {
438
+ type: "text",
439
+ placeholder: "Enter your name",
440
+ $model: { value: state.prop("name") }
441
+ });
442
+
443
+ // Mix with regular h() for static elements
444
+ const label = h("label", {}, "Name:");
445
+
446
+ return h("div", {}, [label, input]);
447
+ };
448
+ ```
449
+
450
+ ### Magic Properties
451
+
452
+ **`$bind`**: Bind element properties to reactive state
453
+
454
+ ```typescript
455
+ $bind: { value: state.prop("name") } // Syncs input.value with state.name
456
+ $bind: { checked: state.prop("agreed") } // Syncs checkbox.checked with state.agreed
457
+ $bind: { disabled: state.prop("isDisabled") } // Syncs button.disabled with state
458
+ ```
459
+
460
+ Equivalent to `bindProperty(el, "value", state.prop("name"), signal)` but inline.
461
+
462
+ **`$on`**: Attach event listeners
463
+
464
+ ```typescript
465
+ $on: { click: () => console.log("clicked") }
466
+ $on: { input: (e) => state.set(e.currentTarget.value) }
467
+ $on: { submit: (e) => { e.preventDefault(); handleSubmit(); } }
468
+ ```
469
+
470
+ Equivalent to `on(el, "click", handler, signal)` but inline.
471
+
472
+ **`$model`**: Automatic two-way binding (combines $bind + $on)
473
+
474
+ ```typescript
475
+ $model: { value: state.prop("name") } // Auto two-way binding for input.value
476
+ $model: { checked: state.prop("agreed") } // Auto two-way binding for checkbox.checked
477
+ ```
478
+
479
+ Automatically:
480
+ - Binds property from state to element (like $bind)
481
+ - Sets up event listener to sync changes back to state (like $on)
482
+ - Detects the correct event type based on property and element:
483
+ - `checked` → `change` event
484
+ - `value` on `<select>` → `change` event
485
+ - `value` on other elements → `input` event
486
+
487
+ ### Two-Way Binding Patterns
488
+
489
+ **Explicit two-way binding** with $bind + $on:
490
+
491
+ ```typescript
492
+ const input = hx(signal, "input", {
493
+ type: "email",
494
+ $bind: { value: state.prop("email") },
495
+ $on: { input: (e) => state.prop("email").set(e.currentTarget.value) }
496
+ });
497
+ ```
498
+
499
+ **Automatic two-way binding** with $model (recommended for simple cases):
500
+
501
+ ```typescript
502
+ const input = hx(signal, "input", {
503
+ type: "email",
504
+ $model: { value: state.prop("email") }
505
+ });
506
+ ```
507
+
508
+ Use `$model` when the default event detection works for you. Use explicit `$bind` + `$on` when you need custom event handling or transformation logic.
509
+
510
+ ### When to use hx() vs h()
511
+
512
+ **Use `hx()` when:**
513
+ - Element needs reactive bindings (`$bind`)
514
+ - Element needs event listeners that update state (`$on`)
515
+ - You want inline syntax for cleaner code
516
+
517
+ **Use regular `h()` when:**
518
+ - Element is static (no bindings or event listeners)
519
+ - You prefer explicit `bindProperty()` and `on()` calls
520
+ - Building non-reactive components
521
+
522
+ Both approaches work together - use what fits the situation.
523
+ ```
524
+
525
+ **File: `/packages/fun-web/examples/todo-app/todo-app.ts`**
526
+
527
+ Refactor one component to demonstrate hx():
528
+
529
+ ```typescript
530
+ // Before: verbose with separate calls
531
+ const Todo: Component<TodoProps, [FunWebState<TodoState>]> = (signal, props, state) => {
532
+ const checkbox = h("input", { type: "checkbox" });
533
+ bindProperty(checkbox, "checked", state.prop("checked"), signal);
534
+ on(checkbox, "change", (e) => {
535
+ state.prop("checked").set(e.currentTarget.checked);
536
+ }, signal);
537
+
538
+ // ... more elements ...
539
+ };
540
+
541
+ // After: concise with hx() and $model
542
+ const Todo: Component<TodoProps, [FunWebState<TodoState>]> = (signal, props, state) => {
543
+ const checkbox = hx(signal, "input", {
544
+ type: "checkbox",
545
+ $model: { checked: state.prop("checked") }
546
+ });
547
+
548
+ const labelInput = hx(signal, "input", {
549
+ type: "text",
550
+ $model: { value: state.prop("label") }
551
+ });
552
+
553
+ // ... more elements ...
554
+ };
555
+ ```
556
+
557
+ ## Verification Plan
558
+
559
+ 1. **Tests pass**: All 71 existing tests + new hx() tests
560
+ 2. **Type safety**: IDE autocomplete works, no type errors
561
+ 3. **Examples build**: `yarn build:examples` succeeds
562
+ 4. **Bundle size**: Check impact (expect <500 bytes increase)
563
+ 5. **Manual testing**: Try hx() in examples, verify cleanup works
564
+
565
+ ## Critical Files
566
+
567
+ - `/packages/fun-web/src/hx.ts` - **NEW FILE** - hx() function with magic properties (experimental)
568
+ - `/packages/fun-web/src/hx.test.ts` - **NEW FILE** - Comprehensive test suite for hx()
569
+ - `/packages/fun-web/src/index.ts` - Add exports for hx and MagicAttrs
570
+ - `/packages/fun-web/examples/todo-app/todo-app.ts` - Refactor to demonstrate hx()
571
+ - `/packages/fun-web/README.md` - Document magic properties API
572
+
573
+ ## Next Steps
574
+
575
+ Ready to implement hx() with all three magic properties: `$bind`, `$on`, and `$model`.
package/wip/next.md ADDED
@@ -0,0 +1,22 @@
1
+ Ready to ship:
2
+
3
+ 2. Update root CLAUDE.md - Document fun-web in the monorepo guide.
4
+ 3. Publish to npm - Run lerna publish to release it.
5
+
6
+ Polish before shipping:
7
+
8
+ 5. Performance testing - Benchmark keyedChildren with large lists
9
+ 6. Add more examples - Maybe a form validation example or simple SPA
10
+
11
+ New features:
12
+
13
+ 7. Conditional rendering helper - Similar to keyedChildren but for if/else
14
+ 8. Animation helpers - Utilities for transitions when elements mount/unmount
15
+ 9. Portal support - Render components outside their parent container
16
+ 10. Server integration - Document patterns for hydrating server-rendered HTML
17
+
18
+ Documentation:
19
+
20
+ 11. Migration guide - For users coming from React or other frameworks
21
+ 12. API comparison table - Show fun-web equivalents to React patterns
22
+ 13. Video walkthrough - Record building something with fun-web