lilact 0.2.1 → 0.2.3

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 (36) hide show
  1. package/dist/lilact.development.min.js +2 -9657
  2. package/dist/lilact.development.min.js.LICENSE.txt +1 -1
  3. package/dist/lilact.development.min.js.map +1 -1
  4. package/dist/lilact.production.min.js +1 -1
  5. package/dist/lilact.production.min.js.map +1 -1
  6. package/docs/classes/accessories.ErrorBoundary.html +8 -8
  7. package/docs/classes/accessories.Suspense.html +7 -7
  8. package/docs/classes/components.Component.html +10 -10
  9. package/docs/classes/components.HTMLComponent.html +10 -10
  10. package/docs/classes/components.RootComponent.html +10 -10
  11. package/docs/functions/components.createComponent.html +1 -1
  12. package/docs/functions/components.createRoot.html +1 -1
  13. package/docs/functions/components.render.html +1 -1
  14. package/docs/functions/hooks.useActionState.html +3 -2
  15. package/docs/functions/hooks.useEffect.html +3 -2
  16. package/docs/functions/hooks.useImperativeHandle.html +1 -1
  17. package/docs/functions/hooks.useLayoutEffect.html +3 -2
  18. package/docs/functions/hooks.useLocalStorage.html +3 -2
  19. package/docs/functions/hooks.useMemo.html +3 -2
  20. package/docs/functions/hooks.useRef.html +3 -2
  21. package/docs/static/demos/pane.jsx +31 -0
  22. package/docs/static/index.html +1 -0
  23. package/docs/static/lilact.development.min.js +2 -9657
  24. package/docs/static/lilact.production.min.js +1 -1
  25. package/package.json +1 -1
  26. package/root/demos/pane.jsx +31 -0
  27. package/root/index.html +1 -0
  28. package/root/lilact.development.min.js +2 -9657
  29. package/root/lilact.production.min.js +1 -1
  30. package/src/components.jsx +4 -1
  31. package/src/hooks.jsx +15 -15
  32. package/src/misc.jsx +13 -1
  33. package/src/pane.jsx +207 -269
  34. package/webpack.config.js +1 -1
  35. package/docs/static/index 2.html +0 -95
  36. package/root/index 2.html +0 -95
package/src/pane.jsx CHANGED
@@ -1,9 +1,11 @@
1
1
  import { useEffect, useLayoutEffect, useMemo, useRef, useState, forwardRef, useImperativeHandle } from "./hooks.jsx";
2
2
  import { Children } from "./misc.jsx";
3
3
 
4
- function clamp(n, min, max) {
5
- return Math.max(min, Math.min(max, n));
6
- }
4
+
5
+ const clamp = (n, min, max) => {
6
+ if (!Number.isFinite(n)) return n;
7
+ return Math.min(max, Math.max(min, n));
8
+ };
7
9
 
8
10
  /**
9
11
  * ResizablePane
@@ -17,271 +19,207 @@ function clamp(n, min, max) {
17
19
  * - Callback when size changes via onSizeChange
18
20
  * - Children are rendered in two separate containers (no portals)
19
21
  */
