@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.
@@ -1,3 +1,7 @@
1
+ import {
2
+ PaintGradientEditorView
3
+ } from "../chunk-MO354USR.js";
4
+
1
5
  // src/paint-editor/usePaintEditor.ts
2
6
  import { useMemo, useState, useSyncExternalStore } from "react";
3
7
  import { PaintDirector, PaintEditorController, PaintPresence } from "@pixerate/editor/paint-editor";
@@ -550,7 +554,7 @@ import {
550
554
  brushEngineControls,
551
555
  brushLoadColor,
552
556
  downloadBytes as downloadBytes2,
553
- multiColorSliders,
557
+ brushTakesPaint,
554
558
  patchBrushValue,
555
559
  patchPath,
556
560
  readPath,
@@ -568,7 +572,15 @@ import {
568
572
  STAMP_LABELS,
569
573
  toggleStamp
570
574
  } from "@pixerate/editor/paint-editor";
571
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
575
+ import { DEFAULT_PAINT_GRADIENT } from "@pixerate/editor/paint-editor";
576
+
577
+ // src/paint-editor/PaintGradientEditor.tsx
578
+ import { PAINT_GRADIENT_KIT } from "@pixerate/editor/paint-editor";
579
+ import { jsx as jsx4 } from "react/jsx-runtime";
580
+ var PaintGradientEditor = (props) => /* @__PURE__ */ jsx4(PaintGradientEditorView, { ...props, kit: PAINT_GRADIENT_KIT });
581
+
582
+ // src/paint-editor/BrushPanel.tsx
583
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
572
584
  var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, className }) => {
573
585
  const state = usePaintEditorState(controller);
574
586
  const [error, attempt] = useAttempt();
@@ -583,12 +595,10 @@ var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, clas
583
595
  [controller, brushes, paper, thumbnailWidth, thumbnailHeight]
584
596
  );
585
597
  const engine = brushEngineControls(brush);
586
- const mc = brush.behavior.multicolor;
587
598
  const carried = brushLoadColor(state.mixerLoad);
588
- const setColors = (colors) => controller.setBrush({ behavior: { multicolor: { colors } } });
589
599
  const slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "brush";
