@pixerate/editor-react 0.76.0 → 0.77.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.
@@ -24,6 +24,7 @@ __export(paint_editor_exports, {
24
24
  ColorPanel: () => ColorPanel,
25
25
  PaintCanvas: () => PaintCanvas,
26
26
  PaintEditor: () => PaintEditor,
27
+ PaintGradientEditor: () => PaintGradientEditor,
27
28
  PaintLayersPanel: () => PaintLayersPanel,
28
29
  PaintPresenceLayer: () => PaintPresenceLayer,
29
30
  PaintSlider: () => PaintSlider,
@@ -571,29 +572,159 @@ var PaintToolbar = ({ controller, tool, onToolChange, fileName = "painting", cla
571
572
  };
572
573
 
573
574
  // src/paint-editor/BrushPanel.tsx
574
- var import_react5 = require("react");
575
+ var import_react6 = require("react");
576
+ var import_paint_editor5 = require("@pixerate/editor/paint-editor");
577
+ var import_paint_editor6 = require("@pixerate/editor/paint-editor");
578
+
579
+ // src/paint-editor/PaintGradientEditor.tsx
575
580
  var import_paint_editor4 = require("@pixerate/editor/paint-editor");
581
+
582
+ // src/paint-editor/PaintGradientEditorView.tsx
583
+ var import_react5 = require("react");
576
584
  var import_jsx_runtime4 = require("react/jsx-runtime");
585
+ var button2 = "px-2 py-1 rounded text-[11px] bg-slate-800 text-slate-200 hover:bg-slate-700 disabled:opacity-40";
586
+ var select = "rounded bg-slate-800 px-1 py-0.5 text-[11px]";
587
+ var number = "w-16 rounded bg-slate-800 px-1 py-0.5 text-[11px]";
588
+ var pct = (v) => Math.round(v * 1e3) / 10;
589
+ var PaintGradientEditorView = ({ value, onChange, colors, className, kit }) => {
590
+ const {
591
+ PAINT_GRADIENT_MAPPINGS,
592
+ PAINT_GRADIENT_MAPPING_LABELS,
593
+ PAINT_GRADIENT_PRESETS,
594
+ PAINT_GRADIENT_REPEATS,
595
+ PAINT_GRADIENT_REPEAT_LABELS,
596
+ PAINT_GRADIENT_SPACES,
597
+ PAINT_GRADIENT_SPACE_LABELS,
598
+ addPaintGradientStop,
599
+ paintGradientMappingFor,
600
+ paintSourceCss: paintSourceCss2,
601
+ removePaintGradientStop,
602
+ reversePaintGradient,
603
+ updatePaintGradientStop
604
+ } = kit;
605
+ const [selected, setSelected] = (0, import_react5.useState)(0);
606
+ const sel = Math.min(selected, value.stops.length - 1);
607
+ const stop = value.stops[sel];
608
+ const map = value.map;
609
+ const stopHex = (c) => c === "paint" ? colors.paint : c === "secondary" ? colors.secondary : c;
610
+ const patchStop = (patch) => {
611
+ const r = updatePaintGradientStop(value, sel, patch);
612
+ setSelected(r.index);
613
+ onChange(r.paint);
614
+ };
615
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `flex flex-col gap-1.5 text-xs text-slate-200 ${className ?? ""}`, "data-testid": "paint-gradient-editor", "aria-label": "Gradient paint", children: [
616
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-1", children: [
617
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("select", { "aria-label": "Gradient preset", className: select, value: "", onChange: (e) => e.target.value && (setSelected(0), onChange(PAINT_GRADIENT_PRESETS[Number(e.target.value)].paint)), children: [
618
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "", children: "Presets\u2026" }),
619
+ PAINT_GRADIENT_PRESETS.map((p, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: i, children: p.name }, p.name))
620
+ ] }),
621
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: button2, onClick: () => onChange(reversePaintGradient(value)), children: "Reverse" })
622
+ ] }),
623
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
624
+ "div",
625
+ {
626
+ role: "slider",
627
+ tabIndex: 0,
628
+ "aria-label": "Gradient ramp: click to add a stop",
629
+ "aria-valuemin": 0,
630
+ "aria-valuemax": 100,
631
+ "aria-valuenow": pct(stop.offset),
632
+ "data-testid": "paint-gradient-ramp",
633
+ className: "relative h-6 cursor-copy rounded ring-1 ring-slate-700",
634
+ style: { background: paintSourceCss2(value, colors) },
635
+ onClick: (e) => {
636
+ const rect = e.currentTarget.getBoundingClientRect();
637
+ const r = addPaintGradientStop(value, rect.width ? (e.clientX - rect.left) / rect.width : 0.5, colors);
638
+ if (r.index >= 0) {
639
+ setSelected(r.index);
640
+ onChange(r.paint);
641
+ }
642
+ }
643
+ }
644
+ ),
645
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "relative h-4", "aria-label": "Stops", children: value.stops.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
646
+ "button",
647
+ {
648
+ type: "button",
649
+ "aria-label": `Stop ${i + 1} at ${pct(s.offset)}%`,
650
+ "aria-pressed": i === sel,
651
+ className: `absolute top-0 h-4 w-3 -translate-x-1/2 rounded-sm ring-1 ${i === sel ? "ring-white" : "ring-slate-600"}`,
652
+ style: { left: `${s.offset * 100}%`, background: stopHex(s.color) },
653
+ onClick: () => setSelected(i)
654
+ },
655
+ i
656
+ )) }),
657
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-wrap items-center gap-1", "data-testid": "paint-gradient-stop", children: [
658
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("select", { "aria-label": "Stop colour source", className: select, value: stop.color === "paint" || stop.color === "secondary" ? stop.color : "color", onChange: (e) => patchStop({ color: e.target.value === "color" ? stopHex(stop.color) : e.target.value }), children: [
659
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "color", children: "Colour" }),
660
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "paint", children: "Paint colour" }),
661
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "secondary", children: "Second colour" })
662
+ ] }),
663
+ stop.color !== "paint" && stop.color !== "secondary" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "color", "aria-label": "Stop colour", value: stop.color, onChange: (e) => patchStop({ color: e.target.value }), className: "h-6 w-8 rounded bg-transparent" }),
664
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center gap-1", children: [
665
+ "At",
666
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "number", "aria-label": "Stop position", min: 0, max: 100, step: 1, value: pct(stop.offset), onChange: (e) => patchStop({ offset: Number(e.target.value) / 100 }), className: number }),
667
+ "%"
668
+ ] }),
669
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center gap-1", children: [
670
+ "Opacity",
671
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "number", "aria-label": "Stop opacity", min: 0, max: 100, step: 5, value: pct(stop.opacity ?? 1), onChange: (e) => patchStop({ opacity: Number(e.target.value) / 100 }), className: number }),
672
+ "%"
673
+ ] }),
674
+ sel < value.stops.length - 1 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center gap-1", children: [
675
+ "Midpoint",
676
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "number", "aria-label": "Stop midpoint", min: 5, max: 95, step: 1, value: pct(stop.midpoint ?? 0.5), onChange: (e) => patchStop({ midpoint: Number(e.target.value) / 100 }), className: number }),
677
+ "%"
678
+ ] }),
679
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: button2, disabled: value.stops.length <= 2, onClick: () => (setSelected(Math.max(0, sel - 1)), onChange(removePaintGradientStop(value, sel))), children: "Remove stop" })
680
+ ] }),
681
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-wrap items-center gap-1", "data-testid": "paint-gradient-mapping", children: [
682
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { "aria-label": "Gradient follows", className: select, value: map.by, onChange: (e) => onChange({ ...value, map: paintGradientMappingFor(e.target.value, map) }), children: PAINT_GRADIENT_MAPPINGS.map((m) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: m, children: PAINT_GRADIENT_MAPPING_LABELS[m] }, m)) }),
683
+ map.by === "distance" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
684
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "number", "aria-label": "Gradient length", min: 1, max: 1e5, value: map.length, onChange: (e) => onChange({ ...value, map: { ...map, length: Math.max(1, Number(e.target.value) || 1) } }), className: number }),
685
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("select", { "aria-label": "Gradient length unit", className: select, value: map.unit ?? "px", onChange: (e) => onChange({ ...value, map: { ...map, unit: e.target.value } }), children: [
686
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "px", children: "px" }),
687
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "dabs", children: "dabs" })
688
+ ] })
689
+ ] }),
690
+ map.by === "time" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "number", "aria-label": "Gradient duration", min: 1, max: 6e5, value: map.duration, onChange: (e) => onChange({ ...value, map: { ...map, duration: Math.max(1, Number(e.target.value) || 1) } }), className: number }),
691
+ map.by === "random" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "number", "aria-label": "Dabs per pick", min: 1, max: 500, value: map.every ?? 1, onChange: (e) => onChange({ ...value, map: { ...map, every: Math.max(1, Math.round(Number(e.target.value) || 1)) } }), className: number }),
692
+ (map.by === "distance" || map.by === "time") && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { "aria-label": "Gradient repeat", className: select, value: map.repeat ?? "clamp", onChange: (e) => onChange({ ...value, map: { ...map, repeat: e.target.value } }), children: PAINT_GRADIENT_REPEATS.map((r) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: r, children: PAINT_GRADIENT_REPEAT_LABELS[r] }, r)) })
693
+ ] }),
694
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-1", children: [
695
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { "aria-label": "Gradient mixing", className: select, value: value.space, onChange: (e) => onChange({ ...value, space: e.target.value }), children: PAINT_GRADIENT_SPACES.map((s) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: s, children: PAINT_GRADIENT_SPACE_LABELS[s] }, s)) }),
696
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex flex-1 items-center gap-1", children: [
697
+ "Softness",
698
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "range", "aria-label": "Gradient softness", min: 0, max: 1, step: 0.05, value: value.softness ?? 1, onChange: (e) => onChange({ ...value, softness: Number(e.target.value) }), className: "flex-1 accent-indigo-500" })
699
+ ] })
700
+ ] })
701
+ ] });
702
+ };
703
+
704
+ // src/paint-editor/PaintGradientEditor.tsx
705
+ var import_jsx_runtime5 = require("react/jsx-runtime");
706
+ var PaintGradientEditor = (props) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PaintGradientEditorView, { ...props, kit: import_paint_editor4.PAINT_GRADIENT_KIT });
707
+
708
+ // src/paint-editor/BrushPanel.tsx
709
+ var import_jsx_runtime6 = require("react/jsx-runtime");
577
710
  var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, className }) => {
578
711
  const state = usePaintEditorState(controller);
579
712
  const [error, attempt] = useAttempt();
580
- const [saveName, setSaveName] = (0, import_react5.useState)("");
581
- const fileRef = (0, import_react5.useRef)(null);
713
+ const [saveName, setSaveName] = (0, import_react6.useState)("");
714
+ const fileRef = (0, import_react6.useRef)(null);
582
715
  const { brush, brushes } = state;
583
716
  const paper = state.document.paper;
584
- const thumbs = (0, import_react5.useMemo)(
717
+ const thumbs = (0, import_react6.useMemo)(
585
718
  () => new Map(brushes.map((b) => [b.id, controller.brushThumbnail(b.id, { width: thumbnailWidth, height: thumbnailHeight, color: "#26324a" })])),
586
719
  // Thumbnails follow the library and the paper.
587
720
  // eslint-disable-next-line react-hooks/exhaustive-deps
588
721
  [controller, brushes, paper, thumbnailWidth, thumbnailHeight]
589
722
  );
590
- const engine = (0, import_paint_editor4.brushEngineControls)(brush);
591
- const mc = brush.behavior.multicolor;
592
- const carried = (0, import_paint_editor4.brushLoadColor)(state.mixerLoad);
593
- const setColors = (colors) => controller.setBrush({ behavior: { multicolor: { colors } } });
723
+ const engine = (0, import_paint_editor5.brushEngineControls)(brush);
724
+ const carried = (0, import_paint_editor5.brushLoadColor)(state.mixerLoad);
594
725
  const slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "brush";
595
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `${panel} ${className ?? ""}`, "data-testid": "brush-panel", children: [
596
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "grid grid-cols-2 gap-1", role: "listbox", "aria-label": "Brushes", children: brushes.map((b) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
726
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: `${panel} ${className ?? ""}`, "data-testid": "brush-panel", children: [
727
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "grid grid-cols-2 gap-1", role: "listbox", "aria-label": "Brushes", children: brushes.map((b) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
597
728
  "button",
598
729
  {
599
730
  type: "button",
@@ -603,128 +734,116 @@ var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, clas
603
734
  className: `flex flex-col items-start gap-0.5 rounded p-1 text-left ${brush.id === b.id ? "bg-indigo-600/30 ring-1 ring-indigo-500" : "bg-slate-900 hover:bg-slate-800"}`,
604
735
  onClick: () => controller.setBrush({ id: b.id }),
605
736
  children: [
606
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PixelCanvas, { pixels: thumbs.get(b.id) ?? null, label: `${b.name} thumbnail`, className: "rounded", style: { width: "100%", aspectRatio: `${thumbnailWidth} / ${thumbnailHeight}` } }),
607
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "truncate text-[10px] text-slate-300", children: b.name })
737
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PixelCanvas, { pixels: thumbs.get(b.id) ?? null, label: `${b.name} thumbnail`, className: "rounded", style: { width: "100%", aspectRatio: `${thumbnailWidth} / ${thumbnailHeight}` } }),
738
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "truncate text-[10px] text-slate-300", children: b.name })
608
739
  ]
609
740
  },
