@davidsouther/jiffies 2026.24.0 → 2026.24.2

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 (226) hide show
  1. package/lib/esm/assert.d.ts +26 -0
  2. package/lib/esm/assert.js +38 -0
  3. package/lib/esm/awaitable.js +1 -0
  4. package/lib/esm/case.d.ts +1 -0
  5. package/lib/esm/case.js +5 -0
  6. package/lib/esm/components/accordion.d.ts +5 -0
  7. package/lib/esm/components/accordion.js +9 -0
  8. package/lib/esm/components/alert.d.ts +7 -0
  9. package/lib/esm/components/alert.js +31 -0
  10. package/lib/esm/components/button_bar.d.ts +8 -0
  11. package/lib/esm/components/button_bar.js +25 -0
  12. package/lib/esm/components/card.d.ts +8 -0
  13. package/lib/esm/components/card.js +31 -0
  14. package/lib/esm/components/children.d.ts +2 -0
  15. package/{src/components/children.ts → lib/esm/components/children.js} +2 -6
  16. package/lib/esm/components/form.d.ts +5 -0
  17. package/lib/esm/components/form.js +13 -0
  18. package/{src/components/index.ts → lib/esm/components/index.d.ts} +2 -15
  19. package/lib/esm/components/index.js +10 -0
  20. package/lib/esm/components/inline_edit.d.ts +12 -0
  21. package/lib/esm/components/inline_edit.js +48 -0
  22. package/lib/esm/components/link.d.ts +5 -0
  23. package/lib/esm/components/link.js +11 -0
  24. package/lib/esm/components/logger.d.ts +6 -0
  25. package/lib/esm/components/logger.js +22 -0
  26. package/lib/esm/components/modal.d.ts +2 -0
  27. package/{src/components/modal.ts → lib/esm/components/modal.js} +3 -8
  28. package/lib/esm/components/nav.d.ts +11 -0
  29. package/lib/esm/components/nav.js +27 -0
  30. package/lib/esm/components/property.d.ts +9 -0
  31. package/lib/esm/components/property.js +16 -0
  32. package/lib/esm/components/select.d.ts +10 -0
  33. package/lib/esm/components/select.js +3 -0
  34. package/lib/esm/components/tabs.d.ts +20 -0
  35. package/lib/esm/components/tabs.js +45 -0
  36. package/lib/esm/components/virtual_scroll.d.ts +42 -0
  37. package/lib/esm/components/virtual_scroll.js +94 -0
  38. package/lib/esm/debounce.d.ts +1 -0
  39. package/lib/esm/debounce.js +11 -0
  40. package/lib/esm/diff.d.ts +15 -0
  41. package/lib/esm/diff.js +50 -0
  42. package/lib/esm/display.d.ts +5 -0
  43. package/lib/esm/display.js +11 -0
  44. package/lib/esm/dom/css/border.d.ts +11 -0
  45. package/lib/esm/dom/css/border.js +27 -0
  46. package/lib/esm/dom/css/constants.d.ts +31 -0
  47. package/lib/esm/dom/css/constants.js +28 -0
  48. package/lib/esm/dom/css/core.d.ts +5 -0
  49. package/lib/esm/dom/css/core.js +24 -0
  50. package/lib/esm/dom/css/fstyle.d.ts +5 -0
  51. package/lib/esm/dom/css/fstyle.js +32 -0
  52. package/lib/esm/dom/css/sizing.d.ts +5 -0
  53. package/lib/esm/dom/css/sizing.js +10 -0
  54. package/lib/esm/dom/dom.d.ts +36 -0
  55. package/lib/esm/dom/dom.js +217 -0
  56. package/lib/esm/dom/fc.d.ts +10 -0
  57. package/lib/esm/dom/fc.js +32 -0
  58. package/lib/esm/dom/form/form.app.d.ts +1 -0
  59. package/lib/esm/dom/form/form.app.js +19 -0
  60. package/lib/esm/dom/form/form.d.ts +27 -0
  61. package/lib/esm/dom/form/form.js +65 -0
  62. package/lib/esm/dom/html.d.ts +112 -0
  63. package/{src/dom/html.ts → lib/esm/dom/html.js} +2 -14
  64. package/lib/esm/dom/hydrate.d.ts +39 -0
  65. package/lib/esm/dom/hydrate.js +187 -0
  66. package/lib/esm/dom/index.js +2 -0
  67. package/lib/esm/dom/navigation/index.d.ts +76 -0
  68. package/lib/esm/dom/navigation/index.js +292 -0
  69. package/lib/esm/dom/observable.d.ts +2 -0
  70. package/lib/esm/dom/observable.js +6 -0
  71. package/lib/esm/dom/provide.d.ts +3 -0
  72. package/lib/esm/dom/provide.js +7 -0
  73. package/lib/esm/dom/render.d.ts +8 -0
  74. package/lib/esm/dom/render.js +28 -0
  75. package/lib/esm/dom/router/link.d.ts +6 -0
  76. package/lib/esm/dom/router/link.js +3 -0
  77. package/lib/esm/dom/router/router.d.ts +13 -0
  78. package/lib/esm/dom/router/router.js +52 -0
  79. package/lib/esm/dom/svg.d.ts +64 -0
  80. package/{src/dom/svg.ts → lib/esm/dom/svg.js} +2 -19
  81. package/lib/esm/dom/types/css.d.ts +6590 -0
  82. package/lib/esm/dom/types/css.js +1 -0
  83. package/lib/esm/dom/types/dom.js +1 -0
  84. package/lib/esm/dom/types/html.d.ts +614 -0
  85. package/lib/esm/dom/types/html.js +1 -0
  86. package/lib/esm/dom/xml.d.ts +1 -0
  87. package/lib/esm/dom/xml.js +4 -0
  88. package/lib/esm/equal.d.ts +11 -0
  89. package/lib/esm/equal.js +43 -0
  90. package/lib/esm/fs.d.ts +72 -0
  91. package/lib/esm/fs.js +227 -0
  92. package/lib/esm/fs_node.d.ts +15 -0
  93. package/lib/esm/fs_node.js +45 -0
  94. package/lib/esm/generator.d.ts +1 -0
  95. package/lib/esm/generator.js +10 -0
  96. package/lib/esm/lock.d.ts +1 -0
  97. package/lib/esm/lock.js +23 -0
  98. package/lib/esm/log.d.ts +69 -0
  99. package/lib/esm/log.js +211 -0
  100. package/lib/esm/observable/event.d.ts +35 -0
  101. package/lib/esm/observable/event.js +46 -0
  102. package/lib/esm/observable/observable.d.ts +134 -0
  103. package/lib/esm/observable/observable.js +349 -0
  104. package/lib/esm/range.d.ts +1 -0
  105. package/lib/esm/range.js +7 -0
  106. package/lib/esm/result.d.ts +31 -0
  107. package/lib/esm/result.js +66 -0
  108. package/lib/esm/safe.d.ts +1 -0
  109. package/lib/esm/safe.js +10 -0
  110. package/lib/esm/server/http/apps.d.ts +5 -0
  111. package/lib/esm/server/http/apps.js +23 -0
  112. package/lib/esm/server/http/css.d.ts +5 -0
  113. package/lib/esm/server/http/css.js +43 -0
  114. package/lib/esm/server/http/index.d.ts +16 -0
  115. package/lib/esm/server/http/index.js +78 -0
  116. package/lib/esm/server/http/response.d.ts +4 -0
  117. package/lib/esm/server/http/response.js +43 -0
  118. package/lib/esm/server/http/sitemap.d.ts +2 -0
  119. package/lib/esm/server/http/sitemap.js +22 -0
  120. package/lib/esm/server/http/static.d.ts +2 -0
  121. package/lib/esm/server/http/static.js +22 -0
  122. package/lib/esm/server/http/typescript.d.ts +5 -0
  123. package/lib/esm/server/http/typescript.js +40 -0
  124. package/lib/esm/server/live-reload.d.ts +46 -0
  125. package/lib/esm/server/live-reload.js +161 -0
  126. package/lib/esm/server/main.d.ts +2 -0
  127. package/{src/server/main.ts → lib/esm/server/main.js} +8 -15
  128. package/lib/esm/server/ws/frame.d.ts +2 -0
  129. package/lib/esm/server/ws/frame.js +35 -0
  130. package/lib/esm/server/ws/handshake.d.ts +4 -0
  131. package/lib/esm/server/ws/handshake.js +32 -0
  132. package/lib/esm/server/ws/index.d.ts +14 -0
  133. package/lib/esm/server/ws/index.js +68 -0
  134. package/lib/esm/ssg/bundle.d.ts +14 -0
  135. package/lib/esm/ssg/bundle.js +73 -0
  136. package/lib/esm/ssg/copy-public.d.ts +6 -0
  137. package/lib/esm/ssg/copy-public.js +34 -0
  138. package/lib/esm/ssg/discover.d.ts +15 -0
  139. package/lib/esm/ssg/discover.js +117 -0
  140. package/lib/esm/ssg/main.d.ts +2 -0
  141. package/lib/esm/ssg/main.js +122 -0
  142. package/lib/esm/ssg/rewrite.d.ts +9 -0
  143. package/{src/ssg/rewrite.ts → lib/esm/ssg/rewrite.js} +6 -9
  144. package/lib/esm/ssg/ssg.d.ts +26 -0
  145. package/lib/esm/ssg/ssg.js +84 -0
  146. package/lib/esm/transpile.d.mts +3 -0
  147. package/lib/esm/transpile.mjs +12 -0
  148. package/package.json +11 -7
  149. package/src/404.html +0 -14
  150. package/src/assert.ts +0 -56
  151. package/src/case.ts +0 -5
  152. package/src/components/_notes +0 -33
  153. package/src/components/accordion.ts +0 -25
  154. package/src/components/alert.ts +0 -47
  155. package/src/components/button_bar.ts +0 -42
  156. package/src/components/card.ts +0 -54
  157. package/src/components/form.ts +0 -25
  158. package/src/components/inline_edit.ts +0 -78
  159. package/src/components/link.ts +0 -22
  160. package/src/components/logger.ts +0 -35
  161. package/src/components/nav.ts +0 -42
  162. package/src/components/property.ts +0 -32
  163. package/src/components/select.ts +0 -22
  164. package/src/components/tabs.ts +0 -82
  165. package/src/components/virtual_scroll.ts +0 -199
  166. package/src/debounce.ts +0 -14
  167. package/src/diff.ts +0 -82
  168. package/src/display.ts +0 -18
  169. package/src/dom/README.md +0 -107
  170. package/src/dom/SKILL.md +0 -201
  171. package/src/dom/css/border.ts +0 -47
  172. package/src/dom/css/constants.ts +0 -34
  173. package/src/dom/css/core.ts +0 -28
  174. package/src/dom/css/fstyle.ts +0 -42
  175. package/src/dom/css/sizing.ts +0 -11
  176. package/src/dom/dom.ts +0 -327
  177. package/src/dom/fc.ts +0 -81
  178. package/src/dom/form/form.app.ts +0 -44
  179. package/src/dom/form/form.ts +0 -151
  180. package/src/dom/form/index.html +0 -15
  181. package/src/dom/hydrate.ts +0 -206
  182. package/src/dom/navigation/index.ts +0 -349
  183. package/src/dom/observable.ts +0 -11
  184. package/src/dom/provide.ts +0 -11
  185. package/src/dom/render.ts +0 -41
  186. package/src/dom/router/link.ts +0 -14
  187. package/src/dom/router/router.ts +0 -72
  188. package/src/dom/types/css.ts +0 -10088
  189. package/src/dom/types/html.ts +0 -629
  190. package/src/dom/xml.ts +0 -11
  191. package/src/equal.ts +0 -66
  192. package/src/favicon.ico +0 -0
  193. package/src/fs.ts +0 -300
  194. package/src/fs_node.ts +0 -57
  195. package/src/generator.ts +0 -12
  196. package/src/hooks/_notes +0 -6
  197. package/src/lock.ts +0 -23
  198. package/src/log.ts +0 -307
  199. package/src/observable/_notes +0 -26
  200. package/src/observable/event.ts +0 -93
  201. package/src/observable/observable.ts +0 -484
  202. package/src/range.ts +0 -7
  203. package/src/result.ts +0 -107
  204. package/src/safe.ts +0 -12
  205. package/src/server/http/apps.ts +0 -26
  206. package/src/server/http/css.ts +0 -49
  207. package/src/server/http/index.ts +0 -127
  208. package/src/server/http/response.ts +0 -60
  209. package/src/server/http/sitemap.ts +0 -24
  210. package/src/server/http/static.ts +0 -28
  211. package/src/server/http/typescript.ts +0 -46
  212. package/src/server/live-reload.ts +0 -208
  213. package/src/server/ws/frame.ts +0 -36
  214. package/src/server/ws/handshake.ts +0 -42
  215. package/src/server/ws/index.ts +0 -100
  216. package/src/ssg/bundle.ts +0 -85
  217. package/src/ssg/copy-public.ts +0 -44
  218. package/src/ssg/discover.ts +0 -143
  219. package/src/ssg/main.ts +0 -168
  220. package/src/ssg/ssg.ts +0 -134
  221. package/src/transpile.mjs +0 -16
  222. package/src/zip/spec.txt +0 -3260
  223. package/tsconfig.json +0 -34
  224. /package/{src/awaitable.ts → lib/esm/awaitable.d.ts} +0 -0
  225. /package/{src/dom/index.ts → lib/esm/dom/index.d.ts} +0 -0
  226. /package/{src/dom/types/dom.ts → lib/esm/dom/types/dom.d.ts} +0 -0