590
600
  return /* @__PURE__ */ jsxs4("div", { className: `${panel} ${className ?? ""}`, "data-testid": "brush-panel", children: [
591
- /* @__PURE__ */ jsx4("div", { className: "grid grid-cols-2 gap-1", role: "listbox", "aria-label": "Brushes", children: brushes.map((b) => /* @__PURE__ */ jsxs4(
601
+ /* @__PURE__ */ jsx5("div", { className: "grid grid-cols-2 gap-1", role: "listbox", "aria-label": "Brushes", children: brushes.map((b) => /* @__PURE__ */ jsxs4(
592
602
  "button",
593
603
  {
594
604
  type: "button",
@@ -598,52 +608,52 @@ var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, clas
598
608
  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"}`,
599
609
  onClick: () => controller.setBrush({ id: b.id }),
600
610
  children: [
601
- /* @__PURE__ */ jsx4(PixelCanvas, { pixels: thumbs.get(b.id) ?? null, label: `${b.name} thumbnail`, className: "rounded", style: { width: "100%", aspectRatio: `${thumbnailWidth} / ${thumbnailHeight}` } }),
602
- /* @__PURE__ */ jsx4("span", { className: "truncate text-[10px] text-slate-300", children: b.name })
611
+ /* @__PURE__ */ jsx5(PixelCanvas, { pixels: thumbs.get(b.id) ?? null, label: `${b.name} thumbnail`, className: "rounded", style: { width: "100%", aspectRatio: `${thumbnailWidth} / ${thumbnailHeight}` } }),
612
+ /* @__PURE__ */ jsx5("span", { className: "truncate text-[10px] text-slate-300", children: b.name })
603
613
  ]
604
614
  },
605
615
  b.id
606
616
  )) }),
607
617
  /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-1.5", children: [
608
- BRUSH_SLIDERS.map((c) => /* @__PURE__ */ jsx4(PaintSlider, { control: c, value: readPath(brush, c.path), onChange: (v) => controller.setBrush(patchPath(brush, c.path, v)) }, c.path)),
618
+ BRUSH_SLIDERS.map((c) => /* @__PURE__ */ jsx5(PaintSlider, { control: c, value: readPath(brush, c.path), onChange: (v) => controller.setBrush(patchPath(brush, c.path, v)) }, c.path)),
609
619
  /* @__PURE__ */ jsxs4("label", { className: "flex items-center justify-between gap-2", children: [
610
- /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "Rotation" }),
611
- /* @__PURE__ */ jsx4("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: BRUSH_ROTATIONS.map((r) => /* @__PURE__ */ jsx4("option", { value: r, children: r === "stroke" ? "Follow stroke" : r === "barrel" ? "Pen barrel" : "Fixed" }, r)) })
620
+ /* @__PURE__ */ jsx5("span", { className: "text-slate-400", children: "Rotation" }),
621
+ /* @__PURE__ */ jsx5("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: BRUSH_ROTATIONS.map((r) => /* @__PURE__ */ jsx5("option", { value: r, children: r === "stroke" ? "Follow stroke" : r === "barrel" ? "Pen barrel" : "Fixed" }, r)) })
612
622
  ] }),
613
623
  /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-2", children: [
614
- /* @__PURE__ */ jsx4("input", { type: "checkbox", checked: brush.tip.pressureSize, onChange: (e) => controller.setBrush({ tip: { pressureSize: e.target.checked } }) }),
624
+ /* @__PURE__ */ jsx5("input", { type: "checkbox", checked: brush.tip.pressureSize, onChange: (e) => controller.setBrush({ tip: { pressureSize: e.target.checked } }) }),
615
625
  " Pressure \u2192 size"
616
626
  ] }),
617
627
  /* @__PURE__ */ jsxs4("label", { className: "flex items-center gap-2", children: [
618
- /* @__PURE__ */ jsx4("input", { type: "checkbox", checked: brush.tip.pressureOpacity, onChange: (e) => controller.setBrush({ tip: { pressureOpacity: e.target.checked } }) }),
628
+ /* @__PURE__ */ jsx5("input", { type: "checkbox", checked: brush.tip.pressureOpacity, onChange: (e) => controller.setBrush({ tip: { pressureOpacity: e.target.checked } }) }),
619
629
  " Pressure \u2192 opacity"
620
630
  ] }),
621
631
  brush.engine === "dab" && /* @__PURE__ */ jsxs4("label", { className: "flex items-center justify-between gap-2", children: [
622
- /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "Blend" }),
623
- /* @__PURE__ */ jsx4("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: BLEND_MENU_GROUPS.map((g) => /* @__PURE__ */ jsx4("optgroup", { label: g.label, children: g.modes.map((m) => /* @__PURE__ */ jsx4("option", { value: m.id, title: m.description, children: m.label }, m.id)) }, g.label)) })
632
+ /* @__PURE__ */ jsx5("span", { className: "text-slate-400", children: "Blend" }),
633
+ /* @__PURE__ */ jsx5("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: BLEND_MENU_GROUPS.map((g) => /* @__PURE__ */ jsx5("optgroup", { label: g.label, children: g.modes.map((m) => /* @__PURE__ */ jsx5("option", { value: m.id, title: m.description, children: m.label }, m.id)) }, g.label)) })
624
634
  ] })
625
635
  ] }),
626
636
  engine && /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-1.5", "data-testid": "brush-engine", children: [
627
- /* @__PURE__ */ jsx4("span", { className: "font-medium text-slate-300", children: engine.title }),
637
+ /* @__PURE__ */ jsx5("span", { className: "font-medium text-slate-300", children: engine.title }),
628
638
  engine.wetTool && /* @__PURE__ */ jsxs4("label", { className: "flex items-center justify-between gap-2", children: [
629
- /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "Wet tool" }),
630
- /* @__PURE__ */ jsx4("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: WET_TOOLS.map((t) => /* @__PURE__ */ jsx4("option", { value: t, children: WET_TOOL_LABELS[t] }, t)) })
639
+ /* @__PURE__ */ jsx5("span", { className: "text-slate-400", children: "Wet tool" }),
640
+ /* @__PURE__ */ jsx5("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: WET_TOOLS.map((t) => /* @__PURE__ */ jsx5("option", { value: t, children: WET_TOOL_LABELS[t] }, t)) })
631
641
  ] }),
632
642
  engine.wetTool && /* @__PURE__ */ jsxs4("label", { className: "flex items-center justify-between gap-2", children: [
633
- /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "Medium" }),
634
- /* @__PURE__ */ jsx4("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: WET_MEDIA.map((m) => /* @__PURE__ */ jsx4("option", { value: m, children: WET_MEDIUM_LABELS[m] }, m)) })
643
+ /* @__PURE__ */ jsx5("span", { className: "text-slate-400", children: "Medium" }),
644
+ /* @__PURE__ */ jsx5("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: WET_MEDIA.map((m) => /* @__PURE__ */ jsx5("option", { value: m, children: WET_MEDIUM_LABELS[m] }, m)) })
635
645
  ] }),
636
646
  engine.particleMenus && /* @__PURE__ */ jsxs4(Fragment2, { children: [
637
647
  /* @__PURE__ */ jsxs4("label", { className: "flex items-center justify-between gap-2", children: [
638
- /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "Particles" }),
639
- /* @__PURE__ */ jsx4("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: PARTICLE_KINDS.map((k) => /* @__PURE__ */ jsx4("option", { value: k, children: PARTICLE_KIND_LABELS[k] }, k)) })
648
+ /* @__PURE__ */ jsx5("span", { className: "text-slate-400", children: "Particles" }),
649
+ /* @__PURE__ */ jsx5("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: PARTICLE_KINDS.map((k) => /* @__PURE__ */ jsx5("option", { value: k, children: PARTICLE_KIND_LABELS[k] }, k)) })
640
650
  ] }),
641
651
  /* @__PURE__ */ jsxs4("label", { className: "flex items-center justify-between gap-2", children: [
642
- /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "Drawn as" }),
643
- /* @__PURE__ */ jsx4("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: PARTICLE_RENDERS.map((k) => /* @__PURE__ */ jsx4("option", { value: k, children: PARTICLE_RENDER_LABELS[k] }, k)) })
652
+ /* @__PURE__ */ jsx5("span", { className: "text-slate-400", children: "Drawn as" }),
653
+ /* @__PURE__ */ jsx5("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: PARTICLE_RENDERS.map((k) => /* @__PURE__ */ jsx5("option", { value: k, children: PARTICLE_RENDER_LABELS[k] }, k)) })
644
654
  ] })
645
655
  ] }),
646
- engine.stickerStamps && /* @__PURE__ */ jsx4("div", { className: "flex flex-wrap gap-1", role: "group", "aria-label": "Sticker stamps", children: STAMP_KINDS.map((k) => /* @__PURE__ */ jsx4(
656
+ engine.stickerStamps && /* @__PURE__ */ jsx5("div", { className: "flex flex-wrap gap-1", role: "group", "aria-label": "Sticker stamps", children: STAMP_KINDS.map((k) => /* @__PURE__ */ jsx5(
647
657
  "button",
648
658
  {
649
659
  type: "button",
@@ -655,12 +665,12 @@ var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, clas
655
665
  k
656
666
  )) }),
657
667
  engine.impastoTool && /* @__PURE__ */ jsxs4("label", { className: "flex items-center justify-between gap-2", children: [
658
- /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "Tool" }),
659
- /* @__PURE__ */ jsx4("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: IMPASTO_TOOLS.map((t) => /* @__PURE__ */ jsx4("option", { value: t, children: IMPASTO_TOOL_LABELS[t] }, t)) })
668
+ /* @__PURE__ */ jsx5("span", { className: "text-slate-400", children: "Tool" }),
669
+ /* @__PURE__ */ jsx5("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: IMPASTO_TOOLS.map((t) => /* @__PURE__ */ jsx5("option", { value: t, children: IMPASTO_TOOL_LABELS[t] }, t)) })
660
670
  ] }),
661
- engine.sliders.map((c) => /* @__PURE__ */ jsx4(PaintSlider, { control: c, value: readPath(brush, c.path), onChange: (v) => controller.setBrush(patchPath(brush, c.path, v)) }, c.path)),
671
+ engine.sliders.map((c) => /* @__PURE__ */ jsx5(PaintSlider, { control: c, value: readPath(brush, c.path), onChange: (v) => controller.setBrush(patchPath(brush, c.path, v)) }, c.path)),
662
672
  engine.mixingPath && /* @__PURE__ */ jsxs4("label", { className: "flex items-center justify-between gap-2", children: [
663
- /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "Mixing" }),
673
+ /* @__PURE__ */ jsx5("span", { className: "text-slate-400", children: "Mixing" }),
664
674
  /* @__PURE__ */ jsxs4(
665
675
  "select",
666
676
  {
@@ -669,57 +679,45 @@ var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, clas
669
679
  onChange: (e) => controller.setBrush(patchBrushValue(brush, engine.mixingPath, e.target.value)),
670
680
  className: "rounded bg-slate-800 px-1 py-0.5",
671
681
  children: [
672
- /* @__PURE__ */ jsx4("option", { value: "pigment", children: "Pigment" }),
673
- /* @__PURE__ */ jsx4("option", { value: "rgb", children: "RGB" })
682
+ /* @__PURE__ */ jsx5("option", { value: "pigment", children: "Pigment" }),
683
+ /* @__PURE__ */ jsx5("option", { value: "rgb", children: "RGB" })
674
684
  ]
675
685
  }
676
686
  )
677
687
  ] }),
678
688
  engine.dirty && /* @__PURE__ */ jsxs4("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: [
679
- /* @__PURE__ */ jsx4("input", { type: "checkbox", "aria-label": "Dirty brush", checked: brush.behavior.dirty, onChange: (e) => controller.setBrush({ behavior: { dirty: e.target.checked } }) }),
689
+ /* @__PURE__ */ jsx5("input", { type: "checkbox", "aria-label": "Dirty brush", checked: brush.behavior.dirty, onChange: (e) => controller.setBrush({ behavior: { dirty: e.target.checked } }) }),
680
690
  " Dirty brush"
681
691
  ] }),
682
- engine.multicolor && /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-1.5", "data-testid": "multicolor", children: [
683
- /* @__PURE__ */ jsxs4("label", { className: "flex items-center justify-between gap-2", children: [
684
- /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "Multi-colour" }),
685
- /* @__PURE__ */ jsxs4("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: [
686
- /* @__PURE__ */ jsx4("option", { value: "off", children: "Off" }),
687
- /* @__PURE__ */ jsx4("option", { value: "tip", children: "Across the tip" }),
688
- /* @__PURE__ */ jsx4("option", { value: "dab", children: "Per dab" })
689
- ] })
692
+ brushTakesPaint(brush) && /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-1.5", "data-testid": "brush-gradient", children: [
693
+ /* @__PURE__ */ jsxs4("div", { className: "flex items-center justify-between gap-2", children: [
694
+ /* @__PURE__ */ jsx5("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" }),
695
+ /* @__PURE__ */ jsx5(
696
+ "button",
697
+ {
698
+ type: "button",
699
+ "aria-pressed": brush.paint !== null,
700
+ className: chip(brush.paint !== null),
701
+ onClick: () => attempt(() => controller.setBrush({ paint: brush.paint ? null : DEFAULT_PAINT_GRADIENT })),
702
+ children: brush.paint ? "On" : "Off"
703
+ }
704
+ )
690
705
  ] }),
691
- mc.mode !== "off" && /* @__PURE__ */ jsxs4(Fragment2, { children: [
692
- /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-1", "aria-label": "Multi-colour colours", children: [
693
- mc.colors.map((col, i) => /* @__PURE__ */ jsxs4("span", { className: "flex items-center", children: [
694
- /* @__PURE__ */ jsx4("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" }),
695
- /* @__PURE__ */ jsx4("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" })
696
- ] }, i)),
697
- mc.colors.length < 8 && /* @__PURE__ */ jsx4("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: "+" }),
698
- mc.colors.length < 2 && /* @__PURE__ */ jsx4("span", { className: "text-[10px] text-slate-500", children: "Uses the paint and second colours" })
699
- ] }),
700
- mc.mode === "dab" && /* @__PURE__ */ jsxs4("label", { className: "flex items-center justify-between gap-2", children: [
701
- /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "Order" }),
702
- /* @__PURE__ */ jsxs4("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: [
703
- /* @__PURE__ */ jsx4("option", { value: "sequence", children: "In order" }),
704
- /* @__PURE__ */ jsx4("option", { value: "random", children: "Random" })
705
- ] })
706
- ] }),
707
- multiColorSliders(mc.mode).map((c) => /* @__PURE__ */ jsx4(PaintSlider, { control: c, value: readPath(brush, c.path), onChange: (v) => controller.setBrush(patchPath(brush, c.path, v)) }, c.path))
708
- ] })
706
+ brush.paint && /* @__PURE__ */ jsx5(PaintGradientEditor, { value: brush.paint, colors: { paint: state.color, secondary: state.secondaryColor }, onChange: (g) => attempt(() => controller.setBrush({ paint: g })) })
709
707
  ] }),
710
708
  engine.load && /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-1", children: [