610
741
  b.id
611
742
  )) }),
612
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col gap-1.5", children: [
613
- import_paint_editor4.BRUSH_SLIDERS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor4.readPath)(brush, c.path), onChange: (v) => controller.setBrush((0, import_paint_editor4.patchPath)(brush, c.path, v)) }, c.path)),
614
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
615
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Rotation" }),
616
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { "aria-label": "Brush rotation", value: brush.rotation, onChange: (e) => controller.setBrush({ rotation: e.target.value }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor4.BRUSH_ROTATIONS.map((r) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: r, children: r === "stroke" ? "Follow stroke" : r === "barrel" ? "Pen barrel" : "Fixed" }, r)) })
743
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-col gap-1.5", children: [
744
+ import_paint_editor5.BRUSH_SLIDERS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor5.readPath)(brush, c.path), onChange: (v) => controller.setBrush((0, import_paint_editor5.patchPath)(brush, c.path, v)) }, c.path)),
745
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
746
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-slate-400", children: "Rotation" }),
747
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("select", { "aria-label": "Brush rotation", value: brush.rotation, onChange: (e) => controller.setBrush({ rotation: e.target.value }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor5.BRUSH_ROTATIONS.map((r) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: r, children: r === "stroke" ? "Follow stroke" : r === "barrel" ? "Pen barrel" : "Fixed" }, r)) })
617
748
  ] }),
618
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center gap-2", children: [
619
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "checkbox", checked: brush.tip.pressureSize, onChange: (e) => controller.setBrush({ tip: { pressureSize: e.target.checked } }) }),
749
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex items-center gap-2", children: [
750
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "checkbox", checked: brush.tip.pressureSize, onChange: (e) => controller.setBrush({ tip: { pressureSize: e.target.checked } }) }),
620
751
  " Pressure \u2192 size"
621
752
  ] }),
622
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center gap-2", children: [
623
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "checkbox", checked: brush.tip.pressureOpacity, onChange: (e) => controller.setBrush({ tip: { pressureOpacity: e.target.checked } }) }),
753
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex items-center gap-2", children: [
754
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "checkbox", checked: brush.tip.pressureOpacity, onChange: (e) => controller.setBrush({ tip: { pressureOpacity: e.target.checked } }) }),
624
755
  " Pressure \u2192 opacity"
625
756
  ] }),
626
- brush.engine === "dab" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
627
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Blend" }),
628
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { "aria-label": "Brush blend", value: brush.tip.blend, onChange: (e) => controller.setBrush({ tip: { blend: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor4.BLEND_MENU_GROUPS.map((g) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("optgroup", { label: g.label, children: g.modes.map((m) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: m.id, title: m.description, children: m.label }, m.id)) }, g.label)) })
757
+ brush.engine === "dab" && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
758
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-slate-400", children: "Blend" }),
759
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("select", { "aria-label": "Brush blend", value: brush.tip.blend, onChange: (e) => controller.setBrush({ tip: { blend: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor5.BLEND_MENU_GROUPS.map((g) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("optgroup", { label: g.label, children: g.modes.map((m) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: m.id, title: m.description, children: m.label }, m.id)) }, g.label)) })
629
760
  ] })
630
761
  ] }),
631
- engine && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col gap-1.5", "data-testid": "brush-engine", children: [
632
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "font-medium text-slate-300", children: engine.title }),
633
- engine.wetTool && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
634
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Wet tool" }),
635
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { "aria-label": "Wet tool", value: brush.wet.tool, onChange: (e) => controller.setBrush({ wet: { tool: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor4.WET_TOOLS.map((t) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: t, children: import_paint_editor4.WET_TOOL_LABELS[t] }, t)) })
762
+ engine && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-col gap-1.5", "data-testid": "brush-engine", children: [
763
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "font-medium text-slate-300", children: engine.title }),
764
+ engine.wetTool && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
765
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-slate-400", children: "Wet tool" }),
766
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("select", { "aria-label": "Wet tool", value: brush.wet.tool, onChange: (e) => controller.setBrush({ wet: { tool: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor5.WET_TOOLS.map((t) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: t, children: import_paint_editor5.WET_TOOL_LABELS[t] }, t)) })
636
767
  ] }),
637
- engine.wetTool && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
638
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Medium" }),
639
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { "aria-label": "Wet medium", value: brush.wet.medium, onChange: (e) => controller.setBrush({ wet: { medium: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor4.WET_MEDIA.map((m) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: m, children: import_paint_editor4.WET_MEDIUM_LABELS[m] }, m)) })
768
+ engine.wetTool && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
769
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-slate-400", children: "Medium" }),
770
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("select", { "aria-label": "Wet medium", value: brush.wet.medium, onChange: (e) => controller.setBrush({ wet: { medium: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor5.WET_MEDIA.map((m) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: m, children: import_paint_editor5.WET_MEDIUM_LABELS[m] }, m)) })
640
771
  ] }),
641
- engine.particleMenus && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
642
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
643
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Particles" }),
644
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { "aria-label": "Particle kind", value: brush.particle.kind, onChange: (e) => controller.setBrush({ particle: { kind: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor4.PARTICLE_KINDS.map((k) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: k, children: import_paint_editor4.PARTICLE_KIND_LABELS[k] }, k)) })
772
+ engine.particleMenus && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
773
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
774
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-slate-400", children: "Particles" }),
775
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("select", { "aria-label": "Particle kind", value: brush.particle.kind, onChange: (e) => controller.setBrush({ particle: { kind: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor5.PARTICLE_KINDS.map((k) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: k, children: import_paint_editor5.PARTICLE_KIND_LABELS[k] }, k)) })
645
776
  ] }),
646
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
647
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Drawn as" }),
648
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { "aria-label": "Particle render", value: brush.particle.render, onChange: (e) => controller.setBrush({ particle: { render: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor4.PARTICLE_RENDERS.map((k) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: k, children: import_paint_editor4.PARTICLE_RENDER_LABELS[k] }, k)) })
777
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
778
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-slate-400", children: "Drawn as" }),
779
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("select", { "aria-label": "Particle render", value: brush.particle.render, onChange: (e) => controller.setBrush({ particle: { render: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor5.PARTICLE_RENDERS.map((k) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: k, children: import_paint_editor5.PARTICLE_RENDER_LABELS[k] }, k)) })
649
780
  ] })
650
781
  ] }),
651
- engine.stickerStamps && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex flex-wrap gap-1", role: "group", "aria-label": "Sticker stamps", children: import_paint_editor4.STAMP_KINDS.map((k) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
782
+ engine.stickerStamps && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex flex-wrap gap-1", role: "group", "aria-label": "Sticker stamps", children: import_paint_editor5.STAMP_KINDS.map((k) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
652
783
  "button",
653
784
  {
654
785
  type: "button",
655
786
  "aria-pressed": brush.sticker.stamps.includes(k),
656
787
  className: `px-1.5 py-0.5 rounded text-[10px] ${brush.sticker.stamps.includes(k) ? "bg-indigo-600 text-white" : "bg-slate-800 text-slate-300"}`,
657
- onClick: () => controller.setBrush({ sticker: { stamps: (0, import_paint_editor4.toggleStamp)(brush.sticker.stamps, k) } }),
658
- children: import_paint_editor4.STAMP_LABELS[k]
788
+ onClick: () => controller.setBrush({ sticker: { stamps: (0, import_paint_editor5.toggleStamp)(brush.sticker.stamps, k) } }),
789
+ children: import_paint_editor5.STAMP_LABELS[k]
659
790
  },
660
791
  k
661
792
  )) }),
662
- engine.impastoTool && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
663
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Tool" }),
664
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { "aria-label": "Impasto tool", value: brush.impasto.tool, onChange: (e) => controller.setBrush({ impasto: { tool: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor4.IMPASTO_TOOLS.map((t) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: t, children: import_paint_editor4.IMPASTO_TOOL_LABELS[t] }, t)) })
793
+ engine.impastoTool && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
794
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-slate-400", children: "Tool" }),
795
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("select", { "aria-label": "Impasto tool", value: brush.impasto.tool, onChange: (e) => controller.setBrush({ impasto: { tool: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor5.IMPASTO_TOOLS.map((t) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: t, children: import_paint_editor5.IMPASTO_TOOL_LABELS[t] }, t)) })
665
796
  ] }),
