vite-plugin-nora 0.1.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 (45) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +26 -0
  3. package/README.md +113 -0
  4. package/licenses/geist-OFL.txt +93 -0
  5. package/licenses/lucide-ISC.txt +17 -0
  6. package/package.json +90 -0
  7. package/src/cli.js +101 -0
  8. package/src/client/App.jsx +216 -0
  9. package/src/client/canvas/Canvas.jsx +95 -0
  10. package/src/client/canvas/ErrorBoundary.jsx +56 -0
  11. package/src/client/canvas/Preview.jsx +70 -0
  12. package/src/client/canvas/Viewport.jsx +211 -0
  13. package/src/client/chords.js +38 -0
  14. package/src/client/css.d.ts +5 -0
  15. package/src/client/fonts.css +48 -0
  16. package/src/client/frame.css +107 -0
  17. package/src/client/frame.html +11 -0
  18. package/src/client/frame.jsx +73 -0
  19. package/src/client/index.html +12 -0
  20. package/src/client/main.jsx +10 -0
  21. package/src/client/open-folder.js +84 -0
  22. package/src/client/selection.js +22 -0
  23. package/src/client/styles.css +1264 -0
  24. package/src/client/sweep/SweepPanel.jsx +206 -0
  25. package/src/client/sweep/measure.js +230 -0
  26. package/src/client/sweep/report.js +54 -0
  27. package/src/client/sweep/run-sweep.js +185 -0
  28. package/src/client/toolbar/Picker.jsx +343 -0
  29. package/src/client/toolbar/Toolbar.jsx +231 -0
  30. package/src/client/toolbar/ViewportMenu.jsx +83 -0
  31. package/src/client/toolbar/bar-shape.js +402 -0
  32. package/src/client/toolbar/icons.jsx +129 -0
  33. package/src/client/toolbar/use-draggable-bar.js +204 -0
  34. package/src/client/viewports.js +43 -0
  35. package/src/client/virtual.d.ts +28 -0
  36. package/src/index.js +4 -0
  37. package/src/server/create-server.js +147 -0
  38. package/src/server/plugin.js +207 -0
  39. package/src/server/safe-path.js +42 -0
  40. package/src/server/scan.js +350 -0
  41. package/types/index.d.ts +3 -0
  42. package/types/server/create-server.d.ts +22 -0
  43. package/types/server/plugin.d.ts +20 -0
  44. package/types/server/safe-path.d.ts +25 -0
  45. package/types/server/scan.d.ts +106 -0