711
- /* @__PURE__ */ jsx4("span", { className: "text-slate-400", children: "Carries" }),
712
- /* @__PURE__ */ jsx4("span", { "aria-label": "Brush load", title: carried ?? "Clean", className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: carried ?? "transparent" } }),
713
- brush.engine === "mixer" && /* @__PURE__ */ jsx4("button", { type: "button", className: button, onClick: () => controller.loadMixerBrush(), children: "Load" }),
714
- /* @__PURE__ */ jsx4("button", { type: "button", className: button, onClick: () => controller.cleanMixerBrush(brush.engine === "mixer"), children: "Clean" })
709
+ /* @__PURE__ */ jsx5("span", { className: "text-slate-400", children: "Carries" }),
710
+ /* @__PURE__ */ jsx5("span", { "aria-label": "Brush load", title: carried ?? "Clean", className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: carried ?? "transparent" } }),
711
+ brush.engine === "mixer" && /* @__PURE__ */ jsx5("button", { type: "button", className: button, onClick: () => controller.loadMixerBrush(), children: "Load" }),
712
+ /* @__PURE__ */ jsx5("button", { type: "button", className: button, onClick: () => controller.cleanMixerBrush(brush.engine === "mixer"), children: "Clean" })
715
713
  ] })
716
714
  ] }),
717
715
  /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-1", children: [
718
- /* @__PURE__ */ jsx4("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" }),
719
- /* @__PURE__ */ jsx4("button", { type: "button", className: button, disabled: !saveName.trim(), onClick: () => attempt(() => (controller.saveBrush({ id: slug(saveName), name: saveName.trim() }), setSaveName(""))), children: "Save brush" }),
720
- /* @__PURE__ */ jsx4("button", { type: "button", className: button, onClick: () => attempt(() => downloadBytes2(JSON.stringify(controller.exportBrushes(), null, 2), "brushes.json", "application/json")), children: "Export" }),
721
- /* @__PURE__ */ jsx4("button", { type: "button", className: button, onClick: () => fileRef.current?.click(), children: "Import\u2026" }),
722
- /* @__PURE__ */ jsx4(
716
+ /* @__PURE__ */ jsx5("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" }),
717
+ /* @__PURE__ */ jsx5("button", { type: "button", className: button, disabled: !saveName.trim(), onClick: () => attempt(() => (controller.saveBrush({ id: slug(saveName), name: saveName.trim() }), setSaveName(""))), children: "Save brush" }),
718
+ /* @__PURE__ */ jsx5("button", { type: "button", className: button, onClick: () => attempt(() => downloadBytes2(JSON.stringify(controller.exportBrushes(), null, 2), "brushes.json", "application/json")), children: "Export" }),
719
+ /* @__PURE__ */ jsx5("button", { type: "button", className: button, onClick: () => fileRef.current?.click(), children: "Import\u2026" }),
720
+ /* @__PURE__ */ jsx5(
723
721
  "input",
724
722
  {
725
723
  ref: fileRef,
@@ -735,14 +733,14 @@ var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, clas
735
733
  }
736
734
  )
737
735
  ] }),
738
- /* @__PURE__ */ jsx4(ErrorLine, { error })
736
+ /* @__PURE__ */ jsx5(ErrorLine, { error })
739
737
  ] });
740
738
  };
741
739
 
742
740
  // src/paint-editor/PaperPanel.tsx
743
741
  import { useMemo as useMemo4 } from "react";
744
742
  import { LIGHT_SLIDERS, PAPER_FAMILIES, PAPER_LIBRARY, PAPER_SLIDERS, patchPath as patchPath2, readPath as readPath2, renderPaper } from "@pixerate/editor/paint-editor";
745
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
743
+ import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
746
744
  var SWATCH = { x: 0, y: 0, width: 64, height: 40 };
747
745
  var PaperPanel = ({ controller, className }) => {
748
746
  const state = usePaintEditorState(controller);
@@ -751,8 +749,8 @@ var PaperPanel = ({ controller, className }) => {
751
749
  const previews = useMemo4(() => new Map(PAPER_LIBRARY.map((p) => [p.preset, { ...SWATCH, data: renderPaper({ ...p, shading: Math.max(0.8, p.shading) }, SWATCH) }])), []);
752
750
  return /* @__PURE__ */ jsxs5("div", { className: `${panel} ${className ?? ""}`, "data-testid": "paper-panel", children: [
753
751
  PAPER_FAMILIES.map((family) => /* @__PURE__ */ jsxs5("div", { children: [
754
- /* @__PURE__ */ jsx5("div", { className: "mb-0.5 text-[10px] uppercase tracking-wide text-slate-500", children: family }),
755
- /* @__PURE__ */ jsx5("div", { className: "flex flex-wrap gap-1", role: "listbox", "aria-label": `${family} papers`, children: PAPER_LIBRARY.filter((p) => p.family === family).map((p) => /* @__PURE__ */ jsx5(
752
+ /* @__PURE__ */ jsx6("div", { className: "mb-0.5 text-[10px] uppercase tracking-wide text-slate-500", children: family }),
753
+ /* @__PURE__ */ jsx6("div", { className: "flex flex-wrap gap-1", role: "listbox", "aria-label": `${family} papers`, children: PAPER_LIBRARY.filter((p) => p.family === family).map((p) => /* @__PURE__ */ jsx6(
756
754
  "button",
757
755
  {
758
756
  type: "button",
@@ -761,28 +759,28 @@ var PaperPanel = ({ controller, className }) => {
761
759
  title: p.name,
762
760
  className: `rounded p-0.5 ${paper.preset === p.preset ? "ring-2 ring-indigo-500" : "ring-1 ring-slate-700 hover:ring-slate-500"}`,
763
761
  onClick: () => attempt(() => controller.setPaper({ preset: p.preset })),
764
- children: /* @__PURE__ */ jsx5(PixelCanvas, { pixels: previews.get(p.preset) ?? null, label: p.name, style: { width: 48, height: 30 } })
762
+ children: /* @__PURE__ */ jsx6(PixelCanvas, { pixels: previews.get(p.preset) ?? null, label: p.name, style: { width: 48, height: 30 } })
765
763
  },
766
764
  p.preset
767
765
  )) })
768
766
  ] }, family)),
769
- /* @__PURE__ */ jsx5("div", { className: "text-slate-300", children: paper.name }),
767
+ /* @__PURE__ */ jsx6("div", { className: "text-slate-300", children: paper.name }),
770
768
  /* @__PURE__ */ jsxs5("label", { className: "flex items-center justify-between", children: [
771
- /* @__PURE__ */ jsx5("span", { className: "text-slate-400", children: "Colour" }),
772
- /* @__PURE__ */ jsx5(CommitColorInput, { label: "Paper colour", value: paper.color, onCommit: (v) => attempt(() => controller.setPaper({ color: v })), className: "h-6 w-10 rounded bg-transparent" })
769
+ /* @__PURE__ */ jsx6("span", { className: "text-slate-400", children: "Colour" }),
770
+ /* @__PURE__ */ jsx6(CommitColorInput, { label: "Paper colour", value: paper.color, onCommit: (v) => attempt(() => controller.setPaper({ color: v })), className: "h-6 w-10 rounded bg-transparent" })
773
771
  ] }),
774
- PAPER_SLIDERS.map((c) => /* @__PURE__ */ jsx5(PaintSlider, { control: c, value: readPath2(paper, c.path), onCommit: (v) => attempt(() => controller.setPaper(patchPath2(paper, c.path, v))) }, c.path)),
772
+ PAPER_SLIDERS.map((c) => /* @__PURE__ */ jsx6(PaintSlider, { control: c, value: readPath2(paper, c.path), onCommit: (v) => attempt(() => controller.setPaper(patchPath2(paper, c.path, v))) }, c.path)),
775
773
  /* @__PURE__ */ jsxs5("label", { className: "flex items-center gap-2", children: [
776
- /* @__PURE__ */ jsx5("input", { type: "checkbox", checked: paper.tiling === "mirror", onChange: (e) => attempt(() => controller.setPaper({ tiling: e.target.checked ? "mirror" : "repeat" })) }),
774
+ /* @__PURE__ */ jsx6("input", { type: "checkbox", checked: paper.tiling === "mirror", onChange: (e) => attempt(() => controller.setPaper({ tiling: e.target.checked ? "mirror" : "repeat" })) }),
777
775
  " Mirror tiling"
778
776
  ] }),
779
- /* @__PURE__ */ jsx5("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" }),
777
+ /* @__PURE__ */ jsx6("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" }),
780
778
  /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-1.5", "data-testid": "light-panel", children: [
781
- /* @__PURE__ */ jsx5("span", { className: "font-medium text-slate-300", children: "Light (thick paint, metallic paper)" }),
782
- LIGHT_SLIDERS.map((c) => /* @__PURE__ */ jsx5(PaintSlider, { control: c, value: readPath2(controller.getLighting(), c.path), onChange: (v) => attempt(() => controller.setLighting({ [c.path]: v })) }, c.path)),
783
- /* @__PURE__ */ jsx5("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" })
779
+ /* @__PURE__ */ jsx6("span", { className: "font-medium text-slate-300", children: "Light (thick paint, metallic paper)" }),
780
+ LIGHT_SLIDERS.map((c) => /* @__PURE__ */ jsx6(PaintSlider, { control: c, value: readPath2(controller.getLighting(), c.path), onChange: (v) => attempt(() => controller.setLighting({ [c.path]: v })) }, c.path)),
781
+ /* @__PURE__ */ jsx6("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" })
784
782
  ] }),
785
- /* @__PURE__ */ jsx5(ErrorLine, { error })
783
+ /* @__PURE__ */ jsx6(ErrorLine, { error })
786
784
  ] });