666
- engine.sliders.map((c) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor4.readPath)(brush, c.path), onChange: (v) => controller.setBrush((0, import_paint_editor4.patchPath)(brush, c.path, v)) }, c.path)),
667
- engine.mixingPath && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
668
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Mixing" }),
669
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
797
+ engine.sliders.map((c) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor5.readPath)(brush, c.path), onChange: (v) => controller.setBrush((0, import_paint_editor5.patchPath)(brush, c.path, v)) }, c.path)),
798
+ engine.mixingPath && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
799
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-slate-400", children: "Mixing" }),
800
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
670
801
  "select",
671
802
  {
672
803
  "aria-label": "Brush mixing",
673
804
  value: engine.mixingPath === "blender.mixing" ? brush.blender.mixing : engine.mixingPath === "smudge.mixing" ? brush.smudge.mixing : brush.behavior.mixing,
674
- onChange: (e) => controller.setBrush((0, import_paint_editor4.patchBrushValue)(brush, engine.mixingPath, e.target.value)),
805
+ onChange: (e) => controller.setBrush((0, import_paint_editor5.patchBrushValue)(brush, engine.mixingPath, e.target.value)),
675
806
  className: "rounded bg-slate-800 px-1 py-0.5",
676
807
  children: [
677
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "pigment", children: "Pigment" }),
678
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "rgb", children: "RGB" })
808
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "pigment", children: "Pigment" }),
809
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: "rgb", children: "RGB" })
679
810
  ]
680
811
  }
681
812
  )
682
813
  ] }),
683
- engine.dirty && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center gap-2", title: "Keep the colour the brush picked up for the next stroke (needs Pick-up above 0)", children: [
684
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "checkbox", "aria-label": "Dirty brush", checked: brush.behavior.dirty, onChange: (e) => controller.setBrush({ behavior: { dirty: e.target.checked } }) }),
814
+ engine.dirty && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex items-center gap-2", title: "Keep the colour the brush picked up for the next stroke (needs Pick-up above 0)", children: [
815
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "checkbox", "aria-label": "Dirty brush", checked: brush.behavior.dirty, onChange: (e) => controller.setBrush({ behavior: { dirty: e.target.checked } }) }),
685
816
  " Dirty brush"
686
817
  ] }),
687
- engine.multicolor && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col gap-1.5", "data-testid": "multicolor", children: [
688
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
689
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Multi-colour" }),
690
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("select", { "aria-label": "Multi-colour", value: mc.mode, onChange: (e) => controller.setBrush({ behavior: { multicolor: { mode: e.target.value } } }), className: "rounded bg-slate-800 px-1 py-0.5", children: [
691
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "off", children: "Off" }),
692
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "tip", children: "Across the tip" }),
693
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "dab", children: "Per dab" })
694
- ] })
818
+ (0, import_paint_editor5.brushTakesPaint)(brush) && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-col gap-1.5", "data-testid": "brush-gradient", children: [
819
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex items-center justify-between gap-2", children: [
820
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-slate-400", title: "A gradient of the brush's own, used instead of the paint (the multi-colour brush)", children: "Brush gradient" }),
821
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
822
+ "button",
823
+ {
824
+ type: "button",
825
+ "aria-pressed": brush.paint !== null,
826
+ className: chip(brush.paint !== null),
827
+ onClick: () => attempt(() => controller.setBrush({ paint: brush.paint ? null : import_paint_editor6.DEFAULT_PAINT_GRADIENT })),
828
+ children: brush.paint ? "On" : "Off"
829
+ }
830
+ )
695
831
  ] }),
696
- mc.mode !== "off" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
697
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-wrap items-center gap-1", "aria-label": "Multi-colour colours", children: [
698
- mc.colors.map((col, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex items-center", children: [
699
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "color", "aria-label": `Multi-colour ${i + 1}`, value: col, onChange: (e) => setColors(mc.colors.map((x, j) => j === i ? e.target.value : x)), className: "h-5 w-6 rounded bg-transparent" }),
700
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", "aria-label": `Remove multi-colour ${i + 1}`, className: "px-0.5 text-slate-500 hover:text-white", onClick: () => setColors(mc.colors.filter((_, j) => j !== i)), children: "\xD7" })
701
- ] }, i)),
702
- mc.colors.length < 8 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: button, "aria-label": "Add multi-colour", onClick: () => setColors([...mc.colors.length ? mc.colors : [state.color, state.secondaryColor], state.color].slice(0, 8)), children: "+" }),
703
- mc.colors.length < 2 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-[10px] text-slate-500", children: "Uses the paint and second colours" })
704
- ] }),
705
- mc.mode === "dab" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
706
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Order" }),
707
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("select", { "aria-label": "Colour order", value: mc.order, onChange: (e) => controller.setBrush({ behavior: { multicolor: { order: e.target.value } } }), className: "rounded bg-slate-800 px-1 py-0.5", children: [
708
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "sequence", children: "In order" }),
709
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "random", children: "Random" })
710
- ] })
711
- ] }),
712
- (0, import_paint_editor4.multiColorSliders)(mc.mode).map((c) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor4.readPath)(brush, c.path), onChange: (v) => controller.setBrush((0, import_paint_editor4.patchPath)(brush, c.path, v)) }, c.path))
713
- ] })
832
+ brush.paint && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PaintGradientEditor, { value: brush.paint, colors: { paint: state.color, secondary: state.secondaryColor }, onChange: (g) => attempt(() => controller.setBrush({ paint: g })) })
714
833
  ] }),
715
- engine.load && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-1", children: [
716
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Carries" }),
717
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { "aria-label": "Brush load", title: carried ?? "Clean", className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: carried ?? "transparent" } }),
718
- brush.engine === "mixer" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: button, onClick: () => controller.loadMixerBrush(), children: "Load" }),
719
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: button, onClick: () => controller.cleanMixerBrush(brush.engine === "mixer"), children: "Clean" })
834
+ engine.load && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex items-center gap-1", children: [
835
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-slate-400", children: "Carries" }),
836
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { "aria-label": "Brush load", title: carried ?? "Clean", className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: carried ?? "transparent" } }),
837
+ brush.engine === "mixer" && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: button, onClick: () => controller.loadMixerBrush(), children: "Load" }),
838
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: button, onClick: () => controller.cleanMixerBrush(brush.engine === "mixer"), children: "Clean" })
720
839
  ] })
721
840
  ] }),
722
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-wrap items-center gap-1", children: [
723
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { value: saveName, placeholder: "Brush name", "aria-label": "Brush name", onChange: (e) => setSaveName(e.target.value), className: "w-28 rounded bg-slate-800 px-1.5 py-0.5" }),
724
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: button, disabled: !saveName.trim(), onClick: () => attempt(() => (controller.saveBrush({ id: slug(saveName), name: saveName.trim() }), setSaveName(""))), children: "Save brush" }),
725
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => (0, import_paint_editor4.downloadBytes)(JSON.stringify(controller.exportBrushes(), null, 2), "brushes.json", "application/json")), children: "Export" }),
726
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: button, onClick: () => fileRef.current?.click(), children: "Import\u2026" }),
727
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
841
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-wrap items-center gap-1", children: [
842
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { value: saveName, placeholder: "Brush name", "aria-label": "Brush name", onChange: (e) => setSaveName(e.target.value), className: "w-28 rounded bg-slate-800 px-1.5 py-0.5" }),
843
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: button, disabled: !saveName.trim(), onClick: () => attempt(() => (controller.saveBrush({ id: slug(saveName), name: saveName.trim() }), setSaveName(""))), children: "Save brush" }),
844
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => (0, import_paint_editor5.downloadBytes)(JSON.stringify(controller.exportBrushes(), null, 2), "brushes.json", "application/json")), children: "Export" }),
845
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: button, onClick: () => fileRef.current?.click(), children: "Import\u2026" }),
846
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
728
847
  "input",
729
848
  {
730
849
  ref: fileRef,
@@ -740,24 +859,24 @@ var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, clas
740
859
  }
741
860
  )
742
861
  ] }),
743
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ErrorLine, { error })
862
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ErrorLine, { error })
744
863
  ] });
745
864
  };
746
865
 
747
866
  // src/paint-editor/PaperPanel.tsx
748
- var import_react6 = require("react");
749
- var import_paint_editor5 = require("@pixerate/editor/paint-editor");
750
- var import_jsx_runtime5 = require("react/jsx-runtime");
867
+ var import_react7 = require("react");
868
+ var import_paint_editor7 = require("@pixerate/editor/paint-editor");
869
+ var import_jsx_runtime7 = require("react/jsx-runtime");
751
870
  var SWATCH = { x: 0, y: 0, width: 64, height: 40 };
752
871
  var PaperPanel = ({ controller, className }) => {
753
872
  const state = usePaintEditorState(controller);
754
873
  const [error, attempt] = useAttempt();
755
874
  const paper = state.document.paper;
756
- const previews = (0, import_react6.useMemo)(() => new Map(import_paint_editor5.PAPER_LIBRARY.map((p) => [p.preset, { ...SWATCH, data: (0, import_paint_editor5.renderPaper)({ ...p, shading: Math.max(0.8, p.shading) }, SWATCH) }])), []);
757
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: `${panel} ${className ?? ""}`, "data-testid": "paper-panel", children: [
758
- import_paint_editor5.PAPER_FAMILIES.map((family) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { children: [
759
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "mb-0.5 text-[10px] uppercase tracking-wide text-slate-500", children: family }),
760
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex flex-wrap gap-1", role: "listbox", "aria-label": `${family} papers`, children: import_paint_editor5.PAPER_LIBRARY.filter((p) => p.family === family).map((p) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
875
+ const previews = (0, import_react7.useMemo)(() => new Map(import_paint_editor7.PAPER_LIBRARY.map((p) => [p.preset, { ...SWATCH, data: (0, import_paint_editor7.renderPaper)({ ...p, shading: Math.max(0.8, p.shading) }, SWATCH) }])), []);
876
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `${panel} ${className ?? ""}`, "data-testid": "paper-panel", children: [
877
+ import_paint_editor7.PAPER_FAMILIES.map((family) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { children: [
878
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "mb-0.5 text-[10px] uppercase tracking-wide text-slate-500", children: family }),
879
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "flex flex-wrap gap-1", role: "listbox", "aria-label": `${family} papers`, children: import_paint_editor7.PAPER_LIBRARY.filter((p) => p.family === family).map((p) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
761
880
  "button",
762
881
  {
763
882
  type: "button",
@@ -766,92 +885,93 @@ var PaperPanel = ({ controller, className }) => {
766
885
  title: p.name,
767
886
  className: `rounded p-0.5 ${paper.preset === p.preset ? "ring-2 ring-indigo-500" : "ring-1 ring-slate-700 hover:ring-slate-500"}`,
768
887
  onClick: () => attempt(() => controller.setPaper({ preset: p.preset })),
769
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PixelCanvas, { pixels: previews.get(p.preset) ?? null, label: p.name, style: { width: 48, height: 30 } })
888
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PixelCanvas, { pixels: previews.get(p.preset) ?? null, label: p.name, style: { width: 48, height: 30 } })
770
889
  },
771
890
  p.preset
772
891
  )) })
773
892
  ] }, family)),