@@ -0,0 +1,402 @@
1
+ import { useEffect, useLayoutEffect, useRef } from "react";
2
+
3
+ /**
4
+ * The bar's silhouette — one closed path, generated from the cluster boxes.
5
+ *
6
+ * The controls are grouped into clusters, and the shape wraps them: each
7
+ * cluster is a pill, and consecutive pills are joined by a concave bridge that
8
+ * runs tangent to both. The result reads as one blob that has been pinched
9
+ * rather than as three shapes sitting next to each other.
10
+ *
11
+ * That distinction is the whole reason this file exists. The first four
12
+ * attempts drew the clusters as separate rounded elements and painted bridges
13
+ * between them, and every one of them showed a hairline ridge at the junction.
14
+ * The cause is not the geometry: it is that two independently rasterised
15
+ * antialiased edges meeting on the same pixel never sum back to full opacity.
16
+ * A shape with no interior edges cannot seam, so the fix was to stop composing
17
+ * and start describing — the outline below is traversed once, clockwise, and
18
+ * filled once.
19
+ *
20
+ * The buttons then sit on top in ordinary DOM with transparent backgrounds, so
21
+ * text and icons stay crisp. Nothing about the silhouette feeds back into the
22
+ * layout: the boxes are measured from the controls, and the controls never
23
+ * read the shape. The dependency runs one way, which is what keeps it from
24
+ * oscillating the way the viewport framing rule once did.
25
+ */
26
+
27
+ /** How deeply the bridge between two clusters is pinched. Larger = shallower. */
28
+ export const FILLET = 34;
29
+
30
+ /**
31
+ * Build the whole outline as one closed path.
32
+ *
33
+ * Clockwise: along the top from the far left, around the last cluster's right
34
+ * end, back along the bottom, closed. Every pill end is an arc of radius R;
35
+ * every bridge is a pair of arcs of radius f tangent to the two ends it joins.
36
+ */
37
+ export function barPath(boxes, H, f) {
38
+ const R = H / 2;
39
+ const top = [];
40
+ const bottom = [];
41
+
42
+ // Tangent points and fillet centre for the bridge between two end circles.
43
+ //
44
+ // A fillet of radius f can only span ends whose centres are at most 2(R + f)
45
+ // apart — past that, (R + f)² − (D/2)² goes negative, the square root is
46
+ // imaginary, and every coordinate downstream becomes NaN. Rather than trust
47
+ // callers to keep f large enough for whatever gap the layout produces, the
48
+ // requirement is enforced here: f is raised to whatever the span needs.
49
+ const bridge = (c1x, c2x, sign) => {
50
+ const D = c2x - c1x;
51
+ const ff = Math.max(f, D / 2 - R + 0.01);
52
+ const hf = Math.sqrt(Math.max(0, (R + ff) ** 2 - (D / 2) ** 2));
53
+ const fc = { x: (c1x + c2x) / 2, y: R + sign * hf };
54
+ const at = (cx) => {
55
+ const dx = fc.x - cx;
56
+ const dy = fc.y - R;
57
+ const len = Math.hypot(dx, dy) || 1;
58
+ return { x: cx + (R * dx) / len, y: R + (R * dy) / len };
59
+ };
60
+ return { p1: at(c1x), p2: at(c2x), f: ff };
61
+ };
62
+
63
+ boxes.forEach((b, i) => {
64
+ const lc = b.left + R;
65
+ const rc = b.right - R;
66
+
67
+ if (i === 0) top.push(`M ${b.left} ${R}`, `A ${R} ${R} 0 0 1 ${lc} 0`);
68
+ top.push(`L ${rc} 0`);
69
+
70
+ if (i < boxes.length - 1) {
71
+ const nlc = boxes[i + 1].left + R;
72
+ const { p1, p2, f: ff } = bridge(rc, nlc, -1); // fillet centre above
73
+ top.push(`A ${R} ${R} 0 0 1 ${p1.x} ${p1.y}`); // around this end, down to tangency
74
+ top.push(`A ${ff} ${ff} 0 0 0 ${p2.x} ${p2.y}`); // the concave bridge, sweeping back
75
+ top.push(`A ${R} ${R} 0 0 1 ${nlc} 0`); // around the next end, back up to the top
76
+ } else {
77
+ top.push(`A ${R} ${R} 0 0 1 ${b.right} ${R}`);
78
+ }
79
+ });
80
+
81
+ for (let i = boxes.length - 1; i >= 0; i--) {
82
+ const b = boxes[i];
83
+ const lc = b.left + R;
84
+ const rc = b.right - R;
85
+
86
+ if (i === boxes.length - 1) bottom.push(`A ${R} ${R} 0 0 1 ${rc} ${H}`);
87
+ bottom.push(`L ${lc} ${H}`);
88
+
89
+ if (i > 0) {
90
+ const prc = boxes[i - 1].right - R;
91
+ const { p1, p2, f: ff } = bridge(prc, lc, +1); // fillet centre below
92
+ bottom.push(`A ${R} ${R} 0 0 1 ${p2.x} ${p2.y}`);
93
+ bottom.push(`A ${ff} ${ff} 0 0 0 ${p1.x} ${p1.y}`);
94
+ bottom.push(`A ${R} ${R} 0 0 1 ${prc} ${H}`);
95
+ } else {
96
+ bottom.push(`A ${R} ${R} 0 0 1 ${b.left} ${R}`);
97
+ }
98
+ }
99
+
100
+ return [...top, ...bottom, "Z"].join(" ");
101
+ }
102
+
103
+ /**
104
+ * Turn measured rectangles into boxes in the bar's own layout space.
105
+ *
106
+ * Neither obvious way of measuring is correct on its own.
107
+ * `getBoundingClientRect` keeps sub-pixel precision but is multiplied by any
108
+ * ancestor `zoom` or `transform: scale()` — which would scale the boxes while
109
+ * the fillet stayed in CSS pixels, and did: at 14× magnification R came out as
110
+ * 308 against a fillet of 34, the tangency solve went imaginary, and the bar
111
+ * disappeared. `offsetWidth` ignores that scaling but rounds to whole pixels,
112
+ * and this is a tangency solve; a box rounded by half a pixel puts the curve
113
+ * visibly off the buttons it is meant to wrap. Their ratio recovers the scale
114
+ * factor, so it can be divided out while the precision is kept.
115
+ *
116
+ * Measuring each cluster against the *controls'* own rect rather than the
117
+ * pill's divides out every scale at once, whatever any of them happen to be
118
+ * mid-flight, because a cluster and the container it is compared to always sit
119
+ * in the same scaled space. This was once load-bearing for a subtler reason
120
+ * too: the controls carried a scale of their own that a layout effect read at
121
+ * its *from* value, so every expansion drew a silhouette a few percent narrower
122
+ * than the buttons it wrapped. That transform is gone, but measuring through
123
+ * the container is still the only form of this that cannot be caught out.
124
+ *
125
+ * The horizontal origin comes in as `offsetLeft` — layout space, which sees no
126
+ * transform at all. Callers mid-morph pass 0 and shift the result themselves,
127
+ * because the pill's own width is changing underneath them.
128
+ *
129
+ * Pure on purpose: it takes plain numbers, so the normalisation above can be
130
+ * tested without a browser. See test/bar-shape.test.js.
131
+ *
132
+ * @param {{ height: number, offsetHeight: number }} pill
133
+ * @param {{ left: number, width: number, offsetWidth: number, offsetLeft: number }} controls
134
+ * @param {{ left: number, right: number }[]} clusters client rects, in order
135
+ * @returns {{ H: number, boxes: { left: number, right: number }[] } | null}
136
+ */
137
+ export function normaliseBoxes(pill, controls, clusters) {
138
+ const k = pill.offsetHeight > 0 ? pill.height / pill.offsetHeight : 1;
139
+ if (!Number.isFinite(k) || k <= 0) return null;
140
+
141
+ const H = pill.height / k;
142
+ if (!(H > 0)) return null;
143
+
144
+ const kc = controls.offsetWidth > 0 ? controls.width / controls.offsetWidth : 1;
145
+ if (!Number.isFinite(kc) || kc <= 0) return null;
146
+
147
+ const x0 = controls.offsetLeft;
148
+ const boxes = clusters.map((r) => ({
149
+ left: x0 + (r.left - controls.left) / kc,
150
+ right: x0 + (r.right - controls.left) / kc,
151
+ }));
152
+ return boxes.length ? { H, boxes } : null;
153
+ }
154
+
155
+ /**
156
+ * Read the live geometry, in the controls' own layout space.
157
+ *
158
+ * Controls space, not pill space: the controls are a fixed width pinned to the
159
+ * bar's held edge, so these boxes are the same numbers whether the bar is open,
160
+ * shut, or somewhere in between. The pill's width is the thing that moves, and
161
+ * the caller adds it back as a shift.
162
+ */
163
+ function measure(pill, controls, clusters) {
164
+ const pr = pill.getBoundingClientRect();
165
+ const cr = controls.getBoundingClientRect();
166
+ return normaliseBoxes(
167
+ { height: pr.height, offsetHeight: pill.offsetHeight },
168
+ {
169
+ left: cr.left,
170
+ width: cr.width,
171
+ offsetWidth: controls.offsetWidth,
172
+ offsetLeft: 0,
173
+ },
174
+ clusters.map((c) => c.getBoundingClientRect()),
175
+ );
176
+ }
177
+
178
+ function draw(svg, path, boxes, H) {
179
+ const w = boxes[boxes.length - 1].right;
180
+ const d = barPath(boxes, H, FILLET);
181
+
182
+ // Last line of defence. A single non-finite number anywhere makes the whole
183
+ // path invalid, and an invalid path is not a glitch — it is a bar that
184
+ // vanishes. A non-positive width is just as fatal: SVG rejects it outright.
185
+ // Either way, keep the last good outline rather than painting nothing.
186
+ if (!(w > 0) || !(H > 0) || /NaN|Infinity|undefined/.test(d)) {
187
+ console.warn(
188
+ `[nora] refusing an invalid bar path — w=${w} H=${H} boxes=${JSON.stringify(boxes)}`,
189
+ );
190
+ return null;
191
+ }
192
+
193
+ svg.setAttribute("width", w);
194
+ svg.setAttribute("height", H);
195
+ svg.setAttribute("viewBox", `0 0 ${w} ${H}`);
196
+ path.setAttribute("d", d);
197
+ return d;
198
+ }
199
+
200
+ /** How long the bar takes to open or shut. */
201
+ const DURATION = 240;
202
+
203
+ /** easeInOutCubic — slow at both ends, so the two rests are the quiet part. */
204
+ const ease = (t) => (t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2);
205
+
206
+ const lerp = (a, b, t) => a + (b - a) * t;
207
+
208
+ const reducedMotion = () =>
209
+ typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
210
+
211
+ /**
212
+ * Keep the silhouette in sync with the controls, and animate it between the
213
+ * two resting states.
214
+ *
215
+ * The shape is a path, and a path is the one thing CSS cannot interpolate, so
216
+ * opening the bar is a real animation driven from here: every frame lerps the
217
+ * cluster boxes between the shut circle and the open layout, rebuilds `d`, and
218
+ * writes the pill's width to match. Nothing else moves. The controls are a
219
+ * fixed-width element pinned to the bar's held edge, so they neither reflow nor
220
+ * translate while this runs — they only fade, and they fade on a squared curve
221
+ * so the text is still nearly invisible while the outline is narrow enough to
222
+ * crop it.
223
+ *
224
+ * That restraint is the lesson from the version before this one, which also
225
+ * interpolated the boxes and dropped frames on every expansion. The geometry
226
+ * was never the cost. Each frame also assigned `clip-path: path(...)` to the
227
+ * controls — a fresh several-hundred-character path for the style system to
228
+ * parse and the compositor to re-rasterise the clipped subtree against — while
229
+ * those same controls ran a `filter: blur()` transition. Three of the most
230
+ * expensive things a browser can be asked to do, scheduled on the same frames.
231
+ * Pulling the other two out leaves one `setAttribute` per frame on a path with
232
+ * about twenty commands, which is nothing.
233
+ *
234
+ * `barPath` handles the shut end with no special case: every cluster collapses
235
+ * to the *same* circle, coincident end circles make each bridge a zero-length
236
+ * arc, and SVG treats that as a no-op, so the path resolves to a plain circle.
237
+ * That is also why the morph can lerp box-for-box — the two states have the
238
+ * same number of boxes throughout.
239
+ */
240
+ export function useBarShape(pillRef, controlsRef, expanded, signature) {
241
+ const svgRef = useRef(null);
242
+ const pathRef = useRef(null);
243
+
244
+ /** 0 shut, 1 open. Kept across renders so a reversal starts where it is. */
245
+ const at = useRef(expanded ? 1 : 0);
246
+ const raf = useRef(0);
247
+ const morphing = useRef(false);
248
+ const mounted = useRef(false);
249
+
250
+ /**
251
+ * Everything one frame needs. Null until the controls are in the DOM.
252
+ *
253
+ * @returns {{ H: number, open: {left:number,right:number}[], shut: {left:number,right:number}[], full: number, dia: number } | null}
254
+ */
255
+ const geometry = () => {
256
+ const pill = pillRef.current;
257
+ const controls = controlsRef.current;
258
+ if (!pill || !controls) return null;
259
+
260
+ const m = measure(pill, controls, [...controls.querySelectorAll(".nora-cluster")]);
261
+ if (!m) return null;
262
+
263
+ const full = controls.offsetWidth;
264
+ const dia = m.H;
265
+ if (!(full > 0) || !(dia > 0)) return null;
266
+
267
+ // Shut, every cluster is the same circle, sitting against the held edge.
268
+ const shut = m.boxes.map(() => ({ left: full - dia, right: full }));
269
+ return { H: dia, open: m.boxes, shut, full, dia };
270
+ };
271
+
272
+ /** Paint progress `t`. Boxes are in controls space; the pill's width shifts them. */
273
+ const paint = (g, t) => {
274
+ const svg = svgRef.current;
275
+ const path = pathRef.current;
276
+ const pill = pillRef.current;
277
+ const controls = controlsRef.current;
278
+ if (!svg || !path || !pill || !controls) return;
279
+
280
+ const e = ease(t);
281
+ const w = lerp(g.dia, g.full, e);
282
+ const shift = w - g.full;
283
+
284
+ draw(
285
+ svg,
286
+ path,
287
+ g.open.map((o, i) => ({
288
+ left: lerp(g.shut[i].left, o.left, e) + shift,
289
+ right: lerp(g.shut[i].right, o.right, e) + shift,
290
+ })),
291
+ g.H,
292
+ );
293
+
294
+ pill.style.width = `${w}px`;
295
+ controls.style.opacity = String(e * e);
296
+ // Everything left of the outline is clipped away rather than allowed to
297
+ // fade outside it. The controls are pinned to the held edge and the shape
298
+ // grows from that edge, so the subject label — the far end — would
299
+ // otherwise hang in open canvas for most of the opening. `inset()` is a
300
+ // rectangle the compositor can apply directly; the `path()` clip this file
301
+ // used to write per frame is the one that cost the frames.
302
+ controls.style.clipPath = `inset(0 0 0 ${Math.max(0, g.full - w + 10)}px)`;
303
+ at.current = t;
304
+ };
305
+
306
+ /** Drop every inline value and redraw from the DOM, so rest is measured. */
307
+ const settle = (g) => {
308
+ const pill = pillRef.current;
309
+ const controls = controlsRef.current;
310
+ morphing.current = false;
311
+ pill?.style.removeProperty("width");
312
+ pill?.removeAttribute("data-morphing");
313
+ controls?.style.removeProperty("opacity");
314
+ controls?.style.removeProperty("clip-path");
315
+
316
+ const fresh = geometry() ?? g;
317
+ if (!fresh) return;
318
+ const boxes = expanded
319
+ ? fresh.open
320
+ : fresh.shut.map((b) => ({
321
+ left: b.left - (fresh.full - fresh.dia),
322
+ right: b.right - (fresh.full - fresh.dia),
323
+ }));
324
+ const svg = svgRef.current;
325
+ const path = pathRef.current;
326
+ if (svg && path) draw(svg, path, boxes, fresh.H);
327
+ at.current = expanded ? 1 : 0;
328
+ };
329
+
330
+ // Repaint on anything that can move a cluster edge: the open/shut state
331
+ // itself, the selected name, the viewport label, an active panel's accent.
332
+ useLayoutEffect(() => {
333
+ cancelAnimationFrame(raf.current);
334
+
335
+ const g = geometry();
336
+ if (!g) return;
337
+
338
+ const goal = expanded ? 1 : 0;
339
+ const jump = !mounted.current || at.current === goal || reducedMotion();
340
+ mounted.current = true;
341
+
342
+ if (jump) {
343
+ settle(g);
344
+ return;
345
+ }
346
+
347
+ morphing.current = true;
348
+ pillRef.current?.setAttribute("data-morphing", "true");
349
+
350
+ const from = at.current;
351
+ const span = Math.abs(goal - from);
352
+
353
+ // Paint the starting frame here, synchronously, before yielding. React has
354
+ // already flipped `data-state`, so the pill is sitting at the *other* end's
355
+ // CSS width with the old path still on it; leaving that to the first rAF
356
+ // shows one frame of a shape that fits nothing.
357
+ paint(g, from);
358
+
359
+ const start = performance.now();
360
+
361
+ const step = (now) => {
362
+ const p = span > 0 ? Math.min(1, (now - start) / (DURATION * span)) : 1;
363
+ paint(g, from + (goal - from) * p);
364
+ if (p < 1) raf.current = requestAnimationFrame(step);
365
+ else settle(g);
366
+ };
367
+ raf.current = requestAnimationFrame(step);
368
+
369
+ return () => cancelAnimationFrame(raf.current);
370
+ // eslint-disable-next-line react-hooks/exhaustive-deps
371
+ }, [expanded, signature]);
372
+
373
+ // The observer below is installed once but must call the *current* closure,
374
+ // which knows whether the bar is open. Assigned after render rather than
375
+ // during it: a render that writes a ref is not pure, and React is entitled to
376
+ // discard it. The observer only ever fires asynchronously, so it always sees
377
+ // what this effect wrote, and the initial value covers the first paint.
378
+ const latest = useRef(settle);
379
+ useEffect(() => {
380
+ latest.current = settle;
381
+ });
382
+
383
+ // …and when the browser moves an edge without telling React: a window
384
+ // resize, a zoom change, or the fonts arriving after first paint. Never
385
+ // mid-morph — the pill's own width is changing on every one of those frames,
386
+ // and answering it here would fight the animation for the same attribute.
387
+ useEffect(() => {
388
+ const pill = pillRef.current;
389
+ if (!pill) return;
390
+ const repaint = () => {
391
+ if (!morphing.current) latest.current(null);
392
+ };
393
+ const observer = new ResizeObserver(repaint);
394
+ observer.observe(pill);
395
+ if (controlsRef.current) observer.observe(controlsRef.current);
396
+ document.fonts?.ready.then(repaint);
397
+ return () => observer.disconnect();
398
+ // eslint-disable-next-line react-hooks/exhaustive-deps
399
+ }, []);
400
+
401
+ return { svgRef, pathRef };
402
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Icons — Lucide 1.34, inlined.
3
+ *
4
+ * The paths are copied rather than imported from `lucide-react`, which keeps the
5
+ * package free of runtime dependencies: the client is compiled by the consumer's
6
+ * own Vite, so anything we depend on becomes something their project has to
7
+ * resolve. Ten glyphs are not worth that.
8
+ *
9
+ * Lucide — ISC License. Copyright (c) 2026 Lucide Icons and Contributors.
10
+ * https://github.com/lucide-icons/lucide/blob/main/LICENSE
11
+ *
12
+ * Stroke 1.75 at 17px rather than a lighter 1.5: light strokes optically thin
13
+ * out on a dark ground, because white on black spreads less than black on
14
+ * white. 1.5 reads as fuzzy here, not as refined.
15
+ */
16
+
17
+ const Icon = ({ size = 17, strokeWidth = 1.75, children, ...rest }) => (
18
+ <svg
19
+ width={size}
20
+ height={size}
21
+ viewBox="0 0 24 24"
22
+ fill="none"
23
+ stroke="currentColor"
24
+ strokeWidth={strokeWidth}
25
+ strokeLinecap="round"
26
+ strokeLinejoin="round"
27
+ aria-hidden="true"
28
+ style={{ display: "block" }}
29
+ {...rest}
30
+ >
31
+ {children}
32
+ </svg>
33
+ );
34
+
35
+ /** Lucide `grip` */
36
+ export const GridIcon = (p) => (
37
+ <Icon {...p}>
38
+ <circle cx="12" cy="5" r="1" />
39
+ <circle cx="19" cy="5" r="1" />
40
+ <circle cx="5" cy="5" r="1" />
41
+ <circle cx="12" cy="12" r="1" />
42
+ <circle cx="19" cy="12" r="1" />
43
+ <circle cx="5" cy="12" r="1" />
44
+ <circle cx="12" cy="19" r="1" />
45
+ <circle cx="19" cy="19" r="1" />
46
+ <circle cx="5" cy="19" r="1" />
47
+ </Icon>
48
+ );
49
+
50
+ /** Lucide `folder` */
51
+ export const FolderIcon = (p) => (
52
+ <Icon {...p}>
53
+ <path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
54
+ </Icon>
55
+ );
56
+
57
+ /** Lucide `search` */
58
+ export const SearchIcon = (p) => (
59
+ <Icon {...p}>
60
+ <path d="m21 21-4.34-4.34" />
61
+ <circle cx="11" cy="11" r="8" />
62
+ </Icon>
63
+ );
64
+
65
+ /** Lucide `scan-line` */
66
+ export const SweepIcon = (p) => (
67
+ <Icon {...p}>
68
+ <path d="M3 7V5a2 2 0 0 1 2-2h2" />
69
+ <path d="M17 3h2a2 2 0 0 1 2 2v2" />
70
+ <path d="M21 17v2a2 2 0 0 1-2 2h-2" />
71
+ <path d="M7 21H5a2 2 0 0 1-2-2v-2" />
72
+ <path d="M7 12h10" />
73
+ </Icon>
74
+ );
75
+
76
+ /** Lucide `sun` */
77
+ export const SunIcon = (p) => (
78
+ <Icon {...p}>
79
+ <circle cx="12" cy="12" r="4" />
80
+ <path d="M12 2v2" />
81
+ <path d="M12 20v2" />
82
+ <path d="m4.93 4.93 1.41 1.41" />
83
+ <path d="m17.66 17.66 1.41 1.41" />
84
+ <path d="M2 12h2" />
85
+ <path d="M20 12h2" />
86
+ <path d="m6.34 17.66-1.41 1.41" />
87
+ <path d="m19.07 4.93-1.41 1.41" />
88
+ </Icon>
89
+ );
90
+
91
+ /** Lucide `moon` */
92
+ export const MoonIcon = (p) => (
93
+ <Icon {...p}>
94
+ <path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401" />
95
+ </Icon>
96
+ );
97
+
98
+ /** Lucide `minimize-2` */
99
+ export const CollapseIcon = (p) => (
100
+ <Icon {...p}>
101
+ <path d="m14 10 7-7" />
102
+ <path d="M20 10h-6V4" />
103
+ <path d="m3 21 7-7" />
104
+ <path d="M4 14h6v6" />
105
+ </Icon>
106
+ );
107
+
108
+ /** Lucide `chevron-up` */
109
+ export const ChevronUpIcon = (p) => (
110
+ <Icon {...p}>
111
+ <path d="m18 15-6-6-6 6" />
112
+ </Icon>
113
+ );
114
+
115
+ /** Lucide `circle-alert` */
116
+ export const AlertIcon = (p) => (
117
+ <Icon {...p}>
118
+ <circle cx="12" cy="12" r="10" />
119
+ <line x1="12" x2="12" y1="8" y2="12" />
120
+ <line x1="12" x2="12.01" y1="16" y2="16" />
121
+ </Icon>
122
+ );
123
+
124
+ /** Lucide `check` */
125
+ export const CheckIcon = (p) => (
126
+ <Icon {...p}>
127
+ <path d="M20 6 9 17l-5-5" />
128
+ </Icon>
129
+ );