20
- export const ResizablePane = forwardRef(function Pane(
21
- {
22
- mode = "horizontal", // "horizontal" | "vertical"
23
- initialPosition, // optional (alias-ish to position)
24
- position, // controlled position (number)
25
- defaultPosition = 0.5, // used if position/initialPosition not provided
26
- min = 0.1, // clamp in "percent of container" units: 0..1
27
- max = 0.9, // clamp in "percent of container" units: 0..1
28
- splitterSize = 8, // px thickness of the drag handle
29
- onSizeChange, // (newPosition: number) => void
30
- style,
31
- className,
32
- leftPaneStyle,
33
- rightPaneStyle,
34
- splitterStyle,
35
- children, // expects two children: [A, B]
36
- },
37
- ref
22
+ export const ResizablePane = forwardRef(function ResizablePane(
23
+ {
24
+ mode = "horizontal",
25
+ position, // controlled: number | undefined/null
26
+ defaultPosition = 0.5,
27
+ min = 0.1,
28
+ max = 0.9,
29
+ splitterSize = 8,
30
+ onSizeChange,
31
+ style,
32
+ className,
33
+ leftPaneStyle,
34
+ rightPaneStyle,
35
+ splitterStyle,
36
+ children,
37
+ },
38
+ ref
38
39
  ) {
39
- const [internalMode, setInternalMode] = useState(mode);
40
- const [pos, setPos] = useState(() => {
41
- const start =
42
- position ?? initialPosition ?? (defaultPosition != null ? defaultPosition : 0.5);
43
- return clamp(start, min, max);
44
- });
45
-
46
- const containerRef = useRef(null);
47
- const draggingRef = useRef(false);
48
-
49
- const panes = Children.toArray(children);
50
- const leftChild = panes[0] ?? null;
51
- const rightChild = panes[1] ?? null;
52
-
53
- const modeResolved = mode != null ? mode : internalMode;
54
-
55
- // Keep internalMode aligned if mode is provided as a prop
56
- useEffect(() => {
57
- if (mode != null) setInternalMode(mode);
58
- }, [mode]);
59
-
60
- // Controlled position: update state when prop changes
61
- useEffect(() => {
62
- if (position == null) return;
63
- setPos(clamp(position, min, max));
64
- }, [position, min, max]);
65
-
66
- const setPosition = (newPos) => {
67
- const next = clamp(newPos, min, max);
68
- if (position == null) setPos(next); // only update internal state if uncontrolled
69
- onSizeChange?.(next);
70
- };
71
-
72
- const setMode = (nextMode) => {
73
- const m = nextMode === "vertical" ? "vertical" : "horizontal";
74
- if (mode == null) setInternalMode(m);
75
- // If mode is controlled via prop, consumer should re-render with new prop.
76
- };
77
-
78
- useImperativeHandle(
79
- ref,
80
- () => ({
81
- setPosition,
82
- setMode,
83
- getPosition: () => (position == null ? pos : clamp(position, min, max)),
84
- getMode: () => modeResolved,
85
- }),
86
- [pos, position, min, max, modeResolved, onSizeChange, mode]
87
- );
88
-
89
- const updateFromClientXorY = (clientX, clientY) => {
90
- const el = containerRef.current;
91
- if (!el) return;
92
-
93
- const rect = el.getBoundingClientRect();
94
- let next;
95
-
96
- if (modeResolved === "horizontal") {
97
- const usable = rect.width;
98
- if (usable <= 0) return;
99
- next = (clientX - rect.left) / usable;
100
- } else {
101
- const usable = rect.height;
102
- if (usable <= 0) return;
103
- next = (clientY - rect.top) / usable;
104
- }
105
- setPosition(next);
106
- };
107
-
108
- const onPointerDown = (e) => {
109
- e.preventDefault();
110
- draggingRef.current = true;
111
-
112
- // Capture pointer so we still get events outside the handle area.
113
- try {
114
- e.currentTarget.setPointerCapture?.(e.pointerId);
115
- } catch {
116
- // ignore
117
- }
118
-
119
- updateFromClientXorY(e.clientX, e.clientY);
120
- };
121
-
122
- const onPointerMove = (e) => {
123
- if (!draggingRef.current) return;
124
- updateFromClientXorY(e.clientX, e.clientY);
125
- };
126
-
127
- const stopDragging = () => {
128
- draggingRef.current = false;
129
- };
130
-
131
- // Attach global listeners to ensure smooth dragging even if pointer capture fails
132
- useEffect(() => {
133
- const handleMove = (e) => {
134
- if (!draggingRef.current) return;
135
- updateFromClientXorY(e.clientX, e.clientY);
136
- };
137
- const handleUp = () => stopDragging();
138
-
139
- window.addEventListener("pointermove", handleMove, { passive: false });
140
- window.addEventListener("pointerup", handleUp, { passive: true });
141
- window.addEventListener("pointercancel", handleUp, { passive: true });
142
-
143
- return () => {
144
- window.removeEventListener("pointermove", handleMove);
145
- window.removeEventListener("pointerup", handleUp);
146
- window.removeEventListener("pointercancel", handleUp);
147
- };
148
- }, [modeResolved, min, max, position, onSizeChange]);
149
-
150
- const posResolved = position == null ? pos : clamp(position, min, max);
151
-
152
- const sizes = useMemo(() => {
153
- if (modeResolved === "horizontal") {
154
- // left width = pos, splitter width = splitterSize, right flexes
155
- // We set left/right as percentages and keep splitter fixed in px.
156
- return {
157
- left: `${posResolved * 100}%`,
158
- right: `calc(${100 - posResolved * 100}% - ${splitterSize}px)`,
159
- };
160
- }
161
- return {
162
- left: `${posResolved * 100}%`,
163
- right: `calc(${100 - posResolved * 100}% - ${splitterSize}px)`,
164
- };
165
- }, [modeResolved, posResolved, splitterSize]);
166
-
167
- // Improve keyboard accessibility (arrow keys when handle is focused)
168
- const onSplitterKeyDown = (e) => {
169
- const step = 0.02;
170
- let delta = 0;
171
- if (modeResolved === "horizontal") {
172
- if (e.key === "ArrowLeft") delta = -step;
173
- if (e.key === "ArrowRight") delta = step;
174
- } else {
175
- if (e.key === "ArrowUp") delta = -step;
176
- if (e.key === "ArrowDown") delta = step;
177
- }
178
- if (delta !== 0) {
179
- e.preventDefault();
180
- setPosition(posResolved + delta);
181
- }
182
- };
183
-
184
- // Ensure position clamps correctly if min/max change
185
- useLayoutEffect(() => {
186
- setPos((p) => clamp(p, min, max));
187
- }, [min, max]);
188
-
189
- const rootStyle = {
190
- display: "flex",
191
- width: "100%",
192
- height: "100%",
193
- overflow: "hidden",
194
- touchAction: "none",
195
- ...(style || {}),
196
- flexDirection: modeResolved === "horizontal" ? "row" : "column",
197
- };
198
-
199
- const leftPaneComputed =
200
- modeResolved === "horizontal"
201
- ? {
202
- width: sizes.left,
203
- flex: `0 0 ${sizes.left}`,
204
- overflow: "auto",
205
- ...(leftPaneStyle || {}),
206
- }
207
- : {
208
- height: sizes.left,
209
- flex: `0 0 ${sizes.left}`,
210
- overflow: "auto",
211
- ...(leftPaneStyle || {}),
212
- };
213
-
214
- const rightPaneComputed =
215
- modeResolved === "horizontal"
216
- ? {
217
- width: `calc(${100 - posResolved * 100}% - ${splitterSize}px)`,
218
- flex: `1 1 auto`,
219
- overflow: "auto",
220
- ...(rightPaneStyle || {}),
221
- }
222
- : {
223
- height: `calc(${100 - posResolved * 100}% - ${splitterSize}px)`,
224
- flex: `1 1 auto`,
225
- overflow: "auto",
226
- ...(rightPaneStyle || {}),
227
- };
228
-
229
- const splitterComputed =
230
- modeResolved === "horizontal"
231
- ? {
232
- width: `${splitterSize}px`,
233
- flex: `0 0 ${splitterSize}px`,
234
- cursor: "col-resize",
235
- background: "transparent",
236
- ...(splitterStyle || {}),
237
- }
238
- : {
239
- height: `${splitterSize}px`,
240
- flex: `0 0 ${splitterSize}px`,
241
- cursor: "row-resize",
242
- background: "transparent",
243
- ...(splitterStyle || {}),
244
- };
245
-
246
- const dividerVisualStyle =
247
- modeResolved === "horizontal"
248
- ? {
249
- height: "100%",
250
- width: "100%",
251
- background: "rgba(0,0,0,0.08)",
252
- }
253
- : {
254
- width: "100%",
255
- height: "100%",
256
- background: "rgba(0,0,0,0.08)",
257
- };
258
-
259
- return (
260
- <div
261
- ref={containerRef}
262
- className={className}
263
- style={rootStyle}
264
- onPointerMove={onPointerMove}
265
- >
266
- <div style={leftPaneComputed}>{leftChild}</div>
267
-
268
- <div
269
- role="separator"
270
- aria-orientation={modeResolved === "horizontal" ? "vertical" : "horizontal"}
271
- aria-valuemin={min}
272
- aria-valuemax={max}
273
- aria-valuenow={posResolved}
274
- tabIndex={0}
275
- onPointerDown={onPointerDown}
276
- onPointerUp={stopDragging}
277
- onPointerCancel={stopDragging}
278
- onKeyDown={onSplitterKeyDown}
279
- style={splitterComputed}
280
- >
281
- <div style={dividerVisualStyle} />
282
- </div>
283
-
284
- <div style={rightPaneComputed}>{rightChild}</div>
285
- </div>
286
- );
40
+ const containerRef = useRef(null);
41
+ const panes = Children.toArray(children);
42
+ const leftChild = panes[0] ?? null;
43
+ const rightChild = panes[1] ?? null;
44
+
45
+ const isControlled = position != null;
46
+
47
+ const [internalMode, setInternalMode] = useState(mode);
48
+
49
+ const [posUncontrolled, setPosUncontrolled] = useState(() =>
50
+ clamp(
51
+ position ?? defaultPosition,
52
+ min,
53
+ max
54
+ )
55
+ );
56
+
57
+ useEffect(() => {
58
+ if (mode != null) setInternalMode(mode);
59
+ }, [mode]);
60
+
61
+ // keep internal position clamped if min/max change (uncontrolled only)
62
+ useLayoutEffect(() => {
63
+ if (isControlled) return;
64
+ setPosUncontrolled((p) => clamp(p, min, max));
65
+ }, [min, max, isControlled]);
66
+
67
+ const posResolved = isControlled ? clamp(position, min, max) : posUncontrolled;
68
+
69
+ const setPosition = (next) => {
70
+ const clamped = clamp(next, min, max);
71
+ if (!isControlled) setPosUncontrolled(clamped);
72
+ onSizeChange?.(clamped);
73
+ };
74
+
75
+ const draggingRef = useRef(false);
76
+ const pointerIdRef = useRef(null);
77
+
78
+ const updateFromClient = (clientX, clientY) => {
79
+ const el = containerRef.current;
80
+ if (!el) return;
81
+
82
+ const rect = el.getBoundingClientRect();
83
+
84
+ if (internalMode === "horizontal") {
85
+ const usable = rect.width;
86
+ if (!Number.isFinite(usable) || usable <= 0) return; // no jump on init
87
+ const raw = (clientX - rect.left) / usable;
88
+ if (!Number.isFinite(raw)) return;
89
+ setPosition(raw);
90
+ } else {
91
+ const usable = rect.height;
92
+ if (!Number.isFinite(usable) || usable <= 0) return; // no jump on init
93
+ const raw = (clientY - rect.top) / usable;
94
+ if (!Number.isFinite(raw)) return;
95
+ setPosition(raw);
96
+ }
97
+ };
98
+
99
+ // stable global listeners: only act while draggingRef.current === true
100
+ useEffect(() => {
101
+ const onMove = (e) => {
102
+ if (!draggingRef.current) return;
103
+ if (pointerIdRef.current != null && e.pointerId !== pointerIdRef.current) return;
104
+ updateFromClient(e.clientX, e.clientY);
105
+ };
106
+
107
+ const stop = (e) => {
108
+ if (!draggingRef.current) return;
109
+ if (pointerIdRef.current != null && e.pointerId !== pointerIdRef.current) return;
110
+ draggingRef.current = false;
111
+ pointerIdRef.current = null;
112
+ };
113
+
114
+ window.addEventListener("pointermove", onMove, { passive: true });
115
+ window.addEventListener("pointerup", stop, { passive: true });
116
+ window.addEventListener("pointercancel", stop, { passive: true });
117
+
118
+ return () => {
119
+ window.removeEventListener("pointermove", onMove);
120
+ window.removeEventListener("pointerup", stop);
121
+ window.removeEventListener("pointercancel", stop);
122
+ };
123
+ }, [internalMode]); // updateFromClient uses internalMode
124
+
125
+ const onPointerDown = (e) => {
126
+ if (e.button != null && e.button !== 0) return;
127
+ e.preventDefault();
128
+
129
+ draggingRef.current = true;
130
+ pointerIdRef.current = e.pointerId;
131
+
132
+ try {
133
+ e.currentTarget.setPointerCapture?.(e.pointerId);
134
+ } catch {}
135
+
136
+ updateFromClient(e.clientX, e.clientY); // first update only if rect is sane
137
+ };
138
+
139
+ const onKeyDown = (e) => {
140
+ const step = 0.02;
141
+ let delta = 0;
142
+
143
+ if (internalMode === "horizontal") {
144
+ if (e.key === "ArrowLeft") delta = -step;
145
+ if (e.key === "ArrowRight") delta = step;
146
+ } else {
147
+ if (e.key === "ArrowUp") delta = -step;
148
+ if (e.key === "ArrowDown") delta = step;
149
+ }
150
+
151
+ if (delta !== 0) {
152
+ e.preventDefault();
153
+ setPosition(posResolved + delta);
154
+ }
155
+ };
156
+
157
+ const sizes = useMemo(() => {
158
+ const leftPct = `${posResolved * 100}%`;
159
+ const rightCalc = `calc(${100 - posResolved * 100}% - ${splitterSize}px)`;
160
+ return { leftPct, rightCalc };
161
+ }, [posResolved, splitterSize]);
162
+
163
+ const rootStyle = {
164
+ display: "flex",
165
+ width: "100%",
166
+ height: "100%",
167
+ overflow: "hidden",
168
+ touchAction: "none",
169
+ ...(style || {}),
170
+ flexDirection: internalMode === "horizontal" ? "row" : "column",
171
+ };
172
+
173
+ const leftPaneComputed =
174
+ internalMode === "horizontal"
175
+ ? { width: sizes.leftPct, flex: `0 0 ${sizes.leftPct}`, overflow: "auto", ...(leftPaneStyle || {}) }
176
+ : { height: sizes.leftPct, flex: `0 0 ${sizes.leftPct}`, overflow: "auto", ...(leftPaneStyle || {}) };
177
+
178
+ const rightPaneComputed =
179
+ internalMode === "horizontal"
180
+ ? { width: sizes.rightCalc, flex: "1 1 auto", overflow: "auto", ...(rightPaneStyle || {}) }
181
+ : { height: sizes.rightCalc, flex: "1 1 auto", overflow: "auto", ...(rightPaneStyle || {}) };
182
+
183
+ const splitterComputed =
184
+ internalMode === "horizontal"
185
+ ? { width: `${splitterSize}px`, height: '100%', flex: `0 0 ${splitterSize}px`, background: "rgba(0,0,0,0.08)", cursor: "col-resize", ...(splitterStyle || {}) }
186
+ : { height: `${splitterSize}px`, width: '100%', flex: `0 0 ${splitterSize}px`, background: "rgba(0,0,0,0.08)", cursor: "row-resize", ...(splitterStyle || {}) };
187
+
188
+ const dividerVisualStyle =
189
+ internalMode === "horizontal"
190
+ ? { height: "100%", width: "100%" }
191
+ : { width: "100%", height: "100%" };
192
+
193
+ useImperativeHandle(ref, () => ({
194
+ setPosition,
195
+ setMode: (nextMode) => setInternalMode(nextMode === "vertical" ? "vertical" : "horizontal"),
196
+ getPosition: () => posResolved,
197
+ getMode: () => internalMode,
198
+ }));
199
+
200
+ return (
201
+ <div ref={containerRef} className={className} style={rootStyle}>
202
+ <div style={leftPaneComputed}>{leftChild}</div>
203
+
204
+ <div
205
+ role="separator"
206
+ aria-orientation={internalMode === "horizontal" ? "vertical" : "horizontal"}
207
+ aria-valuemin={min}
208
+ aria-valuemax={max}
209
+ aria-valuenow={posResolved}
210
+ tabIndex={0}
211
+ onPointerDown={onPointerDown}
212
+ onPointerCancel={() => {
213
+ draggingRef.current = false;
214
+ pointerIdRef.current = null;
215
+ }}
216
+ onKeyDown={onKeyDown}
217
+ style={splitterComputed}
218
+ >
219
+ <div style={dividerVisualStyle} />
220
+ </div>
221
+
222
+ <div style={rightPaneComputed}>{rightChild}</div>
223
+ </div>
224
+ );
287
225
  });
package/webpack.config.js CHANGED
@@ -56,7 +56,7 @@ export default (env, argv) => {
56
56
  optimization: {
57
57
  concatenateModules: true, // scope hoisting
58
58
  moduleIds: 'deterministic', // smaller stable ids (or 'hashed')
59
- minimize: mode === 'production',
59
+ minimize: true, //mode === 'production',
60
60
  },
61
61
  experiments: {
62
62
  outputModule: true // enable module output
@@ -1,95 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8"/>
5
- <title>Lilact Demo</title>
6
- <script src='./lilact.development.min.js' type="module"></script>
7
-
8
- <script src="prismjs/prism.min.js"></script>
9
- <script src="prismjs/prism-jsx.min.js"></script>
10
- <link rel="stylesheet" href="prismjs/prism.min.css">
11
-
12
- </head>
13
-
14
- <body></body>
15
-
16
- <script type='text/jsx'>
17
-
18
- const { useRef, useEffect, useState, Suspense, Spinner, run, render, lazy, require } = Lilact;
19
- const { css,cx,injectGlobal } = Lilact.emotion;
20
-
21
-
22
-
23
- injectGlobal({"*:not(code):not(code *)" : {fontFamily: "Ubuntu, 'Segoe UI', 'Noto Sans', helvetica, sans", fontSize: "14px"}});
24
-
25
-
26
- injectGlobal({ code: { whiteSpace: "pre !important",
27
- padding: "10px 0",
28
- fontSize: "14px !important",
29
- display: "block",
30
- overflowWrap: "anywhere" }
31
- });
32
-
33
- const outer = css({
34
- display: "flex",
35
- flexDirection: "row",
36
- gap: 2,
37
- alignItems: "stretch",
38
- width: "100%",
39
- });
40
-
41
- const collapsible = css({
42
- width: "100%",
43
- display: "block",
44
- fontSize: "16px",
45
- border: "1px solid #888",
46
- padding: "5px"
47
- });
48
-
49
- const responsiveOuter = css({
50
- "@media (max-width: 600px)": {
51
- flexDirection: "column",
52
- },
53
- });
54
-
55
- const pane = css({ flex: "1 1 0",
56
- border: "1px solid",
57
- height: "400px",
58
- minHeight: "400px",
59
- overflowY: "auto",
60
- padding: "10px"
61
- });
62
-
63
- const left = css({ float: "left" });
64
- const right = css({ float: "right" });
65
-
66
-
67
-
68
- const Mod = lazy( ()=>require('demos/stopwatch.jsx') );
69
-
70
- function DemoBlock({file})
71
- {
72
- return <>
73
- <div>
74
- <Suspense fallback={<Spinner/>}>
75
- <Mod/>
76
- </Suspense>
77
- </div>
78
- </>
79
- ;
80
- }
81
-
82
-
83
- function App()
84
- {
85
-
86
- return <>
87
- <DemoBlock/>
88
- </>
89
- }
90
-
91
- render(<App/>, document.body);
92
-
93
- </script>
94
-
95
- </html>