774
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-slate-300", children: paper.name }),
775
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "flex items-center justify-between", children: [
776
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-slate-400", children: "Colour" }),
777
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(CommitColorInput, { label: "Paper colour", value: paper.color, onCommit: (v) => attempt(() => controller.setPaper({ color: v })), className: "h-6 w-10 rounded bg-transparent" })
893
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "text-slate-300", children: paper.name }),
894
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "flex items-center justify-between", children: [
895
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "text-slate-400", children: "Colour" }),
896
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(CommitColorInput, { label: "Paper colour", value: paper.color, onCommit: (v) => attempt(() => controller.setPaper({ color: v })), className: "h-6 w-10 rounded bg-transparent" })
778
897
  ] }),
779
- import_paint_editor5.PAPER_SLIDERS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor5.readPath)(paper, c.path), onCommit: (v) => attempt(() => controller.setPaper((0, import_paint_editor5.patchPath)(paper, c.path, v))) }, c.path)),
780
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("label", { className: "flex items-center gap-2", children: [
781
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("input", { type: "checkbox", checked: paper.tiling === "mirror", onChange: (e) => attempt(() => controller.setPaper({ tiling: e.target.checked ? "mirror" : "repeat" })) }),
898
+ import_paint_editor7.PAPER_SLIDERS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor7.readPath)(paper, c.path), onCommit: (v) => attempt(() => controller.setPaper((0, import_paint_editor7.patchPath)(paper, c.path, v))) }, c.path)),
899
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "flex items-center gap-2", children: [
900
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("input", { type: "checkbox", checked: paper.tiling === "mirror", onChange: (e) => attempt(() => controller.setPaper({ tiling: e.target.checked ? "mirror" : "repeat" })) }),
782
901
  " Mirror tiling"
783
902
  ] }),
784
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "self-start px-2 py-1 rounded text-[11px] bg-slate-800 text-slate-200 hover:bg-slate-700", onClick: () => attempt(() => controller.setPaper({ seed: paper.seed + 1 })), children: "New sheet" }),
785
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex flex-col gap-1.5", "data-testid": "light-panel", children: [
786
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "font-medium text-slate-300", children: "Light (thick paint, metallic paper)" }),
787
- import_paint_editor5.LIGHT_SLIDERS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor5.readPath)(controller.getLighting(), c.path), onChange: (v) => attempt(() => controller.setLighting({ [c.path]: v })) }, c.path)),
788
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("button", { type: "button", className: "self-start px-2 py-1 rounded text-[11px] bg-slate-800 text-slate-200 hover:bg-slate-700", onClick: () => attempt(() => controller.flattenRelief()), children: "Flatten relief" })
903
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "self-start px-2 py-1 rounded text-[11px] bg-slate-800 text-slate-200 hover:bg-slate-700", onClick: () => attempt(() => controller.setPaper({ seed: paper.seed + 1 })), children: "New sheet" }),
904
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex flex-col gap-1.5", "data-testid": "light-panel", children: [
905
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "font-medium text-slate-300", children: "Light (thick paint, metallic paper)" }),
906
+ import_paint_editor7.LIGHT_SLIDERS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor7.readPath)(controller.getLighting(), c.path), onChange: (v) => attempt(() => controller.setLighting({ [c.path]: v })) }, c.path)),
907
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: "self-start px-2 py-1 rounded text-[11px] bg-slate-800 text-slate-200 hover:bg-slate-700", onClick: () => attempt(() => controller.flattenRelief()), children: "Flatten relief" })
789
908
  ] }),
790
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ErrorLine, { error })
909
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ErrorLine, { error })
791
910
  ] });
792
911
  };
793
912
 
794
913
  // src/paint-editor/ColorPanel.tsx
795
- var import_react7 = require("react");
796
- var import_paint_editor6 = require("@pixerate/editor/paint-editor");
797
- var import_jsx_runtime6 = require("react/jsx-runtime");
914
+ var import_react8 = require("react");
915
+ var import_paint_editor8 = require("@pixerate/editor/paint-editor");
916
+ var import_paint_editor9 = require("@pixerate/editor/paint-editor");
917
+ var import_jsx_runtime8 = require("react/jsx-runtime");
798
918
  var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className }) => {
799
919
  const state = usePaintEditorState(controller);
800
920
  const [error, attempt] = useAttempt();
801
- const [wheel, setWheel] = (0, import_react7.useState)("ryb");
802
- const [kind, setKind] = (0, import_react7.useState)("complementary");
803
- const [padTool, setPadTool] = (0, import_react7.useState)("paint");
804
- const [sample, setSample] = (0, import_react7.useState)(3);
805
- const [parts, setParts] = (0, import_react7.useState)([1, 1]);
806
- const [padName, setPadName] = (0, import_react7.useState)("");
807
- const padPoints = (0, import_react7.useRef)(null);
808
- const ring = (0, import_react7.useMemo)(() => (0, import_paint_editor6.renderColorWheel)(wheelSize, wheel), [wheelSize, wheel]);
921
+ const [wheel, setWheel] = (0, import_react8.useState)("ryb");
922
+ const [kind, setKind] = (0, import_react8.useState)("complementary");
923
+ const [padTool, setPadTool] = (0, import_react8.useState)("paint");
924
+ const [sample, setSample] = (0, import_react8.useState)(3);
925
+ const [parts, setParts] = (0, import_react8.useState)([1, 1]);
926
+ const [padName, setPadName] = (0, import_react8.useState)("");
927
+ const padPoints = (0, import_react8.useRef)(null);
928
+ const ring = (0, import_react8.useMemo)(() => (0, import_paint_editor8.renderColorWheel)(wheelSize, wheel), [wheelSize, wheel]);
809
929
  const color = state.color;
810
- const harmony = (0, import_react7.useMemo)(() => controller.harmony(kind, { color, wheel }), [controller, kind, color, wheel]);
811
- const [r, g, b] = (0, import_paint_editor6.parseHexColor)(color);
812
- const hsv = (0, import_paint_editor6.rgbToHsv)(r, g, b);
813
- const pad = (0, import_react7.useMemo)(() => ({ width: import_paint_editor6.MIXING_PAD_WIDTH, height: import_paint_editor6.MIXING_PAD_HEIGHT, data: controller.mixingPad.pixels() }), [controller, state.mixingPadVersion]);
930
+ const harmony = (0, import_react8.useMemo)(() => controller.harmony(kind, { color, wheel }), [controller, kind, color, wheel]);
931
+ const [r, g, b] = (0, import_paint_editor8.parseHexColor)(color);
932
+ const hsv = (0, import_paint_editor8.rgbToHsv)(r, g, b);
933
+ const pad = (0, import_react8.useMemo)(() => ({ width: import_paint_editor8.MIXING_PAD_WIDTH, height: import_paint_editor8.MIXING_PAD_HEIGHT, data: controller.mixingPad.pixels() }), [controller, state.mixingPadVersion]);
814
934
  const R = wheelSize / 2;
815
935
  const mid = R * 0.82;
816
- const setHsv = (s, v) => controller.setColor((0, import_paint_editor6.rgbToHexColor)(...(0, import_paint_editor6.hsvToRgb)(hsv.h, s, v)));
936
+ const setHsv = (s, v) => controller.setColor((0, import_paint_editor8.rgbToHexColor)(...(0, import_paint_editor8.hsvToRgb)(hsv.h, s, v)));
817
937
  const padPoint = (e) => {
818
938
  const rect = e.currentTarget.getBoundingClientRect();
819
- const kx = import_paint_editor6.MIXING_PAD_WIDTH / (rect.width || import_paint_editor6.MIXING_PAD_WIDTH);
820
- const ky = import_paint_editor6.MIXING_PAD_HEIGHT / (rect.height || import_paint_editor6.MIXING_PAD_HEIGHT);
821
- const x = Math.min(import_paint_editor6.MIXING_PAD_WIDTH, Math.max(0, (e.clientX - rect.left) * kx));
822
- const y = Math.min(import_paint_editor6.MIXING_PAD_HEIGHT, Math.max(0, (e.clientY - rect.top) * ky));
939
+ const kx = import_paint_editor8.MIXING_PAD_WIDTH / (rect.width || import_paint_editor8.MIXING_PAD_WIDTH);
940
+ const ky = import_paint_editor8.MIXING_PAD_HEIGHT / (rect.height || import_paint_editor8.MIXING_PAD_HEIGHT);
941
+ const x = Math.min(import_paint_editor8.MIXING_PAD_WIDTH, Math.max(0, (e.clientX - rect.left) * kx));
942
+ const y = Math.min(import_paint_editor8.MIXING_PAD_HEIGHT, Math.max(0, (e.clientY - rect.top) * ky));
823
943
  return e.pointerType === "pen" && e.pressure > 0 ? { x, y, pressure: e.pressure } : { x, y };
824
944
  };
825
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: `${panel} ${className ?? ""}`, "data-testid": "color-panel", children: [
826
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "relative self-center", style: { width: wheelSize, height: wheelSize }, children: [
827
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
945
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: `${panel} ${className ?? ""}`, "data-testid": "color-panel", children: [
946
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "relative self-center", style: { width: wheelSize, height: wheelSize }, children: [
947
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
828
948
  "div",
829
949
  {
830
950
  role: "slider",
831
951
  "aria-label": "Hue wheel",
832
952
  "aria-valuemin": 0,
833
953
  "aria-valuemax": 360,
834
- "aria-valuenow": Math.round((0, import_paint_editor6.hueToWheel)(hsv.h, wheel)),
954
+ "aria-valuenow": Math.round((0, import_paint_editor8.hueToWheel)(hsv.h, wheel)),
835
955
  tabIndex: 0,
836
956
  className: "absolute inset-0 cursor-crosshair",
837
957
  onPointerDown: (e) => {
838
958
  const rect = e.currentTarget.getBoundingClientRect();
839
- controller.setColor((0, import_paint_editor6.wheelPick)(e.clientX - rect.left - R, e.clientY - rect.top - R, wheel, color));
959
+ controller.setColor((0, import_paint_editor8.wheelPick)(e.clientX - rect.left - R, e.clientY - rect.top - R, wheel, color));
840
960
  },
841
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PixelCanvas, { pixels: ring, label: "Colour wheel", style: { width: wheelSize, height: wheelSize } })
961
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PixelCanvas, { pixels: ring, label: "Colour wheel", style: { width: wheelSize, height: wheelSize } })
842
962
  }
843
963
  ),
844
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("svg", { className: "pointer-events-none absolute inset-0", width: wheelSize, height: wheelSize, "aria-hidden": "true", children: harmony.map((h, i) => {
845
- const p = (0, import_paint_editor6.wheelPoint)(h.angle, mid);
846
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("circle", { cx: R + p.x, cy: R + p.y, r: i === 0 ? 7 : 5, fill: h.color, stroke: "#fff", strokeWidth: i === 0 ? 2 : 1 }, i);
964
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("svg", { className: "pointer-events-none absolute inset-0", width: wheelSize, height: wheelSize, "aria-hidden": "true", children: harmony.map((h, i) => {
965
+ const p = (0, import_paint_editor8.wheelPoint)(h.angle, mid);
966
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("circle", { cx: R + p.x, cy: R + p.y, r: i === 0 ? 7 : 5, fill: h.color, stroke: "#fff", strokeWidth: i === 0 ? 2 : 1 }, i);
847
967
  }) }),
848
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "absolute rounded-full border border-slate-700", style: { left: R - R * 0.45, top: R - R * 0.45, width: R * 0.9, height: R * 0.9, background: color } })
968
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "absolute rounded-full border border-slate-700", style: { left: R - R * 0.45, top: R - R * 0.45, width: R * 0.9, height: R * 0.9, background: color } })
849
969
  ] }),