787
785
  };
788
786
 
@@ -803,7 +801,8 @@ import {
803
801
  wheelPick,
804
802
  wheelPoint
805
803
  } from "@pixerate/editor/paint-editor";
806
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
804
+ import { DEFAULT_PAINT_GRADIENT as DEFAULT_PAINT_GRADIENT2, paintSourceCss } from "@pixerate/editor/paint-editor";
805
+ import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
807
806
  var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className }) => {
808
807
  const state = usePaintEditorState(controller);
809
808
  const [error, attempt] = useAttempt();
@@ -833,7 +832,7 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
833
832
  };
834
833
  return /* @__PURE__ */ jsxs6("div", { className: `${panel} ${className ?? ""}`, "data-testid": "color-panel", children: [
835
834
  /* @__PURE__ */ jsxs6("div", { className: "relative self-center", style: { width: wheelSize, height: wheelSize }, children: [
836
- /* @__PURE__ */ jsx6(
835
+ /* @__PURE__ */ jsx7(
837
836
  "div",
838
837
  {
839
838
  role: "slider",
@@ -847,20 +846,20 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
847
846
  const rect = e.currentTarget.getBoundingClientRect();
848
847
  controller.setColor(wheelPick(e.clientX - rect.left - R, e.clientY - rect.top - R, wheel, color));
849
848
  },
850
- children: /* @__PURE__ */ jsx6(PixelCanvas, { pixels: ring, label: "Colour wheel", style: { width: wheelSize, height: wheelSize } })
849
+ children: /* @__PURE__ */ jsx7(PixelCanvas, { pixels: ring, label: "Colour wheel", style: { width: wheelSize, height: wheelSize } })
851
850
  }
852
851
  ),
853
- /* @__PURE__ */ jsx6("svg", { className: "pointer-events-none absolute inset-0", width: wheelSize, height: wheelSize, "aria-hidden": "true", children: harmony.map((h, i) => {
852
+ /* @__PURE__ */ jsx7("svg", { className: "pointer-events-none absolute inset-0", width: wheelSize, height: wheelSize, "aria-hidden": "true", children: harmony.map((h, i) => {
854
853
  const p = wheelPoint(h.angle, mid);
855
- return /* @__PURE__ */ jsx6("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);
854
+ return /* @__PURE__ */ jsx7("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);
856
855
  }) }),
857
- /* @__PURE__ */ jsx6("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 } })
856
+ /* @__PURE__ */ jsx7("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 } })
858
857
  ] }),
859
858
  /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", children: [
860
- /* @__PURE__ */ jsx6("input", { type: "color", "aria-label": "Paint colour", value: color, onChange: (e) => controller.setColor(e.target.value), className: "h-7 w-9 rounded bg-transparent" }),
861
- /* @__PURE__ */ jsx6("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" }),
862
- /* @__PURE__ */ jsx6("button", { type: "button", className: button, title: "Swap colours (X)", onClick: () => controller.swapColors(), children: "Swap" }),
863
- /* @__PURE__ */ jsx6(
859
+ /* @__PURE__ */ jsx7("input", { type: "color", "aria-label": "Paint colour", value: color, onChange: (e) => controller.setColor(e.target.value), className: "h-7 w-9 rounded bg-transparent" }),
860
+ /* @__PURE__ */ jsx7("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" }),
861
+ /* @__PURE__ */ jsx7("button", { type: "button", className: button, title: "Swap colours (X)", onClick: () => controller.swapColors(), children: "Swap" }),
862
+ /* @__PURE__ */ jsx7(
864
863
  "input",
865
864
  {
866
865
  "aria-label": "Hex colour",
@@ -871,35 +870,58 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
871
870
  color
872
871
  )
873
872
  ] }),
873
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2", "data-testid": "paint-source", children: [
874
+ /* @__PURE__ */ jsx7(
875
+ "span",
876
+ {
877
+ className: "h-5 flex-1 rounded ring-1 ring-slate-700",
878
+ "aria-label": state.paint.type === "gradient" ? "Gradient paint" : "Paint colour",
879
+ "data-testid": "paint-well",
880
+ style: { background: paintSourceCss(state.paint, { paint: color, secondary: state.secondaryColor }) }
881
+ }
882
+ ),
883
+ /* @__PURE__ */ jsx7(
884
+ "button",
885
+ {
886
+ type: "button",
887
+ className: chip(state.paint.type === "gradient"),
888
+ "aria-pressed": state.paint.type === "gradient",
889
+ title: "Paint that changes colour as the stroke goes",
890
+ onClick: () => attempt(() => state.paint.type === "gradient" ? controller.setColor(color) : controller.setPaint(DEFAULT_PAINT_GRADIENT2)),
891
+ children: "Gradient"
892
+ }
893
+ )
894
+ ] }),
895
+ state.paint.type === "gradient" && /* @__PURE__ */ jsx7(PaintGradientEditor, { value: state.paint, colors: { paint: color, secondary: state.secondaryColor }, onChange: (g2) => attempt(() => controller.setPaint(g2)) }),
874
896
  /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-2", children: [
875
- /* @__PURE__ */ jsx6("span", { className: "w-16 text-slate-400", children: "Saturation" }),
876
- /* @__PURE__ */ jsx6("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" })
897
+ /* @__PURE__ */ jsx7("span", { className: "w-16 text-slate-400", children: "Saturation" }),
898
+ /* @__PURE__ */ jsx7("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" })
877
899
  ] }),
878
900
  /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-2", children: [
879
- /* @__PURE__ */ jsx6("span", { className: "w-16 text-slate-400", children: "Value" }),
880
- /* @__PURE__ */ jsx6("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" })
901
+ /* @__PURE__ */ jsx7("span", { className: "w-16 text-slate-400", children: "Value" }),
902
+ /* @__PURE__ */ jsx7("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" })
881
903
  ] }),
882
904
  /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1", children: [
883
- /* @__PURE__ */ jsx6("select", { "aria-label": "Harmony", value: kind, onChange: (e) => setKind(e.target.value), className: "rounded bg-slate-800 px-1 py-0.5", children: HARMONY_KINDS.map((k) => /* @__PURE__ */ jsx6("option", { value: k, children: k }, k)) }),
884
- /* @__PURE__ */ jsx6("button", { type: "button", className: chip(wheel === "ryb"), "aria-pressed": wheel === "ryb", onClick: () => setWheel("ryb"), children: "RYB" }),
885
- /* @__PURE__ */ jsx6("button", { type: "button", className: chip(wheel === "rgb"), "aria-pressed": wheel === "rgb", onClick: () => setWheel("rgb"), children: "RGB" })
905
+ /* @__PURE__ */ jsx7("select", { "aria-label": "Harmony", value: kind, onChange: (e) => setKind(e.target.value), className: "rounded bg-slate-800 px-1 py-0.5", children: HARMONY_KINDS.map((k) => /* @__PURE__ */ jsx7("option", { value: k, children: k }, k)) }),
906
+ /* @__PURE__ */ jsx7("button", { type: "button", className: chip(wheel === "ryb"), "aria-pressed": wheel === "ryb", onClick: () => setWheel("ryb"), children: "RYB" }),
907
+ /* @__PURE__ */ jsx7("button", { type: "button", className: chip(wheel === "rgb"), "aria-pressed": wheel === "rgb", onClick: () => setWheel("rgb"), children: "RGB" })
886
908
  ] }),
887
909
  /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap gap-1", "aria-label": "Harmony colours", children: [
