@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.
- package/lib/esm/assert.d.ts +26 -0
- package/lib/esm/assert.js +38 -0
- package/lib/esm/awaitable.js +1 -0
- package/lib/esm/case.d.ts +1 -0
- package/lib/esm/case.js +5 -0
- package/lib/esm/components/accordion.d.ts +5 -0
- package/lib/esm/components/accordion.js +9 -0
- package/lib/esm/components/alert.d.ts +7 -0
- package/lib/esm/components/alert.js +31 -0
- package/lib/esm/components/button_bar.d.ts +8 -0
- package/lib/esm/components/button_bar.js +25 -0
- package/lib/esm/components/card.d.ts +8 -0
- package/lib/esm/components/card.js +31 -0
- package/lib/esm/components/children.d.ts +2 -0
- package/{src/components/children.ts → lib/esm/components/children.js} +2 -6
- package/lib/esm/components/form.d.ts +5 -0
- package/lib/esm/components/form.js +13 -0
- package/{src/components/index.ts → lib/esm/components/index.d.ts} +2 -15
- package/lib/esm/components/index.js +10 -0
- package/lib/esm/components/inline_edit.d.ts +12 -0
- package/lib/esm/components/inline_edit.js +48 -0
- package/lib/esm/components/link.d.ts +5 -0
- package/lib/esm/components/link.js +11 -0
- package/lib/esm/components/logger.d.ts +6 -0
- package/lib/esm/components/logger.js +22 -0
- package/lib/esm/components/modal.d.ts +2 -0
- package/{src/components/modal.ts → lib/esm/components/modal.js} +3 -8
- package/lib/esm/components/nav.d.ts +11 -0
- package/lib/esm/components/nav.js +27 -0
- package/lib/esm/components/property.d.ts +9 -0
- package/lib/esm/components/property.js +16 -0
- package/lib/esm/components/select.d.ts +10 -0
- package/lib/esm/components/select.js +3 -0
- package/lib/esm/components/tabs.d.ts +20 -0
- package/lib/esm/components/tabs.js +45 -0
- package/lib/esm/components/virtual_scroll.d.ts +42 -0
- package/lib/esm/components/virtual_scroll.js +94 -0
- package/lib/esm/debounce.d.ts +1 -0
- package/lib/esm/debounce.js +11 -0
- package/lib/esm/diff.d.ts +15 -0
- package/lib/esm/diff.js +50 -0
- package/lib/esm/display.d.ts +5 -0
- package/lib/esm/display.js +11 -0
- package/lib/esm/dom/css/border.d.ts +11 -0
- package/lib/esm/dom/css/border.js +27 -0
- package/lib/esm/dom/css/constants.d.ts +31 -0
- package/lib/esm/dom/css/constants.js +28 -0
- package/lib/esm/dom/css/core.d.ts +5 -0
- package/lib/esm/dom/css/core.js +24 -0
- package/lib/esm/dom/css/fstyle.d.ts +5 -0
- package/lib/esm/dom/css/fstyle.js +32 -0
- package/lib/esm/dom/css/sizing.d.ts +5 -0
- package/lib/esm/dom/css/sizing.js +10 -0
- package/lib/esm/dom/dom.d.ts +36 -0
- package/lib/esm/dom/dom.js +217 -0
- package/lib/esm/dom/fc.d.ts +10 -0
- package/lib/esm/dom/fc.js +32 -0
- package/lib/esm/dom/form/form.app.d.ts +1 -0
- package/lib/esm/dom/form/form.app.js +19 -0
- package/lib/esm/dom/form/form.d.ts +27 -0
- package/lib/esm/dom/form/form.js +65 -0
- package/lib/esm/dom/html.d.ts +112 -0
- package/{src/dom/html.ts → lib/esm/dom/html.js} +2 -14
- package/lib/esm/dom/hydrate.d.ts +39 -0
- package/lib/esm/dom/hydrate.js +187 -0
- package/lib/esm/dom/index.js +2 -0
- package/lib/esm/dom/navigation/index.d.ts +76 -0
- package/lib/esm/dom/navigation/index.js +292 -0
- package/lib/esm/dom/observable.d.ts +2 -0
- package/lib/esm/dom/observable.js +6 -0
- package/lib/esm/dom/provide.d.ts +3 -0
- package/lib/esm/dom/provide.js +7 -0
- package/lib/esm/dom/render.d.ts +8 -0
- package/lib/esm/dom/render.js +28 -0
- package/lib/esm/dom/router/link.d.ts +6 -0
- package/lib/esm/dom/router/link.js +3 -0
- package/lib/esm/dom/router/router.d.ts +13 -0
- package/lib/esm/dom/router/router.js +52 -0
- package/lib/esm/dom/svg.d.ts +64 -0
- package/{src/dom/svg.ts → lib/esm/dom/svg.js} +2 -19
- package/lib/esm/dom/types/css.d.ts +6590 -0
- package/lib/esm/dom/types/css.js +1 -0
- package/lib/esm/dom/types/dom.js +1 -0
- package/lib/esm/dom/types/html.d.ts +614 -0
- package/lib/esm/dom/types/html.js +1 -0
- package/lib/esm/dom/xml.d.ts +1 -0
- package/lib/esm/dom/xml.js +4 -0
- package/lib/esm/equal.d.ts +11 -0
- package/lib/esm/equal.js +43 -0
- package/lib/esm/fs.d.ts +72 -0
- package/lib/esm/fs.js +227 -0
- package/lib/esm/fs_node.d.ts +15 -0
- package/lib/esm/fs_node.js +45 -0
- package/lib/esm/generator.d.ts +1 -0
- package/lib/esm/generator.js +10 -0
- package/lib/esm/lock.d.ts +1 -0
- package/lib/esm/lock.js +23 -0
- package/lib/esm/log.d.ts +69 -0
- package/lib/esm/log.js +211 -0
- package/lib/esm/observable/event.d.ts +35 -0
- package/lib/esm/observable/event.js +46 -0
- package/lib/esm/observable/observable.d.ts +134 -0
- package/lib/esm/observable/observable.js +349 -0
- package/lib/esm/range.d.ts +1 -0
- package/lib/esm/range.js +7 -0
- package/lib/esm/result.d.ts +31 -0
- package/lib/esm/result.js +66 -0
- package/lib/esm/safe.d.ts +1 -0
- package/lib/esm/safe.js +10 -0
- package/lib/esm/server/http/apps.d.ts +5 -0
- package/lib/esm/server/http/apps.js +23 -0
- package/lib/esm/server/http/css.d.ts +5 -0
- package/lib/esm/server/http/css.js +43 -0
- package/lib/esm/server/http/index.d.ts +16 -0
- package/lib/esm/server/http/index.js +78 -0
- package/lib/esm/server/http/response.d.ts +4 -0
- package/lib/esm/server/http/response.js +43 -0
- package/lib/esm/server/http/sitemap.d.ts +2 -0
- package/lib/esm/server/http/sitemap.js +22 -0
- package/lib/esm/server/http/static.d.ts +2 -0
- package/lib/esm/server/http/static.js +22 -0
- package/lib/esm/server/http/typescript.d.ts +5 -0
- package/lib/esm/server/http/typescript.js +40 -0
- package/lib/esm/server/live-reload.d.ts +46 -0
- package/lib/esm/server/live-reload.js +161 -0
- package/lib/esm/server/main.d.ts +2 -0
- package/{src/server/main.ts → lib/esm/server/main.js} +8 -15
- package/lib/esm/server/ws/frame.d.ts +2 -0
- package/lib/esm/server/ws/frame.js +35 -0
- package/lib/esm/server/ws/handshake.d.ts +4 -0
- package/lib/esm/server/ws/handshake.js +32 -0
- package/lib/esm/server/ws/index.d.ts +14 -0
- package/lib/esm/server/ws/index.js +68 -0
- package/lib/esm/ssg/bundle.d.ts +14 -0
- package/lib/esm/ssg/bundle.js +73 -0
- package/lib/esm/ssg/copy-public.d.ts +6 -0
- package/lib/esm/ssg/copy-public.js +34 -0
- package/lib/esm/ssg/discover.d.ts +15 -0
- package/lib/esm/ssg/discover.js +117 -0
- package/lib/esm/ssg/main.d.ts +2 -0
- package/lib/esm/ssg/main.js +122 -0
- package/lib/esm/ssg/rewrite.d.ts +9 -0
- package/{src/ssg/rewrite.ts → lib/esm/ssg/rewrite.js} +6 -9
- package/lib/esm/ssg/ssg.d.ts +26 -0
- package/lib/esm/ssg/ssg.js +84 -0
- package/lib/esm/transpile.d.mts +3 -0
- package/lib/esm/transpile.mjs +12 -0
- package/package.json +11 -7
- package/src/404.html +0 -14
- package/src/assert.ts +0 -56
- package/src/case.ts +0 -5
- package/src/components/_notes +0 -33
- package/src/components/accordion.ts +0 -25
- package/src/components/alert.ts +0 -47
- package/src/components/button_bar.ts +0 -42
- package/src/components/card.ts +0 -54
- package/src/components/form.ts +0 -25
- package/src/components/inline_edit.ts +0 -78
- package/src/components/link.ts +0 -22
- package/src/components/logger.ts +0 -35
- package/src/components/nav.ts +0 -42
- package/src/components/property.ts +0 -32
- package/src/components/select.ts +0 -22
- package/src/components/tabs.ts +0 -82
- package/src/components/virtual_scroll.ts +0 -199
- package/src/debounce.ts +0 -14
- package/src/diff.ts +0 -82
- package/src/display.ts +0 -18
- package/src/dom/README.md +0 -107
- package/src/dom/SKILL.md +0 -201
- package/src/dom/css/border.ts +0 -47
- package/src/dom/css/constants.ts +0 -34
- package/src/dom/css/core.ts +0 -28
- package/src/dom/css/fstyle.ts +0 -42
- package/src/dom/css/sizing.ts +0 -11
- package/src/dom/dom.ts +0 -327
- package/src/dom/fc.ts +0 -81
- package/src/dom/form/form.app.ts +0 -44
- package/src/dom/form/form.ts +0 -151
- package/src/dom/form/index.html +0 -15
- package/src/dom/hydrate.ts +0 -206
- package/src/dom/navigation/index.ts +0 -349
- package/src/dom/observable.ts +0 -11
- package/src/dom/provide.ts +0 -11
- package/src/dom/render.ts +0 -41
- package/src/dom/router/link.ts +0 -14
- package/src/dom/router/router.ts +0 -72
- package/src/dom/types/css.ts +0 -10088
- package/src/dom/types/html.ts +0 -629
- package/src/dom/xml.ts +0 -11
- package/src/equal.ts +0 -66
- package/src/favicon.ico +0 -0
- package/src/fs.ts +0 -300
- package/src/fs_node.ts +0 -57
- package/src/generator.ts +0 -12
- package/src/hooks/_notes +0 -6
- package/src/lock.ts +0 -23
- package/src/log.ts +0 -307
- package/src/observable/_notes +0 -26
- package/src/observable/event.ts +0 -93
- package/src/observable/observable.ts +0 -484
- package/src/range.ts +0 -7
- package/src/result.ts +0 -107
- package/src/safe.ts +0 -12
- package/src/server/http/apps.ts +0 -26
- package/src/server/http/css.ts +0 -49
- package/src/server/http/index.ts +0 -127
- package/src/server/http/response.ts +0 -60
- package/src/server/http/sitemap.ts +0 -24
- package/src/server/http/static.ts +0 -28
- package/src/server/http/typescript.ts +0 -46
- package/src/server/live-reload.ts +0 -208
- package/src/server/ws/frame.ts +0 -36
- package/src/server/ws/handshake.ts +0 -42
- package/src/server/ws/index.ts +0 -100
- package/src/ssg/bundle.ts +0 -85
- package/src/ssg/copy-public.ts +0 -44
- package/src/ssg/discover.ts +0 -143
- package/src/ssg/main.ts +0 -168
- package/src/ssg/ssg.ts +0 -134
- package/src/transpile.mjs +0 -16
- package/src/zip/spec.txt +0 -3260
- package/tsconfig.json +0 -34
- /package/{src/awaitable.ts → lib/esm/awaitable.d.ts} +0 -0
- /package/{src/dom/index.ts → lib/esm/dom/index.d.ts} +0 -0
- /package/{src/dom/types/dom.ts → lib/esm/dom/types/dom.d.ts} +0 -0
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { debounce } from "../debounce.ts";
|
|
2
|
-
import { FC, State } from "../dom/fc.ts";
|
|
3
|
-
import { div } from "../dom/html.ts";
|
|
4
|
-
|
|
5
|
-
export interface VirtualScrollSettings {
|
|
6
|
-
minIndex: number;
|
|
7
|
-
maxIndex: number;
|
|
8
|
-
startIndex: number;
|
|
9
|
-
itemHeight: number; // In pixels
|
|
10
|
-
count: number;
|
|
11
|
-
tolerance: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export type VirtualScrollDataAdapter<T> = (
|
|
15
|
-
offset: number,
|
|
16
|
-
limit: number,
|
|
17
|
-
) => Iterable<T>;
|
|
18
|
-
|
|
19
|
-
export function arrayAdapter<T>(data: T[]): VirtualScrollDataAdapter<T> {
|
|
20
|
-
return (offset, limit) => data.slice(offset, offset + limit);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface VirtualScrollProps<T, U extends HTMLElement> {
|
|
24
|
-
settings: Partial<VirtualScrollSettings>;
|
|
25
|
-
get: VirtualScrollDataAdapter<T>;
|
|
26
|
-
row: (t: T) => U;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function fillVirtualScrollSettings(
|
|
30
|
-
settings: Partial<VirtualScrollSettings>,
|
|
31
|
-
): VirtualScrollSettings {
|
|
32
|
-
const {
|
|
33
|
-
minIndex = 0,
|
|
34
|
-
maxIndex = Number.MAX_SAFE_INTEGER,
|
|
35
|
-
startIndex = 0,
|
|
36
|
-
itemHeight = 20,
|
|
37
|
-
count = maxIndex - minIndex + 1,
|
|
38
|
-
tolerance = count,
|
|
39
|
-
} = settings;
|
|
40
|
-
|
|
41
|
-
return { minIndex, maxIndex, startIndex, itemHeight, count, tolerance };
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function initialState<T>(
|
|
45
|
-
settings: VirtualScrollSettings,
|
|
46
|
-
): VirtualScrollState<T> {
|
|
47
|
-
// From Denis Hilt, https://blog.logrocket.com/virtual-scrolling-core-principles-and-basic-implementation-in-react/
|
|
48
|
-
const { minIndex, maxIndex, startIndex, itemHeight, count, tolerance } =
|
|
49
|
-
settings;
|
|
50
|
-
const bufferedItems = count + 2 * tolerance;
|
|
51
|
-
const itemsAbove = Math.max(0, startIndex - tolerance - minIndex);
|
|
52
|
-
|
|
53
|
-
const viewportHeight = count * itemHeight;
|
|
54
|
-
const totalHeight = (maxIndex - minIndex + 1) * itemHeight;
|
|
55
|
-
const toleranceHeight = tolerance * itemHeight;
|
|
56
|
-
const bufferHeight = viewportHeight + 2 * toleranceHeight;
|
|
57
|
-
const topPaddingHeight = itemsAbove * itemHeight;
|
|
58
|
-
const bottomPaddingHeight = totalHeight - (topPaddingHeight + bufferHeight);
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
scrollTop: 0,
|
|
62
|
-
settings,
|
|
63
|
-
viewportHeight,
|
|
64
|
-
totalHeight,
|
|
65
|
-
toleranceHeight,
|
|
66
|
-
bufferedItems,
|
|
67
|
-
topPaddingHeight,
|
|
68
|
-
bottomPaddingHeight,
|
|
69
|
-
data: [],
|
|
70
|
-
rows: [],
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function getData<T>(
|
|
75
|
-
minIndex: number,
|
|
76
|
-
maxIndex: number,
|
|
77
|
-
offset: number,
|
|
78
|
-
limit: number,
|
|
79
|
-
get: VirtualScrollDataAdapter<T>,
|
|
80
|
-
): T[] {
|
|
81
|
-
const start = Math.max(0, minIndex, offset);
|
|
82
|
-
const end = Math.min(maxIndex, offset + limit - 1);
|
|
83
|
-
const data = get(start, end - start);
|
|
84
|
-
return [...data];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function doScroll<T>(
|
|
88
|
-
scrollTop: number,
|
|
89
|
-
state: VirtualScrollState<T>,
|
|
90
|
-
get: VirtualScrollDataAdapter<T>,
|
|
91
|
-
): {
|
|
92
|
-
scrollTop: number;
|
|
93
|
-
topPaddingHeight: number;
|
|
94
|
-
bottomPaddingHeight: number;
|
|
95
|
-
data: T[];
|
|
96
|
-
} {
|
|
97
|
-
const {
|
|
98
|
-
totalHeight,
|
|
99
|
-
toleranceHeight,
|
|
100
|
-
bufferedItems,
|
|
101
|
-
settings: { itemHeight, minIndex, maxIndex },
|
|
102
|
-
} = state;
|
|
103
|
-
const index =
|
|
104
|
-
minIndex + Math.floor((scrollTop - toleranceHeight) / itemHeight);
|
|
105
|
-
const data = getData(minIndex, maxIndex, index, bufferedItems, get);
|
|
106
|
-
const topPaddingHeight = Math.max((index - minIndex) * itemHeight, 0);
|
|
107
|
-
const bottomPaddingHeight = Math.max(
|
|
108
|
-
totalHeight - (topPaddingHeight + data.length * itemHeight),
|
|
109
|
-
0,
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
return { scrollTop, topPaddingHeight, bottomPaddingHeight, data };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
interface VirtualScrollState<T, U extends HTMLElement = HTMLElement> {
|
|
116
|
-
settings: VirtualScrollSettings;
|
|
117
|
-
scrollTop: number; // px
|
|
118
|
-
bufferedItems: number; // count
|
|
119
|
-
totalHeight: number; // px
|
|
120
|
-
viewportHeight: number; // px
|
|
121
|
-
topPaddingHeight: number; // px
|
|
122
|
-
bottomPaddingHeight: number; // px
|
|
123
|
-
toleranceHeight: number; // px
|
|
124
|
-
data: T[];
|
|
125
|
-
rows: U[];
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export interface VirtualScroll<T, U extends HTMLElement> {
|
|
129
|
-
state: VirtualScrollState<T>;
|
|
130
|
-
rows: U[];
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export const VirtualScroll = FC<
|
|
134
|
-
VirtualScrollProps<unknown, HTMLElement>,
|
|
135
|
-
VirtualScrollState<unknown, HTMLElement>
|
|
136
|
-
>("virtual-scroll", (element, props) => {
|
|
137
|
-
const settings = fillVirtualScrollSettings(props.settings);
|
|
138
|
-
const state = {
|
|
139
|
-
...initialState(settings),
|
|
140
|
-
...element[State],
|
|
141
|
-
};
|
|
142
|
-
element[State] = state;
|
|
143
|
-
|
|
144
|
-
const scrollTo = (
|
|
145
|
-
{ target }: { target?: { scrollTop: number } } = { target: state },
|
|
146
|
-
) => {
|
|
147
|
-
const scrollTop = target?.scrollTop ?? state.topPaddingHeight;
|
|
148
|
-
const updatedSate = {
|
|
149
|
-
...state,
|
|
150
|
-
...doScroll(scrollTop, state, props.get),
|
|
151
|
-
};
|
|
152
|
-
setState(updatedSate);
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const viewportElement = div({
|
|
156
|
-
style: { height: `${state.viewportHeight}px`, overflowY: "scroll" },
|
|
157
|
-
events: {
|
|
158
|
-
// @ts-expect-error
|
|
159
|
-
scroll: debounce(scrollTo, 0),
|
|
160
|
-
},
|
|
161
|
-
});
|
|
162
|
-
setTimeout(() => {
|
|
163
|
-
viewportElement.scroll?.({ top: state.scrollTop });
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const setState = (newState: VirtualScrollState<unknown>) => {
|
|
167
|
-
state.scrollTop = newState.scrollTop;
|
|
168
|
-
state.topPaddingHeight = newState.topPaddingHeight;
|
|
169
|
-
state.bottomPaddingHeight = newState.bottomPaddingHeight;
|
|
170
|
-
state.data = newState.data;
|
|
171
|
-
state.rows = state.data.map(props.row);
|
|
172
|
-
|
|
173
|
-
viewportElement.update(
|
|
174
|
-
div({
|
|
175
|
-
class: "VirtualScroll__topPadding",
|
|
176
|
-
style: { height: `${state.topPaddingHeight}px` },
|
|
177
|
-
}),
|
|
178
|
-
...(state.rows ?? []).map((row, i) =>
|
|
179
|
-
div(
|
|
180
|
-
{
|
|
181
|
-
class: `VirtualScroll__item_${i}`,
|
|
182
|
-
style: { height: `${settings.itemHeight}px` },
|
|
183
|
-
},
|
|
184
|
-
row,
|
|
185
|
-
),
|
|
186
|
-
),
|
|
187
|
-
div({
|
|
188
|
-
class: "VirtualScroll__bottomPadding",
|
|
189
|
-
style: { height: `${state.bottomPaddingHeight}px` },
|
|
190
|
-
}),
|
|
191
|
-
);
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
scrollTo();
|
|
195
|
-
|
|
196
|
-
return viewportElement;
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
export default VirtualScroll;
|
package/src/debounce.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export function debounce<T extends unknown[]>(
|
|
2
|
-
fn: (...args: T) => void,
|
|
3
|
-
ms = 32,
|
|
4
|
-
): (...args: T) => void {
|
|
5
|
-
let timer: ReturnType<typeof setTimeout>;
|
|
6
|
-
return (...args: T) => {
|
|
7
|
-
clearTimeout(timer);
|
|
8
|
-
timer = setTimeout(() => {
|
|
9
|
-
clearTimeout(timer);
|
|
10
|
-
return fn(...args);
|
|
11
|
-
}, ms);
|
|
12
|
-
return timer;
|
|
13
|
-
};
|
|
14
|
-
}
|
package/src/diff.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import { range } from "./range.ts";
|
|
2
|
-
import { isSome, None, type Option, Some } from "./result.ts";
|
|
3
|
-
|
|
4
|
-
export const DiffA = Symbol("A");
|
|
5
|
-
export const DiffB = Symbol("B");
|
|
6
|
-
|
|
7
|
-
export type DiffIndex = string | number;
|
|
8
|
-
export type DiffPrimitive = string | number | boolean | null | undefined;
|
|
9
|
-
|
|
10
|
-
interface DiffEntry {
|
|
11
|
-
key: DiffIndex;
|
|
12
|
-
left: DiffPrimitive;
|
|
13
|
-
right: DiffPrimitive;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface DiffList {
|
|
17
|
-
key: DiffIndex;
|
|
18
|
-
children: (DiffEntry | DiffList)[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function doDiff<T>(va: T, vb: T, k: DiffIndex): Option<DiffList | DiffEntry> {
|
|
22
|
-
if (Array.isArray(va)) {
|
|
23
|
-
// @ts-expect-error
|
|
24
|
-
return diffArray(va, vb, k);
|
|
25
|
-
}
|
|
26
|
-
if (typeof va === "object") {
|
|
27
|
-
const d = diffObject(va ?? {}, vb ?? {}, k);
|
|
28
|
-
if (d.children.length === 0) {
|
|
29
|
-
return None();
|
|
30
|
-
}
|
|
31
|
-
return Some(d);
|
|
32
|
-
}
|
|
33
|
-
if (Object.is(va, vb)) {
|
|
34
|
-
return None();
|
|
35
|
-
}
|
|
36
|
-
// @ts-expect-error
|
|
37
|
-
return { key: k, left: va, right: vb };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function diffArray<T>(
|
|
41
|
-
a: Partial<T>[],
|
|
42
|
-
b: Partial<T>[],
|
|
43
|
-
key: DiffIndex,
|
|
44
|
-
): Option<DiffList> {
|
|
45
|
-
const indexes = Math.max(a.length, b.length);
|
|
46
|
-
const children = range(0, indexes)
|
|
47
|
-
.map((i) => doDiff(a[i], b[i], i))
|
|
48
|
-
.filter(isSome);
|
|
49
|
-
return children.length > 0 ? { key, children } : None();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function diffObject<T>(
|
|
53
|
-
a: Partial<T>,
|
|
54
|
-
b: Partial<T>,
|
|
55
|
-
key: DiffIndex = "",
|
|
56
|
-
): DiffList {
|
|
57
|
-
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
58
|
-
const children = [...keys]
|
|
59
|
-
// @ts-expect-error
|
|
60
|
-
.map((k) => doDiff(a[k], b[k], k))
|
|
61
|
-
.filter(isSome);
|
|
62
|
-
return {
|
|
63
|
-
key,
|
|
64
|
-
children,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function diff<T>(
|
|
69
|
-
a: Partial<T>,
|
|
70
|
-
b: Partial<T>,
|
|
71
|
-
): (DiffEntry | DiffList)[] {
|
|
72
|
-
if (typeof a !== "object" && !Object.is(a, b)) {
|
|
73
|
-
// @ts-expect-error
|
|
74
|
-
return [{ key: "", left: a, right: b }];
|
|
75
|
-
}
|
|
76
|
-
return (
|
|
77
|
-
Array.isArray(a)
|
|
78
|
-
? // @ts-ignore
|
|
79
|
-
(diffArray(a, b, "") ?? { children: [] })
|
|
80
|
-
: diffObject(a, b, "")
|
|
81
|
-
).children;
|
|
82
|
-
}
|
package/src/display.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export type Display =
|
|
2
|
-
| string
|
|
3
|
-
| {
|
|
4
|
-
toString(): string;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export const isDisplay = (a: unknown): a is Display =>
|
|
8
|
-
typeof (a as Display)?.toString === "function" ||
|
|
9
|
-
typeof (a as Display) === "string";
|
|
10
|
-
|
|
11
|
-
export const display = (a: unknown | Display): string => {
|
|
12
|
-
if (isDisplay(a)) {
|
|
13
|
-
const str = a.toString();
|
|
14
|
-
if (str === "[object Object]") return JSON.stringify(a);
|
|
15
|
-
return str;
|
|
16
|
-
}
|
|
17
|
-
return JSON.stringify(a);
|
|
18
|
-
};
|
package/src/dom/README.md
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# Jiffies DOM
|
|
2
|
-
|
|
3
|
-
Jiffies DOM is an HTML microframework, exposing access to the DOM in a functional-first way.
|
|
4
|
-
|
|
5
|
-
```js
|
|
6
|
-
import {form, input, label} from 'jiffies/dom/html';
|
|
7
|
-
|
|
8
|
-
export const Form({
|
|
9
|
-
title,
|
|
10
|
-
action="#",
|
|
11
|
-
onSubmit = (event) => {},
|
|
12
|
-
},
|
|
13
|
-
...children
|
|
14
|
-
) =>
|
|
15
|
-
form({
|
|
16
|
-
action,
|
|
17
|
-
events: {
|
|
18
|
-
submit: (event) => {
|
|
19
|
-
event.preventDefault();
|
|
20
|
-
onSubmit(event);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
h3(title),
|
|
25
|
-
...children,
|
|
26
|
-
button({type: "submit"}, "Submit")
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
export const Input(name, type="string") => {
|
|
30
|
-
let id = name.replace(/\s+/g, '_').toLowerCase();
|
|
31
|
-
return label(
|
|
32
|
-
{for: id},
|
|
33
|
-
name,
|
|
34
|
-
input({name, id, type})
|
|
35
|
-
)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
window.document.body.append(
|
|
39
|
-
Form(
|
|
40
|
-
{
|
|
41
|
-
title: "Details",
|
|
42
|
-
onSubmit: (event) => {
|
|
43
|
-
console.log(event.target);
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
Input({name: "First Name"}),
|
|
47
|
-
Input({name: "Last Name"}),
|
|
48
|
-
Input({name: "Number of cats", type: "number"})
|
|
49
|
-
)
|
|
50
|
-
);
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
Exposing HTML as a tree of function calls makes it very easy to compose units of HTML.
|
|
54
|
-
Creating new functions which pass HTML fragments is natural and intuitive, and creating reusable chunks of common HTML patterns is easy.
|
|
55
|
-
|
|
56
|
-
## Functional Components
|
|
57
|
-
|
|
58
|
-
Exposing HTML as functions makes it easy on the programmer to create and compose HTML, but those functions lose context when they return.
|
|
59
|
-
To capture HTML chunks and update them, use the `FC` function.
|
|
60
|
-
The `FC` function creates new WebComponent elements in the DOM, and exposes an interface that matches native HTML elements.
|
|
61
|
-
|
|
62
|
-
```js
|
|
63
|
-
export const GameSquare = FC<{piece: Piece}>('game-square', (el, {piece}) => {
|
|
64
|
-
el.textContent = piece;
|
|
65
|
-
// `el` is retained between updates
|
|
66
|
-
return el;
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
export const GameBoard = FC<{pieces: Piece[]}>('game-board', (el, {pieces} => {
|
|
70
|
-
return el;
|
|
71
|
-
}));
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Style Blocks
|
|
75
|
-
|
|
76
|
-
The `compileFstyle` function streamlines creating nested style rules.
|
|
77
|
-
When used with the `style` HTML tag, this can create complex layouts inline with components.
|
|
78
|
-
|
|
79
|
-
```js
|
|
80
|
-
const MyPage = () => [
|
|
81
|
-
style(
|
|
82
|
-
compileFstyle({
|
|
83
|
-
main: {
|
|
84
|
-
display: "flex",
|
|
85
|
-
flexDirection: "row",
|
|
86
|
-
},
|
|
87
|
-
"@media max-width(768px)": {
|
|
88
|
-
main: {
|
|
89
|
-
flexDirection: "column",
|
|
90
|
-
},
|
|
91
|
-
},
|
|
92
|
-
})
|
|
93
|
-
),
|
|
94
|
-
section("..."),
|
|
95
|
-
section("..."),
|
|
96
|
-
section("..."),
|
|
97
|
-
];
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
## jiffies-css
|
|
101
|
-
|
|
102
|
-
Including [jiffies-css](https://www.npmjs.com/package/@davidsouther/jiffies-css)
|
|
103
|
-
provides a semantic HTML base that styles by element type and ARIA role rather
|
|
104
|
-
than class names. Load it with `jiffiesCssLink()` and build pages from the typed
|
|
105
|
-
component functions in [`dom/components`](./components/index.ts) (`Card`, `Alert`,
|
|
106
|
-
`Nav`, ...), which emit the exact structures jiffies-css targets — no manual class
|
|
107
|
-
annotation.
|
package/src/dom/SKILL.md
DELETED
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: using-jiffies-dom
|
|
3
|
-
description: Use when building or updating UI in a project that depends on @davidsouther/jiffies and you are writing DOM code with its html/svg/fc modules — creating elements, updating a node in place via its .update() method, wiring event handlers, setting class/style, or building stateful FC components. Covers the reentrant create-or-update model and the correct .ts import paths.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Using Jiffies DOM
|
|
7
|
-
|
|
8
|
-
## Overview
|
|
9
|
-
|
|
10
|
-
Jiffies DOM is a tiny functional library that implements **reentrant DOM**, where every
|
|
11
|
-
node is a function that when called again updates its contents. There are exactly two
|
|
12
|
-
operations:
|
|
13
|
-
|
|
14
|
-
- **Create:** calling a tag function (`div(...)`, `button(...)`, `circle(...)`) makes a
|
|
15
|
-
**new** node every time.
|
|
16
|
-
- **Update in place:** every node Jiffies creates carries an `.update(attrs?, ...children)`
|
|
17
|
-
method. Calling it mutates **that same node** — same identity, no replacement.
|
|
18
|
-
|
|
19
|
-
To update a node later, you must **keep a reference to it** and call `.update()` on it.
|
|
20
|
-
Calling the tag function again does not update the old node; it builds a new one.
|
|
21
|
-
|
|
22
|
-
> Ideally, the object returned from the tag function would itself be directly callable, so instead of `const el = div({'class': 'off'}); el.update({'class': 'on'});`, you could just do `const el = div({'class': 'off'}); el({'class': 'on})';`. This is possible by wrapping the returned `HTMLDivElement` in a `Proxy` that implements a call interceptor, but `Proxy` cannot be passed to DOM APIs like appendChildren or addEventListener.
|
|
23
|
-
|
|
24
|
-
## Import paths (get this right first)
|
|
25
|
-
|
|
26
|
-
The package is `@davidsouther/jiffies` and its export map is `"./*.ts": "./src/*.ts"`.
|
|
27
|
-
Imports use the real subpath **with the `.ts` extension**:
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
import { div, button, span, ul, li, p } from "@davidsouther/jiffies/dom/html.ts";
|
|
31
|
-
import { FC, State } from "@davidsouther/jiffies/dom/fc.ts";
|
|
32
|
-
import { svg, circle } from "@davidsouther/jiffies/dom/svg.ts";
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
The package READMEs show `jiffies/dom/html` — that path is **wrong**. Only `html` and `fc`
|
|
36
|
-
are re-exported from `dom/index.ts`; reach `svg`, `observable`, `router`, `provide`, and
|
|
37
|
-
`xml` by their own deep paths.
|
|
38
|
-
|
|
39
|
-
## Quick reference
|
|
40
|
-
|
|
41
|
-
| You want to… | Do this |
|
|
42
|
-
|---|---|
|
|
43
|
-
| Create an element | `div(attrs?, ...children)` — first arg is attrs only if a plain object |
|
|
44
|
-
| Update a node in place | hold a reference to the node, then call `node.update(attrs?, ...children)` |
|
|
45
|
-
| Set children only | `node.update("new text")` or `node.update(child1, child2)` |
|
|
46
|
-
| Empty children | `import { CLEAR } from ".../dom/dom.ts"; node.update(CLEAR)` |
|
|
47
|
-
| Add an event | `button({ events: { click: (e) => {...} } })` |
|
|
48
|
-
| Replace an event handler | `node.update({ events: { click: newHandler } })` — old listener is removed first |
|
|
49
|
-
| Remove an event | `node.update({ events: { click: null } })` |
|
|
50
|
-
| Set a class | `div({ class: "a b" })` or `{ class: ["a", "b"] }` |
|
|
51
|
-
| Remove a class on update | `node.update({ class: "!hidden" })` (`!` prefix removes) |
|
|
52
|
-
| Inline style | `{ style: { flexDirection: "column" } }` or `{ style: "color:red" }` |
|
|
53
|
-
| Boolean attribute | `{ disabled: true }` (falsy removes the attribute) |
|
|
54
|
-
|
|
55
|
-
## The argument rule (common error source)
|
|
56
|
-
|
|
57
|
-
The first argument is treated as an **attributes object** only if it is a plain object with
|
|
58
|
-
no `nodeType`. A string, a Node, or `CLEAR` is treated as the **first child**.
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
div({ class: "row" }, span("a"), span("b")); // attrs + 2 children
|
|
62
|
-
div(span("a"), span("b")); // 0 attrs, 2 children
|
|
63
|
-
div("hello"); // 0 attrs, 1 text child
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Example: in-place Counter
|
|
67
|
-
|
|
68
|
-
The number node is created once and reused on every click. The tag function is the create;
|
|
69
|
-
`display.update(...)` is the reentrant update.
|
|
70
|
-
|
|
71
|
-
```ts
|
|
72
|
-
import { div, span, button } from "@davidsouther/jiffies/dom/html.ts";
|
|
73
|
-
|
|
74
|
-
export function Counter(start = 0) {
|
|
75
|
-
let count = start;
|
|
76
|
-
const display = span(`${count}`); // created ONCE — keep the reference
|
|
77
|
-
|
|
78
|
-
return div(
|
|
79
|
-
display,
|
|
80
|
-
button(
|
|
81
|
-
{ events: { click: () => { count += 1; display.update(`${count}`); } } },
|
|
82
|
-
"+1",
|
|
83
|
-
),
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## FC: stateful components
|
|
89
|
-
|
|
90
|
-
Use `FC` when a component owns state and should re-render itself from props. `FC(name, render)`
|
|
91
|
-
defines a custom element and returns a constructor. Calling it creates the element; calling
|
|
92
|
-
`.update(props)` **merges** props, re-runs `render`, and reconciles the rendered output into the host.
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
import { FC } from "@davidsouther/jiffies/dom/fc.ts";
|
|
96
|
-
import { section, h2, ul, li } from "@davidsouther/jiffies/dom/html.ts";
|
|
97
|
-
|
|
98
|
-
export const TodoList = FC<{ title: string; items: string[] }>(
|
|
99
|
-
"todo-list",
|
|
100
|
-
(_el, { title, items }) =>
|
|
101
|
-
section(h2(title ?? "Todos"), ul(...items.map((i) => li(i)))),
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
const list = TodoList({ title: "Chores", items: ["wash", "fold"] });
|
|
105
|
-
document.body.append(list);
|
|
106
|
-
list.update({ items: ["wash", "fold", "iron"] }); // title is retained (props merge)
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
The render function is `(el, props, children) => Element | Element[]`. `el` is the host
|
|
110
|
-
custom element (also typed to carry `.update()` and the `State` symbol). It may return a
|
|
111
|
-
single node or an array. Persist component state on the host via the `State` symbol:
|
|
112
|
-
|
|
113
|
-
```ts
|
|
114
|
-
import { FC, State } from "@davidsouther/jiffies/dom/fc.ts";
|
|
115
|
-
|
|
116
|
-
export const Toggle = FC<{ label: string }, { on: boolean }>(
|
|
117
|
-
"app-toggle",
|
|
118
|
-
(el, { label }) => {
|
|
119
|
-
el[State] ??= { on: false }; // initialise once; retained across updates
|
|
120
|
-
const s = el[State];
|
|
121
|
-
return button(
|
|
122
|
-
{ events: { click: () => { s.on = !s.on; el.update(); } } }, // el.update() re-renders
|
|
123
|
-
`${label}: ${s.on ? "on" : "off"}`,
|
|
124
|
-
);
|
|
125
|
-
},
|
|
126
|
-
);
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
## Typing nodes
|
|
130
|
-
|
|
131
|
-
You do not need a Jiffies-specific type to hold a node. Jiffies augments the global DOM
|
|
132
|
-
`Element` interface with `.update()`, so standard lib types already carry it:
|
|
133
|
-
|
|
134
|
-
```ts
|
|
135
|
-
const display: HTMLSpanElement = span("0");
|
|
136
|
-
display.update("1"); // .update is in scope on every Element
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
`dom/dom.ts` also exports `DOMElement` (`Element & ElementCSSInlineStyle`) for the general case.
|
|
140
|
-
|
|
141
|
-
## Updates reconcile children by identity
|
|
142
|
-
|
|
143
|
-
`.update(...children)` reconciles the new child list against the mounted children **by node
|
|
144
|
-
object identity**. A child you pass back by the **same reference** is left in place and never
|
|
145
|
-
detached, so its focus, scroll position, text selection, event listeners, and any descendant
|
|
146
|
-
state survive the update. A freshly built node (or a string) has no matching identity, so it
|
|
147
|
-
is inserted; a mounted child you omit is removed; order follows the argument list.
|
|
148
|
-
|
|
149
|
-
This means you can update a parent and keep a specific subtree alive by passing the same node
|
|
150
|
-
reference back through it:
|
|
151
|
-
|
|
152
|
-
```ts
|
|
153
|
-
const panel = div(span("Title"), input({ name: "q" })); // keep this reference
|
|
154
|
-
const root = div(panel, p("status"));
|
|
155
|
-
// ...later: replace the status line but keep the panel (and its focused input):
|
|
156
|
-
root.update(panel, p("ready")); // panel is reused in place; only the <p> is rebuilt
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
Strings always rebuild (they carry no identity). The same reference must not appear twice in
|
|
160
|
-
one update — a DOM node can occupy only one position.
|
|
161
|
-
|
|
162
|
-
For fine-grained leaf updates (e.g. a counter), you can still hold a reference to the specific
|
|
163
|
-
child node and call `.update()` on **that** node directly. **FC is the exception:** its
|
|
164
|
-
`render` builds fresh nodes each update, so identity is not preserved inside an FC's own
|
|
165
|
-
output (see the common mistake below).
|
|
166
|
-
|
|
167
|
-
## Testing
|
|
168
|
-
|
|
169
|
-
Tests use the `scope` microframework and run under Node (jsdom loads automatically when
|
|
170
|
-
`window` is undefined). Run with `npm test` (`node ./src/test.mjs`).
|
|
171
|
-
|
|
172
|
-
```ts
|
|
173
|
-
import { describe, it, expect } from "@davidsouther/jiffies/scope/index.ts";
|
|
174
|
-
import { button } from "@davidsouther/jiffies/dom/html.ts";
|
|
175
|
-
|
|
176
|
-
describe("counter", () => {
|
|
177
|
-
it("reuses the node on update", () => {
|
|
178
|
-
const b = button("0");
|
|
179
|
-
b.update("1");
|
|
180
|
-
expect(b.textContent).toBe("1");
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
## Common mistakes
|
|
186
|
-
|
|
187
|
-
- **Calling the tag function again to "update."** That creates a new node. Hold the
|
|
188
|
-
reference and call `.update()` on it.
|
|
189
|
-
- **Expecting event handlers to stack.** Calling `node.update({ events: { click: newFn } })`
|
|
190
|
-
removes the old listener before adding the new one. Each event key tracks exactly one
|
|
191
|
-
listener; the last one set is the one that fires.
|
|
192
|
-
- **Importing `jiffies/dom/html`.** Use `@davidsouther/jiffies/dom/html.ts` (full name, `.ts`).
|
|
193
|
-
- **Passing a config object as the first child.** A plain object becomes attrs; wrap text/nodes
|
|
194
|
-
as children explicitly.
|
|
195
|
-
- **Expecting FC `.update()` to preserve child DOM identity.** It re-renders and replaces
|
|
196
|
-
children. For stable child nodes, update them directly.
|
|
197
|
-
- **Using namespace-qualified SVG attributes.** `update()` always calls `setAttribute`, not
|
|
198
|
-
`setAttributeNS`. Attributes on SVG elements are plain unqualified names; read them back with
|
|
199
|
-
`getAttribute`, not `getAttributeNS`.
|
|
200
|
-
- **Copying README examples verbatim.** The package README snippets are illustrative and not
|
|
201
|
-
all valid TypeScript; rely on this skill and the `*.test.ts` files for working code.
|
package/src/dom/css/border.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type { Properties } from "../types/css.ts";
|
|
2
|
-
import type { Side, Size } from "./constants.ts";
|
|
3
|
-
import { getSide, getSize, isSide } from "./core.ts";
|
|
4
|
-
|
|
5
|
-
export function rounded(size: Size = "", side: Side = "") {
|
|
6
|
-
if (isSide(size)) {
|
|
7
|
-
side = size;
|
|
8
|
-
size = "";
|
|
9
|
-
}
|
|
10
|
-
const sized = getSize(size);
|
|
11
|
-
return getSide(side).reduce((prev, curr) => {
|
|
12
|
-
if (curr === "") {
|
|
13
|
-
prev.borderRadius = sized;
|
|
14
|
-
} else {
|
|
15
|
-
// @ts-expect-error
|
|
16
|
-
prev[`border${curr}Radius`] = sized;
|
|
17
|
-
}
|
|
18
|
-
return prev;
|
|
19
|
-
}, {} as Properties);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function border({
|
|
23
|
-
side: _side = "",
|
|
24
|
-
style: _style = "solid",
|
|
25
|
-
radius: _radius = "",
|
|
26
|
-
width: _width = 1,
|
|
27
|
-
color: _color = "black",
|
|
28
|
-
}: {
|
|
29
|
-
side?: Side;
|
|
30
|
-
style?: "solid" | "dotted" | "dashed" | "double" | "none";
|
|
31
|
-
radius?: Size;
|
|
32
|
-
width?: 0 | 1 | 2 | 4 | 8;
|
|
33
|
-
color?: string;
|
|
34
|
-
}) {
|
|
35
|
-
return {};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function inset(
|
|
39
|
-
width: 0 | 1 | 2 | 4 | 8,
|
|
40
|
-
color1 = "gray",
|
|
41
|
-
color2 = "lightgray",
|
|
42
|
-
) {
|
|
43
|
-
return {
|
|
44
|
-
...border({ side: "tl", width, color: color1, radius: "none" }),
|
|
45
|
-
...border({ side: "br", width, color: color2, radius: "none" }),
|
|
46
|
-
};
|
|
47
|
-
}
|