850
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex items-center gap-2", children: [
851
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "color", "aria-label": "Paint colour", value: color, onChange: (e) => controller.setColor(e.target.value), className: "h-7 w-9 rounded bg-transparent" }),
852
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "color", "aria-label": "Second colour", value: state.secondaryColor, onChange: (e) => controller.setSecondaryColor(e.target.value), className: "h-5 w-7 rounded bg-transparent" }),
853
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: button, title: "Swap colours (X)", onClick: () => controller.swapColors(), children: "Swap" }),
854
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
970
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center gap-2", children: [
971
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("input", { type: "color", "aria-label": "Paint colour", value: color, onChange: (e) => controller.setColor(e.target.value), className: "h-7 w-9 rounded bg-transparent" }),
972
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("input", { type: "color", "aria-label": "Second colour", value: state.secondaryColor, onChange: (e) => controller.setSecondaryColor(e.target.value), className: "h-5 w-7 rounded bg-transparent" }),
973
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: button, title: "Swap colours (X)", onClick: () => controller.swapColors(), children: "Swap" }),
974
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
855
975
  "input",
856
976
  {
857
977
  "aria-label": "Hex colour",
@@ -862,35 +982,58 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
862
982
  color
863
983
  )
864
984
  ] }),
865
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex items-center gap-2", children: [
866
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "w-16 text-slate-400", children: "Saturation" }),
867
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "range", "aria-label": "Saturation", min: 0, max: 1, step: 0.01, value: hsv.s, onChange: (e) => setHsv(Number(e.target.value), hsv.v), className: "flex-1 accent-indigo-500" })
985
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center gap-2", "data-testid": "paint-source", children: [
986
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
987
+ "span",
988
+ {
989
+ className: "h-5 flex-1 rounded ring-1 ring-slate-700",
990
+ "aria-label": state.paint.type === "gradient" ? "Gradient paint" : "Paint colour",
991
+ "data-testid": "paint-well",
992
+ style: { background: (0, import_paint_editor9.paintSourceCss)(state.paint, { paint: color, secondary: state.secondaryColor }) }
993
+ }
994
+ ),
995
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
996
+ "button",
997
+ {
998
+ type: "button",
999
+ className: chip(state.paint.type === "gradient"),
1000
+ "aria-pressed": state.paint.type === "gradient",
1001
+ title: "Paint that changes colour as the stroke goes",
1002
+ onClick: () => attempt(() => state.paint.type === "gradient" ? controller.setColor(color) : controller.setPaint(import_paint_editor9.DEFAULT_PAINT_GRADIENT)),
1003
+ children: "Gradient"
1004
+ }
1005
+ )
868
1006
  ] }),
869
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex items-center gap-2", children: [
870
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "w-16 text-slate-400", children: "Value" }),
871
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "range", "aria-label": "Value", min: 0, max: 1, step: 0.01, value: hsv.v, onChange: (e) => setHsv(hsv.s, Number(e.target.value)), className: "flex-1 accent-indigo-500" })
1007
+ state.paint.type === "gradient" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PaintGradientEditor, { value: state.paint, colors: { paint: color, secondary: state.secondaryColor }, onChange: (g2) => attempt(() => controller.setPaint(g2)) }),
1008
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { className: "flex items-center gap-2", children: [
1009
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "w-16 text-slate-400", children: "Saturation" }),
1010
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("input", { type: "range", "aria-label": "Saturation", min: 0, max: 1, step: 0.01, value: hsv.s, onChange: (e) => setHsv(Number(e.target.value), hsv.v), className: "flex-1 accent-indigo-500" })
872
1011
  ] }),
873
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex items-center gap-1", children: [
874
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("select", { "aria-label": "Harmony", value: kind, onChange: (e) => setKind(e.target.value), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor6.HARMONY_KINDS.map((k) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: k, children: k }, k)) }),
875
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: chip(wheel === "ryb"), "aria-pressed": wheel === "ryb", onClick: () => setWheel("ryb"), children: "RYB" }),
876
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: chip(wheel === "rgb"), "aria-pressed": wheel === "rgb", onClick: () => setWheel("rgb"), children: "RGB" })
1012
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { className: "flex items-center gap-2", children: [
1013
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "w-16 text-slate-400", children: "Value" }),
1014
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("input", { type: "range", "aria-label": "Value", min: 0, max: 1, step: 0.01, value: hsv.v, onChange: (e) => setHsv(hsv.s, Number(e.target.value)), className: "flex-1 accent-indigo-500" })
877
1015
  ] }),
878
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-wrap gap-1", "aria-label": "Harmony colours", children: [
879
- harmony.map((h, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", "aria-label": `Use ${h.color}`, title: h.color, className: "h-6 w-6 rounded ring-1 ring-slate-700", style: { background: h.color }, onClick: () => controller.setColor(h.color) }, i)),
880
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: button, onClick: () => harmony.forEach((h) => controller.addSwatch(h.color)), children: "+ swatches" })
1016
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center gap-1", children: [
1017
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("select", { "aria-label": "Harmony", value: kind, onChange: (e) => setKind(e.target.value), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor8.HARMONY_KINDS.map((k) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: k, children: k }, k)) }),
1018
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: chip(wheel === "ryb"), "aria-pressed": wheel === "ryb", onClick: () => setWheel("ryb"), children: "RYB" }),
1019
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: chip(wheel === "rgb"), "aria-pressed": wheel === "rgb", onClick: () => setWheel("rgb"), children: "RGB" })
881
1020
  ] }),
882
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-wrap gap-1", role: "listbox", "aria-label": "Swatches", children: [
883
- state.swatches.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1021
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-wrap gap-1", "aria-label": "Harmony colours", children: [
1022
+ harmony.map((h, i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", "aria-label": `Use ${h.color}`, title: h.color, className: "h-6 w-6 rounded ring-1 ring-slate-700", style: { background: h.color }, onClick: () => controller.setColor(h.color) }, i)),
1023
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: button, onClick: () => harmony.forEach((h) => controller.addSwatch(h.color)), children: "+ swatches" })
1024
+ ] }),
1025
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-wrap gap-1", role: "listbox", "aria-label": "Swatches", children: [
1026
+ state.swatches.map((s, i) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
884
1027
  "button",
885
1028
  {
886
1029
  type: "button",
887
1030
  role: "option",
888
- "aria-selected": s.color === color,
889
- "aria-label": s.name ?? s.color,
890
- title: `${s.name ?? s.color} (right-click to remove)`,
1031
+ "aria-selected": s.paint ? state.paint === s.paint : state.paint.type === "color" && s.color === color,
1032
+ "aria-label": s.name ?? (s.paint ? "Gradient" : s.color),
1033
+ title: `${s.name ?? (s.paint ? "Gradient" : s.color)} (right-click to remove)`,
891
1034
  className: "h-5 w-5 rounded ring-1 ring-slate-700",
892
- style: { background: s.color },
893
- onClick: () => controller.setColor(s.color),
1035
+ style: { background: s.paint ? (0, import_paint_editor9.paintSourceCss)(s.paint, { paint: color, secondary: state.secondaryColor }) : s.color },
1036
+ onClick: () => attempt(() => s.paint ? controller.setPaint(s.paint) : controller.setColor(s.color)),
894
1037
  onContextMenu: (e) => {
895
1038
  e.preventDefault();
896
1039
  controller.removeSwatch(i);
@@ -898,15 +1041,15 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
898
1041
  },
899
1042
  i
900
1043
  )),
901
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: button, "aria-label": "Add swatch", onClick: () => controller.addSwatch(color), children: "+" })
1044
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: button, "aria-label": "Add swatch", onClick: () => attempt(() => controller.addSwatch(state.paint)), children: "+" })
902
1045
  ] }),
903
- mixingPad && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-col gap-1", "data-testid": "mixing-pad", children: [
904
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex items-center gap-1", children: [
905
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-slate-400", children: "Mixing pad" }),
906
- ["paint", "knife", "pick"].map((t) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", "aria-pressed": padTool === t, className: chip(padTool === t), onClick: () => setPadTool(t), children: t === "paint" ? "Paint" : t === "knife" ? "Knife" : "Pick" }, t)),
907
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: button, onClick: () => controller.clearMixingPad(), children: "Clear" })
1046
+ mixingPad && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-col gap-1", "data-testid": "mixing-pad", children: [
1047
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center gap-1", children: [
1048
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-slate-400", children: "Mixing pad" }),
1049
+ ["paint", "knife", "pick"].map((t) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", "aria-pressed": padTool === t, className: chip(padTool === t), onClick: () => setPadTool(t), children: t === "paint" ? "Paint" : t === "knife" ? "Knife" : "Pick" }, t)),
1050
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: button, onClick: () => controller.clearMixingPad(), children: "Clear" })
908
1051
  ] }),
909
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1052
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
910
1053
  "div",
