@enigmax/primitives 0.24.0 → 0.26.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 (59) hide show
  1. package/dist/chunk-4SZ5N7BA.js +121 -0
  2. package/dist/chunk-EYZ366LP.js +189 -0
  3. package/dist/chunk-ISXB5RUH.js +380 -0
  4. package/dist/chunk-JJVZYW5C.js +946 -0
  5. package/dist/chunk-KVLSOQTI.js +66 -0
  6. package/dist/chunk-QIIV2QSV.js +108 -0
  7. package/dist/{chunk-QJJ34E4G.js → chunk-RMNXVWQ4.js} +32 -15
  8. package/dist/chunk-RUEQ3TGP.js +155 -0
  9. package/dist/clipboard-menu-BvXJCSFN.d.ts +93 -0
  10. package/dist/{color-OPV3BJV6.js → color-T63FLJNH.js} +183 -120
  11. package/dist/context-BfjYGPnH.d.ts +54 -0
  12. package/dist/{clipboard-menu-B_ouitfS.d.ts → context-menu-D3FtTn7v.d.ts} +1 -91
  13. package/dist/{index-ZPvlf9vs.d.ts → index-qXOkYQCU.d.ts} +4 -0
  14. package/dist/index.d.ts +266 -2
  15. package/dist/index.js +4 -2
  16. package/dist/menu-MSQSE6U5.js +54 -0
  17. package/dist/menu-PNQRTRNV.js +33 -0
  18. package/dist/next/index.d.ts +7 -3
  19. package/dist/next/index.js +11 -7
  20. package/dist/react/context-menu.d.ts +8 -54
  21. package/dist/react/image.d.ts +109 -0
  22. package/dist/react/image.js +3 -0
  23. package/dist/react/index.d.ts +8 -4
  24. package/dist/react/index.js +11 -7
  25. package/dist/react/input.d.ts +1 -1
  26. package/dist/react/input.js +2 -2
  27. package/dist/react/video.d.ts +120 -0
  28. package/dist/react/video.js +3 -0
  29. package/dist/react-router/index.d.ts +7 -3
  30. package/dist/react-router/index.js +11 -7
  31. package/dist/viewer-73LSE6RR.js +3 -0
  32. package/package.json +15 -2
  33. package/recipes/color/styles.css +20 -0
  34. package/recipes/image/styles.css +182 -0
  35. package/recipes/video/styles.css +154 -0
  36. package/registry.json +477 -3
  37. package/src/core/image-viewer.ts +222 -0
  38. package/src/core/player.ts +303 -0
  39. package/src/index.ts +52 -0
  40. package/src/react/image/icons.tsx +55 -0
  41. package/src/react/image/index.tsx +185 -0
  42. package/src/react/image/menu.tsx +76 -0
  43. package/src/react/image/styles.ts +213 -0
  44. package/src/react/image/types.ts +113 -0
  45. package/src/react/image/viewer.tsx +571 -0
  46. package/src/react/index.ts +3 -0
  47. package/src/react/input/color-styles.ts +20 -0
  48. package/src/react/input/color-swatch.tsx +96 -0
  49. package/src/react/input/color.tsx +84 -23
  50. package/src/react/input/index.tsx +27 -4
  51. package/src/react/input/types.ts +4 -0
  52. package/src/react/video/icons.tsx +135 -0
  53. package/src/react/video/index.tsx +724 -0
  54. package/src/react/video/menu.tsx +66 -0
  55. package/src/react/video/rail.tsx +99 -0
  56. package/src/react/video/styles.ts +161 -0
  57. package/src/react/video/types.ts +135 -0
  58. package/dist/chunk-S7EE57YB.js +0 -9
  59. /package/dist/{chunk-MSOCCQGH.js → chunk-T7I6WJSN.js} +0 -0