888
- harmony.map((h, i) => /* @__PURE__ */ jsx6("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)),
889
- /* @__PURE__ */ jsx6("button", { type: "button", className: button, onClick: () => harmony.forEach((h) => controller.addSwatch(h.color)), children: "+ swatches" })
910
+ harmony.map((h, i) => /* @__PURE__ */ jsx7("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)),
911
+ /* @__PURE__ */ jsx7("button", { type: "button", className: button, onClick: () => harmony.forEach((h) => controller.addSwatch(h.color)), children: "+ swatches" })
890
912
  ] }),
891
913
  /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap gap-1", role: "listbox", "aria-label": "Swatches", children: [
892
- state.swatches.map((s, i) => /* @__PURE__ */ jsx6(
914
+ state.swatches.map((s, i) => /* @__PURE__ */ jsx7(
893
915
  "button",
894
916
  {
895
917
  type: "button",
896
918
  role: "option",
897
- "aria-selected": s.color === color,
898
- "aria-label": s.name ?? s.color,
899
- title: `${s.name ?? s.color} (right-click to remove)`,
919
+ "aria-selected": s.paint ? state.paint === s.paint : state.paint.type === "color" && s.color === color,
920
+ "aria-label": s.name ?? (s.paint ? "Gradient" : s.color),
921
+ title: `${s.name ?? (s.paint ? "Gradient" : s.color)} (right-click to remove)`,
900
922
  className: "h-5 w-5 rounded ring-1 ring-slate-700",
901
- style: { background: s.color },
902
- onClick: () => controller.setColor(s.color),
923
+ style: { background: s.paint ? paintSourceCss(s.paint, { paint: color, secondary: state.secondaryColor }) : s.color },
924
+ onClick: () => attempt(() => s.paint ? controller.setPaint(s.paint) : controller.setColor(s.color)),
903
925
  onContextMenu: (e) => {
904
926
  e.preventDefault();
905
927
  controller.removeSwatch(i);
@@ -907,15 +929,15 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
907
929
  },
908
930
  i
909
931
  )),
910
- /* @__PURE__ */ jsx6("button", { type: "button", className: button, "aria-label": "Add swatch", onClick: () => controller.addSwatch(color), children: "+" })
932
+ /* @__PURE__ */ jsx7("button", { type: "button", className: button, "aria-label": "Add swatch", onClick: () => attempt(() => controller.addSwatch(state.paint)), children: "+" })
911
933
  ] }),
912
934
  mixingPad && /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-1", "data-testid": "mixing-pad", children: [
913
935
  /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1", children: [
914
- /* @__PURE__ */ jsx6("span", { className: "text-slate-400", children: "Mixing pad" }),
915
- ["paint", "knife", "pick"].map((t) => /* @__PURE__ */ jsx6("button", { type: "button", "aria-pressed": padTool === t, className: chip(padTool === t), onClick: () => setPadTool(t), children: t === "paint" ? "Paint" : t === "knife" ? "Knife" : "Pick" }, t)),
916
- /* @__PURE__ */ jsx6("button", { type: "button", className: button, onClick: () => controller.clearMixingPad(), children: "Clear" })
936
+ /* @__PURE__ */ jsx7("span", { className: "text-slate-400", children: "Mixing pad" }),
937
+ ["paint", "knife", "pick"].map((t) => /* @__PURE__ */ jsx7("button", { type: "button", "aria-pressed": padTool === t, className: chip(padTool === t), onClick: () => setPadTool(t), children: t === "paint" ? "Paint" : t === "knife" ? "Knife" : "Pick" }, t)),
938
+ /* @__PURE__ */ jsx7("button", { type: "button", className: button, onClick: () => controller.clearMixingPad(), children: "Clear" })
917
939
  ] }),
918
- /* @__PURE__ */ jsx6(
940
+ /* @__PURE__ */ jsx7(
919
941
  "div",
920
942
  {
921
943
  role: "application",
@@ -936,20 +958,20 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
936
958
  padPoints.current = null;
937
959
  if (pts?.length) attempt(() => controller.mixingPadStroke(pts, { tool: padTool === "knife" ? "knife" : "paint" }));
938
960
  },
939
- children: /* @__PURE__ */ jsx6(PixelCanvas, { pixels: pad, label: "Mixing pad pixels", className: "w-full rounded", style: { aspectRatio: `${MIXING_PAD_WIDTH} / ${MIXING_PAD_HEIGHT}` } })
961
+ children: /* @__PURE__ */ jsx7(PixelCanvas, { pixels: pad, label: "Mixing pad pixels", className: "w-full rounded", style: { aspectRatio: `${MIXING_PAD_WIDTH} / ${MIXING_PAD_HEIGHT}` } })
940
962
  }
941
963
  ),
942
- /* @__PURE__ */ jsx6("div", { className: "flex flex-wrap items-center gap-1", children: /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-1", children: [
943
- /* @__PURE__ */ jsx6("span", { className: "text-slate-400", children: "Sample" }),
944
- /* @__PURE__ */ jsx6("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: PAD_SAMPLE_SIZES.map((n) => /* @__PURE__ */ jsx6("option", { value: n, children: n === 1 ? "1 px" : `${n}\xD7${n}` }, n)) })
964
+ /* @__PURE__ */ jsx7("div", { className: "flex flex-wrap items-center gap-1", children: /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-1", children: [
965
+ /* @__PURE__ */ jsx7("span", { className: "text-slate-400", children: "Sample" }),
966
+ /* @__PURE__ */ jsx7("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: PAD_SAMPLE_SIZES.map((n) => /* @__PURE__ */ jsx7("option", { value: n, children: n === 1 ? "1 px" : `${n}\xD7${n}` }, n)) })
945
967
  ] }) }),
946
968
  /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap items-center gap-1", children: [
947
- /* @__PURE__ */ jsx6("span", { className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: color } }),
948
- /* @__PURE__ */ jsx6("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" }),
949
- /* @__PURE__ */ jsx6("span", { className: "text-slate-500", children: ":" }),
950
- /* @__PURE__ */ jsx6("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" }),
951
- /* @__PURE__ */ jsx6("span", { className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: state.secondaryColor } }),
952
- /* @__PURE__ */ jsx6(
969
+ /* @__PURE__ */ jsx7("span", { className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: color } }),
970
+ /* @__PURE__ */ jsx7("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" }),
971
+ /* @__PURE__ */ jsx7("span", { className: "text-slate-500", children: ":" }),
972
+ /* @__PURE__ */ jsx7("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" }),
973
+ /* @__PURE__ */ jsx7("span", { className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: state.secondaryColor } }),
974
+ /* @__PURE__ */ jsx7(
953
975
  "button",
954
976
  {
955
977
  type: "button",
@@ -969,22 +991,22 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
969
991
  )
970
992
  ] }),
971
993
  /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap items-center gap-1", children: [
972
- /* @__PURE__ */ jsx6("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" }),
973
- /* @__PURE__ */ jsx6("button", { type: "button", className: button, onClick: () => attempt(() => (controller.saveMixingPad(padName || void 0), setPadName(""))), children: "Keep pad" })
994
+ /* @__PURE__ */ jsx7("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" }),
995
+ /* @__PURE__ */ jsx7("button", { type: "button", className: button, onClick: () => attempt(() => (controller.saveMixingPad(padName || void 0), setPadName(""))), children: "Keep pad" })
974
996
  ] }),
975
- state.savedMixingPads.length > 0 && /* @__PURE__ */ jsx6("ul", { className: "flex flex-col gap-0.5", "aria-label": "Kept pads", children: state.savedMixingPads.map((p) => /* @__PURE__ */ jsxs6("li", { className: "flex items-center gap-1", children: [
976
- /* @__PURE__ */ jsx6("button", { type: "button", className: `${button} flex-1 text-left`, onClick: () => attempt(() => controller.loadMixingPad(p.id)), children: p.name }),
977
- /* @__PURE__ */ jsx6("button", { type: "button", className: button, "aria-label": `Remove ${p.name}`, onClick: () => attempt(() => controller.removeMixingPad(p.id)), children: "\xD7" })
997
+ state.savedMixingPads.length > 0 && /* @__PURE__ */ jsx7("ul", { className: "flex flex-col gap-0.5", "aria-label": "Kept pads", children: state.savedMixingPads.map((p) => /* @__PURE__ */ jsxs6("li", { className: "flex items-center gap-1", children: [
998
+ /* @__PURE__ */ jsx7("button", { type: "button", className: `${button} flex-1 text-left`, onClick: () => attempt(() => controller.loadMixingPad(p.id)), children: p.name }),
999
+ /* @__PURE__ */ jsx7("button", { type: "button", className: button, "aria-label": `Remove ${p.name}`, onClick: () => attempt(() => controller.removeMixingPad(p.id)), children: "\xD7" })
978
1000
  ] }, p.id)) })
979
1001
  ] }),
980
- /* @__PURE__ */ jsx6(ErrorLine, { error })
1002
+ /* @__PURE__ */ jsx7(ErrorLine, { error })
981
1003
  ] });
982
1004
  };
983
1005
 
984
1006
  // src/paint-editor/PaintLayersPanel.tsx
985
1007
  import { useState as useState7 } from "react";
986
1008
  import { BLEND_MENU_GROUPS as BLEND_MENU_GROUPS2 } from "@pixerate/editor/paint-editor";