911
1054
  {
912
1055
  role: "application",
@@ -915,7 +1058,7 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
915
1058
  onPointerDown: (e) => {
916
1059
  const p = padPoint(e);
917
1060
  if (padTool === "pick") {
918
- attempt(() => controller.mixingPadPick(p.x, p.y, { setColor: true, radius: (0, import_paint_editor6.padSampleRadius)(sample) }));
1061
+ attempt(() => controller.mixingPadPick(p.x, p.y, { setColor: true, radius: (0, import_paint_editor8.padSampleRadius)(sample) }));
919
1062
  return;
920
1063
  }
921
1064
  padPoints.current = [p];
@@ -927,20 +1070,20 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
927
1070
  padPoints.current = null;
928
1071
  if (pts?.length) attempt(() => controller.mixingPadStroke(pts, { tool: padTool === "knife" ? "knife" : "paint" }));
929
1072
  },
930
- children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PixelCanvas, { pixels: pad, label: "Mixing pad pixels", className: "w-full rounded", style: { aspectRatio: `${import_paint_editor6.MIXING_PAD_WIDTH} / ${import_paint_editor6.MIXING_PAD_HEIGHT}` } })
1073
+ children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PixelCanvas, { pixels: pad, label: "Mixing pad pixels", className: "w-full rounded", style: { aspectRatio: `${import_paint_editor8.MIXING_PAD_WIDTH} / ${import_paint_editor8.MIXING_PAD_HEIGHT}` } })
931
1074
  }
932
1075
  ),
933
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex flex-wrap items-center gap-1", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex items-center gap-1", children: [
934
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-slate-400", children: "Sample" }),
935
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("select", { "aria-label": "Pick sample size", value: sample, onChange: (e) => setSample(Number(e.target.value)), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor6.PAD_SAMPLE_SIZES.map((n) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: n, children: n === 1 ? "1 px" : `${n}\xD7${n}` }, n)) })
1076
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "flex flex-wrap items-center gap-1", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { className: "flex items-center gap-1", children: [
1077
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-slate-400", children: "Sample" }),
1078
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("select", { "aria-label": "Pick sample size", value: sample, onChange: (e) => setSample(Number(e.target.value)), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor8.PAD_SAMPLE_SIZES.map((n) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: n, children: n === 1 ? "1 px" : `${n}\xD7${n}` }, n)) })
936
1079
  ] }) }),
937
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-wrap items-center gap-1", children: [
938
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: color } }),
939
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "number", "aria-label": "Paint colour parts", min: 0, max: 20, value: parts[0], onChange: (e) => setParts([Number(e.target.value), parts[1]]), className: "w-10 rounded bg-slate-800 px-1 py-0.5" }),
940
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-slate-500", children: ":" }),
941
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "number", "aria-label": "Second colour parts", min: 0, max: 20, value: parts[1], onChange: (e) => setParts([parts[0], Number(e.target.value)]), className: "w-10 rounded bg-slate-800 px-1 py-0.5" }),
942
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: state.secondaryColor } }),
943
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1080
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-wrap items-center gap-1", children: [
1081
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: color } }),
1082
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("input", { type: "number", "aria-label": "Paint colour parts", min: 0, max: 20, value: parts[0], onChange: (e) => setParts([Number(e.target.value), parts[1]]), className: "w-10 rounded bg-slate-800 px-1 py-0.5" }),
1083
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-slate-500", children: ":" }),
1084
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("input", { type: "number", "aria-label": "Second colour parts", min: 0, max: 20, value: parts[1], onChange: (e) => setParts([parts[0], Number(e.target.value)]), className: "w-10 rounded bg-slate-800 px-1 py-0.5" }),
1085
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: state.secondaryColor } }),
1086
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
944
1087
  "button",
945
1088
  {
946
1089
  type: "button",
@@ -959,23 +1102,23 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
959
1102
  }
960
1103
  )
961
1104
  ] }),
962
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-wrap items-center gap-1", children: [
963
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { value: padName, placeholder: "Pad name", "aria-label": "Pad name", onChange: (e) => setPadName(e.target.value), className: "w-24 rounded bg-slate-800 px-1.5 py-0.5" }),
964
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => (controller.saveMixingPad(padName || void 0), setPadName(""))), children: "Keep pad" })
1105
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-wrap items-center gap-1", children: [
1106
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("input", { value: padName, placeholder: "Pad name", "aria-label": "Pad name", onChange: (e) => setPadName(e.target.value), className: "w-24 rounded bg-slate-800 px-1.5 py-0.5" }),
1107
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => (controller.saveMixingPad(padName || void 0), setPadName(""))), children: "Keep pad" })
965
1108
  ] }),
966
- state.savedMixingPads.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className: "flex flex-col gap-0.5", "aria-label": "Kept pads", children: state.savedMixingPads.map((p) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("li", { className: "flex items-center gap-1", children: [
967
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: `${button} flex-1 text-left`, onClick: () => attempt(() => controller.loadMixingPad(p.id)), children: p.name }),
968
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: button, "aria-label": `Remove ${p.name}`, onClick: () => attempt(() => controller.removeMixingPad(p.id)), children: "\xD7" })
1109
+ state.savedMixingPads.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("ul", { className: "flex flex-col gap-0.5", "aria-label": "Kept pads", children: state.savedMixingPads.map((p) => /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("li", { className: "flex items-center gap-1", children: [
1110
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: `${button} flex-1 text-left`, onClick: () => attempt(() => controller.loadMixingPad(p.id)), children: p.name }),
1111
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: button, "aria-label": `Remove ${p.name}`, onClick: () => attempt(() => controller.removeMixingPad(p.id)), children: "\xD7" })
969
1112
  ] }, p.id)) })
970
1113
  ] }),
971
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ErrorLine, { error })
1114
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ErrorLine, { error })
972
1115
  ] });
973
1116
  };
974
1117
 
975
1118
  // src/paint-editor/PaintLayersPanel.tsx
976
- var import_react8 = require("react");
977
- var import_paint_editor7 = require("@pixerate/editor/paint-editor");
978
- var import_jsx_runtime7 = require("react/jsx-runtime");
1119
+ var import_react9 = require("react");
1120
+ var import_paint_editor10 = require("@pixerate/editor/paint-editor");
1121
+ var import_jsx_runtime9 = require("react/jsx-runtime");
979
1122
  function siblingIndex(layers, l) {
980
1123
  const sibs = layers.filter((x) => x.parentId === l.parentId).reverse();
981
1124
  return { index: sibs.findIndex((x) => x.id === l.id), count: sibs.length };
@@ -983,7 +1126,7 @@ function siblingIndex(layers, l) {
983
1126
  var PaintLayersPanel = ({ controller, className }) => {
984
1127
  usePaintEditorState(controller);
985
1128
  const [error, attempt] = useAttempt();
986
- const [renaming, setRenaming] = (0, import_react8.useState)(null);
1129
+ const [renaming, setRenaming] = (0, import_react9.useState)(null);
987
1130
  const layers = controller.listLayers();
988
1131
  const active = layers.find((l) => l.active) ?? null;
989
1132
  const move = (delta) => {
@@ -992,17 +1135,17 @@ var PaintLayersPanel = ({ controller, className }) => {
992
1135
  const to = Math.min(count - 1, Math.max(0, index + delta));
993
1136
  if (to !== index) attempt(() => controller.moveLayer(active.id, to));
994
1137
  };
995
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: `${panel} ${className ?? ""}`, "data-testid": "paint-layers-panel", children: [
996
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
997
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => controller.addLayer()), children: "New layer" }),
998
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => controller.addLayer({ type: "group" })), children: "New group" }),
999
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: button, disabled: !active, onClick: () => active && attempt(() => controller.duplicateLayer(active.id)), children: "Duplicate" }),
1000
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: button, disabled: !active, "aria-label": "Move layer up", onClick: () => move(1), children: "\u2191" }),
1001
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: button, disabled: !active, "aria-label": "Move layer down", onClick: () => move(-1), children: "\u2193" }),
1002
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: button, disabled: !active, onClick: () => active && attempt(() => controller.mergeDown(active.id)), children: "Merge down" }),
1003
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", className: button, disabled: !active || layers.length < 2, onClick: () => active && attempt(() => controller.removeLayer(active.id)), children: "Delete" })
1138
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: `${panel} ${className ?? ""}`, "data-testid": "paint-layers-panel", children: [
1139
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
1140
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => controller.addLayer()), children: "New layer" }),
1141
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => controller.addLayer({ type: "group" })), children: "New group" }),
1142
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: button, disabled: !active, onClick: () => active && attempt(() => controller.duplicateLayer(active.id)), children: "Duplicate" }),
1143
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: button, disabled: !active, "aria-label": "Move layer up", onClick: () => move(1), children: "\u2191" }),
1144
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: button, disabled: !active, "aria-label": "Move layer down", onClick: () => move(-1), children: "\u2193" }),
1145
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: button, disabled: !active, onClick: () => active && attempt(() => controller.mergeDown(active.id)), children: "Merge down" }),
1146
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: button, disabled: !active || layers.length < 2, onClick: () => active && attempt(() => controller.removeLayer(active.id)), children: "Delete" })
1004
1147
  ] }),
1005
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { role: "tree", "aria-label": "Layers", className: "flex flex-col gap-0.5", children: layers.map((l) => /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1148
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("ul", { role: "tree", "aria-label": "Layers", className: "flex flex-col gap-0.5", children: layers.map((l) => /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1006
1149
  "li",
1007
1150
  {
1008
1151
  role: "treeitem",
@@ -1011,7 +1154,7 @@ var PaintLayersPanel = ({ controller, className }) => {
1011
1154
  style: { paddingLeft: 6 + l.depth * 12 },
1012
1155
  onClick: () => controller.setActiveLayer(l.id),
1013
1156
  children: [
1014
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1157
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1015
1158
  "button",
1016
1159
  {
1017
1160
  type: "button",
@@ -1024,7 +1167,7 @@ var PaintLayersPanel = ({ controller, className }) => {
1024
1167
  children: l.visible ? "\u25CF" : "\u25CB"
1025
1168
  }
1026
1169
  ),
1027
- renaming === l.id ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1170
+ renaming === l.id ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1028
1171
  "input",
1029
1172
  {
1030
1173
  autoFocus: true,
@@ -1041,19 +1184,19 @@ var PaintLayersPanel = ({ controller, className }) => {
1041
1184
  if (e.key === "Escape") setRenaming(null);
1042
1185
  }
1043
1186
  }
1044
- ) : /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "flex-1 truncate", onDoubleClick: () => setRenaming(l.id), children: [
1187
+ ) : /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "flex-1 truncate", onDoubleClick: () => setRenaming(l.id), children: [
1045
1188
  l.type === "group" ? "\u25B8 " : "",
1046
1189
  l.name
1047
1190
  ] }),
1048
- l.locks.pixels || l.locks.all ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { title: "Locked", children: "\u{1F512}" }) : null
1191
+ l.locks.pixels || l.locks.all ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { title: "Locked", children: "\u{1F512}" }) : null
1049
1192
  ]
1050
1193
  },
1051
1194
  l.id
1052
1195
  )) }),