@@ -1,34 +0,0 @@
1
- export const Sizes = {
2
- none: "0px",
3
- sm: "0.125rem",
4
- "": "0.25rem",
5
- md: "0.375rem",
6
- lg: "0.5rem",
7
- xl: "0.75rem",
8
- "2xl": "1rem",
9
- "3xl": "1.5rem",
10
- full: "9999px",
11
- };
12
-
13
- export const Sides = {
14
- "": "",
15
- t: "Top",
16
- r: "Right",
17
- l: "Left",
18
- b: "Bottom",
19
- tl: "TopLeft",
20
- tr: "TopRight",
21
- bl: "BottomLeft",
22
- br: "BottomRight",
23
- };
24
-
25
- export const Widths = {
26
- "1/4": "25%",
27
- "1/2": "50%",
28
- "3/4": "75%",
29
- full: "100%",
30
- };
31
-
32
- export type Size = keyof typeof Sizes;
33
- export type Side = keyof typeof Sides;
34
- export type Width = keyof typeof Widths;
@@ -1,28 +0,0 @@
1
- import { type Side, Sides, type Size, Sizes } from "./constants.ts";
2
-
3
- export function isSide(v: string): v is Side {
4
- return Sides[v as keyof typeof Sides] !== undefined;
5
- }
6
-
7
- export function isSize(v: string): v is Size {
8
- return Sizes[v as keyof typeof Sizes] !== undefined;
9
- }
10
-
11
- export function getSize(size: keyof typeof Sizes) {
12
- return Sizes[size];
13
- }
14
-
15
- export function getSide(side: Side): string[] {
16
- switch (side) {
17
- case "t":
18
- return [...getSide("tl"), ...getSide("tr")];
19
- case "r":
20
- return [...getSide("tr"), ...getSide("br")];
21
- case "b":
22
- return [...getSide("br"), ...getSide("bl")];
23
- case "l":
24
- return [...getSide("tl"), ...getSide("bl")];
25
- default:
26
- return [Sides[side]];
27
- }
28
- }
@@ -1,42 +0,0 @@
1
- import { dashCase } from "../../case.ts";
2
- import type { Properties } from "../types/css.ts";
3
-
4
- export type FStyle =
5
- | Properties
6
- | {
7
- [k: string]: FStyle;
8
- };
9
-
10
- export function compileFStyle(fstyle: FStyle, prefix = ""): string {
11
- const properties: { key: string; value: string }[] = [];
12
- const rules: { key: string; value: FStyle }[] = [];
13
-
14
- for (const [key, value] of Object.entries(fstyle)) {
15
- if (typeof value === "string") {
16
- properties.push({ key, value });
17
- } else {
18
- rules.push({ key, value });
19
- }
20
- }
21
-
22
- let rule = "";
23
-
24
- if (properties.length > 0) {
25
- rule += `${prefix} {\n`;
26
- for (const { key, value } of properties) {
27
- rule += ` ${dashCase(key)}: ${value};\n`;
28
- }
29
- rule += "}\n\n";
30
- }
31
-
32
- for (const { key, value } of rules) {
33
- if (key.startsWith("@media")) {
34
- rule += `${key} {\n`;
35
- rule += compileFStyle(value, " ");
36
- rule += "}\n\n";
37
- } else {
38
- rule += compileFStyle(value, `${prefix} ${key}`);
39
- }
40
- }
41
- return rule;
42
- }
@@ -1,11 +0,0 @@
1
- import { type Width, Widths } from "./constants.ts";
2
-
3
- export function width(amount: Width, block?: "inline") {
4
- if (amount === undefined && Widths[block as Width] !== undefined) {
5
- amount = block as Width;
6
- }
7
- return {
8
- ...(block === "inline" ? { display: "inline-block" } : {}),
9
- width: Widths[amount] ?? "0",
10
- };
11
- }
package/src/dom/dom.ts DELETED
@@ -1,327 +0,0 @@
1
- import { assert, assertExists } from "../assert.ts";
2
- import type { Properties as SVGProperties } from "./types/css.ts";
3
-
4
- if (typeof window === "undefined") {
5
- const { JSDOM } = await import("jsdom");
6
- // biome-ignore lint/suspicious/noGlobalAssign: Load JSDom globally
7
- window = global.window = new JSDOM().window as unknown as Window &
8
- typeof globalThis;
9
- global.HTMLElement ??= window.HTMLElement;
10
- global.customElements ??= window.customElements;
11
- // Unconditional: jsdom's dispatchEvent instanceof-checks its own Event class, so Node's native Event must be replaced.
12
- global.Event = window.Event as unknown as typeof Event;
13
- global.MouseEvent ??= window.MouseEvent as unknown as typeof MouseEvent;
14
- global.Element ??= window.Element as unknown as typeof Element;
15
- }
16
-
17
- export const XHTML_NAMESPACE_URI = "http://www.w3.org/1999/xhtml";
18
- export const SVG_NAMESPACE_URI = "http://www.w3.org/2000/svg";
19
-
20
- const Events = Symbol("events");
21
- export const CLEAR = Symbol("Clear children");
22
-
23
- // Node.ELEMENT_NODE; the Node global is not installed in the jsdom bootstrap
24
- // above, so the numeric constant is used directly (cf. nodeType 3 for text).
25
- const ELEMENT_NODE = 1;
26
-
27
- export type EventHandler = EventListenerOrEventListenerObject;
28
- export type DenormChildren =
29
- | Node
30
- | string
31
- | typeof CLEAR
32
- | null
33
- | undefined
34
- | false;
35
-
36
- export type DOMElement = Element & ElementCSSInlineStyle;
37
-
38
- export type DomAttrs = {
39
- class: string | string[];
40
- style: Partial<SVGProperties> | string;
41
- role: "button" | "list" | "listbox";
42
- events: Partial<{
43
- [K in keyof HTMLElementEventMap]: EventHandler | null;
44
- }>;
45
- };
46
-
47
- export type Attrs<E extends Omit<Element, "update">, S = object> = Partial<
48
- Omit<{ [k in keyof E]: string | number | boolean }, "style" | "toString"> &
49
- S &
50
- DomAttrs
51
- >;
52
-
53
- export type DenormAttrs<E extends Omit<Element, "update">, S = object> =
54
- | Attrs<E, S>
55
- | DenormChildren;
56
-
57
- declare global {
58
- interface Element {
59
- [Events]: Map<string, EventHandler>;
60
- update(attrs?: DenormAttrs<Element>, ...children: DenormChildren[]): this;
61
- }
62
- }
63
-
64
- export type DOMUpdates<E extends Element = Element> =
65
- | [DenormAttrs<E>, ...DenormChildren[]]
66
- | DenormChildren[];
67
-
68
- function isAttrs<E extends Element>(
69
- attrs: DenormAttrs<E> | undefined,
70
- ): attrs is Attrs<E> {
71
- if (!attrs) {
72
- return false;
73
- }
74
- if (typeof attrs === "object") {
75
- return !(attrs as Node).nodeType;
76
- }
77
- return false;
78
- }
79
-
80
- export function normalizeArguments<E extends Element>(
81
- attrs?: DenormAttrs<E>,
82
- children: DenormChildren[] = [],
83
- defaultAttrs: Attrs<E> = {},
84
- ): [Attrs<E>, DenormChildren[]] {
85
- let attributes: Attrs<E>;
86
- if (isAttrs(attrs)) {
87
- attributes = attrs;
88
- } else {
89
- if (attrs !== undefined) {
90
- children.unshift(attrs as DenormChildren);
91
- }
92
- attributes = defaultAttrs;
93
- }
94
- // Drop conditional/absent children (React's `{cond && <X/>}` idiom): null,
95
- // undefined, and false. `0` and `""` are kept — they are legitimate text
96
- // nodes, and dropping them would reintroduce the React `0`-renders-nothing bug.
97
- return [attributes, children.flat().filter((c) => c != null && c !== false)];
98
- }
99
-
100
- export function up<E extends Element>(
101
- element: Omit<E, "update">,
102
- attrs?: DenormAttrs<E>,
103
- ...children: DenormChildren[]
104
- ): E {
105
- return update(element, ...normalizeArguments(attrs, children)) as E;
106
- }
107
-
108
- /**
109
- * (Re)attach a single listener for `type`, replacing any handler `events`
110
- * already tracks for it, so each event has exactly one live handler — no
111
- * stacking, no orphans. `events` is the element's own `[Events]` map; it
112
- * stays the single source of truth.
113
- */
114
- function setListener(
115
- target: EventTarget,
116
- events: Map<string, EventHandler>,
117
- type: string,
118
- handler: EventHandler,
119
- ): void {
120
- if (events.has(type)) {
121
- target.removeEventListener(type, assertExists(events.get(type)));
122
- }
123
- target.addEventListener(type, handler);
124
- events.set(type, handler);
125
- }
126
-
127
- /** Detach the listener `events` tracks for `type`, if any, and forget it. */
128
- function clearListener(
129
- target: EventTarget,
130
- events: Map<string, EventHandler>,
131
- type: string,
132
- ): void {
133
- if (events.has(type)) {
134
- target.removeEventListener(type, assertExists(events.get(type)));
135
- events.delete(type);
136
- }
137
- }
138
-
139
- export function update(
140
- element: Omit<Element, "update">,
141
- attrs: Attrs<Element>,
142
- children: DenormChildren[],
143
- ): Element {
144
- element[Events] ??= new Map<string, EventHandler>();
145
- const $events = element[Events];
146
-
147
- for (const [k, v] of Object.entries(attrs.events ?? {})) {
148
- if (v === null) {
149
- clearListener(element, $events, k);
150
- } else if (v !== undefined) {
151
- setListener(element, $events, k, v);
152
- }
153
- }
154
- element.toggleAttribute("data-hydrate", $events.size > 0);
155
-
156
- const _style = (element as { style?: Partial<CSSStyleDeclaration> }).style;
157
- if (_style) {
158
- if (typeof attrs.style === "string") {
159
- _style.cssText = attrs.style;
160
- } else {
161
- for (const [k, v] of Object.entries(
162
- (attrs.style as Partial<CSSStyleDeclaration>) ?? {},
163
- )) {
164
- // @ts-expect-error Object.entries is unable to statically look into args
165
- _style[k] = v;
166
- }
167
- }
168
- }
169
-
170
- for (const [k, v] of Object.entries(attrs)) {
171
- if (k === "style") {
172
- continue;
173
- }
174
-
175
- if (k === "events") {
176
- continue;
177
- }
178
-
179
- if (k === "class") {
180
- const cs = Array.isArray(v) ? v : String(v).split(/\s+/m).filter(Boolean);
181
- for (const c of cs) {
182
- if (c.startsWith("!")) {
183
- element.classList.remove(c.substring(1));
184
- } else {
185
- element.classList.add(c);
186
- }
187
- }
188
- continue;
189
- }
190
-
191
- if (!v) {
192
- element.removeAttribute(k);
193
- } else if (v === true) {
194
- element.setAttribute(k, k);
195
- } else {
196
- element.setAttribute(k, String(v));
197
- }
198
- }
199
-
200
- if (children?.length > 0) {
201
- reconcileChildren(
202
- element,
203
- children[0] === CLEAR ? [] : (children as (string | Node)[]),
204
- );
205
- }
206
-
207
- (element as Element).update ??= (attrs, ...children) =>
208
- update(element, ...normalizeArguments(attrs, children));
209
-
210
- return element as Element;
211
- }
212
-
213
- /**
214
- * Reconcile `element`'s mounted children against expected `children`, mutating the live DOM in place.
215
- */
216
- export function reconcileChildren(
217
- element: Node,
218
- children: (string | Node)[],
219
- ): void {
220
- const desired = findDesiredNodes(element, children);
221
-
222
- const { mountedSet, unclaimed } = findUnclaimedNodes(desired, element);
223
-
224
- patchUnclaimedNodes(desired, mountedSet, unclaimed);
225
-
226
- clearUnwantedNodes(desired, element);
227
-
228
- insertDesiredNodes(element, desired);
229
- }
230
-
231
- function findDesiredNodes(element: Node, children: (string | Node)[]): Node[] {
232
- const doc = element.ownerDocument ?? window.document;
233
- const desired: Node[] = children.map((child) =>
234
- typeof child === "string" ? doc.createTextNode(child) : child,
235
- );
236
- return desired;
237
- }
238
-
239
- function insertDesiredNodes(element: Node, desired: Node[]) {
240
- let cursor: ChildNode | null = element.firstChild;
241
- for (const node of desired) {
242
- if (node === cursor) {
243
- cursor = cursor.nextSibling;
244
- } else {
245
- element.insertBefore(node, cursor);
246
- }
247
- }
248
- }
249
-
250
- function clearUnwantedNodes(desired: Node[], element: Node) {
251
- const keep = new Set(desired);
252
- for (const mounted of Array.from(element.childNodes)) {
253
- if (!keep.has(mounted)) {
254
- element.removeChild(mounted);
255
- }
256
- }
257
- }
258
-
259
- function patchUnclaimedNodes(
260
- desired: Node[],
261
- mountedSet: Set<Node>,
262
- unclaimed: Node[],
263
- ) {
264
- let claim = 0;
265
- for (let i = 0; i < desired.length; i++) {
266
- const node = desired[i];
267
- if (node.nodeType !== ELEMENT_NODE || mountedSet.has(node)) {
268
- continue;
269
- }
270
- if (claim < unclaimed.length) {
271
- if (unclaimed[claim].nodeName === node.nodeName) {
272
- patchNode(unclaimed[claim] as Element, node as Element);
273
- desired[i] = unclaimed[claim];
274
- }
275
- claim++;
276
- }
277
- }
278
- }
279
-
280
- function findUnclaimedNodes(
281
- desired: Node[],
282
- element: Node,
283
- ): { mountedSet: Set<Node>; unclaimed: Node[] } {
284
- const unclaimed: Node[] = [];
285
- const desiredSet = new Set<Node>(desired);
286
- const mountedSet = new Set<Node>(element.childNodes);
287
- for (const mounted of Array.from(element.childNodes)) {
288
- if (mounted.nodeType === ELEMENT_NODE && !desiredSet.has(mounted)) {
289
- unclaimed.push(mounted);
290
- }
291
- }
292
- return { mountedSet, unclaimed };
293
- }
294
-
295
- export function patchNode(kept: Element, fresh: Element): void {
296
- assert(kept.nodeName === fresh.nodeName, "patching nodes of different types");
297
-
298
- // Remove `kept` attributes that aren't on `fresh`, then add `fresh` attributes not on `kept`.
299
- for (const { name } of Array.from(kept.attributes)) {
300
- if (!fresh.hasAttribute(name)) {
301
- kept.removeAttribute(name);
302
- }
303
- }
304
- for (const { name, value } of Array.from(fresh.attributes)) {
305
- if (kept.getAttribute(name) !== value) {
306
- kept.setAttribute(name, value);
307
- }
308
- }
309
-
310
- // Similar to attributes, but operating in a map on the side rather than the node itself.
311
- kept[Events] ??= new Map<string, EventHandler>();
312
- const keptEvents = kept[Events];
313
- const freshEvents = fresh[Events] ?? new Map<string, EventHandler>();
314
- for (const [type] of keptEvents) {
315
- if (!freshEvents.has(type)) {
316
- clearListener(kept, keptEvents, type);
317
- }
318
- }
319
- for (const [type, handler] of freshEvents) {
320
- setListener(kept, keptEvents, type, handler);
321
- }
322
-
323
- // Custom elements rebuild their own subtrees
324
- if (customElements.get(kept.localName)) return;
325
-
326
- reconcileChildren(kept, Array.from(fresh.childNodes));
327
- }
package/src/dom/fc.ts DELETED
@@ -1,81 +0,0 @@
1
- import {
2
- CLEAR,
3
- type DenormChildren,
4
- type DomAttrs,
5
- normalizeArguments,
6
- reconcileChildren,
7
- update,
8
- } from "./dom.ts";
9
-
10
- export type Attrs<S> = S & Partial<DomAttrs>;
11
-
12
- export const State = Symbol();
13
- export interface FCComponent<Props extends object, State extends object>
14
- extends Element {
15
- [State]?: Partial<State>;
16
- update(
17
- attrs?: Partial<Attrs<Props> & DomAttrs> | DenormChildren,
18
- ...children: DenormChildren[]
19
- ): this;
20
- }
21
- export type RenderFn<Props extends object, State extends object> = (
22
- el: FCComponent<Props, State>,
23
- attrs: Attrs<Props>,
24
- children: DenormChildren[],
25
- ) => Element | Element[];
26
-
27
- export type FCComponentCtor<Props extends object, State extends object> = (
28
- attrs?: Attrs<Props> | DenormChildren,
29
- ...children: DenormChildren[]
30
- ) => FCComponent<Props, State>;
31
-
32
- export function FC<Props extends object, State extends object = object>(
33
- name: string,
34
- component: RenderFn<Props, State>,
35
- ): FCComponentCtor<Props, State> {
36
- class FCImpl extends HTMLElement implements FCComponent<Props, State> {
37
- [State]: Partial<State> = {};
38
- #attrs: Attrs<Props> = {} as Attrs<Props>;
39
- #children: DenormChildren[] = [];
40
-
41
- update(
42
- attrs?: Attrs<Props> | DenormChildren,
43
- ...children: DenormChildren[]
44
- ) {
45
- [attrs, children] = normalizeArguments(attrs, children) as [
46
- Attrs<Props>,
47
- DenormChildren[],
48
- ];
49
- if (children[0] === CLEAR) {
50
- this.#children = [];
51
- } else if (children.length > 0) {
52
- this.#children = children;
53
- }
54
- this.#attrs = { ...this.#attrs, ...(attrs as Attrs<Props>) };
55
-
56
- // Apply updates from the attrs to the dom node itself
57
- update(this, this.#attrs, []);
58
-
59
- // Re-run the component function using new element, attrs, and children.
60
- const rendered = [component(this, this.#attrs, this.#children)];
61
- reconcileChildren(this, rendered.flat());
62
- return this;
63
- }
64
- }
65
-
66
- customElements.define(name, FCImpl);
67
-
68
- const ctor: FCComponentCtor<Props, State> = (
69
- attrs?: Attrs<Props> | DenormChildren,
70
- ...children: DenormChildren[]
71
- ): FCComponent<Props, State> => {
72
- const element = window.document.createElement(name) as FCComponent<
73
- Props,
74
- State
75
- >;
76
- element.update(attrs, ...children);
77
- return element;
78
- };
79
-
80
- return ctor;
81
- }
@@ -1,44 +0,0 @@
1
- import { button, div, main, small } from "../html.ts";
2
- import { Form, Input } from "./form.ts";
3
-
4
- export const App = () =>
5
- main(
6
- Form(
7
- {
8
- events: {
9
- submit(event) {
10
- console.log(
11
- "Should see fields for firstname, lastname, email, etc",
12
- );
13
- console.log(event);
14
- },
15
- },
16
- },
17
- div(
18
- { class: "grid" },
19
- Input({ id: "firstname", placeholder: "First name" }),
20
- Input({ id: "lastname", placeholder: "Last name" }),
21
- ),
22
- Input(
23
- {
24
- id: "email",
25
- type: "email",
26
- placeholder: "Email address",
27
- required: true,
28
- },
29
- small("We will never share your email with anyone."),
30
- ),
31
- button({ type: "submit" }, "Submit"),
32
- div(
33
- { class: "grid" },
34
- Input({ id: "valid", placeholder: "Valid", "aria-invalid": "false" }),
35
- Input({
36
- id: "invalid",
37
- placeholder: "Invalid",
38
- "aria-invalid": "true",
39
- }),
40
- Input({ id: "disabled", placeholder: "Disabled", disabled: true }),
41
- Input({ id: "readonly", value: "Readonly", readOnly: true }),
42
- ),
43
- ),
44
- );