987
- import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
1009
+ import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
988
1010
  function siblingIndex(layers, l) {
989
1011
  const sibs = layers.filter((x) => x.parentId === l.parentId).reverse();
990
1012
  return { index: sibs.findIndex((x) => x.id === l.id), count: sibs.length };
@@ -1003,15 +1025,15 @@ var PaintLayersPanel = ({ controller, className }) => {
1003
1025
  };
1004
1026
  return /* @__PURE__ */ jsxs7("div", { className: `${panel} ${className ?? ""}`, "data-testid": "paint-layers-panel", children: [
1005
1027
  /* @__PURE__ */ jsxs7("div", { className: "flex flex-wrap gap-1", children: [
1006
- /* @__PURE__ */ jsx7("button", { type: "button", className: button, onClick: () => attempt(() => controller.addLayer()), children: "New layer" }),
1007
- /* @__PURE__ */ jsx7("button", { type: "button", className: button, onClick: () => attempt(() => controller.addLayer({ type: "group" })), children: "New group" }),
1008
- /* @__PURE__ */ jsx7("button", { type: "button", className: button, disabled: !active, onClick: () => active && attempt(() => controller.duplicateLayer(active.id)), children: "Duplicate" }),
1009
- /* @__PURE__ */ jsx7("button", { type: "button", className: button, disabled: !active, "aria-label": "Move layer up", onClick: () => move(1), children: "\u2191" }),
1010
- /* @__PURE__ */ jsx7("button", { type: "button", className: button, disabled: !active, "aria-label": "Move layer down", onClick: () => move(-1), children: "\u2193" }),
1011
- /* @__PURE__ */ jsx7("button", { type: "button", className: button, disabled: !active, onClick: () => active && attempt(() => controller.mergeDown(active.id)), children: "Merge down" }),
1012
- /* @__PURE__ */ jsx7("button", { type: "button", className: button, disabled: !active || layers.length < 2, onClick: () => active && attempt(() => controller.removeLayer(active.id)), children: "Delete" })
1028
+ /* @__PURE__ */ jsx8("button", { type: "button", className: button, onClick: () => attempt(() => controller.addLayer()), children: "New layer" }),
1029
+ /* @__PURE__ */ jsx8("button", { type: "button", className: button, onClick: () => attempt(() => controller.addLayer({ type: "group" })), children: "New group" }),
1030
+ /* @__PURE__ */ jsx8("button", { type: "button", className: button, disabled: !active, onClick: () => active && attempt(() => controller.duplicateLayer(active.id)), children: "Duplicate" }),
1031
+ /* @__PURE__ */ jsx8("button", { type: "button", className: button, disabled: !active, "aria-label": "Move layer up", onClick: () => move(1), children: "\u2191" }),
1032
+ /* @__PURE__ */ jsx8("button", { type: "button", className: button, disabled: !active, "aria-label": "Move layer down", onClick: () => move(-1), children: "\u2193" }),
1033
+ /* @__PURE__ */ jsx8("button", { type: "button", className: button, disabled: !active, onClick: () => active && attempt(() => controller.mergeDown(active.id)), children: "Merge down" }),
1034
+ /* @__PURE__ */ jsx8("button", { type: "button", className: button, disabled: !active || layers.length < 2, onClick: () => active && attempt(() => controller.removeLayer(active.id)), children: "Delete" })
1013
1035
  ] }),
1014
- /* @__PURE__ */ jsx7("ul", { role: "tree", "aria-label": "Layers", className: "flex flex-col gap-0.5", children: layers.map((l) => /* @__PURE__ */ jsxs7(
1036
+ /* @__PURE__ */ jsx8("ul", { role: "tree", "aria-label": "Layers", className: "flex flex-col gap-0.5", children: layers.map((l) => /* @__PURE__ */ jsxs7(
1015
1037
  "li",
1016
1038
  {
1017
1039
  role: "treeitem",
@@ -1020,7 +1042,7 @@ var PaintLayersPanel = ({ controller, className }) => {
1020
1042
  style: { paddingLeft: 6 + l.depth * 12 },
1021
1043
  onClick: () => controller.setActiveLayer(l.id),
1022
1044
  children: [
1023
- /* @__PURE__ */ jsx7(
1045
+ /* @__PURE__ */ jsx8(
1024
1046
  "button",
1025
1047
  {
1026
1048
  type: "button",
@@ -1033,7 +1055,7 @@ var PaintLayersPanel = ({ controller, className }) => {
1033
1055
  children: l.visible ? "\u25CF" : "\u25CB"
1034
1056
  }
1035
1057
  ),
1036
- renaming === l.id ? /* @__PURE__ */ jsx7(
1058
+ renaming === l.id ? /* @__PURE__ */ jsx8(
1037
1059
  "input",
1038
1060
  {
1039
1061
  autoFocus: true,
@@ -1054,15 +1076,15 @@ var PaintLayersPanel = ({ controller, className }) => {
1054
1076
  l.type === "group" ? "\u25B8 " : "",
1055
1077
  l.name
1056
1078
  ] }),
1057
- l.locks.pixels || l.locks.all ? /* @__PURE__ */ jsx7("span", { title: "Locked", children: "\u{1F512}" }) : null
1079
+ l.locks.pixels || l.locks.all ? /* @__PURE__ */ jsx8("span", { title: "Locked", children: "\u{1F512}" }) : null
1058
1080
  ]
1059
1081
  },
1060
1082
  l.id
1061
1083
  )) }),
1062
1084
  active && /* @__PURE__ */ jsxs7("div", { className: "flex flex-col gap-1.5", children: [
1063
1085
  /* @__PURE__ */ jsxs7("label", { className: "flex items-center gap-2", children: [
1064
- /* @__PURE__ */ jsx7("span", { className: "w-14 text-slate-400", children: "Opacity" }),
1065
- /* @__PURE__ */ jsx7(
1086
+ /* @__PURE__ */ jsx8("span", { className: "w-14 text-slate-400", children: "Opacity" }),
1087
+ /* @__PURE__ */ jsx8(
1066
1088
  "input",
1067
1089
  {
1068
1090
  type: "range",
@@ -1077,24 +1099,24 @@ var PaintLayersPanel = ({ controller, className }) => {
1077
1099
  )
1078
1100
  ] }),
1079
1101
  /* @__PURE__ */ jsxs7("label", { className: "flex items-center gap-2", children: [
1080
- /* @__PURE__ */ jsx7("span", { className: "w-14 text-slate-400", children: "Blend" }),
1102
+ /* @__PURE__ */ jsx8("span", { className: "w-14 text-slate-400", children: "Blend" }),
1081
1103
  /* @__PURE__ */ jsxs7("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: [
1082
- active.type === "group" && /* @__PURE__ */ jsx7("option", { value: "pass-through", children: "Pass through" }),
1083
- BLEND_MENU_GROUPS2.map((g) => /* @__PURE__ */ jsx7("optgroup", { label: g.label, children: g.modes.map((m) => /* @__PURE__ */ jsx7("option", { value: m.id, title: m.description, children: m.label }, m.id)) }, g.label))
1104
+ active.type === "group" && /* @__PURE__ */ jsx8("option", { value: "pass-through", children: "Pass through" }),
1105
+ BLEND_MENU_GROUPS2.map((g) => /* @__PURE__ */ jsx8("optgroup", { label: g.label, children: g.modes.map((m) => /* @__PURE__ */ jsx8("option", { value: m.id, title: m.description, children: m.label }, m.id)) }, g.label))
1084
1106
  ] })
1085
1107
  ] }),
1086
1108
  /* @__PURE__ */ jsxs7("div", { className: "flex gap-1", children: [
1087
- ["pixels", "transparency"].map((k) => /* @__PURE__ */ jsx7("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)),
1088
- /* @__PURE__ */ jsx7("button", { type: "button", "aria-pressed": active.clipped, className: chip(active.clipped), onClick: () => attempt(() => controller.updateLayer(active.id, { clipped: !active.clipped })), children: "Clip" })
1109
+ ["pixels", "transparency"].map((k) => /* @__PURE__ */ jsx8("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)),
1110
+ /* @__PURE__ */ jsx8("button", { type: "button", "aria-pressed": active.clipped, className: chip(active.clipped), onClick: () => attempt(() => controller.updateLayer(active.id, { clipped: !active.clipped })), children: "Clip" })
1089
1111
  ] })
1090
1112
  ] }),
1091
- /* @__PURE__ */ jsx7(ErrorLine, { error })
1113
+ /* @__PURE__ */ jsx8(ErrorLine, { error })
1092
1114
  ] });
1093
1115
  };
1094
1116
 
1095
1117
  // src/paint-editor/WetPanel.tsx
1096
- import { WET_FLOW_KINDS, WET_PARAM_SLIDERS, WET_SPEED_SLIDER, WET_TILT_SLIDERS, readPath as readPath3 } from "@pixerate/editor/paint-editor";
1097
- import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
1118
+ import { WET_FLOW_KINDS, WET_GPU_OPTIONS, WET_PARAM_SLIDERS, WET_SPEED_SLIDER, WET_TILT_SLIDERS, readPath as readPath3, wetEngineSummary } from "@pixerate/editor/paint-editor";
1119
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1098
1120
  var FLOW_LABELS = { none: "None", linear: "One way", swirl: "Swirl", radial: "Outwards", turbulent: "Eddies" };
1099
1121
  var WetPanel = ({ controller, className }) => {
1100
1122
  const state = usePaintEditorState(controller);
@@ -1105,29 +1127,29 @@ var WetPanel = ({ controller, className }) => {
1105
1127
  const backend = controller.wetState();
1106
1128
  return /* @__PURE__ */ jsxs8("div", { className: `${panel} ${className ?? ""}`, "data-testid": "wet-panel", children: [
1107
1129
  /* @__PURE__ */ jsxs8("div", { className: "flex items-center justify-between", children: [
1108
- /* @__PURE__ */ jsx8("span", { className: wet.wet ? "text-sky-300" : "text-slate-400", "data-testid": "wet-status", children: wet.wet ? wet.paused ? "Wet (paused)" : "Wet" : "Dry" }),
1130
+ /* @__PURE__ */ jsx9("span", { className: wet.wet ? "text-sky-300" : "text-slate-400", "data-testid": "wet-status", children: wet.wet ? wet.paused ? "Wet (paused)" : "Wet" : "Dry" }),
1109
1131
  /* @__PURE__ */ jsxs8("span", { className: "text-[10px] text-slate-500", children: [
1110
1132
  "step ",
1111
1133
  wet.clock
1112
1134
  ] })
1113
1135
  ] }),
1114
1136
  /* @__PURE__ */ jsxs8("div", { className: "flex flex-wrap gap-1", children: [
1115
- /* @__PURE__ */ jsx8("button", { type: "button", className: button, disabled: !wet.layerIds.length, onClick: () => attempt(async () => (await controller.wetIdle(), controller.dryPaint())), children: "Dry now" }),
1116
- /* @__PURE__ */ jsx8("button", { type: "button", className: chip(wet.paused), "aria-pressed": wet.paused, onClick: () => controller.setWetPaused(!wet.paused), children: wet.paused ? "Resume" : "Pause" }),
1117
- /* @__PURE__ */ jsx8("button", { type: "button", className: chip(wet.overlay), "aria-pressed": wet.overlay, onClick: () => controller.setWaterOverlay(!wet.overlay), children: "Water" }),
1118
- /* @__PURE__ */ jsx8("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" }),
1119
- /* @__PURE__ */ jsx8("button", { type: "button", className: button, disabled: !layerId, onClick: () => attempt(async () => (await controller.wetIdle(), controller.wetPaper({}))), children: "Wet paper" })
1137
+ /* @__PURE__ */ jsx9("button", { type: "button", className: button, disabled: !wet.layerIds.length, onClick: () => attempt(async () => (await controller.wetIdle(), controller.dryPaint())), children: "Dry now" }),
1138
+ /* @__PURE__ */ jsx9("button", { type: "button", className: chip(wet.paused), "aria-pressed": wet.paused, onClick: () => controller.setWetPaused(!wet.paused), children: wet.paused ? "Resume" : "Pause" }),
1139
+ /* @__PURE__ */ jsx9("button", { type: "button", className: chip(wet.overlay), "aria-pressed": wet.overlay, onClick: () => controller.setWaterOverlay(!wet.overlay), children: "Water" }),
1140
+ /* @__PURE__ */ jsx9("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" }),
1141
+ /* @__PURE__ */ jsx9("button", { type: "button", className: button, disabled: !layerId, onClick: () => attempt(async () => (await controller.wetIdle(), controller.wetPaper({}))), children: "Wet paper" })
1120
1142
  ] }),
1121
- /* @__PURE__ */ jsx8(PaintSlider, { control: WET_SPEED_SLIDER, value: wet.speed, onChange: (v) => controller.setWetSpeed(v) }),
1122
- WET_TILT_SLIDERS.map((c) => /* @__PURE__ */ jsx8(PaintSlider, { control: c, value: readPath3(wet.tilt, c.path), onCommit: (v) => attempt(async () => (await controller.wetIdle(), controller.setCanvasTilt({ [c.path]: v }))) }, c.path)),
1143
+ /* @__PURE__ */ jsx9(PaintSlider, { control: WET_SPEED_SLIDER, value: wet.speed, onChange: (v) => controller.setWetSpeed(v) }),
1144
+ WET_TILT_SLIDERS.map((c) => /* @__PURE__ */ jsx9(PaintSlider, { control: c, value: readPath3(wet.tilt, c.path), onCommit: (v) => attempt(async () => (await controller.wetIdle(), controller.setCanvasTilt({ [c.path]: v }))) }, c.path)),
1123
1145
  info && /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-1.5", "data-testid": "wet-layer", children: [
1124
- /* @__PURE__ */ jsx8("span", { className: "font-medium text-slate-300", children: "This layer" }),
1125
- WET_PARAM_SLIDERS.map((c) => /* @__PURE__ */ jsx8(PaintSlider, { control: c, value: readPath3(info.params, c.path), onCommit: (v) => attempt(async () => (await controller.wetIdle(), controller.setWetParams(layerId, { [c.path]: v }))) }, c.path)),
1146
+ /* @__PURE__ */ jsx9("span", { className: "font-medium text-slate-300", children: "This layer" }),
1147
+ WET_PARAM_SLIDERS.map((c) => /* @__PURE__ */ jsx9(PaintSlider, { control: c, value: readPath3(info.params, c.path), onCommit: (v) => attempt(async () => (await controller.wetIdle(), controller.setWetParams(layerId, { [c.path]: v }))) }, c.path)),
1126
1148
  /* @__PURE__ */ jsxs8("label", { className: "flex items-center justify-between gap-2", children: [
1127
- /* @__PURE__ */ jsx8("span", { className: "text-slate-400", children: "Flow map" }),
1128
- /* @__PURE__ */ jsx8("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: WET_FLOW_KINDS.map((k) => /* @__PURE__ */ jsx8("option", { value: k, children: FLOW_LABELS[k] }, k)) })
1149
+ /* @__PURE__ */ jsx9("span", { className: "text-slate-400", children: "Flow map" }),
1150
+ /* @__PURE__ */ jsx9("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: WET_FLOW_KINDS.map((k) => /* @__PURE__ */ jsx9("option", { value: k, children: FLOW_LABELS[k] }, k)) })
1129
1151
  ] }),
1130
- info.flow.kind !== "none" && /* @__PURE__ */ jsx8(
1152
+ info.flow.kind !== "none" && /* @__PURE__ */ jsx9(
1131
1153
  PaintSlider,
1132
1154
  {
1133
1155
  control: { path: "strength", label: "Flow strength", min: 0, max: 1, step: 0.01 },
@@ -1136,16 +1158,21 @@ var WetPanel = ({ controller, className }) => {
1136
1158
  }
1137
1159
  )
1138
1160
  ] }),
1139
- backend.message && /* @__PURE__ */ jsx8("p", { className: "text-[10px] text-amber-300", "data-testid": "wet-message", children: backend.message }),
1140
- backend.backend && /* @__PURE__ */ jsx8("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" }),
1141
- /* @__PURE__ */ jsx8(ErrorLine, { error })
1161
+ backend.message && /* @__PURE__ */ jsx9("p", { className: "text-[10px] text-amber-300", "data-testid": "wet-message", children: backend.message }),
1162
+ backend.backend && /* @__PURE__ */ jsx9("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" }),
1163
+ /* @__PURE__ */ jsxs8("label", { className: "flex items-center justify-between gap-2", children: [
1164
+ /* @__PURE__ */ jsx9("span", { className: "text-slate-400", children: "Engine" }),
1165
+ /* @__PURE__ */ jsx9("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: WET_GPU_OPTIONS.map((o) => /* @__PURE__ */ jsx9("option", { value: o.value, children: o.label }, o.value)) })
1166
+ ] }),
1167
+ /* @__PURE__ */ jsx9("p", { className: `text-[10px] ${backend.engine.state === "active" ? "text-emerald-300" : "text-slate-500"}`, "data-testid": "wet-engine", children: wetEngineSummary(backend.engine) }),
1168
+ /* @__PURE__ */ jsx9(ErrorLine, { error })
1142
1169
  ] });
1143
1170
  };
1144
1171
 
1145
1172
  // src/paint-editor/StickerPanel.tsx
1146
1173
  import { useState as useState8 } from "react";
1147
1174
  import { STAMP_KINDS as STAMP_KINDS2, STAMP_LABELS as STAMP_LABELS2, STICKER_SLIDERS, readPath as readPath4 } from "@pixerate/editor/paint-editor";
1148
- import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
1175
+ import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1149
1176
  var StickerPanel = ({ controller, className }) => {
1150
1177
  const state = usePaintEditorState(controller);
1151
1178
  const [error, attempt] = useAttempt();
@@ -1160,49 +1187,49 @@ var StickerPanel = ({ controller, className }) => {
1160
1187
  stickers.length,
1161
1188
  ")"
1162
1189
  ] }),
1163
- /* @__PURE__ */ jsx9("div", { className: "flex max-h-24 flex-wrap gap-1 overflow-y-auto", role: "listbox", "aria-label": "Stickers", children: stickers.map((s) => /* @__PURE__ */ jsx9("button", { type: "button", role: "option", "aria-selected": s.id === picked, title: `${STAMP_LABELS2[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)) }),
1190
+ /* @__PURE__ */ jsx10("div", { className: "flex max-h-24 flex-wrap gap-1 overflow-y-auto", role: "listbox", "aria-label": "Stickers", children: stickers.map((s) => /* @__PURE__ */ jsx10("button", { type: "button", role: "option", "aria-selected": s.id === picked, title: `${STAMP_LABELS2[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)) }),
1164
1191
  sel && /* @__PURE__ */ jsxs9("div", { className: "flex flex-col gap-1.5", "data-testid": "sticker-edit", children: [
1165
1192
  /* @__PURE__ */ jsxs9("label", { className: "flex items-center justify-between gap-2", children: [
1166
- /* @__PURE__ */ jsx9("span", { className: "text-slate-400", children: "Stamp" }),
1167
- /* @__PURE__ */ jsx9("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: STAMP_KINDS2.map((k) => /* @__PURE__ */ jsx9("option", { value: k, children: STAMP_LABELS2[k] }, k)) })
1193
+ /* @__PURE__ */ jsx10("span", { className: "text-slate-400", children: "Stamp" }),
1194
+ /* @__PURE__ */ jsx10("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: STAMP_KINDS2.map((k) => /* @__PURE__ */ jsx10("option", { value: k, children: STAMP_LABELS2[k] }, k)) })
1168
1195
  ] }),
1169
- STICKER_SLIDERS.map((c) => /* @__PURE__ */ jsx9(PaintSlider, { control: c, value: readPath4(sel, c.path), onCommit: (v) => attempt(() => controller.updateSticker(layerId, sel.id, { [c.path]: v })) }, c.path)),
1196
+ STICKER_SLIDERS.map((c) => /* @__PURE__ */ jsx10(PaintSlider, { control: c, value: readPath4(sel, c.path), onCommit: (v) => attempt(() => controller.updateSticker(layerId, sel.id, { [c.path]: v })) }, c.path)),
1170
1197
  /* @__PURE__ */ jsxs9("label", { className: "flex items-center justify-between", children: [
1171
- /* @__PURE__ */ jsx9("span", { className: "text-slate-400", children: "Colour" }),
1172
- /* @__PURE__ */ jsx9(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" })
1198
+ /* @__PURE__ */ jsx10("span", { className: "text-slate-400", children: "Colour" }),
1199
+ /* @__PURE__ */ jsx10(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" })
1173
1200
  ] }),
1174
1201
  /* @__PURE__ */ jsxs9("div", { className: "flex flex-wrap gap-1", children: [
1175
- /* @__PURE__ */ jsx9("button", { type: "button", className: button, onClick: () => attempt(() => controller.arrangeStickers(layerId, [sel.id], "front")), children: "To front" }),
1176
- /* @__PURE__ */ jsx9("button", { type: "button", className: button, onClick: () => attempt(() => controller.arrangeStickers(layerId, [sel.id], "back")), children: "To back" }),
1177
- /* @__PURE__ */ jsx9("button", { type: "button", className: button, onClick: () => attempt(() => (controller.removeStickers(layerId, [sel.id]), setPicked(null))), children: "Delete" })
1202
+ /* @__PURE__ */ jsx10("button", { type: "button", className: button, onClick: () => attempt(() => controller.arrangeStickers(layerId, [sel.id], "front")), children: "To front" }),
1203
+ /* @__PURE__ */ jsx10("button", { type: "button", className: button, onClick: () => attempt(() => controller.arrangeStickers(layerId, [sel.id], "back")), children: "To back" }),
1204
+ /* @__PURE__ */ jsx10("button", { type: "button", className: button, onClick: () => attempt(() => (controller.removeStickers(layerId, [sel.id]), setPicked(null))), children: "Delete" })
1178
1205
  ] })
1179
1206
  ] }),
1180
- /* @__PURE__ */ jsx9(ErrorLine, { error })
1207
+ /* @__PURE__ */ jsx10(ErrorLine, { error })
1181
1208
  ] });
1182
1209
  };
1183
1210
 
1184
1211
  // src/paint-editor/PaintEditor.tsx
1185
1212
  import { useState as useState9 } from "react";
1186
- import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
1213
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
1187
1214
  var PaintEditor = ({ controller, fileName, height = 720, presence, className }) => {
1188
1215
  const [tool, setTool] = useState9("brush");
1189
1216
  const [tab, setTab] = useState9("color");
1190
1217
  const [error, setError] = useState9(null);
1191
1218
  return /* @__PURE__ */ jsxs10("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: [
1192
- /* @__PURE__ */ jsx10(PaintToolbar, { controller, tool, onToolChange: setTool, fileName }),
1219
+ /* @__PURE__ */ jsx11(PaintToolbar, { controller, tool, onToolChange: setTool, fileName }),
1193
1220
  /* @__PURE__ */ jsxs10("div", { className: "flex min-h-0 flex-1 gap-2", children: [
1194
- /* @__PURE__ */ jsx10(BrushPanel, { controller, className: "w-60 shrink-0 overflow-y-auto pr-1" }),
1221
+ /* @__PURE__ */ jsx11(BrushPanel, { controller, className: "w-60 shrink-0 overflow-y-auto pr-1" }),
1195
1222
  /* @__PURE__ */ jsxs10("div", { className: "flex min-w-0 flex-1 flex-col", children: [
1196
- /* @__PURE__ */ jsx10(PaintCanvas, { controller, tool, onError: setError, presence, className: "flex-1 rounded-lg" }),
1197
- error && /* @__PURE__ */ jsx10("p", { role: "alert", className: "mt-1 text-[11px] text-rose-400", children: error })
1223
+ /* @__PURE__ */ jsx11(PaintCanvas, { controller, tool, onError: setError, presence, className: "flex-1 rounded-lg" }),
1224
+ error && /* @__PURE__ */ jsx11("p", { role: "alert", className: "mt-1 text-[11px] text-rose-400", children: error })
1198
1225
  ] }),
1199
1226
  /* @__PURE__ */ jsxs10("div", { className: "flex w-64 shrink-0 flex-col gap-2 overflow-y-auto pr-1", children: [
1200
- /* @__PURE__ */ jsx10("div", { className: "flex gap-1", role: "tablist", "aria-label": "Paint panels", children: ["color", "wet", "paper", "layers"].map((t) => /* @__PURE__ */ jsx10("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)) }),
1201
- tab === "color" && /* @__PURE__ */ jsx10(ColorPanel, { controller }),
1202
- tab === "wet" && /* @__PURE__ */ jsx10(WetPanel, { controller }),
1203
- tab === "paper" && /* @__PURE__ */ jsx10(PaperPanel, { controller }),
1204
- tab === "layers" && /* @__PURE__ */ jsx10(PaintLayersPanel, { controller }),
1205
- tab === "layers" && /* @__PURE__ */ jsx10(StickerPanel, { controller })
1227
+ /* @__PURE__ */ jsx11("div", { className: "flex gap-1", role: "tablist", "aria-label": "Paint panels", children: ["color", "wet", "paper", "layers"].map((t) => /* @__PURE__ */ jsx11("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)) }),
1228
+ tab === "color" && /* @__PURE__ */ jsx11(ColorPanel, { controller }),
1229
+ tab === "wet" && /* @__PURE__ */ jsx11(WetPanel, { controller }),
1230
+ tab === "paper" && /* @__PURE__ */ jsx11(PaperPanel, { controller }),
1231
+ tab === "layers" && /* @__PURE__ */ jsx11(PaintLayersPanel, { controller }),
1232
+ tab === "layers" && /* @__PURE__ */ jsx11(StickerPanel, { controller })
1206
1233
  ] })
1207
1234
  ] })
1208
1235
  ] });
@@ -1212,6 +1239,7 @@ export {
1212
1239
  ColorPanel,
1213
1240
  PaintCanvas,
1214
1241
  PaintEditor,
1242
+ PaintGradientEditor,
1215
1243
  PaintLayersPanel,
1216
1244
  PaintPresenceLayer,
1217
1245
  PaintSlider,