1053
- active && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex flex-col gap-1.5", children: [
1054
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "flex items-center gap-2", children: [
1055
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "w-14 text-slate-400", children: "Opacity" }),
1056
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1196
+ active && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex flex-col gap-1.5", children: [
1197
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("label", { className: "flex items-center gap-2", children: [
1198
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "w-14 text-slate-400", children: "Opacity" }),
1199
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1057
1200
  "input",
1058
1201
  {
1059
1202
  type: "range",
@@ -1067,25 +1210,25 @@ var PaintLayersPanel = ({ controller, className }) => {
1067
1210
  }
1068
1211
  )
1069
1212
  ] }),
1070
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("label", { className: "flex items-center gap-2", children: [
1071
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "w-14 text-slate-400", children: "Blend" }),
1072
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("select", { "aria-label": "Layer blend", value: active.blend, onChange: (e) => attempt(() => controller.updateLayer(active.id, { blend: e.target.value })), className: "flex-1 rounded bg-slate-800 px-1 py-0.5", children: [
1073
- active.type === "group" && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: "pass-through", children: "Pass through" }),
1074
- import_paint_editor7.BLEND_MENU_GROUPS.map((g) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("optgroup", { label: g.label, children: g.modes.map((m) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("option", { value: m.id, title: m.description, children: m.label }, m.id)) }, g.label))
1213
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("label", { className: "flex items-center gap-2", children: [
1214
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "w-14 text-slate-400", children: "Blend" }),
1215
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("select", { "aria-label": "Layer blend", value: active.blend, onChange: (e) => attempt(() => controller.updateLayer(active.id, { blend: e.target.value })), className: "flex-1 rounded bg-slate-800 px-1 py-0.5", children: [
1216
+ active.type === "group" && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: "pass-through", children: "Pass through" }),
1217
+ import_paint_editor10.BLEND_MENU_GROUPS.map((g) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("optgroup", { label: g.label, children: g.modes.map((m) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: m.id, title: m.description, children: m.label }, m.id)) }, g.label))
1075
1218
  ] })
1076
1219
  ] }),
1077
- /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "flex gap-1", children: [
1078
- ["pixels", "transparency"].map((k) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", "aria-pressed": Boolean(active.locks[k]), className: chip(Boolean(active.locks[k])), onClick: () => attempt(() => controller.updateLayer(active.id, { locks: { [k]: !active.locks[k] } })), children: k === "pixels" ? "Lock pixels" : "Lock transparency" }, k)),
1079
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("button", { type: "button", "aria-pressed": active.clipped, className: chip(active.clipped), onClick: () => attempt(() => controller.updateLayer(active.id, { clipped: !active.clipped })), children: "Clip" })
1220
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex gap-1", children: [
1221
+ ["pixels", "transparency"].map((k) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", "aria-pressed": Boolean(active.locks[k]), className: chip(Boolean(active.locks[k])), onClick: () => attempt(() => controller.updateLayer(active.id, { locks: { [k]: !active.locks[k] } })), children: k === "pixels" ? "Lock pixels" : "Lock transparency" }, k)),
1222
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", "aria-pressed": active.clipped, className: chip(active.clipped), onClick: () => attempt(() => controller.updateLayer(active.id, { clipped: !active.clipped })), children: "Clip" })
1080
1223
  ] })
1081
1224
  ] }),
1082
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(ErrorLine, { error })
1225
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ErrorLine, { error })
1083
1226
  ] });
1084
1227
  };
1085
1228
 
1086
1229
  // src/paint-editor/WetPanel.tsx
1087
- var import_paint_editor8 = require("@pixerate/editor/paint-editor");
1088
- var import_jsx_runtime8 = require("react/jsx-runtime");
1230
+ var import_paint_editor11 = require("@pixerate/editor/paint-editor");
1231
+ var import_jsx_runtime10 = require("react/jsx-runtime");
1089
1232
  var FLOW_LABELS = { none: "None", linear: "One way", swirl: "Swirl", radial: "Outwards", turbulent: "Eddies" };
1090
1233
  var WetPanel = ({ controller, className }) => {
1091
1234
  const state = usePaintEditorState(controller);
@@ -1094,31 +1237,31 @@ var WetPanel = ({ controller, className }) => {
1094
1237
  const layerId = state.document.activeLayerId;
1095
1238
  const info = layerId ? controller.wetLayerInfo(layerId) : null;
1096
1239
  const backend = controller.wetState();
1097
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: `${panel} ${className ?? ""}`, "data-testid": "wet-panel", children: [
1098
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center justify-between", children: [
1099
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: wet.wet ? "text-sky-300" : "text-slate-400", "data-testid": "wet-status", children: wet.wet ? wet.paused ? "Wet (paused)" : "Wet" : "Dry" }),
1100
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "text-[10px] text-slate-500", children: [
1240
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: `${panel} ${className ?? ""}`, "data-testid": "wet-panel", children: [
1241
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex items-center justify-between", children: [
1242
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: wet.wet ? "text-sky-300" : "text-slate-400", "data-testid": "wet-status", children: wet.wet ? wet.paused ? "Wet (paused)" : "Wet" : "Dry" }),
1243
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("span", { className: "text-[10px] text-slate-500", children: [
1101
1244
  "step ",
1102
1245
  wet.clock
1103
1246
  ] })
1104
1247
  ] }),
1105
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
1106
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: button, disabled: !wet.layerIds.length, onClick: () => attempt(async () => (await controller.wetIdle(), controller.dryPaint())), children: "Dry now" }),
1107
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: chip(wet.paused), "aria-pressed": wet.paused, onClick: () => controller.setWetPaused(!wet.paused), children: wet.paused ? "Resume" : "Pause" }),
1108
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: chip(wet.overlay), "aria-pressed": wet.overlay, onClick: () => controller.setWaterOverlay(!wet.overlay), children: "Water" }),
1109
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: chip(wet.instantDry), "aria-pressed": wet.instantDry, title: "Each wet stroke dries as soon as it ends", onClick: () => controller.setInstantDry(!wet.instantDry), children: "Instant dry" }),
1110
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: button, disabled: !layerId, onClick: () => attempt(async () => (await controller.wetIdle(), controller.wetPaper({}))), children: "Wet paper" })
1248
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
1249
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: button, disabled: !wet.layerIds.length, onClick: () => attempt(async () => (await controller.wetIdle(), controller.dryPaint())), children: "Dry now" }),
1250
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: chip(wet.paused), "aria-pressed": wet.paused, onClick: () => controller.setWetPaused(!wet.paused), children: wet.paused ? "Resume" : "Pause" }),
1251
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: chip(wet.overlay), "aria-pressed": wet.overlay, onClick: () => controller.setWaterOverlay(!wet.overlay), children: "Water" }),
1252
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: chip(wet.instantDry), "aria-pressed": wet.instantDry, title: "Each wet stroke dries as soon as it ends", onClick: () => controller.setInstantDry(!wet.instantDry), children: "Instant dry" }),
1253
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", className: button, disabled: !layerId, onClick: () => attempt(async () => (await controller.wetIdle(), controller.wetPaper({}))), children: "Wet paper" })
1111
1254
  ] }),
1112
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PaintSlider, { control: import_paint_editor8.WET_SPEED_SLIDER, value: wet.speed, onChange: (v) => controller.setWetSpeed(v) }),
1113
- import_paint_editor8.WET_TILT_SLIDERS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor8.readPath)(wet.tilt, c.path), onCommit: (v) => attempt(async () => (await controller.wetIdle(), controller.setCanvasTilt({ [c.path]: v }))) }, c.path)),
1114
- info && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex flex-col gap-1.5", "data-testid": "wet-layer", children: [
1115
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "font-medium text-slate-300", children: "This layer" }),
1116
- import_paint_editor8.WET_PARAM_SLIDERS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor8.readPath)(info.params, c.path), onCommit: (v) => attempt(async () => (await controller.wetIdle(), controller.setWetParams(layerId, { [c.path]: v }))) }, c.path)),
1117
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
1118
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "text-slate-400", children: "Flow map" }),
1119
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("select", { "aria-label": "Flow map", value: info.flow.kind, onChange: (e) => attempt(async () => (await controller.wetIdle(), controller.setFlowMap(layerId, { kind: e.target.value }))), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor8.WET_FLOW_KINDS.map((k) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("option", { value: k, children: FLOW_LABELS[k] }, k)) })
1255
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PaintSlider, { control: import_paint_editor11.WET_SPEED_SLIDER, value: wet.speed, onChange: (v) => controller.setWetSpeed(v) }),
1256
+ import_paint_editor11.WET_TILT_SLIDERS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor11.readPath)(wet.tilt, c.path), onCommit: (v) => attempt(async () => (await controller.wetIdle(), controller.setCanvasTilt({ [c.path]: v }))) }, c.path)),
1257
+ info && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex flex-col gap-1.5", "data-testid": "wet-layer", children: [
1258
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "font-medium text-slate-300", children: "This layer" }),
1259
+ import_paint_editor11.WET_PARAM_SLIDERS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor11.readPath)(info.params, c.path), onCommit: (v) => attempt(async () => (await controller.wetIdle(), controller.setWetParams(layerId, { [c.path]: v }))) }, c.path)),
1260
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
1261
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "text-slate-400", children: "Flow map" }),
1262
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("select", { "aria-label": "Flow map", value: info.flow.kind, onChange: (e) => attempt(async () => (await controller.wetIdle(), controller.setFlowMap(layerId, { kind: e.target.value }))), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor11.WET_FLOW_KINDS.map((k) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("option", { value: k, children: FLOW_LABELS[k] }, k)) })
1120
1263
  ] }),
1121
- info.flow.kind !== "none" && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
1264
+ info.flow.kind !== "none" && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
1122
1265
  PaintSlider,
1123
1266
  {
1124
1267
  control: { path: "strength", label: "Flow strength", min: 0, max: 1, step: 0.01 },
@@ -1127,73 +1270,78 @@ var WetPanel = ({ controller, className }) => {
1127
1270
  }
1128
1271
  )
1129
1272
  ] }),
1130
- backend.message && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-[10px] text-amber-300", "data-testid": "wet-message", children: backend.message }),
1131
- backend.backend && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { className: "text-[10px] text-slate-500", children: backend.backend.kind === "wasm-threads" ? `WebAssembly, ${backend.backend.threads} threads` : backend.backend.kind === "wasm" ? "WebAssembly, one thread" : "JavaScript" }),
1132
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ErrorLine, { error })
1273
+ backend.message && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-[10px] text-amber-300", "data-testid": "wet-message", children: backend.message }),
1274
+ backend.backend && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: "text-[10px] text-slate-500", children: backend.backend.kind === "wasm-threads" ? `WebAssembly, ${backend.backend.threads} threads` : backend.backend.kind === "wasm" ? "WebAssembly, one thread" : "JavaScript" }),
1275
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
1276
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "text-slate-400", children: "Engine" }),
1277
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("select", { "aria-label": "Wet engine", value: wet.gpu, onChange: (e) => attempt(() => controller.setWetEngine({ gpu: e.target.value })), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor11.WET_GPU_OPTIONS.map((o) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("option", { value: o.value, children: o.label }, o.value)) })
1278
+ ] }),
1279
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { className: `text-[10px] ${backend.engine.state === "active" ? "text-emerald-300" : "text-slate-500"}`, "data-testid": "wet-engine", children: (0, import_paint_editor11.wetEngineSummary)(backend.engine) }),
1280
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ErrorLine, { error })
1133
1281
  ] });