@@ -0,0 +1,96 @@
1
+ "use client";
2
+
3
+ import type { CSSProperties, ReactNode, Ref } from "react";
4
+
5
+ /**
6
+ * The swatch in the field, and the box the panel hangs from.
7
+ *
8
+ * Its own module, and deliberately NOT part of the picker's chunk. The picker is imported on
9
+ * demand, so a swatch that lived in there would not exist for as long as that request takes -
10
+ * the field renders as a bare text box, the affordance appears late and shifts the row, and a
11
+ * press in that window reaches nothing at all. On a fast connection that is a blink; on a
12
+ * cold cache it is the second or two somebody spends clicking a swatch that is not there yet.
13
+ *
14
+ * So the swatch ships with the field and the panel arrives behind it. It costs the markup
15
+ * below in the base chunk and no colour maths at all: the fill is handed to the browser, which
16
+ * is the only parser that already knows every notation a value could be written in.
17
+ */
18
+
19
+ /**
20
+ * Enough of a look to be a swatch, for the frames before the panel's stylesheet exists.
21
+ *
22
+ * The sheet ships with the picker, so the swatch rendered ahead of it would otherwise be a
23
+ * default browser button for as long as that request takes - which trades a missing control
24
+ * for an ugly one. Written against the same custom properties, so a project that themed the
25
+ * picker themes this too, and dropped entirely the moment the real swatch replaces it.
26
+ */
27
+ const BASELINE: CSSProperties = {
28
+ display: "block",
29
+ boxSizing: "border-box",
30
+ width: "1.25rem",
31
+ height: "1.25rem",
32
+ padding: 0,
33
+ background: "none",
34
+ border: "1px solid var(--enigma-color-border, #404040)",
35
+ borderRadius: "0.3125rem",
36
+ overflow: "hidden",
37
+ cursor: "pointer"
38
+ };
39
+
40
+ export interface ColorSwatchProps {
41
+ /** The value to paint, exactly as the field holds it. Anything unparseable paints nothing. */
42
+ value?: string;
43
+ /** Whether the panel is up, for `aria-expanded`. */
44
+ open?: boolean;
45
+ /** The value is not a colour: the swatch shows the chequerboard rather than a stale fill. */
46
+ invalid?: boolean;
47
+ /** The field is disabled or read-only. */
48
+ locked?: boolean;
49
+ label: string;
50
+ anchorRef?: Ref<HTMLSpanElement>;
51
+ buttonRef?: Ref<HTMLButtonElement>;
52
+ onPress?: () => void;
53
+ /** The picker's stylesheet has not arrived yet, so carry the few declarations it needs. */
54
+ baseline?: boolean;
55
+ /** The panel, once its code is here. */
56
+ children?: ReactNode;
57
+ }
58
+
59
+ export function ColorSwatch({ value, open = false, invalid = false, locked = false, label, anchorRef, buttonRef, onPress, baseline = false, children }: ColorSwatchProps): ReactNode {
60
+ return (
61
+ <span
62
+ ref={anchorRef}
63
+ data-enigma-color=""
64
+ data-open={open ? "" : undefined}
65
+ style={baseline ? { position: "relative", display: "inline-flex", flex: "none" } : undefined}
66
+ >
67
+ <button
68
+ ref={buttonRef}
69
+ // Never a bare `<button>`: inside a form it would default to submit, so opening
70
+ // the picker would post the half-filled form. The same trap the reveal has.
71
+ type="button"
72
+ data-enigma-color-swatch=""
73
+ data-enigma-color-checkers=""
74
+ data-invalid={invalid ? "" : undefined}
75
+ aria-haspopup="dialog"
76
+ aria-expanded={open}
77
+ aria-label={label}
78
+ title={label}
79
+ disabled={locked}
80
+ style={baseline ? BASELINE : undefined}
81
+ onClick={onPress}
82
+ >
83
+ {/* The browser parses the value for us: an unparseable string simply applies
84
+ nothing, which is why the swatch needs no colour maths to fall back on. */}
85
+ <span
86
+ data-enigma-color-fill=""
87
+ style={{
88
+ background: invalid ? undefined : value,
89
+ ...(baseline ? { display: "block", width: "100%", height: "100%" } : null)
90
+ }}
91
+ />
92
+ </button>
93
+ {children}
94
+ </span>
95
+ );
96
+ }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { writeValue } from "@/react/input/write-value";
4
+ import { ColorSwatch } from "@/react/input/color-swatch";
4
5
  import { COLOR_STYLES } from "@/react/input/color-styles";
5
6
  import type { ColorLabels, ColorPanelPlacement } from "@/react/input/types";
6
7
  import { parseColor, formatColor, rgbToHsv, hsvToRgb, toHex, colorEquals, type Hsv, type ColorFormat } from "@/core/color";
@@ -47,6 +48,10 @@ const STEP = { fine: 0.01, coarse: 0.1, hue: 1, hueCoarse: 10 };
47
48
  /** Kept clear of the window edge when deciding which side to open on. */
