@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,112 @@
1
+ /**
2
+ * Example: Simple counter component demonstrating fun-web basics
3
+ */
4
+ import {
5
+ h,
6
+ funState,
7
+ mount,
8
+ type Component,
9
+ type FunState,
10
+ } from "../../src/index";
11
+ import { prop } from "@fun-land/accessor";
12
+
13
+ // Component state (dynamic data)
14
+ interface CounterState {
15
+ count: number;
16
+ }
17
+
18
+ // Component props (static configuration + state)
19
+ interface CounterProps {
20
+ label: string;
21
+ onReset: () => void;
22
+ state: FunState<CounterState>;
23
+ }
24
+
25
+ // Counter component - evaluates once, subscriptions handle updates
26
+ const Counter: Component<CounterProps> = (
27
+ signal,
28
+ { state, onReset, label }
29
+ ) => {
30
+ // Create DOM elements
31
+ const display = h(
32
+ "div",
33
+ { className: "count-display" },
34
+ String(state.get().count)
35
+ );
36
+ const incrementBtn = h("button", { textContent: "+" });
37
+ const decrementBtn = h("button", { textContent: "-" });
38
+ const resetBtn = h("button", { textContent: "Reset" });
39
+
40
+ // Subscribe to state changes
41
+ state.prop("count").subscribe(signal, (count) => {
42
+ display.textContent = String(count);
43
+ });
44
+
45
+ // Event listeners with cleanup
46
+ incrementBtn.addEventListener(
47
+ "click",
48
+ () => {
49
+ state.mod((s) => ({ count: s.count + 1 }));
50
+ },
51
+ { signal }
52
+ );
53
+
54
+ decrementBtn.addEventListener(
55
+ "click",
56
+ () => {
57
+ state.mod((s) => ({ count: s.count - 1 }));
58
+ },
59
+ { signal }
60
+ );
61
+
62
+ resetBtn.addEventListener("click", onReset, { signal });
63
+
64
+ // Build and return DOM tree
65
+ return h("div", { className: "counter" }, [
66
+ h("h2", { textContent: label }),
67
+ display,
68
+ h("div", { className: "buttons" }, [incrementBtn, decrementBtn, resetBtn]),
69
+ ]);
70
+ };
71
+
72
+ // App component demonstrating composition
73
+ interface AppState {
74
+ title: string;
75
+ counterValue: CounterState;
76
+ }
77
+
78
+ const App: Component = (signal) => {
79
+ const state = funState<AppState>({
80
+ title: "Fun-Web Counter Example",
81
+ counterValue: { count: 0 },
82
+ });
83
+ const heading = h("h1", { textContent: state.get().title });
84
+
85
+ // Subscribe to title changes
86
+ state.prop("title").subscribe(signal, (title) => {
87
+ heading.textContent = title;
88
+ });
89
+
90
+ // Create child component with focused state
91
+ const counter = Counter(
92
+ signal, // pass same signal - cleanup cascades
93
+ {
94
+ label: "Click Counter",
95
+ onReset: () => state.prop("counterValue").set({ count: 0 }),
96
+ state: state.focus(prop<AppState>()("counterValue")),
97
+ }
98
+ );
99
+
100
+ return h("div", { className: "app" }, [heading, counter]);
101
+ };
102
+
103
+ // Usage
104
+ export const runExample = () => {
105
+ const container = document.getElementById("app");
106
+ if (!container) throw new Error("No #app element found");
107
+
108
+ const mounted = mount(App, {}, container);
109
+
110
+ // Return cleanup function
111
+ return () => mounted.unmount();
112
+ };
@@ -0,0 +1,44 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>fun-web Counter Example</title>
7
+ <style>
8
+ body {
9
+ font-family: system-ui, -apple-system, sans-serif;
10
+ max-width: 600px;
11
+ margin: 50px auto;
12
+ padding: 20px;
13
+ background: #f5f5f5;
14
+ }
15
+ #app {
16
+ background: white;
17
+ padding: 40px;
18
+ border-radius: 8px;
19
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
20
+ text-align: center;
21
+ }
22
+ button {
23
+ font-size: 24px;
24
+ padding: 15px 30px;
25
+ background: #0066cc;
26
+ color: white;
27
+ border: none;
28
+ border-radius: 4px;
29
+ cursor: pointer;
30
+ transition: background 0.2s;
31
+ }
32
+ button:hover {
33
+ background: #0052a3;
34
+ }
35
+ button:active {
36
+ transform: translateY(1px);
37
+ }
38
+ </style>
39
+ </head>
40
+ <body>
41
+ <div id="app"></div>
42
+ <script src="bundle.js"></script>
43
+ </body>
44
+ </html>
@@ -0,0 +1,79 @@
1
+ import {
2
+ h,
3
+ bindProperty,
4
+ on,
5
+ type Component,
6
+ type FunState,
7
+ } from "../../src/index";
8
+
9
+ export interface TodoState {
10
+ key: string;
11
+ checked: boolean;
12
+ priority: number;
13
+ label: string;
14
+ }
15
+
16
+ export interface TodoProps {
17
+ removeItem: () => void;
18
+ state: FunState<TodoState>;
19
+ }
20
+
21
+ export const Todo: Component<TodoProps> = (signal, { state, removeItem }) => {
22
+ // h is a much more ergonomic way of creating html but it's just document.createElement under the hood
23
+ const prioritySelect = h("select", {}, [
24
+ h("option", { value: "0" }, "High"),
25
+ h("option", { value: "1" }, "Low"),
26
+ ]);
27
+ // 😓 You can manually set initial state
28
+ prioritySelect.value = String(state.get().priority);
29
+ // 😓 and listen to when a state updates to update the element
30
+ state.prop("priority").subscribe(signal, (priority) => {
31
+ prioritySelect.value = String(priority);
32
+ });
33
+ // 😓 native event binding works but requires casting and dev must remember to pass signal so they don't leak memory
34
+ prioritySelect.addEventListener(
35
+ "change",
36
+ (e) => {
37
+ state.prop("priority").set(+(e.currentTarget as HTMLSelectElement).value);
38
+ },
39
+ { signal }
40
+ );
41
+
42
+ const checkbox = h("input", { type: "checkbox" });
43
+ // 😎 For easier binding you can bind property to a reactive state
44
+ bindProperty(checkbox, "checked", state.prop("checked"), signal); // when state.checked updates the checkbox.checked updates
45
+ // 😎 For easier event binding use `on` helper for better type inferrence and you can't forget to cleanup
46
+ on(
47
+ checkbox,
48
+ "change",
49
+ (e) => {
50
+ state.prop("checked").set(e.currentTarget.checked);
51
+ },
52
+ signal
53
+ );
54
+
55
+ // 😎 or do both at the same time since they both return the element
56
+ const labelInput = on(
57
+ bindProperty(
58
+ h("input", {
59
+ type: "text",
60
+ }),
61
+ "value",
62
+ state.prop("label"),
63
+ signal
64
+ ),
65
+ "input",
66
+ (e) => {
67
+ state.prop("label").set(e.currentTarget.value);
68
+ },
69
+ signal
70
+ );
71
+
72
+ return h("li", {}, [
73
+ checkbox,
74
+ prioritySelect,
75
+ labelInput,
76
+ // you can even inline to go tacit
77
+ on(h("button", { textContent: "X" }), "click", removeItem, signal),
78
+ ]);
79
+ };
@@ -0,0 +1,142 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>fun-web Todo App</title>
7
+ <style>
8
+ /* CSS is all inlined here but I recommend using some CSS-in-JS library like typestyle or vanilla-extract so you can have your css colocated with components */
9
+ * {
10
+ box-sizing: border-box;
11
+ }
12
+ body {
13
+ font-family:
14
+ system-ui,
15
+ -apple-system,
16
+ sans-serif;
17
+ max-width: 600px;
18
+ margin: 50px auto;
19
+ padding: 20px;
20
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
21
+ min-height: 100vh;
22
+ }
23
+ #app {
24
+ background: white;
25
+ padding: 30px;
26
+ border-radius: 12px;
27
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
28
+ }
29
+ .todo-app h1 {
30
+ margin: 0 0 20px 0;
31
+ color: #333;
32
+ font-size: 28px;
33
+ }
34
+ .todo-app form {
35
+ display: flex;
36
+ gap: 10px;
37
+ margin-bottom: 20px;
38
+ }
39
+ .todo-app form input[type="text"] {
40
+ flex: 1;
41
+ padding: 12px;
42
+ border: 2px solid #e0e0e0;
43
+ border-radius: 6px;
44
+ font-size: 14px;
45
+ transition: border-color 0.2s;
46
+ }
47
+ .todo-app form input[type="text"]:focus {
48
+ outline: none;
49
+ border-color: #667eea;
50
+ }
51
+ .todo-app form button {
52
+ padding: 12px 24px;
53
+ background: #667eea;
54
+ color: white;
55
+ border: none;
56
+ border-radius: 6px;
57
+ font-size: 14px;
58
+ font-weight: 600;
59
+ cursor: pointer;
60
+ transition: background 0.2s;
61
+ }
62
+ .todo-app form button:hover {
63
+ background: #5568d3;
64
+ }
65
+ .todo-app > div {
66
+ margin-bottom: 20px;
67
+ }
68
+ .todo-app > div button {
69
+ padding: 8px 16px;
70
+ background: #10b981;
71
+ color: white;
72
+ border: none;
73
+ border-radius: 6px;
74
+ font-size: 13px;
75
+ cursor: pointer;
76
+ transition: background 0.2s;
77
+ }
78
+ .todo-app > div button:hover {
79
+ background: #059669;
80
+ }
81
+ .todo-app > div span {
82
+ margin-left: 10px;
83
+ color: #10b981;
84
+ font-weight: 600;
85
+ }
86
+ .todo-app ul {
87
+ list-style: none;
88
+ padding: 0;
89
+ margin: 0;
90
+ }
91
+ .todo-app li {
92
+ display: flex;
93
+ align-items: center;
94
+ gap: 10px;
95
+ padding: 12px;
96
+ background: #f9fafb;
97
+ border-radius: 6px;
98
+ margin-bottom: 8px;
99
+ transition: background 0.2s;
100
+ }
101
+ .todo-app li:hover {
102
+ background: #f3f4f6;
103
+ }
104
+ .todo-app li input[type="checkbox"] {
105
+ width: 18px;
106
+ height: 18px;
107
+ cursor: pointer;
108
+ }
109
+ .todo-app li select {
110
+ padding: 6px 8px;
111
+ border: 1px solid #e0e0e0;
112
+ border-radius: 4px;
113
+ font-size: 13px;
114
+ cursor: pointer;
115
+ }
116
+ .todo-app li input[type="text"] {
117
+ flex: 1;
118
+ padding: 8px;
119
+ border: 1px solid #e0e0e0;
120
+ border-radius: 4px;
121
+ font-size: 14px;
122
+ }
123
+ .todo-app li button {
124
+ padding: 6px 12px;
125
+ background: #ef4444;
126
+ color: white;
127
+ border: none;
128
+ border-radius: 4px;
129
+ font-size: 13px;
130
+ cursor: pointer;
131
+ transition: background 0.2s;
132
+ }
133
+ .todo-app li button:hover {
134
+ background: #dc2626;
135
+ }
136
+ </style>
137
+ </head>
138
+ <body>
139
+ <div id="app"></div>
140
+ <script src="todo-bundle.js"></script>
141
+ </body>
142
+ </html>
@@ -0,0 +1,120 @@
1
+ import {
2
+ h,
3
+ funState,
4
+ mount,
5
+ bindProperty,
6
+ on,
7
+ keyedChildren,
8
+ type Component,
9
+ type FunState,
10
+ } from "../../src/index";
11
+ import { prepend, flow, Acc } from "@fun-land/accessor";
12
+ import { TodoState, Todo } from "./Todo";
13
+
14
+ // ===== Types =====
15
+
16
+ interface TodoAppState {
17
+ value: string;
18
+ items: TodoState[];
19
+ }
20
+
21
+ // ===== Accessors and helpers =====
22
+
23
+ const stateFoci = Acc<TodoAppState>();
24
+
25
+ const addItem = (state: TodoAppState): TodoAppState =>
26
+ stateFoci.prop("items").mod(
27
+ prepend<TodoState>({
28
+ checked: false,
29
+ label: state.value,
30
+ priority: 1,
31
+ key: crypto.randomUUID(),
32
+ })
33
+ )(state);
34
+
35
+ const clearValue = stateFoci.prop("value").set("");
36
+
37
+ const markAllDone = stateFoci.prop("items").all().prop("checked").set(true);
38
+
39
+ const removeByKey = (key: string) =>
40
+ stateFoci.prop("items").mod((xs) => xs.filter((t) => t.key !== key));
41
+
42
+ // ===== Todo App Component =====
43
+
44
+ const initialState: TodoAppState = {
45
+ value: "",
46
+ items: [
47
+ { checked: false, label: "Learn fun-web", priority: 0, key: "asdf" },
48
+ { checked: true, label: "Build something cool", priority: 1, key: "fdas" },
49
+ ],
50
+ };
51
+
52
+ const TodoApp: Component = (signal) => {
53
+ const state = funState(initialState);
54
+ const input = bindProperty(
55
+ h("input", {
56
+ type: "text",
57
+ value: state.get().value,
58
+ placeholder: "Add a todo...",
59
+ }),
60
+ "value",
61
+ state.prop("value"),
62
+ signal
63
+ );
64
+ on(
65
+ input,
66
+ "input",
67
+ (e) => {
68
+ state.prop("value").set(e.currentTarget.value);
69
+ },
70
+ signal
71
+ );
72
+
73
+ const addBtn = h("button", { type: "submit", textContent: "Add" });
74
+
75
+ const form = on(
76
+ h("form", {}, [input, addBtn]),
77
+ "submit",
78
+ (e) => {
79
+ e.preventDefault();
80
+ if (state.get().value.trim()) {
81
+ state.mod(flow(addItem, clearValue));
82
+ }
83
+ },
84
+ signal
85
+ );
86
+
87
+ // Because `on` returns the element you can pipe through
88
+ const markAllBtn = on(
89
+ h("button", { textContent: "Mark All Done" }),
90
+ "click",
91
+ () => {
92
+ state.mod(markAllDone);
93
+ },
94
+ signal
95
+ );
96
+ const allDoneText = h("span", { textContent: "" });
97
+
98
+ const todoList = h("ul", {});
99
+ keyedChildren(todoList, signal, state.prop("items"), (row) =>
100
+ Todo(row.signal, {
101
+ removeItem: row.remove,
102
+ state: row.state,
103
+ })
104
+ );
105
+
106
+ return h("div", { className: "todo-app" }, [
107
+ h("h1", { textContent: "Todo App" }),
108
+ form,
109
+ h("div", {}, [markAllBtn, allDoneText]),
110
+ todoList,
111
+ ]);
112
+ };
113
+
114
+ // ===== Initialize =====
115
+
116
+ const app = document.getElementById("app");
117
+
118
+ if (app) {
119
+ mount(TodoApp, {}, app);
120
+ }