1134
1282
  };
1135
1283
 
1136
1284
  // src/paint-editor/StickerPanel.tsx
1137
- var import_react9 = require("react");
1138
- var import_paint_editor9 = require("@pixerate/editor/paint-editor");
1139
- var import_jsx_runtime9 = require("react/jsx-runtime");
1285
+ var import_react10 = require("react");
1286
+ var import_paint_editor12 = require("@pixerate/editor/paint-editor");
1287
+ var import_jsx_runtime11 = require("react/jsx-runtime");
1140
1288
  var StickerPanel = ({ controller, className }) => {
1141
1289
  const state = usePaintEditorState(controller);
1142
1290
  const [error, attempt] = useAttempt();
1143
- const [picked, setPicked] = (0, import_react9.useState)(null);
1291
+ const [picked, setPicked] = (0, import_react10.useState)(null);
1144
1292
  const layerId = state.document.activeLayerId;
1145
1293
  const stickers = layerId ? state.document.stickers?.[layerId] ?? [] : [];
1146
1294
  if (!stickers.length) return null;
1147
1295
  const sel = stickers.find((s) => s.id === picked) ?? null;
1148
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: `${panel} ${className ?? ""}`, "data-testid": "sticker-panel", children: [
1149
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("span", { className: "font-medium text-slate-300", children: [
1296
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: `${panel} ${className ?? ""}`, "data-testid": "sticker-panel", children: [
1297
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("span", { className: "font-medium text-slate-300", children: [
1150
1298
  "Stickers (",
1151
1299
  stickers.length,
1152
1300
  ")"
1153
1301
  ] }),
1154
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "flex max-h-24 flex-wrap gap-1 overflow-y-auto", role: "listbox", "aria-label": "Stickers", children: stickers.map((s) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", role: "option", "aria-selected": s.id === picked, title: `${import_paint_editor9.STAMP_LABELS[s.stamp]} ${s.id}`, className: `h-5 w-5 rounded ring-1 ${s.id === picked ? "ring-indigo-400" : "ring-slate-700"}`, style: { background: s.color }, onClick: () => setPicked(s.id) }, s.id)) }),
1155
- sel && /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex flex-col gap-1.5", "data-testid": "sticker-edit", children: [
1156
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
1157
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-slate-400", children: "Stamp" }),
1158
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("select", { "aria-label": "Sticker stamp", value: sel.stamp, onChange: (e) => attempt(() => controller.updateSticker(layerId, sel.id, { stamp: e.target.value })), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor9.STAMP_KINDS.map((k) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("option", { value: k, children: import_paint_editor9.STAMP_LABELS[k] }, k)) })
1302
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "flex max-h-24 flex-wrap gap-1 overflow-y-auto", role: "listbox", "aria-label": "Stickers", children: stickers.map((s) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", role: "option", "aria-selected": s.id === picked, title: `${import_paint_editor12.STAMP_LABELS[s.stamp]} ${s.id}`, className: `h-5 w-5 rounded ring-1 ${s.id === picked ? "ring-indigo-400" : "ring-slate-700"}`, style: { background: s.color }, onClick: () => setPicked(s.id) }, s.id)) }),
1303
+ sel && /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-col gap-1.5", "data-testid": "sticker-edit", children: [
1304
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
1305
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "text-slate-400", children: "Stamp" }),
1306
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("select", { "aria-label": "Sticker stamp", value: sel.stamp, onChange: (e) => attempt(() => controller.updateSticker(layerId, sel.id, { stamp: e.target.value })), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor12.STAMP_KINDS.map((k) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("option", { value: k, children: import_paint_editor12.STAMP_LABELS[k] }, k)) })
1159
1307
  ] }),
1160
- import_paint_editor9.STICKER_SLIDERS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor9.readPath)(sel, c.path), onCommit: (v) => attempt(() => controller.updateSticker(layerId, sel.id, { [c.path]: v })) }, c.path)),
1161
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("label", { className: "flex items-center justify-between", children: [
1162
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-slate-400", children: "Colour" }),
1163
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(CommitColorInput, { label: "Sticker colour", value: sel.color, onCommit: (v) => attempt(() => controller.updateSticker(layerId, sel.id, { color: v })), className: "h-6 w-10 rounded bg-transparent" })
1308
+ import_paint_editor12.STICKER_SLIDERS.map((c) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor12.readPath)(sel, c.path), onCommit: (v) => attempt(() => controller.updateSticker(layerId, sel.id, { [c.path]: v })) }, c.path)),
1309
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("label", { className: "flex items-center justify-between", children: [
1310
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "text-slate-400", children: "Colour" }),
1311
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(CommitColorInput, { label: "Sticker colour", value: sel.color, onCommit: (v) => attempt(() => controller.updateSticker(layerId, sel.id, { color: v })), className: "h-6 w-10 rounded bg-transparent" })
1164
1312
  ] }),
1165
- /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
1166
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => controller.arrangeStickers(layerId, [sel.id], "front")), children: "To front" }),
1167
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => controller.arrangeStickers(layerId, [sel.id], "back")), children: "To back" }),
1168
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => (controller.removeStickers(layerId, [sel.id]), setPicked(null))), children: "Delete" })
1313
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "flex flex-wrap gap-1", children: [
1314
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => controller.arrangeStickers(layerId, [sel.id], "front")), children: "To front" }),
1315
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => controller.arrangeStickers(layerId, [sel.id], "back")), children: "To back" }),
1316
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => (controller.removeStickers(layerId, [sel.id]), setPicked(null))), children: "Delete" })
1169
1317
  ] })
1170
1318
  ] }),
1171
- /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ErrorLine, { error })
1319
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(ErrorLine, { error })
1172
1320
  ] });
1173
1321
  };
1174
1322
 
1175
1323
  // src/paint-editor/PaintEditor.tsx
1176
- var import_react10 = require("react");
1177
- var import_jsx_runtime10 = require("react/jsx-runtime");
1324
+ var import_react11 = require("react");
1325
+ var import_jsx_runtime12 = require("react/jsx-runtime");
1178
1326
  var PaintEditor = ({ controller, fileName, height = 720, presence, className }) => {
1179
- const [tool, setTool] = (0, import_react10.useState)("brush");
1180
- const [tab, setTab] = (0, import_react10.useState)("color");
1181
- const [error, setError] = (0, import_react10.useState)(null);
1182
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: `flex flex-col gap-2 rounded-xl border border-slate-800 bg-slate-900 p-2 ${className ?? ""}`, style: { height }, "data-testid": "paint-editor", children: [
1183
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PaintToolbar, { controller, tool, onToolChange: setTool, fileName }),
1184
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex min-h-0 flex-1 gap-2", children: [
1185
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(BrushPanel, { controller, className: "w-60 shrink-0 overflow-y-auto pr-1" }),
1186
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col", children: [
1187
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PaintCanvas, { controller, tool, onError: setError, presence, className: "flex-1 rounded-lg" }),
1188
- error && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("p", { role: "alert", className: "mt-1 text-[11px] text-rose-400", children: error })
1327
+ const [tool, setTool] = (0, import_react11.useState)("brush");
1328
+ const [tab, setTab] = (0, import_react11.useState)("color");
1329
+ const [error, setError] = (0, import_react11.useState)(null);
1330
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: `flex flex-col gap-2 rounded-xl border border-slate-800 bg-slate-900 p-2 ${className ?? ""}`, style: { height }, "data-testid": "paint-editor", children: [
1331
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PaintToolbar, { controller, tool, onToolChange: setTool, fileName }),
1332
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex min-h-0 flex-1 gap-2", children: [
1333
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(BrushPanel, { controller, className: "w-60 shrink-0 overflow-y-auto pr-1" }),
1334
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col", children: [
1335
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PaintCanvas, { controller, tool, onError: setError, presence, className: "flex-1 rounded-lg" }),
1336
+ error && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("p", { role: "alert", className: "mt-1 text-[11px] text-rose-400", children: error })
1189
1337
  ] }),
1190
- /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex w-64 shrink-0 flex-col gap-2 overflow-y-auto pr-1", children: [
1191
- /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "flex gap-1", role: "tablist", "aria-label": "Paint panels", children: ["color", "wet", "paper", "layers"].map((t) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("button", { type: "button", role: "tab", "aria-selected": tab === t, className: chip(tab === t), onClick: () => setTab(t), children: t === "color" ? "Colour" : t === "wet" ? "Wet" : t === "paper" ? "Paper" : "Layers" }, t)) }),
1192
- tab === "color" && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ColorPanel, { controller }),
1193
- tab === "wet" && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(WetPanel, { controller }),
1194
- tab === "paper" && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PaperPanel, { controller }),
1195
- tab === "layers" && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(PaintLayersPanel, { controller }),
1196
- tab === "layers" && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(StickerPanel, { controller })
1338
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "flex w-64 shrink-0 flex-col gap-2 overflow-y-auto pr-1", children: [
1339
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "flex gap-1", role: "tablist", "aria-label": "Paint panels", children: ["color", "wet", "paper", "layers"].map((t) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("button", { type: "button", role: "tab", "aria-selected": tab === t, className: chip(tab === t), onClick: () => setTab(t), children: t === "color" ? "Colour" : t === "wet" ? "Wet" : t === "paper" ? "Paper" : "Layers" }, t)) }),
1340
+ tab === "color" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(ColorPanel, { controller }),
1341
+ tab === "wet" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(WetPanel, { controller }),
1342
+ tab === "paper" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PaperPanel, { controller }),
1343
+ tab === "layers" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(PaintLayersPanel, { controller }),
1344
+ tab === "layers" && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(StickerPanel, { controller })
1197
1345
  ] })
1198
1346
  ] })
1199
1347
  ] });
@@ -1204,6 +1352,7 @@ var PaintEditor = ({ controller, fileName, height = 720, presence, className })
1204
1352
  ColorPanel,
1205
1353
  PaintCanvas,
1206
1354
  PaintEditor,
1355
+ PaintGradientEditor,
1207
1356
  PaintLayersPanel,
1208
1357
  PaintPresenceLayer,
1209
1358
  PaintSlider,