48
49
  const MARGIN = 12;
49
50
 
51
+ /** The notations the readout cycles through, in the order the button steps over them. */
52
+ const FORMATS: ColorFormat[] = ["hex", "rgb", "hsl"];
53
+ const FORMAT_LABELS: Record<ColorFormat, string> = { hex: "HEX", rgb: "RGB", hsl: "HSL" };
54
+
50
55
  let injected = false;
51
56
 
52
57
  function injectStyles(): void {
@@ -76,6 +81,11 @@ export interface ColorExtrasProps {
76
81
  styles?: boolean;
77
82
  /** The field is disabled or read-only: the swatch is inert and the panel never opens. */
78
83
  locked?: boolean;
84
+ /**
85
+ * The swatch was already pressed, while this module was still being fetched. Opens the
86
+ * panel on the first render rather than asking for the press a second time.
87
+ */
88
+ openOnMount?: boolean;
79
89
  labels?: ColorLabels;
80
90
  }
81
91
 
@@ -89,6 +99,7 @@ export function ColorExtras({
89
99
  placement = "auto",
90
100
  styles = true,
91
101
  locked = false,
102
+ openOnMount = false,
92
103
  labels
93
104
  }: ColorExtrasProps): ReactNode {
94
105
  // Before paint: a sheet applied after the first frame shows the panel unstyled first.
@@ -99,7 +110,7 @@ export function ColorExtras({
99
110
  const swatchRef = useRef<HTMLButtonElement | null>(null);
100
111
  const areaRef = useRef<HTMLDivElement | null>(null);
101
112
 
102
- const [open, setOpen] = useState(false);
113
+ const [open, setOpen] = useState(openOnMount && !locked);
103
114
  const [side, setSide] = useState<"top" | "bottom">(placement === "top" ? "top" : "bottom");
104
115
 
105
116
  const parsed = useMemo(() => parseColor(value), [value]);
@@ -296,6 +307,33 @@ export function ColorExtras({
296
307
  }
297
308
  }, [commit, hsv]);
298
309
 
310
+ /* -------- the readout, which is the value in whichever notation you want to read it -------- */
311
+
312
+ /**
313
+ * Which notation the panel PRINTS the colour in, and only that.
314
+ *
315
+ * `format` is the caller's contract - the spelling their column, their API or their CSS
316
+ * expects - so cycling this button never changes what is written into the field. It
317
+ * changes what you can read out of the panel and paste elsewhere, which is what the
318
+ * browser's own picker offers and what a picker showing no code at all cannot.
319
+ */
320
+ const [shown, setShown] = useState<ColorFormat>(format);
321
+ useEffect(() => setShown(format), [format]);
322
+
323
+ /** What is being typed, while it is being typed. Null means "show the current colour". */
324
+ const [draft, setDraft] = useState<string | null>(null);
325
+
326
+ const readout = useMemo(() => formatColor(rgb, shown, { alpha }), [rgb, shown, alpha]);
327
+ const nextFormat = FORMATS[(FORMATS.indexOf(shown) + 1) % FORMATS.length] as ColorFormat;
328
+
329
+ const typed = useCallback((next: string) => {
330
+ setDraft(next);
331
+ const colour = parseColor(next);
332
+ // Applied as it becomes a colour, so the square and the rails follow a pasted value
333
+ // rather than waiting for a blur. Half-typed text simply is not one yet.
334
+ if (colour) commit(rgbToHsv(alpha ? colour : { ...colour, a: 1 }, hsv.h));
335
+ }, [commit, alpha, hsv.h]);
336
+
299
337
  /* -------- rendering -------- */
300
338
 
301
339
  const css = useMemo(() => formatColor(rgb, "hex", { alpha: true }), [rgb]);
@@ -306,27 +344,16 @@ export function ColorExtras({
306
344
  const panelStyle = { "--enigma-color-hue": Math.round(hsv.h), "--enigma-color-opaque": opaque } as CSSProperties;
307
345
 
308
346
  return (
309
- <span ref={anchorRef} data-enigma-color="" data-open={open ? "" : undefined}>
310
- <button
311
- ref={swatchRef}
312
- // Never a bare `<button>`: inside a form it would default to submit, so opening
313
- // the picker would post the half-filled form. The same trap the reveal has.
314
- type="button"
315
- data-enigma-color-swatch=""
316
- data-enigma-color-checkers=""
317
- data-invalid={parsed ? undefined : ""}
318
- aria-haspopup="dialog"
319
- aria-expanded={open}
320
- aria-label={text.open ?? "Pick a colour"}
321
- title={text.open ?? "Pick a colour"}
322
- disabled={locked}
323
- onClick={() => (open ? close(true) : setOpenState(true))}
324
- >
325
- {/* The browser parses the value for us: an unparseable string simply applies
326
- nothing, which is why the swatch needs no colour maths to fall back on. */}
327
- <span data-enigma-color-fill="" style={{ background: parsed ? css : undefined }} />
328
- </button>
329
-
347
+ <ColorSwatch
348
+ anchorRef={anchorRef}
349
+ buttonRef={swatchRef}
350
+ value={css}
351
+ invalid={!parsed}
352
+ open={open}
353
+ locked={locked}
354
+ label={text.open ?? "Pick a colour"}
355
+ onPress={() => (open ? close(true) : setOpenState(true))}
356
+ >
330
357
  {open && !locked && (
331
358
  <div
332
359
  ref={panelRef}
@@ -421,6 +448,40 @@ export function ColorExtras({
421
448
  </div>
422
449
  </div>
423
450
 
451
+ {/* The colour as text, editable, with the notation next to it. A picker
452
+ that shows no code is one you cannot read a value out of, cannot paste
453
+ one into, and cannot check against the hex somebody sent you. */}
454
+ <div data-enigma-color-value="">
455
+ <button
456
+ type="button"
457
+ data-enigma-color-format=""
458
+ aria-label={`${text.formatAs ?? "Show as"} ${FORMAT_LABELS[nextFormat]}`}
459
+ title={`${text.formatAs ?? "Show as"} ${FORMAT_LABELS[nextFormat]}`}
460
+ onClick={() => { setShown(nextFormat); setDraft(null); }}
461
+ >
462
+ {FORMAT_LABELS[shown]}
463
+ </button>
464
+ <input
465
+ type="text"
466
+ data-enigma-color-input=""
467
+ value={draft ?? readout}
468
+ aria-label={text.value ?? "Colour value"}
469
+ spellCheck={false}
470
+ autoComplete="off"
471
+ autoCorrect="off"
472
+ autoCapitalize="off"
473
+ onChange={(event) => typed(event.target.value)}
474
+ // What was typed goes back to the canonical spelling the moment the
475
+ // field is left, the way the input itself normalizes on blur.
476
+ onBlur={() => setDraft(null)}
477
+ onKeyDown={(event) => {
478
+ if (event.key !== "Enter") return;
479
+ event.preventDefault();
480
+ setDraft(null);
481
+ }}
482
+ />
483
+ </div>
484
+
424
485
  {swatches && swatches.length > 0 && (
425
486
  <div data-enigma-color-swatches="" role="group" aria-label={text.swatches ?? "Preset colours"}>
426
487
  {swatches.map((preset) => {
@@ -445,7 +506,7 @@ export function ColorExtras({
445
506
  )}
446
507
  </div>
447
508
  )}
448
- </span>
509
+ </ColorSwatch>
449
510
  );
450
511
  }
451
512
 
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { Icon } from "@/react/input/icon";
4
+ import { ColorSwatch } from "@/react/input/color-swatch";
4
5
  import { INPUT_ICON_PATHS } from "@/core/input-icons";
5
6
  import { writeValue } from "@/react/input/write-value";
6
7
  import type { AnyInputProps, InputProps, FieldAction, BreachState } from "@/react/input/types";
@@ -96,6 +97,14 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(pro
96
97
  const innerRef = useRef<HTMLInputElement | null>(null);
97
98
  const [element, setElement] = useState<HTMLInputElement | null>(null);
98
99
  const [revealed, setRevealed] = useState(false);
100
+ /**
101
+ * The swatch was pressed before the picker's code arrived.
102
+ *
103
+ * Remembered rather than dropped: the press was a request to open the panel, and a
104
+ * control that ignores one because a chunk was in flight is a control that feels broken
105
+ * for exactly as long as the network is slow.
106
+ */
107
+ const [colorWanted, setColorWanted] = useState(false);
99
108
  const describedBy = useId();
100
109
 
101
110
  const isPassword = type === "password";
@@ -263,12 +272,26 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(pro
263
272
  data-score={score ?? undefined}
264
273
  >
265
274
  <div {...fieldProps} data-enigma-input-field="">
266
- {/* No fallback, for the reason the meter has none: the field is already on
267
- screen and typeable, and a placeholder where a swatch is about to appear
268
- would be a second layout shift rather than one. */}
275
+ {/* The swatch is NOT part of the picker's chunk, unlike the meter, which has
276
+ nothing to show until it has been asked a question. A colour field with no
277
+ swatch until a request lands is a field whose only affordance is missing,
278
+ and a press in that window reaches nothing: so the button is here from the
279
+ first frame, and a press before the panel's code arrives opens it as soon
280
+ as it does. */}
269
281
  {isColor && (
270
- <Suspense fallback={null}>
282
+ <Suspense
283
+ fallback={
284
+ <ColorSwatch
285
+ value={value}
286
+ locked={locked}
287
+ baseline={styles !== false}
288
+ label={colorLabels?.open ?? "Pick a colour"}
289
+ onPress={() => setColorWanted(true)}
290
+ />
291
+ }
292
+ >
271
293
  <ColorExtras
294
+ openOnMount={colorWanted}
272
295
  input={element}
273
296
  value={value}
274
297
  format={format}
@@ -151,6 +151,10 @@ export interface ColorLabels {
151
151
  alpha?: string;
152
152
  eyedropper?: string;
153
153
  swatches?: string;
154
+ /** The editable readout under the rails. Default "Colour value". */
155
+ value?: string;
156
+ /** The button that cycles the readout between HEX, RGB and HSL. Default "Show as". */
157
+ formatAs?: string;
154
158
  /** The word before the colour in what a screen reader announces. Default "Colour". */
155
159
  color?: string;
156
160
  }
@@ -0,0 +1,135 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /**
4
+ * The player's icons, drawn rather than loaded - a sprite would be a request per player.
5
+ *
6
+ * All of them are on ONE 24x24 grid and inside it. That is not a formality: the gear used to
7
+ * carry a path with absolute `V21` and `H4` commands in it, which drew a shape half again the
8
+ * size of the box and hanging off the bottom of its button while every other control sat
9
+ * centred. An icon that leaves the viewBox cannot be centred by the button it is in.
10
+ *
11
+ * The size comes from the stylesheet (`--enigma-video-icon-size`), not from the label's font:
12
+ * `1.25em` of a 13px bar is a 16px icon, and the same markup in a project with larger text
13
+ * silently grew a different player.
14
+ */
15
+
16
+ function Glyph({ children, filled = false }: { children: ReactNode; filled?: boolean; }): ReactNode {
17
+ return (
18
+ <svg
19
+ viewBox="0 0 24 24"
20
+ width="1em"
21
+ height="1em"
22
+ fill={filled ? "currentColor" : "none"}
23
+ stroke={filled ? "none" : "currentColor"}
24
+ strokeWidth={2}
25
+ strokeLinecap="round"
26
+ strokeLinejoin="round"
27
+ aria-hidden="true"
28
+ >
29
+ {children}
30
+ </svg>
31
+ );
32
+ }
33
+
34
+ export function Play(): ReactNode {
35
+ // Centroid at x 11.7 rather than the box's 12: a triangle centred by its bounding box
36
+ // reads as sitting too far left, which is why every player nudges it right.
37
+ return <Glyph filled><path d="M8 5v14l11-7z" /></Glyph>;
38
+ }
39
+
40
+ export function Pause(): ReactNode {
41
+ return <Glyph filled><path d="M7 5h3.5v14H7zM13.5 5H17v14h-3.5z" /></Glyph>;
42
+ }
43
+
44
+ export function Volume(): ReactNode {
45
+ return <Glyph><path d="M11 5 6 9H2v6h4l5 4z" /><path d="M15.5 8.5a5 5 0 0 1 0 7" /><path d="M19 5a10 10 0 0 1 0 14" /></Glyph>;
46
+ }
47
+
48
+ export function VolumeLow(): ReactNode {
49
+ return <Glyph><path d="M11 5 6 9H2v6h4l5 4z" /><path d="M15.5 8.5a5 5 0 0 1 0 7" /></Glyph>;
50
+ }
51
+
52
+ export function Muted(): ReactNode {
53
+ return <Glyph><path d="M11 5 6 9H2v6h4l5 4z" /><path d="m16 9 6 6M22 9l-6 6" /></Glyph>;
54
+ }
55
+
56
+ export function Captions(): ReactNode {
57
+ return <Glyph><rect x="2" y="5" width="20" height="14" rx="2.5" /><path d="M7 11h3M7 15h5M14 11h3M14 15h3" /></Glyph>;
58
+ }
59
+
60
+ /** The same box with the lines struck through, for captions that are off. */
61
+ export function CaptionsOff(): ReactNode {
62
+ return (
63
+ <Glyph>
64
+ <path d="M10.5 5H19.5a2.5 2.5 0 0 1 2.5 2.5v9M18 19H4.5A2.5 2.5 0 0 1 2 16.5v-9A2.5 2.5 0 0 1 4.5 5" />
65
+ <path d="M7 11h3M7 15h5" />
66
+ <path d="m3 3 18 18" />
67
+ </Glyph>
68
+ );
69
+ }
70
+
71
+ export function Settings(): ReactNode {
72
+ return (
73
+ <Glyph>
74
+ <path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2Z" />
75
+ <circle cx="12" cy="12" r="3" />
76
+ </Glyph>
77
+ );
78
+ }
79
+
80
+ export function Pip(): ReactNode {
81
+ return <Glyph><path d="M21 11V6.5A2.5 2.5 0 0 0 18.5 4h-13A2.5 2.5 0 0 0 3 6.5v9A2.5 2.5 0 0 0 5.5 18H9" /><rect x="12" y="12" width="9" height="7" rx="1.5" /></Glyph>;
82
+ }
83
+
84
+ /** The screen with the signal arcs - the glyph every platform casts with. */
85
+ export function Cast(): ReactNode {
86
+ return (
87
+ <Glyph>
88
+ <path d="M3 8V6.5A2.5 2.5 0 0 1 5.5 4h13A2.5 2.5 0 0 1 21 6.5v11a2.5 2.5 0 0 1-2.5 2.5H13" />
89
+ <path d="M3 12a8 8 0 0 1 8 8" />
90
+ <path d="M3 16a4 4 0 0 1 4 4" />
91
+ <path d="M3 20h.01" />
92
+ </Glyph>
93
+ );
94
+ }
95
+
96
+ export function Expand(): ReactNode {
97
+ return <Glyph><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" /></Glyph>;
98
+ }
99
+
100
+ export function Collapse(): ReactNode {
101
+ return <Glyph><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5" /></Glyph>;
102
+ }
103
+
104
+ export function Download(): ReactNode {
105
+ return <Glyph><path d="M12 4v11M7 11l5 5 5-5M4 19h16" /></Glyph>;
106
+ }
107
+
108
+ /** The tick beside a chosen row in the settings panel. */
109
+ export function Check(): ReactNode {
110
+ return <Glyph><path d="m5 12.5 4.5 4.5L19 7" /></Glyph>;
111
+ }
112
+
113
+ export function ChevronRight(): ReactNode {
114
+ return <Glyph><path d="m9 5 7 7-7 7" /></Glyph>;
115
+ }
116
+
117
+ export function ChevronLeft(): ReactNode {
118
+ return <Glyph><path d="m15 5-7 7 7 7" /></Glyph>;
119
+ }
120
+
121
+ export function Loop(): ReactNode {
122
+ return <Glyph><path d="M17 2.5 20.5 6 17 9.5" /><path d="M3.5 12V9a3 3 0 0 1 3-3h14" /><path d="M7 21.5 3.5 18 7 14.5" /><path d="M20.5 12v3a3 3 0 0 1-3 3h-14" /></Glyph>;
123
+ }
124
+
125
+ export function Link(): ReactNode {
126
+ return <Glyph><path d="M10 13a5 5 0 0 0 7.07 0l2.83-2.83a5 5 0 0 0-7.07-7.07L11.5 4.4" /><path d="M14 11a5 5 0 0 0-7.07 0L4.1 13.83a5 5 0 0 0 7.07 7.07l1.3-1.3" /></Glyph>;
127
+ }
128
+
129
+ export function Clock(): ReactNode {
130
+ return <Glyph><circle cx="12" cy="12" r="9" /><path d="M12 7v5l3.5 2" /></Glyph>;
131
+ }
132
+
133
+ export function Speed(): ReactNode {
134
+ return <Glyph><path d="M12 20a8 8 0 1 0-8-8" /><path d="M4 20h16" /><path d="m12 12 4.5-4.5" /></Glyph>;
135
+ }