@weasel-js/ui 1.4.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/dist/chunks/{Badge-BXTDs-zi.js → Badge-7yQseqZb.js} +233 -263
  2. package/dist/chunks/Badge-7yQseqZb.js.map +1 -0
  3. package/dist/chunks/BandEditor-DjOi4ldl.js +252 -0
  4. package/dist/chunks/BandEditor-DjOi4ldl.js.map +1 -0
  5. package/dist/chunks/{CurveEditor-Cpuvl_iN.js → CurveEditor-DoX5Q0_V.js} +192 -195
  6. package/dist/chunks/CurveEditor-DoX5Q0_V.js.map +1 -0
  7. package/dist/chunks/{DataGrid-BWZZUekH.js → DataGrid-DHV0chfL.js} +2 -5
  8. package/dist/chunks/DataGrid-DHV0chfL.js.map +1 -0
  9. package/dist/chunks/{DetentSlider--tQdVQj_.js → DetentSlider-Cd5360U3.js} +2 -2
  10. package/dist/chunks/{DetentSlider--tQdVQj_.js.map → DetentSlider-Cd5360U3.js.map} +1 -1
  11. package/dist/chunks/{GradientEditor-C5pPbMa_.js → GradientEditor-CPeTbkFE.js} +11 -9
  12. package/dist/chunks/{GradientEditor-C5pPbMa_.js.map → GradientEditor-CPeTbkFE.js.map} +1 -1
  13. package/dist/chunks/{LayerStack-htMwIsJ6.js → LayerStack-BVdwhAhG.js} +2 -5
  14. package/dist/chunks/{LayerStack-htMwIsJ6.js.map → LayerStack-BVdwhAhG.js.map} +1 -1
  15. package/dist/chunks/{PaintInput-DtCSGda0.js → PaintInput-6HTUAFL1.js} +2 -2
  16. package/dist/chunks/{PaintInput-DtCSGda0.js.map → PaintInput-6HTUAFL1.js.map} +1 -1
  17. package/dist/chunks/{PointPlotter-CR-QGCIf.js → PointPlotter-CZJE8Ccu.js} +2 -2
  18. package/dist/chunks/{PointPlotter-CR-QGCIf.js.map → PointPlotter-CZJE8Ccu.js.map} +1 -1
  19. package/dist/chunks/{Powerline--gx4j0Zr.js → Powerline-BY_ADnqq.js} +2 -2
  20. package/dist/chunks/{Powerline--gx4j0Zr.js.map → Powerline-BY_ADnqq.js.map} +1 -1
  21. package/dist/chunks/{Properties-BEGlt0Z2.js → Properties-BuNzy7Hb.js} +2 -2
  22. package/dist/chunks/{Properties-BEGlt0Z2.js.map → Properties-BuNzy7Hb.js.map} +1 -1
  23. package/dist/chunks/ResizeHandle-DfqqfaZz.js +79 -0
  24. package/dist/chunks/ResizeHandle-DfqqfaZz.js.map +1 -0
  25. package/dist/chunks/{SelectionPanel-OfbtyDJH.js → SelectionPanel-BU2cmRtp.js} +2 -2
  26. package/dist/chunks/{SelectionPanel-OfbtyDJH.js.map → SelectionPanel-BU2cmRtp.js.map} +1 -1
  27. package/dist/chunks/Slider-BE6jaxyU.js +303 -0
  28. package/dist/chunks/Slider-BE6jaxyU.js.map +1 -0
  29. package/dist/chunks/{Timeline-BE7UWajy.js → Timeline-CWuh0nV0.js} +219 -205
  30. package/dist/chunks/Timeline-CWuh0nV0.js.map +1 -0
  31. package/dist/chunks/useReorderDragList-UvHJFjTZ.js +106 -0
  32. package/dist/chunks/useReorderDragList-UvHJFjTZ.js.map +1 -0
  33. package/dist/components/Badge/Badge.d.ts.map +1 -1
  34. package/dist/components/Badge/index.js +1 -1
  35. package/dist/components/Badge/shapes/Perforated.d.ts.map +1 -1
  36. package/dist/components/Badge/shapes/Shield.d.ts.map +1 -1
  37. package/dist/components/Badge/useSvgBox.d.ts +10 -0
  38. package/dist/components/Badge/useSvgBox.d.ts.map +1 -0
  39. package/dist/components/BandEditor/BandEditor.d.ts.map +1 -1
  40. package/dist/components/BandEditor/index.js +1 -1
  41. package/dist/components/CurveEditor/LayeredCurveEditor.d.ts.map +1 -1
  42. package/dist/components/CurveEditor/index.js +1 -1
  43. package/dist/components/DataGrid/DataGrid.d.ts.map +1 -1
  44. package/dist/components/DataGrid/index.js +1 -1
  45. package/dist/components/DetentSlider/index.js +1 -1
  46. package/dist/components/GradientEditor/index.js +1 -1
  47. package/dist/components/LayerStack/LayerStack.d.ts.map +1 -1
  48. package/dist/components/LayerStack/index.js +1 -1
  49. package/dist/components/PaintInput/index.js +1 -1
  50. package/dist/components/PointPlotter/index.js +1 -1
  51. package/dist/components/Powerline/index.js +2 -2
  52. package/dist/components/Properties/index.js +1 -1
  53. package/dist/components/ResizeHandle/ResizeHandle.d.ts.map +1 -1
  54. package/dist/components/ResizeHandle/index.js +1 -1
  55. package/dist/components/SelectionPanel/index.js +1 -1
  56. package/dist/components/Slider/Slider.d.ts.map +1 -1
  57. package/dist/components/Slider/index.js +1 -1
  58. package/dist/components/Timeline/Lane.d.ts.map +1 -1
  59. package/dist/components/Timeline/Ruler.d.ts.map +1 -1
  60. package/dist/components/Timeline/index.js +1 -1
  61. package/dist/index.js +16 -16
  62. package/dist/useReorderDragList.d.ts +20 -7
  63. package/dist/useReorderDragList.d.ts.map +1 -1
  64. package/package.json +6 -6
  65. package/dist/chunks/Badge-BXTDs-zi.js.map +0 -1
  66. package/dist/chunks/BandEditor-CfLW44uR.js +0 -248
  67. package/dist/chunks/BandEditor-CfLW44uR.js.map +0 -1
  68. package/dist/chunks/CurveEditor-Cpuvl_iN.js.map +0 -1
  69. package/dist/chunks/DataGrid-BWZZUekH.js.map +0 -1
  70. package/dist/chunks/ResizeHandle-DjGZkPpx.js +0 -81
  71. package/dist/chunks/ResizeHandle-DjGZkPpx.js.map +0 -1
  72. package/dist/chunks/Slider-CSwL-Nj9.js +0 -296
  73. package/dist/chunks/Slider-CSwL-Nj9.js.map +0 -1
  74. package/dist/chunks/Timeline-BE7UWajy.js.map +0 -1
  75. package/dist/chunks/useReorderDragList-nQMVwNBd.js +0 -109
  76. package/dist/chunks/useReorderDragList-nQMVwNBd.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { n as e, t } from "./Icon-DBXtBB1f.js";
2
2
  import { t as n } from "./ToggleBar-D0veW91s.js";
3
3
  import { t as r } from "./ColorField-pEkaSppg.js";
4
- import { r as i } from "./GradientEditor-C5pPbMa_.js";
4
+ import { r as i } from "./GradientEditor-CPeTbkFE.js";
5
5
  import { t as a } from "./PatternPicker-G9e0CU5H.js";
6
6
  import { jsx as o, jsxs as s } from "react/jsx-runtime";
7
7
  import { getPaintKind as c, listPaintKinds as l, withGradientKind as u } from "@weasel-js/core";
@@ -115,4 +115,4 @@ function b(b) {
115
115
  //#endregion
116
116
  export { b as t };
117
117
 
118
- //# sourceMappingURL=PaintInput-DtCSGda0.js.map
118
+ //# sourceMappingURL=PaintInput-6HTUAFL1.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"PaintInput-DtCSGda0.js","names":[],"sources":["../../src/components/PaintInput/PaintInput.module.css","../../src/components/PaintInput/PaintInput.tsx"],"sourcesContent":["/* Kind switch stacked over whichever paint body it selects. */\n.root {\n display: flex;\n flex-direction: column;\n gap: 6px;\n min-inline-size: 0;\n}\n\n/* A value is in effect but nobody chose it. Never louder than mixed. */\n.unset {\n opacity: 0.55;\n}\n","import { useCallback, useRef, type ReactElement } from 'react';\nimport {\n getPaintKind,\n listPaintKinds,\n withGradientKind,\n type FillStyle,\n type GradientFill,\n type GradientKind,\n type PaintKind,\n} from '@weasel-js/core';\nimport { Icon } from '../../icons/Icon';\nimport { ICON_PATHS, type IconName } from '../../icons/paths';\nimport { ColorField } from '../ColorField';\nimport { GradientEditor } from '../GradientEditor';\nimport { PatternPicker, type PatternFill } from '../PatternPicker';\nimport { ToggleBar, type ToggleBarItem } from '../ToggleBar';\nimport s from './PaintInput.module.css';\n\n/** What a kind with no color of its own seeds from. */\nconst FALLBACK_COLOR = '#000000ff';\n\n/** The bar's answer for \"no paint at all\". Not a registered kind — absence\n * has no seed, no color and nothing to render — so it is this control's own\n * segment rather than a registry entry. */\nconst NONE = 'none';\n\nconst GRADIENT_KINDS: readonly string[] = [\n 'linear-gradient',\n 'radial-gradient',\n 'conic-gradient',\n];\n\nfunction isGradient(paint: FillStyle | undefined): paint is GradientFill {\n return paint !== undefined && GRADIENT_KINDS.includes(paint.fill ?? '');\n}\n\nfunction isPattern(paint: FillStyle | undefined): paint is PatternFill {\n return paint?.fill === 'pattern';\n}\n\n/** A paint's own discriminant. The solid member leaves `fill` off, and an\n * explicit `null` is the absence of paint rather than an unknown one. */\nfunction kindOf(paint: FillStyle | null | undefined): PaintKind | null {\n if (paint === null) return NONE;\n if (paint === undefined) return null;\n return paint.fill ?? 'solid';\n}\n\n/** Props for {@link PaintInput}. `onInput` fires throughout a gesture and\n * `onChange` once at its end. */\nexport interface PaintInputProps {\n /** The paint being edited. `null` is an explicit \"no paint\"; `undefined`\n * is no value to show. */\n value: FillStyle | null | undefined;\n /** Indeterminate presentation: no kind lit, and the body withheld because\n * there is no single paint to show. */\n mixed?: boolean;\n /** Dim the control — a value is in effect but was never chosen. */\n unset?: boolean;\n /** Restrict the kind bar. Default: every registered kind. */\n kinds?: readonly PaintKind[];\n /** Offer \"None\". Default `true` — the bar answers \"what kind of paint is\n * this?\" and no-paint is one of the answers. Turn it off where the slot\n * cannot express absence. */\n allowNone?: boolean;\n onInput?: (next: FillStyle | null) => void;\n onChange: (next: FillStyle | null) => void;\n /** Names the paint this control edits — `Fill`, `Color`. Carried by the\n * solid body's swatch, which is the part with a value to announce. */\n 'aria-label'?: string;\n className?: string;\n}\n\n/**\n * Editor for a whole `FillStyle` — a kind bar over whichever body that kind\n * wants.\n *\n * The bar is driven by the paint-kind registry rather than a fixed list, so a\n * consumer's registered kind appears in it and renders that entry's `Editor`.\n * The five built-in kinds keep their branch here: their editors live in this\n * package and `@weasel-js/core`, which owns the registry, cannot import it.\n *\n * Geometry is deliberately absent — on a canvas it belongs on the artwork,\n * via `<SceneGradientHandles>`.\n */\nexport function PaintInput(props: PaintInputProps): ReactElement {\n const {\n value, mixed = false, unset = false, kinds, allowNone = true,\n onInput, onChange, className,\n } = props;\n const ariaLabel = props['aria-label'];\n\n // Per-kind memory, so linear → solid → linear comes back with its stops.\n // A ref, not state: it must not survive the control being pointed at a\n // different selection, and nothing renders off it directly.\n const remembered = useRef(new Map<PaintKind, FillStyle>());\n\n const active = mixed ? null : kindOf(value);\n if (value != null && active !== null) remembered.current.set(active, value);\n\n const entries = listPaintKinds().filter(\n (entry) => kinds === undefined || kinds.includes(entry.id),\n );\n const segment = (id: string, label: string, icon: string | undefined): ToggleBarItem<PaintKind> =>\n icon && icon in ICON_PATHS\n // Icon-only: six labelled segments do not fit a property row, and the\n // full label stays the accessible name.\n // 14 to match the panel's other glyph segments. A `sm` bar is 17px tall\n // over 1px of padding, so 15 fills the segment box edge to edge.\n ? { value: id, label: <Icon name={icon as IconName} size={14} />, ariaLabel: label }\n : { value: id, label };\n\n const items: readonly ToggleBarItem<PaintKind>[] = [\n ...(allowNone ? [segment(NONE, 'None', 'paintNone')] : []),\n ...entries.map((entry) => segment(entry.id, entry.label, entry.icon)),\n ];\n\n /** The color a switch seeds from: whatever the current paint shows. */\n const currentColor = useCallback((): string => {\n if (value == null) return FALLBACK_COLOR;\n return getPaintKind(kindOf(value) ?? 'solid')?.colorOf(value) ?? FALLBACK_COLOR;\n }, [value]);\n\n const switchKind = (kind: PaintKind): void => {\n if (kind === active) return;\n if (kind === NONE) {\n onChange(null);\n return;\n }\n const seen = remembered.current.get(kind);\n if (seen !== undefined) {\n onChange(seen);\n return;\n }\n if (value != null && isGradient(value) && GRADIENT_KINDS.includes(kind)) {\n onChange(withGradientKind(value, kind as GradientKind));\n return;\n }\n const entry = getPaintKind(kind);\n if (entry) onChange(entry.seed(currentColor()));\n };\n\n return (\n <div\n className={[s.root, unset && s.unset, className].filter(Boolean).join(' ')}\n title={unset ? 'Not set' : undefined}\n >\n <ToggleBar<PaintKind>\n items={items}\n value={active}\n size=\"sm\"\n ariaLabel=\"Paint kind\"\n onChange={(kind) => kind && switchKind(kind)}\n />\n {renderBody()}\n </div>\n );\n\n function renderBody(): ReactElement | null {\n if (mixed || value === undefined) {\n return (\n <ColorField\n mixed\n aria-label={ariaLabel}\n onChange={(color) => onChange({ fill: 'solid', color })}\n />\n );\n }\n // Absence has nothing to edit. The lit segment is the whole statement.\n if (value === null) return null;\n // A registered `Editor` wins even for a built-in id: re-registering one is\n // how a consumer replaces a kit control, and the built-in branches below\n // are the fallback for the ids that ship without an entry editor.\n const entry = getPaintKind(kindOf(value) ?? 'solid');\n if (entry?.Editor) {\n const Editor = entry.Editor;\n return <Editor value={value} onInput={onInput} onChange={onChange} />;\n }\n if (isGradient(value)) {\n return (\n <GradientEditor\n value={value}\n kindSwitch={false}\n onInput={onInput}\n onChange={onChange}\n />\n );\n }\n if (isPattern(value)) {\n return <PatternPicker value={value} color={currentColor()} onChange={onChange} />;\n }\n return (\n <ColorField\n value={currentColor()}\n alpha\n aria-label={ariaLabel}\n onInput={(color) => onInput?.({ fill: 'solid', color })}\n onChange={(color) => onChange({ fill: 'solid', color })}\n />\n );\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"PaintInput-6HTUAFL1.js","names":[],"sources":["../../src/components/PaintInput/PaintInput.module.css","../../src/components/PaintInput/PaintInput.tsx"],"sourcesContent":["/* Kind switch stacked over whichever paint body it selects. */\n.root {\n display: flex;\n flex-direction: column;\n gap: 6px;\n min-inline-size: 0;\n}\n\n/* A value is in effect but nobody chose it. Never louder than mixed. */\n.unset {\n opacity: 0.55;\n}\n","import { useCallback, useRef, type ReactElement } from 'react';\nimport {\n getPaintKind,\n listPaintKinds,\n withGradientKind,\n type FillStyle,\n type GradientFill,\n type GradientKind,\n type PaintKind,\n} from '@weasel-js/core';\nimport { Icon } from '../../icons/Icon';\nimport { ICON_PATHS, type IconName } from '../../icons/paths';\nimport { ColorField } from '../ColorField';\nimport { GradientEditor } from '../GradientEditor';\nimport { PatternPicker, type PatternFill } from '../PatternPicker';\nimport { ToggleBar, type ToggleBarItem } from '../ToggleBar';\nimport s from './PaintInput.module.css';\n\n/** What a kind with no color of its own seeds from. */\nconst FALLBACK_COLOR = '#000000ff';\n\n/** The bar's answer for \"no paint at all\". Not a registered kind — absence\n * has no seed, no color and nothing to render — so it is this control's own\n * segment rather than a registry entry. */\nconst NONE = 'none';\n\nconst GRADIENT_KINDS: readonly string[] = [\n 'linear-gradient',\n 'radial-gradient',\n 'conic-gradient',\n];\n\nfunction isGradient(paint: FillStyle | undefined): paint is GradientFill {\n return paint !== undefined && GRADIENT_KINDS.includes(paint.fill ?? '');\n}\n\nfunction isPattern(paint: FillStyle | undefined): paint is PatternFill {\n return paint?.fill === 'pattern';\n}\n\n/** A paint's own discriminant. The solid member leaves `fill` off, and an\n * explicit `null` is the absence of paint rather than an unknown one. */\nfunction kindOf(paint: FillStyle | null | undefined): PaintKind | null {\n if (paint === null) return NONE;\n if (paint === undefined) return null;\n return paint.fill ?? 'solid';\n}\n\n/** Props for {@link PaintInput}. `onInput` fires throughout a gesture and\n * `onChange` once at its end. */\nexport interface PaintInputProps {\n /** The paint being edited. `null` is an explicit \"no paint\"; `undefined`\n * is no value to show. */\n value: FillStyle | null | undefined;\n /** Indeterminate presentation: no kind lit, and the body withheld because\n * there is no single paint to show. */\n mixed?: boolean;\n /** Dim the control — a value is in effect but was never chosen. */\n unset?: boolean;\n /** Restrict the kind bar. Default: every registered kind. */\n kinds?: readonly PaintKind[];\n /** Offer \"None\". Default `true` — the bar answers \"what kind of paint is\n * this?\" and no-paint is one of the answers. Turn it off where the slot\n * cannot express absence. */\n allowNone?: boolean;\n onInput?: (next: FillStyle | null) => void;\n onChange: (next: FillStyle | null) => void;\n /** Names the paint this control edits — `Fill`, `Color`. Carried by the\n * solid body's swatch, which is the part with a value to announce. */\n 'aria-label'?: string;\n className?: string;\n}\n\n/**\n * Editor for a whole `FillStyle` — a kind bar over whichever body that kind\n * wants.\n *\n * The bar is driven by the paint-kind registry rather than a fixed list, so a\n * consumer's registered kind appears in it and renders that entry's `Editor`.\n * The five built-in kinds keep their branch here: their editors live in this\n * package and `@weasel-js/core`, which owns the registry, cannot import it.\n *\n * Geometry is deliberately absent — on a canvas it belongs on the artwork,\n * via `<SceneGradientHandles>`.\n */\nexport function PaintInput(props: PaintInputProps): ReactElement {\n const {\n value, mixed = false, unset = false, kinds, allowNone = true,\n onInput, onChange, className,\n } = props;\n const ariaLabel = props['aria-label'];\n\n // Per-kind memory, so linear → solid → linear comes back with its stops.\n // A ref, not state: it must not survive the control being pointed at a\n // different selection, and nothing renders off it directly.\n const remembered = useRef(new Map<PaintKind, FillStyle>());\n\n const active = mixed ? null : kindOf(value);\n if (value != null && active !== null) remembered.current.set(active, value);\n\n const entries = listPaintKinds().filter(\n (entry) => kinds === undefined || kinds.includes(entry.id),\n );\n const segment = (id: string, label: string, icon: string | undefined): ToggleBarItem<PaintKind> =>\n icon && icon in ICON_PATHS\n // Icon-only: six labelled segments do not fit a property row, and the\n // full label stays the accessible name.\n // 14 to match the panel's other glyph segments. A `sm` bar is 17px tall\n // over 1px of padding, so 15 fills the segment box edge to edge.\n ? { value: id, label: <Icon name={icon as IconName} size={14} />, ariaLabel: label }\n : { value: id, label };\n\n const items: readonly ToggleBarItem<PaintKind>[] = [\n ...(allowNone ? [segment(NONE, 'None', 'paintNone')] : []),\n ...entries.map((entry) => segment(entry.id, entry.label, entry.icon)),\n ];\n\n /** The color a switch seeds from: whatever the current paint shows. */\n const currentColor = useCallback((): string => {\n if (value == null) return FALLBACK_COLOR;\n return getPaintKind(kindOf(value) ?? 'solid')?.colorOf(value) ?? FALLBACK_COLOR;\n }, [value]);\n\n const switchKind = (kind: PaintKind): void => {\n if (kind === active) return;\n if (kind === NONE) {\n onChange(null);\n return;\n }\n const seen = remembered.current.get(kind);\n if (seen !== undefined) {\n onChange(seen);\n return;\n }\n if (value != null && isGradient(value) && GRADIENT_KINDS.includes(kind)) {\n onChange(withGradientKind(value, kind as GradientKind));\n return;\n }\n const entry = getPaintKind(kind);\n if (entry) onChange(entry.seed(currentColor()));\n };\n\n return (\n <div\n className={[s.root, unset && s.unset, className].filter(Boolean).join(' ')}\n title={unset ? 'Not set' : undefined}\n >\n <ToggleBar<PaintKind>\n items={items}\n value={active}\n size=\"sm\"\n ariaLabel=\"Paint kind\"\n onChange={(kind) => kind && switchKind(kind)}\n />\n {renderBody()}\n </div>\n );\n\n function renderBody(): ReactElement | null {\n if (mixed || value === undefined) {\n return (\n <ColorField\n mixed\n aria-label={ariaLabel}\n onChange={(color) => onChange({ fill: 'solid', color })}\n />\n );\n }\n // Absence has nothing to edit. The lit segment is the whole statement.\n if (value === null) return null;\n // A registered `Editor` wins even for a built-in id: re-registering one is\n // how a consumer replaces a kit control, and the built-in branches below\n // are the fallback for the ids that ship without an entry editor.\n const entry = getPaintKind(kindOf(value) ?? 'solid');\n if (entry?.Editor) {\n const Editor = entry.Editor;\n return <Editor value={value} onInput={onInput} onChange={onChange} />;\n }\n if (isGradient(value)) {\n return (\n <GradientEditor\n value={value}\n kindSwitch={false}\n onInput={onInput}\n onChange={onChange}\n />\n );\n }\n if (isPattern(value)) {\n return <PatternPicker value={value} color={currentColor()} onChange={onChange} />;\n }\n return (\n <ColorField\n value={currentColor()}\n alpha\n aria-label={ariaLabel}\n onInput={(color) => onInput?.({ fill: 'solid', color })}\n onChange={(color) => onChange({ fill: 'solid', color })}\n />\n );\n }\n}\n"],"mappings":";;;;;;;;;;;GCmBM,IAAiB,aAKjB,IAAO,QAEP,IAAoC;CACxC;CACA;CACA;CACD;AAED,SAAS,EAAW,GAAqD;AACvE,QAAO,MAAU,KAAA,KAAa,EAAe,SAAS,EAAM,QAAQ,GAAG;;AAGzE,SAAS,EAAU,GAAoD;AACrE,QAAO,GAAO,SAAS;;AAKzB,SAAS,EAAO,GAAuD;AAGrE,QAFI,MAAU,OAAa,IACvB,MAAU,KAAA,IAAkB,OACzB,EAAM,QAAQ;;AAwCvB,SAAgB,EAAW,GAAsC;CAC/D,IAAM,EACJ,UAAO,WAAQ,IAAO,WAAQ,IAAO,UAAO,eAAY,IACxD,YAAS,aAAU,iBACjB,GACE,IAAY,EAAM,eAKlB,IAAa,kBAAO,IAAI,KAA2B,CAAC,EAEpD,IAAS,IAAQ,OAAO,EAAO,EAAM;AAC3C,CAAI,KAAS,QAAQ,MAAW,QAAM,EAAW,QAAQ,IAAI,GAAQ,EAAM;CAE3E,IAAM,IAAU,GAAgB,CAAC,QAC9B,MAAU,MAAU,KAAA,KAAa,EAAM,SAAS,EAAM,GAAG,CAC3D,EACK,KAAW,GAAY,GAAe,MAC1C,KAAQ,KAAQ,IAKZ;EAAE,OAAO;EAAI,OAAO,kBAAC,GAAD;GAAM,MAAM;GAAkB,MAAM;GAAM,CAAA;EAAE,WAAW;EAAO,GAClF;EAAE,OAAO;EAAI;EAAO,EAEpB,IAA6C,CACjD,GAAI,IAAY,CAAC,EAAQ,GAAM,QAAQ,YAAY,CAAC,GAAG,EAAE,EACzD,GAAG,EAAQ,KAAK,MAAU,EAAQ,EAAM,IAAI,EAAM,OAAO,EAAM,KAAK,CAAC,CACtE,EAGK,IAAe,QACf,KAAS,OAAa,IACnB,EAAa,EAAO,EAAM,IAAI,QAAQ,EAAE,QAAQ,EAAM,IAAI,GAChE,CAAC,EAAM,CAAC,EAEL,KAAc,MAA0B;AAC5C,MAAI,MAAS,EAAQ;AACrB,MAAI,MAAS,GAAM;AACjB,KAAS,KAAK;AACd;;EAEF,IAAM,IAAO,EAAW,QAAQ,IAAI,EAAK;AACzC,MAAI,MAAS,KAAA,GAAW;AACtB,KAAS,EAAK;AACd;;AAEF,MAAI,KAAS,QAAQ,EAAW,EAAM,IAAI,EAAe,SAAS,EAAK,EAAE;AACvE,KAAS,EAAiB,GAAO,EAAqB,CAAC;AACvD;;EAEF,IAAM,IAAQ,EAAa,EAAK;AAChC,EAAI,KAAO,EAAS,EAAM,KAAK,GAAc,CAAC,CAAC;;AAGjD,QACE,kBAAC,OAAD;EACE,WAAW;GAAC,EAAE;GAAM,KAAS,EAAE;GAAO;GAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI;EAC1E,OAAO,IAAQ,YAAY,KAAA;YAF7B,CAIE,kBAAC,GAAD;GACS;GACP,OAAO;GACP,MAAK;GACL,WAAU;GACV,WAAW,MAAS,KAAQ,EAAW,EAAK;GAC5C,CAAA,EACD,GAAY,CACT;;CAGR,SAAS,IAAkC;AACzC,MAAI,KAAS,MAAU,KAAA,EACrB,QACE,kBAAC,GAAD;GACE,OAAA;GACA,cAAY;GACZ,WAAW,MAAU,EAAS;IAAE,MAAM;IAAS;IAAO,CAAC;GACvD,CAAA;AAIN,MAAI,MAAU,KAAM,QAAO;EAI3B,IAAM,IAAQ,EAAa,EAAO,EAAM,IAAI,QAAQ;AACpD,MAAI,GAAO,QAAQ;GACjB,IAAM,IAAS,EAAM;AACrB,UAAO,kBAAC,GAAD;IAAe;IAAgB;IAAmB;IAAY,CAAA;;AAevE,SAbI,EAAW,EAAM,GAEjB,kBAAC,GAAD;GACS;GACP,YAAY;GACH;GACC;GACV,CAAA,GAGF,EAAU,EAAM,GACX,kBAAC,GAAD;GAAsB;GAAO,OAAO,GAAc;GAAY;GAAY,CAAA,GAGjF,kBAAC,GAAD;GACE,OAAO,GAAc;GACrB,OAAA;GACA,cAAY;GACZ,UAAU,MAAU,IAAU;IAAE,MAAM;IAAS;IAAO,CAAC;GACvD,WAAW,MAAU,EAAS;IAAE,MAAM;IAAS;IAAO,CAAC;GACvD,CAAA"}
@@ -1,4 +1,4 @@
1
- import { n as e } from "./CurveEditor-Cpuvl_iN.js";
1
+ import { n as e } from "./CurveEditor-DoX5Q0_V.js";
2
2
  import { jsx as t } from "react/jsx-runtime";
3
3
  //#region src/components/PointPlotter/PointPlotter.tsx
4
4
  function n(n) {
@@ -13,4 +13,4 @@ function n(n) {
13
13
  //#endregion
14
14
  export { n as t };
15
15
 
16
- //# sourceMappingURL=PointPlotter-CR-QGCIf.js.map
16
+ //# sourceMappingURL=PointPlotter-CZJE8Ccu.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"PointPlotter-CR-QGCIf.js","names":[],"sources":["../../src/components/PointPlotter/PointPlotter.tsx"],"sourcesContent":["import type { CSSProperties, ReactNode } from 'react';\nimport {\n CurveEditor,\n type AnchorRenderProps,\n type ControlPoint,\n type CurveEditorProps,\n} from '../CurveEditor';\nimport type { GridSettings, AxesSettings } from '../Plot2D';\n\n/** Props for {@link PointPlotter}. */\nexport interface PointPlotterProps {\n /** Plotted points; caller-owned. */\n value: readonly ControlPoint[];\n /** Fires every frame during drag with the live in-flight value. */\n onInput: (next: ControlPoint[]) => void;\n /** Fires once per discrete user action (drag-end, add, delete) with\n * the new value and the value at gesture start. Wire history here. */\n onChange?: (next: ControlPoint[], prev: readonly ControlPoint[]) => void;\n /** Model-space x range. Default [0, 1]. */\n xRange?: readonly [number, number];\n /** Model-space y range. Default [0, 1]. */\n yRange?: readonly [number, number];\n width: number;\n height: number;\n grid?: GridSettings | false | null;\n axes?: AxesSettings | false | null;\n /** Minimum allowed point count. User-initiated deletion is refused\n * while `value.length <= minPoints`. */\n minPoints?: number;\n /** Maximum allowed point count. User-initiated insertion is refused\n * while `value.length >= maxPoints`. */\n maxPoints?: number;\n /** How new points are added. Default `'click-empty'`. Set to `'never'`\n * to disable insertion entirely. */\n addPointMode?: 'click-empty' | 'never';\n /** Built-in undo/redo via keyboard when the component has focus.\n * Default `true`. See `CurveEditor.history` for details. */\n history?: boolean;\n /** Custom per-anchor renderer. See `CurveEditor.renderAnchor`. */\n renderAnchor?: (info: AnchorRenderProps) => ReactNode;\n /** Extra SVG content beneath the anchors. See `CurveEditor.decorations`. */\n decorations?: ReactNode;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * 2D point plotter — a CurveEditor with the curve hidden. Anchors,\n * drag, shift+click delete, right-click delete, and click-empty insert\n * are inherited from CurveEditor verbatim, so the two components share\n * gesture semantics and stay in sync as CurveEditor evolves.\n *\n * Differences from CurveEditor: no curve `<path>` drawn, no fill, no\n * endpoint pinning, no interpolation, default `addPointMode` is\n * `'click-empty'` (CurveEditor defaults to `'click-curve'`, which\n * doesn't make sense without a curve to hit-test against).\n */\nexport function PointPlotter(props: PointPlotterProps) {\n const curveProps: CurveEditorProps = {\n ...props,\n addPointMode: props.addPointMode ?? 'click-empty',\n curve: false,\n fill: false,\n endpoints: 'free',\n };\n return <CurveEditor {...curveProps} />;\n}\n"],"mappings":";;;AAyDA,SAAgB,EAAa,GAA0B;AAQrD,QAAO,kBAAC,GAAD;EANL,GAAG;EACH,cAAc,EAAM,gBAAgB;EACpC,OAAO;EACP,MAAM;EACN,WAAW;EAEyB,CAAA"}
1
+ {"version":3,"file":"PointPlotter-CZJE8Ccu.js","names":[],"sources":["../../src/components/PointPlotter/PointPlotter.tsx"],"sourcesContent":["import type { CSSProperties, ReactNode } from 'react';\nimport {\n CurveEditor,\n type AnchorRenderProps,\n type ControlPoint,\n type CurveEditorProps,\n} from '../CurveEditor';\nimport type { GridSettings, AxesSettings } from '../Plot2D';\n\n/** Props for {@link PointPlotter}. */\nexport interface PointPlotterProps {\n /** Plotted points; caller-owned. */\n value: readonly ControlPoint[];\n /** Fires every frame during drag with the live in-flight value. */\n onInput: (next: ControlPoint[]) => void;\n /** Fires once per discrete user action (drag-end, add, delete) with\n * the new value and the value at gesture start. Wire history here. */\n onChange?: (next: ControlPoint[], prev: readonly ControlPoint[]) => void;\n /** Model-space x range. Default [0, 1]. */\n xRange?: readonly [number, number];\n /** Model-space y range. Default [0, 1]. */\n yRange?: readonly [number, number];\n width: number;\n height: number;\n grid?: GridSettings | false | null;\n axes?: AxesSettings | false | null;\n /** Minimum allowed point count. User-initiated deletion is refused\n * while `value.length <= minPoints`. */\n minPoints?: number;\n /** Maximum allowed point count. User-initiated insertion is refused\n * while `value.length >= maxPoints`. */\n maxPoints?: number;\n /** How new points are added. Default `'click-empty'`. Set to `'never'`\n * to disable insertion entirely. */\n addPointMode?: 'click-empty' | 'never';\n /** Built-in undo/redo via keyboard when the component has focus.\n * Default `true`. See `CurveEditor.history` for details. */\n history?: boolean;\n /** Custom per-anchor renderer. See `CurveEditor.renderAnchor`. */\n renderAnchor?: (info: AnchorRenderProps) => ReactNode;\n /** Extra SVG content beneath the anchors. See `CurveEditor.decorations`. */\n decorations?: ReactNode;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * 2D point plotter — a CurveEditor with the curve hidden. Anchors,\n * drag, shift+click delete, right-click delete, and click-empty insert\n * are inherited from CurveEditor verbatim, so the two components share\n * gesture semantics and stay in sync as CurveEditor evolves.\n *\n * Differences from CurveEditor: no curve `<path>` drawn, no fill, no\n * endpoint pinning, no interpolation, default `addPointMode` is\n * `'click-empty'` (CurveEditor defaults to `'click-curve'`, which\n * doesn't make sense without a curve to hit-test against).\n */\nexport function PointPlotter(props: PointPlotterProps) {\n const curveProps: CurveEditorProps = {\n ...props,\n addPointMode: props.addPointMode ?? 'click-empty',\n curve: false,\n fill: false,\n endpoints: 'free',\n };\n return <CurveEditor {...curveProps} />;\n}\n"],"mappings":";;;AAyDA,SAAgB,EAAa,GAA0B;AAQrD,QAAO,kBAAC,GAAD;EANL,GAAG;EACH,cAAc,EAAM,gBAAgB;EACpC,OAAO;EACP,MAAM;EACN,WAAW;EAEyB,CAAA"}
@@ -1,4 +1,4 @@
1
- import { t as e } from "./Badge-BXTDs-zi.js";
1
+ import { t as e } from "./Badge-7yQseqZb.js";
2
2
  import { jsx as t } from "react/jsx-runtime";
3
3
  var n = {
4
4
  row: "_row_q3wun_1",
@@ -33,4 +33,4 @@ function r({ segments: r, startCap: i = "flat", size: a, variant: o, depth: s, g
33
33
  //#endregion
34
34
  export { r as t };
35
35
 
36
- //# sourceMappingURL=Powerline--gx4j0Zr.js.map
36
+ //# sourceMappingURL=Powerline-BY_ADnqq.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Powerline--gx4j0Zr.js","names":[],"sources":["../../src/components/Powerline/Powerline.module.css","../../src/components/Powerline/Powerline.tsx"],"sourcesContent":[".row {\n display: inline-flex;\n align-items: stretch;\n gap: var(--powerline-gap, 0.2em);\n}\n\n.matrix {\n display: grid;\n gap: 12px;\n}\n\n/* Reset Badge's opinionated text defaults — Powerline segments should pick up\n * the surrounding text style instead of forcing uppercase / 10px / extra\n * tracking. Consumers can still override via className on Powerline. */\n.segment {\n font-size: inherit;\n text-transform: none;\n letter-spacing: normal;\n}\n","import type { CSSProperties, ReactNode } from 'react';\nimport { Badge } from '../Badge/Badge';\nimport type { BadgeSize, BadgeTone, BadgeVariant } from '../Badge/types';\nimport type { EdgeCap } from '../Badge/bases/edgeProfiles';\nimport s from './Powerline.module.css';\n\n/**\n * One segment of a {@link Powerline}. Giving it `onClick` or `href` makes it\n * interactive, the same way it does on a `Badge`.\n */\nexport interface PowerlineSegment {\n text: ReactNode;\n /** Cap on this segment's right edge. Next segment's left edge adopts the same profile. */\n endCap?: EdgeCap;\n tone?: BadgeTone;\n variant?: BadgeVariant;\n size?: BadgeSize;\n onClick?: () => void;\n href?: string;\n 'aria-label'?: string;\n}\n\n/** Props for {@link Powerline}. */\nexport interface PowerlineProps {\n segments: PowerlineSegment[];\n /** Left edge of the first segment. Defaults to 'flat'. */\n startCap?: EdgeCap;\n /** Default size for every segment (per-segment `size` wins). */\n size?: BadgeSize;\n /** Default variant for every segment (per-segment `variant` wins). */\n variant?: BadgeVariant;\n /** Protrusion depth in CSS px, passed through to every segment's base. */\n depth?: number;\n /** Visible gap between adjacent segments. Number → px; string → literal CSS length.\n * Default: `0.2em` (scales with the row's font size). Pass `0` for flush. */\n gap?: number | string;\n className?: string;\n 'aria-label'?: string;\n}\n\n/**\n * A row of chevron-linked badge segments, in the style of a shell powerline\n * prompt. Each segment's `endCap` shapes both its own right edge and the next\n * segment's left edge, so the run reads as one continuous chain.\n *\n * Built for showing a path through a sequence of steps — the inspector uses\n * it to display how input routes from tool to gesture to action to target.\n */\nexport function Powerline({\n segments,\n startCap = 'flat',\n size,\n variant,\n depth,\n gap,\n className,\n ...rest\n}: PowerlineProps) {\n const cls = [s.row, className].filter(Boolean).join(' ');\n const style: CSSProperties | undefined = gap !== undefined\n ? { ['--powerline-gap' as never]: typeof gap === 'number' ? `${gap}px` : gap }\n : undefined;\n return (\n <span className={cls} style={style} aria-label={rest['aria-label']}>\n {segments.map((seg, i) => {\n const leftEdge: EdgeCap = i === 0 ? startCap : (segments[i - 1].endCap ?? 'flat');\n const rightEdge: EdgeCap = seg.endCap ?? 'flat';\n return (\n <Badge\n key={i}\n base=\"powerline\"\n baseParams={{ leftEdge, rightEdge, ...(depth !== undefined && { depth }) }}\n tone={seg.tone}\n variant={seg.variant ?? variant}\n size={seg.size ?? size}\n onClick={seg.onClick}\n href={seg.href}\n aria-label={seg['aria-label']}\n className={s.segment}\n >\n {seg.text}\n </Badge>\n );\n })}\n </span>\n );\n}\n"],"mappings":";;;;;;;;;ACgDA,SAAgB,EAAU,EACxB,aACA,cAAW,QACX,SACA,YACA,UACA,QACA,cACA,GAAG,KACc;AAKjB,QACE,kBAAC,QAAD;EAAM,WALI,CAAC,EAAE,KAAK,EAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAKjC;EAAY,OAJU,MAAQ,KAAA,IAE7C,KAAA,IADA,EAAG,mBAA6B,OAAO,KAAQ,WAAW,GAAG,EAAI,MAAM,GAAK;EAG1C,cAAY,EAAK;YAClD,EAAS,KAAK,GAAK,MAIhB,kBAAC,GAAD;GAEE,MAAK;GACL,YAAY;IAAE,UANQ,MAAM,IAAI,IAAY,EAAS,IAAI,GAAG,UAAU;IAM9C,WALD,EAAI,UAAU;IAKF,GAAI,MAAU,KAAA,KAAa,EAAE,UAAO;IAAG;GAC1E,MAAM,EAAI;GACV,SAAS,EAAI,WAAW;GACxB,MAAM,EAAI,QAAQ;GAClB,SAAS,EAAI;GACb,MAAM,EAAI;GACV,cAAY,EAAI;GAChB,WAAW,EAAE;aAEZ,EAAI;GACC,EAZD,EAYC,CAEV;EACG,CAAA"}
1
+ {"version":3,"file":"Powerline-BY_ADnqq.js","names":[],"sources":["../../src/components/Powerline/Powerline.module.css","../../src/components/Powerline/Powerline.tsx"],"sourcesContent":[".row {\n display: inline-flex;\n align-items: stretch;\n gap: var(--powerline-gap, 0.2em);\n}\n\n.matrix {\n display: grid;\n gap: 12px;\n}\n\n/* Reset Badge's opinionated text defaults — Powerline segments should pick up\n * the surrounding text style instead of forcing uppercase / 10px / extra\n * tracking. Consumers can still override via className on Powerline. */\n.segment {\n font-size: inherit;\n text-transform: none;\n letter-spacing: normal;\n}\n","import type { CSSProperties, ReactNode } from 'react';\nimport { Badge } from '../Badge/Badge';\nimport type { BadgeSize, BadgeTone, BadgeVariant } from '../Badge/types';\nimport type { EdgeCap } from '../Badge/bases/edgeProfiles';\nimport s from './Powerline.module.css';\n\n/**\n * One segment of a {@link Powerline}. Giving it `onClick` or `href` makes it\n * interactive, the same way it does on a `Badge`.\n */\nexport interface PowerlineSegment {\n text: ReactNode;\n /** Cap on this segment's right edge. Next segment's left edge adopts the same profile. */\n endCap?: EdgeCap;\n tone?: BadgeTone;\n variant?: BadgeVariant;\n size?: BadgeSize;\n onClick?: () => void;\n href?: string;\n 'aria-label'?: string;\n}\n\n/** Props for {@link Powerline}. */\nexport interface PowerlineProps {\n segments: PowerlineSegment[];\n /** Left edge of the first segment. Defaults to 'flat'. */\n startCap?: EdgeCap;\n /** Default size for every segment (per-segment `size` wins). */\n size?: BadgeSize;\n /** Default variant for every segment (per-segment `variant` wins). */\n variant?: BadgeVariant;\n /** Protrusion depth in CSS px, passed through to every segment's base. */\n depth?: number;\n /** Visible gap between adjacent segments. Number → px; string → literal CSS length.\n * Default: `0.2em` (scales with the row's font size). Pass `0` for flush. */\n gap?: number | string;\n className?: string;\n 'aria-label'?: string;\n}\n\n/**\n * A row of chevron-linked badge segments, in the style of a shell powerline\n * prompt. Each segment's `endCap` shapes both its own right edge and the next\n * segment's left edge, so the run reads as one continuous chain.\n *\n * Built for showing a path through a sequence of steps — the inspector uses\n * it to display how input routes from tool to gesture to action to target.\n */\nexport function Powerline({\n segments,\n startCap = 'flat',\n size,\n variant,\n depth,\n gap,\n className,\n ...rest\n}: PowerlineProps) {\n const cls = [s.row, className].filter(Boolean).join(' ');\n const style: CSSProperties | undefined = gap !== undefined\n ? { ['--powerline-gap' as never]: typeof gap === 'number' ? `${gap}px` : gap }\n : undefined;\n return (\n <span className={cls} style={style} aria-label={rest['aria-label']}>\n {segments.map((seg, i) => {\n const leftEdge: EdgeCap = i === 0 ? startCap : (segments[i - 1].endCap ?? 'flat');\n const rightEdge: EdgeCap = seg.endCap ?? 'flat';\n return (\n <Badge\n key={i}\n base=\"powerline\"\n baseParams={{ leftEdge, rightEdge, ...(depth !== undefined && { depth }) }}\n tone={seg.tone}\n variant={seg.variant ?? variant}\n size={seg.size ?? size}\n onClick={seg.onClick}\n href={seg.href}\n aria-label={seg['aria-label']}\n className={s.segment}\n >\n {seg.text}\n </Badge>\n );\n })}\n </span>\n );\n}\n"],"mappings":";;;;;;;;;ACgDA,SAAgB,EAAU,EACxB,aACA,cAAW,QACX,SACA,YACA,UACA,QACA,cACA,GAAG,KACc;AAKjB,QACE,kBAAC,QAAD;EAAM,WALI,CAAC,EAAE,KAAK,EAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAKjC;EAAY,OAJU,MAAQ,KAAA,IAE7C,KAAA,IADA,EAAG,mBAA6B,OAAO,KAAQ,WAAW,GAAG,EAAI,MAAM,GAAK;EAG1C,cAAY,EAAK;YAClD,EAAS,KAAK,GAAK,MAIhB,kBAAC,GAAD;GAEE,MAAK;GACL,YAAY;IAAE,UANQ,MAAM,IAAI,IAAY,EAAS,IAAI,GAAG,UAAU;IAM9C,WALD,EAAI,UAAU;IAKF,GAAI,MAAU,KAAA,KAAa,EAAE,UAAO;IAAG;GAC1E,MAAM,EAAI;GACV,SAAS,EAAI,WAAW;GACxB,MAAM,EAAI,QAAQ;GAClB,SAAS,EAAI;GACb,MAAM,EAAI;GACV,cAAY,EAAI;GAChB,WAAW,EAAE;aAEZ,EAAI;GACC,EAZD,EAYC,CAEV;EACG,CAAA"}
@@ -2,7 +2,7 @@ import { t as e } from "./dlog-CMYiRF3Q.js";
2
2
  import { n as t, t as n } from "./Tooltip-Bq8Hvj4r.js";
3
3
  import { i as r, n as i } from "./number-D3TDawLC.js";
4
4
  import { t as a } from "./range.module-CttzfN6a.js";
5
- import { n as o } from "./CurveEditor-Cpuvl_iN.js";
5
+ import { n as o } from "./CurveEditor-DoX5Q0_V.js";
6
6
  import { Fragment as s, jsx as c, jsxs as l } from "react/jsx-runtime";
7
7
  import { useCallback as u, useMemo as d, useState as f } from "react";
8
8
  import { Focusable as p } from "react-aria-components";
@@ -659,4 +659,4 @@ function P({ label: e, value: t, options: n, onChange: r, layout: i, description
659
659
  //#endregion
660
660
  export { S as a, N as c, P as d, x as f, h as g, _ as h, C as i, D as l, b as m, k as n, T as o, v as p, M as r, w as s, A as t, j as u };
661
661
 
662
- //# sourceMappingURL=Properties-BEGlt0Z2.js.map
662
+ //# sourceMappingURL=Properties-BuNzy7Hb.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Properties-BEGlt0Z2.js","names":[],"sources":["../../src/components/Properties/CurveField.module.css","../../src/components/Properties/CurveField.tsx","../../src/components/Properties/Properties.module.css","../../src/components/Properties/EffectCard.tsx","../../src/components/Properties/PropertyGroup.tsx","../../src/components/Properties/PropertyPanel.tsx"],"sourcesContent":[".field {\n display: grid;\n gap: 6px;\n grid-column: 1 / -1;\n box-sizing: border-box;\n}\n\n.field * {\n box-sizing: border-box;\n}\n\n.plot {\n position: relative;\n width: 100%;\n}\n\n.marks {\n position: absolute;\n inset: 0;\n pointer-events: none;\n}\n\n.actions {\n display: flex;\n gap: 6px;\n justify-self: start;\n}\n\n.action {\n background: transparent;\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n color: var(--wzl-fg);\n padding: 4px 10px;\n font-size: var(--wzl-font-size-sm);\n cursor: pointer;\n}\n\n.action:hover {\n background: var(--wzl-surface-raised);\n}\n","import { useCallback, useMemo } from 'react';\nimport { dlog } from '../../dlog';\nimport { type ControlPoint, CurveEditor } from '../CurveEditor';\nimport s from './CurveField.module.css';\n\n/** An annotation drawn over a curve plot: a shaded band across a range of x,\n * or a vertical line at one x. */\nexport type CurveMark =\n | { kind: 'band'; x: [number, number]; color?: string }\n | { kind: 'line'; x: number; color?: string };\n\n/** Props for `<CurveField>`. */\nexport interface CurveFieldProps {\n /** Flat [x0, y0, x1, y1, …] — matches how curve-as-array configs\n * serialize in JSON snapshots. */\n values: number[];\n min: number;\n max: number;\n step: number;\n /** Plot width in CSS px. Caller sizes (typically uses a ResizeObserver). */\n width: number;\n /** Plot height in CSS px. Default 110. */\n height?: number;\n /** Flat default curve. When provided, a Reset button restores it. */\n defaults?: readonly number[];\n onChange: (next: number[]) => void;\n /** Optional vertical marks overlaid on the plot (e.g. landmarks). */\n marks?: readonly CurveMark[];\n}\n\n/**\n * A function-domain (y = f(x), x ∈ [0,1]) curve editor with per-stop\n * numeric readouts and flip buttons. Wraps weasel-ui's\n * `CurveEditor`; consumers wanting the raw editor (2D paths,\n * custom anchors) should import that instead.\n */\nexport function CurveField({\n values,\n min,\n max,\n step,\n width,\n height = 110,\n defaults,\n onChange,\n marks,\n}: CurveFieldProps) {\n const points: ControlPoint[] = useMemo(() => {\n const out: ControlPoint[] = [];\n for (let i = 0; i + 1 < values.length; i += 2) out.push({ x: values[i], y: values[i + 1] });\n return out;\n }, [values]);\n\n const handleChange = useCallback(\n (next: ControlPoint[]) => {\n const flat: number[] = new Array(next.length * 2);\n for (let i = 0; i < next.length; i++) {\n const ySnap = Math.round(next[i].y / step) * step;\n const y = Math.max(min, Math.min(max, ySnap));\n flat[i * 2] = next[i].x;\n flat[i * 2 + 1] = y;\n }\n dlog('curve-field', 'handleChange', {\n nLen: next.length,\n flatLen: flat.length,\n sample: flat.slice(0, 4),\n });\n onChange(flat);\n },\n [onChange, min, max, step],\n );\n\n const handleFlip = useCallback(() => {\n const flipped: Array<{ x: number; y: number }> = [];\n for (let i = 0; i + 1 < values.length; i += 2) {\n flipped.push({ x: 1 - values[i], y: values[i + 1] });\n }\n flipped.sort((a, b) => a.x - b.x);\n const out: number[] = [];\n for (const p of flipped) out.push(p.x, p.y);\n onChange(out);\n }, [values, onChange]);\n\n const handleFlipVertical = useCallback(() => {\n const mid = (min + max) / 2;\n const flipped: number[] = new Array(values.length);\n for (let i = 0; i + 1 < values.length; i += 2) {\n flipped[i] = values[i];\n flipped[i + 1] = 2 * mid - values[i + 1];\n }\n onChange(flipped);\n }, [values, min, max, onChange]);\n\n return (\n <div className={s.field}>\n <div className={s.plot}>\n <CurveEditor\n value={points}\n onInput={handleChange}\n domain=\"1d\"\n constrain=\"function\"\n xRange={[0, 1]}\n yRange={[min, max]}\n width={width}\n height={height}\n endpoints=\"pinned-x\"\n addPointMode=\"click-curve\"\n minPoints={2}\n grid={{}}\n history={false}\n />\n {marks && marks.length > 0 && (\n <svg\n className={s.marks}\n width={width}\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n aria-hidden=\"true\"\n >\n {marks.map((m) => {\n const color = m.color ?? '#ffcc00';\n if (m.kind === 'band') {\n const x0 = Math.max(0, Math.min(1, m.x[0])) * width;\n const x1 = Math.max(0, Math.min(1, m.x[1])) * width;\n const left = Math.min(x0, x1);\n const w = Math.abs(x1 - x0);\n return (\n <g key={`band-${m.x[0]}-${m.x[1]}-${color}`}>\n <rect x={left} y={0} width={w} height={height} fill={color} fillOpacity={0.4} />\n <line\n x1={left}\n y1={0}\n x2={left}\n y2={height}\n stroke={color}\n strokeOpacity={0.85}\n strokeWidth={1}\n />\n <line\n x1={left + w}\n y1={0}\n x2={left + w}\n y2={height}\n stroke={color}\n strokeOpacity={0.85}\n strokeWidth={1}\n />\n </g>\n );\n }\n const x = Math.max(0, Math.min(1, m.x)) * width;\n return (\n <line\n key={`mark-${m.x}-${color}`}\n x1={x}\n y1={0}\n x2={x}\n y2={height}\n stroke={color}\n strokeWidth={1}\n />\n );\n })}\n </svg>\n )}\n </div>\n <div className={s.actions}>\n <button type=\"button\" className={s.action} onClick={handleFlip}>\n Flip horizontally\n </button>\n <button type=\"button\" className={s.action} onClick={handleFlipVertical}>\n Flip vertically\n </button>\n {defaults && (\n <button\n type=\"button\"\n className={s.action}\n onClick={() => onChange([...defaults])}\n >\n Reset\n </button>\n )}\n </div>\n </div>\n );\n}\n","/* One stylesheet for the whole property-panel family. PropertyList, PropertyRow,\n PropertyGroup and Subpanel/EffectCard are styled together because the grid\n rules genuinely cross components — `.listPairs > .subpanel` and\n `.groupBody > :not(.rowColor)` name two components in one selector, and a CSS\n Module can only compose local names within its own file. */\n\n/* Authored against a border-box reset (labkit's `.lk-root *`). weasel-ui has no\n global reset, so the family restates it over its own subtrees. */\n.panel,\n.list,\n.row,\n.group,\n.subpanel,\n.card,\n.cardList,\n.panel *,\n.list *,\n.row *,\n.group *,\n.subpanel *,\n.card *,\n.cardList * {\n box-sizing: border-box;\n}\n\n.panel {\n background: var(--wzl-surface-raised);\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-lg);\n padding: 14px 16px;\n backdrop-filter: blur(var(--wzl-glass-blur));\n -webkit-backdrop-filter: blur(var(--wzl-glass-blur));\n}\n\n.panelTitle {\n font-family: var(--wzl-font-display);\n font-weight: var(--wzl-font-weight-light);\n font-size: var(--wzl-font-size-lg);\n line-height: var(--wzl-leading-snug);\n letter-spacing: 0.01em;\n color: var(--wzl-fg);\n margin: 0 0 12px;\n}\n\n.list {\n display: grid;\n grid-template-columns: 1fr 1fr;\n column-gap: 10px;\n row-gap: 4px;\n align-items: stretch;\n /* A property panel is dense: its fields are shorter and tighter than a\n standalone Input. Set here, on the container, so one declaration moves\n every field in the panel. */\n --wzl-field-h: 20px;\n --wzl-field-pad-x: 6px;\n}\n\n/* Default packing: only color rows pair, everything else spans full width.\n Scoped to NOT apply in pairs mode (otherwise this 0,2,0 selector beats the\n pairs override below, which is only 0,1,0). */\n.list:not(.listPairs) > :not(.rowColor) {\n grid-column: 1 / -1;\n}\n\n/* `pack=\"pairs\"` flips that: every row auto-places into the 2-col grid two\n per row, and explicit full-width children opt in via the span helper class. */\n.listPairs > * {\n min-width: 0;\n}\n\n.listPairs > .span,\n.listPairs > .subpanel,\n.listPairs > h3 {\n grid-column: 1 / -1;\n}\n\n/* ── Row ────────────────────────────────────────────────────────\n Default shape: label on top (with optional right-aligned readout),\n control below. */\n\n.row {\n display: flex;\n flex-direction: column;\n /* No gap by default — controls sit flush under their label, matching\n speech-balloons `.field { gap: 0 }`. Slider rows pull tighter still\n via a negative margin on the label below. */\n gap: 0;\n min-width: 0;\n}\n\n/* Slider rows: pull the track up under the label so the row reads as one\n tight block rather than a label + a separate horizontal strip. Also\n bottom-align the track within the row so paired rows in a 2-col grid\n keep their tracks lined up even when one label wraps to two lines. */\n.row:has(> input[type='range']) {\n justify-content: flex-end;\n}\n\n.row:has(> input[type='range']) > .rowLabel {\n /* Baseline-align (not center) so the readout input doesn't inflate the\n label-group height past the text baseline. */\n align-items: baseline;\n justify-content: space-between;\n gap: 4px;\n margin-bottom: -4px;\n}\n\n.rowLabel {\n font-family: var(--wzl-font-display);\n font-weight: var(--wzl-font-weight-light);\n font-size: var(--wzl-font-size-sm);\n line-height: var(--wzl-leading-snug);\n color: var(--wzl-fg-muted);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.readout {\n font-style: normal;\n font-weight: var(--wzl-font-weight-medium);\n color: var(--wzl-accent);\n margin-left: auto;\n}\n\n/* ⓘ help affordance beside a row label. Every dimension is stated because a\n host app's bare-element defaults can be authored at zero specificity —\n labkit's `:where(.lk-root) :where(button)` sets `height: var(--wzl-control-h)`\n and horizontal padding, which would otherwise stretch a one-glyph button into\n a full-height control. */\n.help {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.15em;\n height: 1.15em;\n padding: 0;\n border: 0;\n background: none;\n backdrop-filter: none;\n -webkit-backdrop-filter: none;\n color: var(--wzl-fg-subtle);\n font-size: var(--wzl-font-size-sm);\n line-height: 1;\n letter-spacing: normal;\n cursor: help;\n border-radius: var(--wzl-radius-sm);\n}\n\n.help:hover {\n color: var(--wzl-fg);\n border-color: transparent;\n}\n\n.help:focus-visible {\n outline: 2px solid var(--wzl-focus-ring);\n outline-offset: 1px;\n color: var(--wzl-fg);\n}\n\n/* Single always-rendered <input> styled to read as a text label until focused. */\n.readoutInput {\n font: inherit;\n font-weight: var(--wzl-font-weight-medium);\n /* Force leading-tight — the label parent inherits 1.2, which would make the\n input box ~3px taller than the speech-balloons reference. */\n line-height: var(--wzl-leading-tight);\n color: var(--wzl-accent);\n background: transparent;\n border: 1px solid transparent;\n border-radius: var(--wzl-radius-sm);\n padding: 1px 0;\n margin: 0;\n width: calc(5em - 24px);\n text-align: right;\n cursor: text;\n outline: none;\n transition: background 80ms ease, border-color 80ms ease;\n -moz-appearance: textfield;\n}\n\n.readoutInput:hover {\n background: color-mix(in srgb, currentColor 8%, transparent);\n}\n\n.readoutInput:focus {\n background: var(--wzl-surface-sunken);\n border-color: var(--wzl-focus-ring);\n}\n\n.readoutInput::-webkit-inner-spin-button,\n.readoutInput::-webkit-outer-spin-button {\n -webkit-appearance: none;\n margin: 0;\n}\n\n/* Inline-block (not flex) so the browser's native vertical-align for\n <sub>/<sup> still works inside — flex collapses baseline semantics\n and sinks superscripts onto the baseline. */\n.readoutGroup {\n display: inline-block;\n white-space: nowrap;\n}\n\n/* Word-style units (e.g. \"px\") read as a small dim suffix next to the digits. */\n.readoutUnit {\n color: var(--wzl-fg-muted);\n font-size: var(--wzl-font-size-2xs);\n line-height: var(--wzl-leading-tight);\n pointer-events: none;\n}\n\n.fieldUnitGroup {\n display: inline-block;\n white-space: nowrap;\n min-width: 0;\n}\n\n.fieldUnitGroup .readoutUnit {\n margin-left: 4px;\n}\n\n/* Symbol units (e.g. `<sup>°</sup>`) lean on the browser's native super\n positioning. Just dim them to match — do NOT set line-height: 0, that\n would re-collapse the glyph onto the baseline. */\n.readoutGroup sup,\n.fieldUnitGroup sup {\n color: var(--wzl-fg-muted);\n font-size: var(--wzl-font-size-2xs);\n pointer-events: none;\n}\n\n/* ── Inline layout: label sits to the left of the control. ────── */\n.rowInline {\n flex-direction: row;\n align-items: center;\n /* Label on the left edge, field on the right: fields of different widths\n still line up as one rail down the column. */\n justify-content: space-between;\n gap: 6px;\n}\n\n.rowInline .rowLabel {\n /* The label never gives: shrinking it breaks a name like `silhouette_width`\n mid-word, since it carries no break opportunity of its own. The field is\n what yields as the column narrows. */\n flex: 0 0 auto;\n}\n\n.rowInline > input,\n.rowInline > select,\n.rowInline > .toggle {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n/* Fields size to their content, not to the row they happen to sit in: a number\n is a handful of digits and a short string is a word. Stated as a width and\n not as a `min(100%, …)`, because an auto-width sidebar sizes to what its\n fields ask for and a percentage asks for an input's 20-character default. */\n.row input[type='number']:not(.readoutInput) {\n width: var(--wzl-prop-number-width, 9ch);\n max-width: 100%;\n}\n\n.row input[type='text']:not(.readoutInput) {\n width: var(--wzl-prop-text-width, 16ch);\n max-width: 100%;\n}\n\n/* An inline row stays on one line: the field gives up width to the label as the\n column narrows, down to a floor that still shows a value. */\n.rowInline > input:not(.readoutInput),\n.rowInline > select {\n flex: 0 1 auto;\n min-width: 4ch;\n}\n\n.rowInline input[type='range'] {\n width: auto;\n}\n\n.row input[type='text']:not(.readoutInput),\n.row input[type='number'] {\n background: var(--wzl-surface-sunken);\n color: var(--wzl-fg);\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n /* Height is stated rather than padded to, because the display face's line box\n is half again its font size — padding a field to size makes it 27px tall\n around 13px of text and no amount of trimming the padding fixes that. */\n height: var(--wzl-field-h, var(--wzl-control-h));\n padding: 0 var(--wzl-field-pad-x);\n font-family: var(--wzl-font-ui);\n font-weight: var(--wzl-font-weight-medium);\n font-size: var(--wzl-font-size);\n line-height: var(--wzl-leading-tight);\n}\n\n/* Numbers right-align so a column of them shares a decimal position; text does\n not, so this cannot live in the rule the two share. */\n.row input[type='number'] {\n text-align: right;\n}\n\n.row input[type='text']:not(.readoutInput):focus,\n.row input[type='number']:focus {\n outline: none;\n border-color: var(--wzl-focus-ring);\n box-shadow: 0 0 0 1px var(--wzl-focus-ring);\n}\n\n/* Selects in property rows render borderless — chevron only, transparent\n background, no chrome. Keep the native arrow via appearance: auto so users\n still see something clickable. */\n.row select {\n background: transparent;\n color: var(--wzl-fg);\n border: 0;\n border-radius: 0;\n height: var(--wzl-field-h, var(--wzl-control-h));\n padding: 0;\n font-family: var(--wzl-font-ui);\n font-weight: var(--wzl-font-weight-medium);\n font-size: var(--wzl-font-size);\n line-height: var(--wzl-leading-tight);\n cursor: pointer;\n}\n\n/* An outline rather than the boxed family's inset ring: a property-row select\n is borderless, so there is no border to recolour. */\n.row select:focus {\n outline: 1px solid var(--wzl-focus-ring);\n outline-offset: 2px;\n}\n\n/* ── Color variant: label and swatch side-by-side, two per row in the grid.\n Switches to a 2-row grid when an alpha slider is rendered beneath. */\n.rowColor {\n display: grid;\n grid-template-columns: 1fr auto;\n grid-template-rows: auto;\n align-content: end;\n column-gap: 8px;\n row-gap: 0;\n min-width: 0;\n}\n\n.rowColor .rowLabel {\n grid-column: 1;\n grid-row: 1;\n align-self: center;\n flex: initial;\n min-width: 0;\n}\n\n/* 36×24, hairline border, no padding. Padding: 0 is important — the native\n <input type=\"color\"> in some browsers (esp. Chrome) inflates beyond its\n declared box when given padding, which expands the grid row and pushes\n the paired slider track out of bottom alignment. */\n.rowColor input[type='color'] {\n grid-column: 2;\n grid-row: 1;\n align-self: center;\n box-sizing: border-box;\n width: 36px;\n height: 18px;\n background: transparent;\n border: 0;\n border-radius: var(--wzl-radius-sm);\n padding: 0;\n cursor: pointer;\n flex-shrink: 0;\n}\n\n/* Alpha slider: full-width second row beneath the color swatch. */\n.rowColor input[type='range'].alpha {\n grid-column: 1 / -1;\n grid-row: 2;\n height: 12px;\n width: 100%;\n margin: 0;\n}\n\n/* ── Checkbox variant: control-first inline layout, no uppercase label. */\n.rowCheckbox {\n flex-direction: row;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n}\n\n.rowCheckbox .rowLabel {\n text-transform: none;\n letter-spacing: normal;\n font-size: var(--wzl-font-size);\n color: var(--wzl-fg);\n /* Place the label after the checkbox visually. */\n order: 1;\n}\n\n.rowCheckbox input[type='checkbox'] {\n accent-color: var(--wzl-accent);\n width: 14px;\n height: 14px;\n cursor: pointer;\n}\n\n/* ── Segmented toggle group ───────────────────────────────────── */\n.toggle {\n display: flex;\n width: 100%;\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n overflow: hidden;\n}\n\n.toggleButton {\n flex: 1;\n height: 26px;\n padding: 0 8px;\n background: rgba(0, 0, 0, 0.25);\n color: var(--wzl-fg-muted);\n border: 0;\n border-radius: 0;\n font-family: var(--wzl-font-ui);\n font-weight: var(--wzl-font-weight-medium);\n font-size: var(--wzl-font-size-sm);\n line-height: var(--wzl-leading-tight);\n cursor: pointer;\n backdrop-filter: none;\n -webkit-backdrop-filter: none;\n}\n\n.toggleButton + .toggleButton {\n border-left: 1px solid var(--wzl-border);\n}\n\n.toggleButton:hover {\n color: var(--wzl-fg);\n border-color: var(--wzl-border);\n}\n\n.toggleButtonSelected {\n background: var(--wzl-accent);\n color: var(--wzl-fg-on-accent);\n font-weight: var(--wzl-font-weight-medium);\n}\n\n.toggleButtonSelected:hover {\n color: var(--wzl-fg-on-accent);\n}\n\n/* ── PropertyGroup ──────────────────────────────────────────────────\n Bordered, sunken section inside a list. */\n.group {\n grid-column: 1 / -1;\n background: var(--wzl-surface-sunken);\n border-radius: var(--wzl-radius-md);\n padding: 6px 10px 8px;\n}\n\n.groupTitle {\n display: grid;\n grid-template-columns: 1fr auto 1fr;\n align-items: center;\n gap: 8px;\n margin: 0 0 4px;\n font-family: var(--wzl-font-display);\n font-weight: var(--wzl-font-weight-light);\n font-size: var(--wzl-font-size-sm);\n letter-spacing: var(--wzl-tracking-wider);\n text-transform: uppercase;\n color: var(--wzl-fg-subtle);\n}\n\n.groupTitle hr {\n border: 0;\n border-top: 1px solid var(--wzl-border);\n margin: 0;\n}\n\n.groupBody {\n display: grid;\n grid-template-columns: 1fr 1fr;\n column-gap: 10px;\n row-gap: 4px;\n /* Stretch so paired rows match in height — slider rows then bottom-align\n their tracks (via `margin-top: auto`) and stay lined up even when one\n sibling's label wraps to two lines. */\n align-items: stretch;\n}\n\n.group:not(.groupPairs) .groupBody > :not(.rowColor) {\n grid-column: 1 / -1;\n}\n\n.groupPairs .groupBody > * {\n grid-column: auto;\n}\n\n.groupPairs .groupBody > h3,\n.groupPairs .groupBody > .span {\n grid-column: 1 / -1;\n}\n\n/* ── Subpanel ────────────────────────────────────────────────────────\n Headered group of controls inside a list. The header is a typographic\n label flanked by a horizontal rule — no card chrome, just the divider. */\n.subpanel {\n display: grid;\n grid-template-columns: 1fr 1fr;\n column-gap: 10px;\n row-gap: 3px;\n align-items: stretch;\n background: transparent;\n border: 0;\n padding: 0;\n margin: 0;\n}\n\n.subpanel > * {\n min-width: 0;\n}\n\n.subpanel > .subpanelTitle,\n.subpanel > .span {\n grid-column: 1 / -1;\n}\n\n.subpanelTitle {\n font-family: var(--wzl-font-display);\n font-weight: var(--wzl-font-weight-medium);\n font-size: var(--wzl-font-size-sm);\n line-height: var(--wzl-leading-tight);\n color: var(--wzl-fg);\n text-transform: uppercase;\n letter-spacing: var(--wzl-tracking-wider);\n margin: 0;\n padding: 6px 0 4px;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.subpanelTitle > span {\n flex: 0 0 auto;\n}\n\n.subpanelTitle > hr {\n flex: 1 1 auto;\n border: 0;\n border-top: 1px solid rgba(255, 255, 255, 0.12);\n margin: 0;\n}\n\n/* ── EffectCard ──────────────────────────────────────────────────────\n Draggable, collapsible card. Optional --wzl-effect-card-accent re-binds the\n local --wzl-accent so every descendant control auto-tints. */\n.card {\n background: rgba(0, 0, 0, 0.25);\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n.cardDragging {\n opacity: 0.45;\n}\n\n.cardExpanded {\n background: rgba(0, 0, 0, 0.3);\n}\n\n.cardAccented {\n /* Re-bind --wzl-accent inside the card so every descendant style that paints\n with var(--wzl-accent) (readouts, slider thumb, etc.) picks up the\n per-card tint automatically. */\n --wzl-accent: var(--wzl-effect-card-accent);\n background: color-mix(in srgb, var(--wzl-effect-card-accent) 4%, rgba(0, 0, 0, 0.28));\n border-color: color-mix(in srgb, var(--wzl-effect-card-accent) 28%, var(--wzl-border));\n border-left-width: 4px;\n border-left-color: var(--wzl-effect-card-accent);\n}\n\n.cardAccented.cardExpanded {\n background: color-mix(in srgb, var(--wzl-effect-card-accent) 5%, rgba(0, 0, 0, 0.35));\n}\n\n.cardAccented .cardHead {\n background: color-mix(in srgb, var(--wzl-effect-card-accent) 38%, rgba(0, 0, 0, 0.4));\n}\n\n.cardAccented .cardHead:hover {\n background: color-mix(in srgb, var(--wzl-effect-card-accent) 48%, rgba(0, 0, 0, 0.35));\n}\n\n.cardHead {\n display: flex;\n align-items: baseline;\n gap: 8px;\n padding: 2px 10px;\n cursor: pointer;\n user-select: none;\n}\n\n.cardHead:hover {\n background: rgba(255, 255, 255, 0.03);\n}\n\n.cardHandle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--wzl-fg-muted);\n cursor: grab;\n padding: 0 2px;\n}\n\n.cardHandle:active {\n cursor: grabbing;\n}\n\n.cardIndexBadge {\n /* Rendered as a <button> (keyboard-operable drag/toggle handle); strip the\n native button chrome so it matches the surrounding badge styling. */\n appearance: none;\n border: 0;\n background: none;\n display: inline-flex;\n align-items: baseline;\n justify-content: center;\n min-width: 14px;\n padding: 0 2px;\n color: var(--wzl-effect-card-accent, var(--wzl-fg-muted));\n font-size: var(--wzl-font-size);\n font-weight: var(--wzl-font-weight-bold);\n font-family: var(--wzl-font-mono);\n line-height: 1;\n}\n\n.cardTitle {\n font-family: var(--wzl-font-display);\n font-weight: var(--wzl-font-weight-medium);\n font-size: var(--wzl-font-size);\n line-height: var(--wzl-leading-tight);\n color: var(--wzl-fg);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n.cardPrimary {\n font-family: var(--wzl-font-ui);\n font-weight: var(--wzl-font-weight-medium);\n font-size: var(--wzl-font-size-sm);\n line-height: var(--wzl-leading-tight);\n color: var(--wzl-fg);\n background: rgba(0, 0, 0, 0.3);\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n padding: 2px 6px;\n margin-left: auto;\n}\n\n/* Push the remove button to the right edge — whether it follows a primary\n chip or sits alone after the title. */\n.cardHead .cardRemove,\n.cardHead .cardPrimary + .cardRemove {\n margin-left: auto;\n}\n\n.cardHead .cardPrimary ~ .cardRemove {\n margin-left: 4px;\n}\n\n.cardRemove {\n background: transparent;\n color: var(--wzl-fg-muted);\n border: 1px solid transparent;\n border-radius: var(--wzl-radius-sm);\n width: 22px;\n height: 22px;\n font-size: var(--wzl-font-size);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.cardRemove:hover {\n color: var(--wzl-danger-fg, var(--wzl-danger));\n border-color: color-mix(in srgb, var(--wzl-danger) 40%, transparent);\n background: color-mix(in srgb, var(--wzl-danger) 8%, transparent);\n}\n\n.cardBody {\n padding: 4px 12px 12px;\n border-top: 1px solid var(--wzl-border);\n}\n\n/* ── EffectCardList ────────────────────────────────────────────────── */\n.cardList {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.cardListWrap {\n position: relative;\n}\n\n.cardDropHint {\n height: 0;\n border-top: 2px solid var(--wzl-accent);\n border-radius: var(--wzl-radius-sm);\n margin: 2px 0;\n box-shadow: 0 0 6px color-mix(in srgb, var(--wzl-accent) 60%, transparent);\n}\n\n.cardListEmpty {\n font-family: var(--wzl-font-display);\n font-weight: var(--wzl-font-weight-light);\n font-size: var(--wzl-font-size-sm);\n line-height: var(--wzl-leading);\n color: var(--wzl-fg-muted);\n text-align: center;\n padding: 16px 12px;\n border: 1px dashed var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n}\n","import { type CSSProperties, type ReactNode, useState } from 'react';\nimport s from './Properties.module.css';\n\n// ── Subpanel ─────────────────────────────────────────────────────────\n// Headered group of controls inside a wider panel/list. The header is a\n// label flanked by a horizontal rule — no background, padding, or border;\n// purely a typographic divider. Ported from speech-balloons styles.css:277-313.\n\n/** Props for `<Subpanel>`. */\nexport interface SubpanelProps {\n title: ReactNode;\n children: ReactNode;\n className?: string;\n}\n\n/** A typographic divider inside a property list: a small title flanked by a\n * rule. Purely a heading — use `<PropertyGroup>` for a bordered section. */\nexport function Subpanel({ title, children, className }: SubpanelProps) {\n const cls = className ? `${s.subpanel} ${className}` : s.subpanel;\n return (\n <div className={cls}>\n <h4 className={s.subpanelTitle}>\n <span>{title}</span>\n <hr />\n </h4>\n {children}\n </div>\n );\n}\n\n// ── EffectCard ───────────────────────────────────────────────────────\n// Draggable, collapsible card for stacked effect editors. Optional\n// --wzl-effect-card-accent per-instance recolors the title bar, border-left, and\n// (via re-binding --wzl-accent inside the card) every descendant control that\n// reads from the accent token.\n\n/** Props for `<EffectCard>`. */\nexport interface EffectCardProps {\n /** Card title rendered in the title bar (e.g. effect kind label or a select). */\n title: ReactNode;\n /** Optional badge/chip displayed before the remove button (e.g. primary value summary). */\n primary?: ReactNode;\n /** Optional accent color in any CSS color form. Sets --wzl-effect-card-accent on the card. */\n accent?: string;\n /** Optional ordinal badge that doubles as the drag handle (used by tail cards in SB). */\n index?: number;\n expanded?: boolean;\n onToggleExpanded?: () => void;\n onRemove?: () => void;\n /**\n * Drag-handle wiring. When provided, an inline handle (⋮⋮ icon or `index`)\n * activates HTML5 drag on press. Consumer owns the drag/drop coordination —\n * see EffectCardList for the batteries-included wrapper.\n */\n draggable?: boolean;\n onPressHandle?: () => void;\n onReleaseHandle?: () => void;\n onDragStart?: (e: React.DragEvent<HTMLDivElement>) => void;\n onDragEnd?: (e: React.DragEvent<HTMLDivElement>) => void;\n onDragOver?: (e: React.DragEvent<HTMLDivElement>) => void;\n onDragLeave?: (e: React.DragEvent<HTMLDivElement>) => void;\n onDrop?: (e: React.DragEvent<HTMLDivElement>) => void;\n dragging?: boolean;\n children?: ReactNode;\n className?: string;\n}\n\n/** An accented, collapsible card for one item in a list of like things —\n * effects, layers, tails. The accent color rebinds the theme accent token\n * within the card, so its controls pick it up too. */\nexport function EffectCard({\n title,\n primary,\n accent,\n index,\n expanded = true,\n onToggleExpanded,\n onRemove,\n draggable,\n onPressHandle,\n onReleaseHandle,\n onDragStart,\n onDragEnd,\n onDragOver,\n onDragLeave,\n onDrop,\n dragging,\n children,\n className,\n}: EffectCardProps) {\n const stateClass = `${expanded ? ` ${s.cardExpanded}` : ''}${dragging ? ` ${s.cardDragging}` : ''}${accent ? ` ${s.cardAccented}` : ''}`;\n const cls = `${s.card}${stateClass}${className ? ` ${className}` : ''}`;\n // Per-instance dynamic color → CSS custom property requires inline style.\n // The card's CSS re-binds --wzl-accent to it so descendants tint.\n const style = accent ? ({ '--wzl-effect-card-accent': accent } as CSSProperties) : undefined;\n\n // When a toggle handler is supplied the header acts as a disclosure button:\n // role + tab stop + Enter/Space. Bundled together so the interactive props are\n // applied as a set (no half-interactive element when onToggleExpanded is absent).\n const headProps = onToggleExpanded\n ? {\n role: 'button' as const,\n tabIndex: 0,\n onClick: onToggleExpanded,\n onKeyDown: (e: React.KeyboardEvent) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onToggleExpanded();\n }\n },\n }\n : {};\n\n return (\n // biome-ignore lint/a11y/noStaticElementInteractions: native HTML5 drag-and-drop container — drag events are not click/keyboard interactions and have no semantic element; the in-card handle and header carry the a11y affordances.\n <div\n className={cls}\n style={style}\n draggable={draggable}\n onDragStart={onDragStart}\n onDragEnd={onDragEnd}\n onDragOver={onDragOver}\n onDragLeave={onDragLeave}\n onDrop={onDrop}\n >\n <div className={s.cardHead} {...headProps}>\n {index != null ? (\n <button\n type=\"button\"\n className={`${s.cardIndexBadge} ${s.cardHandle}`}\n title=\"Drag to reorder · click to toggle\"\n aria-label={`Item ${index + 1} — drag to reorder, click to toggle`}\n onMouseDown={onPressHandle}\n onMouseUp={onReleaseHandle}\n onClick={(e) => {\n e.stopPropagation();\n onToggleExpanded?.();\n }}\n >\n {index + 1}\n </button>\n ) : (\n <span\n className={s.cardHandle}\n aria-hidden=\"true\"\n title=\"Drag to reorder\"\n onMouseDown={onPressHandle}\n onMouseUp={onReleaseHandle}\n >\n <DragHandleIcon />\n </span>\n )}\n <span className={s.cardTitle}>{title}</span>\n {primary != null && <span className={s.cardPrimary}>{primary}</span>}\n {onRemove && (\n <button\n type=\"button\"\n className={s.cardRemove}\n onClick={(e) => {\n e.stopPropagation();\n onRemove();\n }}\n title=\"Remove\"\n aria-label=\"Remove\"\n >\n ✕\n </button>\n )}\n </div>\n {expanded && <div className={s.cardBody}>{children}</div>}\n </div>\n );\n}\n\nfunction DragHandleIcon() {\n return (\n <svg width=\"10\" height=\"14\" viewBox=\"0 0 10 14\" aria-hidden=\"true\">\n <circle cx=\"3\" cy=\"3\" r=\"1.3\" fill=\"currentColor\" />\n <circle cx=\"7\" cy=\"3\" r=\"1.3\" fill=\"currentColor\" />\n <circle cx=\"3\" cy=\"7\" r=\"1.3\" fill=\"currentColor\" />\n <circle cx=\"7\" cy=\"7\" r=\"1.3\" fill=\"currentColor\" />\n <circle cx=\"3\" cy=\"11\" r=\"1.3\" fill=\"currentColor\" />\n <circle cx=\"7\" cy=\"11\" r=\"1.3\" fill=\"currentColor\" />\n </svg>\n );\n}\n\n// ── EffectCardList ───────────────────────────────────────────────────\n// Batteries-included list wrapper that coordinates HTML5 drag-and-drop across\n// a list of EffectCards. Consumer supplies items + a renderItem callback;\n// onReorder fires with (sourceId, targetId, position) on drop. Mirrors the\n// LayerStack pattern from speech-balloons Lab.tsx:630-707.\n\n/** The minimum an item must carry to appear in an `<EffectCardList>`. */\nexport interface EffectCardListItem {\n id: string | number;\n}\n\n/** Props for `<EffectCardList>`. */\nexport interface EffectCardListProps<T extends EffectCardListItem> {\n items: ReadonlyArray<T>;\n renderItem: (\n item: T,\n helpers: {\n isExpanded: boolean;\n isDragging: boolean;\n toggleExpanded: () => void;\n cardProps: Pick<\n EffectCardProps,\n | 'expanded'\n | 'dragging'\n | 'draggable'\n | 'onToggleExpanded'\n | 'onPressHandle'\n | 'onReleaseHandle'\n | 'onDragStart'\n | 'onDragEnd'\n | 'onDragOver'\n | 'onDragLeave'\n | 'onDrop'\n >;\n },\n ) => ReactNode;\n onReorder: (sourceId: T['id'], targetId: T['id'], position: 'before' | 'after') => void;\n /** Optional rendered when the list is empty. */\n empty?: ReactNode;\n /** IDs to render expanded on first mount. Defaults to all collapsed. */\n defaultExpandedIds?: ReadonlyArray<T['id']>;\n}\n\n/** A drag-reorderable list of `<EffectCard>`s. Owns the expanded and dragging\n * state and hands it back to `renderItem`, so the caller only supplies each\n * card's contents. */\nexport function EffectCardList<T extends EffectCardListItem>({\n items,\n renderItem,\n onReorder,\n empty,\n defaultExpandedIds,\n}: EffectCardListProps<T>) {\n const [expanded, setExpanded] = useState<Set<T['id']>>(() => new Set(defaultExpandedIds ?? []));\n const [draggingId, setDraggingId] = useState<T['id'] | null>(null);\n const [pressedId, setPressedId] = useState<T['id'] | null>(null);\n const [dropHint, setDropHint] = useState<{ id: T['id']; position: 'before' | 'after' } | null>(\n null,\n );\n\n if (items.length === 0) {\n return <>{empty ?? <div className={s.cardListEmpty}>No items.</div>}</>;\n }\n\n return (\n <div className={s.cardList}>\n {items.map((item) => {\n const isExpanded = expanded.has(item.id);\n const isDragging = draggingId === item.id;\n const isPressed = pressedId === item.id;\n const showHintBefore =\n dropHint && dropHint.id === item.id && dropHint.position === 'before';\n const showHintAfter = dropHint && dropHint.id === item.id && dropHint.position === 'after';\n\n const toggleExpanded = () =>\n setExpanded((prev) => {\n const next = new Set(prev);\n if (next.has(item.id)) next.delete(item.id);\n else next.add(item.id);\n return next;\n });\n\n return (\n <div key={String(item.id)} className={s.cardListWrap}>\n {showHintBefore && <div className={s.cardDropHint} />}\n {renderItem(item, {\n isExpanded,\n isDragging,\n toggleExpanded,\n cardProps: {\n expanded: isExpanded,\n dragging: isDragging,\n draggable: isPressed,\n onToggleExpanded: toggleExpanded,\n onPressHandle: () => setPressedId(item.id),\n onReleaseHandle: () => setPressedId(null),\n onDragStart: (e) => {\n e.dataTransfer.effectAllowed = 'move';\n e.dataTransfer.setData('text/plain', String(item.id));\n setDraggingId(item.id);\n },\n onDragEnd: () => {\n setDraggingId(null);\n setPressedId(null);\n setDropHint(null);\n },\n onDragOver: (e) => {\n if (draggingId === null || draggingId === item.id) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = 'move';\n const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect();\n const position: 'before' | 'after' =\n e.clientY < rect.top + rect.height / 2 ? 'before' : 'after';\n setDropHint({ id: item.id, position });\n },\n onDragLeave: () => {\n setDropHint((h) => (h && h.id === item.id ? null : h));\n },\n onDrop: (e) => {\n e.preventDefault();\n if (draggingId === null || draggingId === item.id) {\n setDropHint(null);\n return;\n }\n const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect();\n const position: 'before' | 'after' =\n e.clientY < rect.top + rect.height / 2 ? 'before' : 'after';\n onReorder(draggingId, item.id, position);\n setDropHint(null);\n },\n },\n })}\n {showHintAfter && <div className={s.cardDropHint} />}\n </div>\n );\n })}\n </div>\n );\n}\n","import type { ReactNode } from 'react';\nimport s from './Properties.module.css';\n\n/** Props for `<PropertyGroup>`. */\nexport interface PropertyGroupProps {\n /** Title rendered between two rules at the top of the group. */\n title: ReactNode;\n /** When true the group renders nothing — useful for conditional sections. */\n hidden?: boolean;\n children: ReactNode;\n className?: string;\n /**\n * How rows pack into the 2-column grid.\n * - `'auto-color'` (default): only color rows pair side-by-side; everything\n * else spans the full width.\n * - `'pairs'`: every row auto-places into the 2-column grid two-per-row.\n */\n pack?: 'auto-color' | 'pairs';\n}\n\n/**\n * Visually-bordered group inside a PropertyList. Use to scope a set of\n * related rows under a heading (e.g. \"Aqua\", \"Bevel\", \"Dome\" sections\n * inside a fill effect's controls).\n */\nexport function PropertyGroup({\n title,\n hidden,\n children,\n className,\n pack = 'auto-color',\n}: PropertyGroupProps) {\n if (hidden) return null;\n const packClass = pack === 'pairs' ? ` ${s.groupPairs}` : '';\n const cls = `${s.group}${packClass}${className ? ` ${className}` : ''}`;\n return (\n <div className={cls}>\n <h3 className={s.groupTitle}>\n <hr />\n <span>{title}</span>\n <hr />\n </h3>\n <div className={s.groupBody}>{children}</div>\n </div>\n );\n}\n","import { type ReactNode, useState } from 'react';\nimport { Focusable } from 'react-aria-components';\nimport { dlog } from '../../dlog';\nimport { formatNumber, parseSignedNumber } from '../../format/number';\nimport { Tooltip, TooltipTrigger } from '../Tooltip';\nimport shared from '../range.module.css';\nimport s from './Properties.module.css';\n\n/** Props for `<PropertyPanel>`. */\nexport interface PropertyPanelProps {\n title?: ReactNode;\n children: ReactNode;\n className?: string;\n}\n\n/** A titled panel holding property rows — the sidebar container the rest of\n * this module's components fill. */\nexport function PropertyPanel({ title, children, className }: PropertyPanelProps) {\n const cls = className ? `${s.panel} ${className}` : s.panel;\n return (\n <div className={cls}>\n {title != null && <h2 className={s.panelTitle}>{title}</h2>}\n {children}\n </div>\n );\n}\n\n/** How a property list packs its rows into two columns. */\nexport type PropertyListPack = 'auto-color' | 'pairs';\n\n/** Props for `<PropertyList>`. */\nexport interface PropertyListProps {\n children: ReactNode;\n className?: string;\n /**\n * How rows pack into the 2-column grid.\n * - `'auto-color'` (default): only color rows pair side-by-side; everything\n * else spans the full width. Right for sparse top-level panels.\n * - `'pairs'`: every row auto-places into the 2-column grid two-per-row.\n * Headers and subpanels still span the full width; wrap any other\n * full-width child in `<PropertySpan>`. Right for dense effect bodies.\n */\n pack?: PropertyListPack;\n}\n\n/**\n * Grid container for PropertyRows. Use standalone for chrome-less layouts, or\n * nest inside <PropertyPanel/> for the standard glass card.\n */\nexport function PropertyList({ children, className, pack = 'auto-color' }: PropertyListProps) {\n const packClass = pack === 'pairs' ? ` ${s.listPairs}` : '';\n const cls = `${s.list}${packClass}${className ? ` ${className}` : ''}`;\n return <div className={cls}>{children}</div>;\n}\n\n/** Props for `<PropertySpan>`. */\nexport interface PropertySpanProps {\n children: ReactNode;\n className?: string;\n}\n\n/**\n * Wrapper that makes an arbitrary child span both columns of a\n * `<PropertyList pack=\"pairs\">`, a `<PropertyGroup pack=\"pairs\">` body, or a\n * `<Subpanel>`. `<PropertyRow span>` covers the row case; this covers\n * everything else a consumer puts in the grid.\n */\nexport function PropertySpan({ children, className }: PropertySpanProps) {\n return <div className={className ? `${s.span} ${className}` : s.span}>{children}</div>;\n}\n\n/** Which control shape a row holds, which decides its intrinsic layout. */\nexport type PropertyRowVariant = 'default' | 'color' | 'checkbox';\n/** Whether a row's label sits above its control or beside it. */\nexport type PropertyRowLayout = 'block' | 'inline';\n\n/** Props for `<PropertyRow>`. */\nexport interface PropertyRowProps {\n label: ReactNode;\n /** Right-aligned readout shown next to the label (e.g. current value). */\n readout?: ReactNode;\n /**\n * Help text for the row, shown in a tooltip off an ⓘ affordance beside the\n * label. Empty or absent renders no affordance.\n */\n description?: string;\n variant?: PropertyRowVariant;\n /**\n * Label position relative to the control. \"block\" (default) stacks\n * label above control; \"inline\" places the label to the left.\n * Color and checkbox variants are always inline by their own nature.\n */\n layout?: PropertyRowLayout;\n /**\n * Take the full width of the enclosing grid. Only has an effect inside a\n * `<PropertyList pack=\"pairs\">` or a `<Subpanel>`; elsewhere rows are already\n * full width.\n */\n span?: boolean;\n children: ReactNode;\n htmlFor?: string;\n className?: string;\n}\n\n/** The label-plus-control frame every typed row below is built from. Use it\n * directly for a control this module does not cover. */\nexport function PropertyRow({\n label,\n readout,\n description,\n variant = 'default',\n layout = 'block',\n span,\n children,\n htmlFor,\n className,\n}: PropertyRowProps) {\n const variantClass = variant === 'color' ? ` ${s.rowColor}` : variant === 'checkbox' ? ` ${s.rowCheckbox}` : '';\n // Layout only meaningfully applies to the default variant; color and\n // checkbox have their own intrinsic orientation.\n const layoutClass = variant === 'default' && layout === 'inline' ? ` ${s.rowInline}` : '';\n const spanClass = span ? ` ${s.span}` : '';\n const cls = `${s.row}${variantClass}${layoutClass}${spanClass}${className ? ` ${className}` : ''}`;\n return (\n <label className={cls} htmlFor={htmlFor}>\n <span className={s.rowLabel}>\n {label}\n {description ? <PropertyRowHelp label={label} description={description} /> : null}\n {readout != null && <em className={s.readout}>{readout}</em>}\n </span>\n {children}\n </label>\n );\n}\n\n/** Tooltip trigger for a row's `description`. A tooltip trigger has to be\n * interactive to be keyboard-reachable, so this is a real button. */\nfunction PropertyRowHelp({ label, description }: { label: ReactNode; description: string }) {\n const name = typeof label === 'string' ? label : 'this setting';\n return (\n <TooltipTrigger>\n <Focusable>\n <button\n type=\"button\"\n className={s.help}\n aria-label={`About ${name}`}\n onClick={(e) => {\n // The wrapping <label> would otherwise actuate the row's control.\n e.preventDefault();\n e.stopPropagation();\n }}\n onMouseDown={(e) => e.stopPropagation()}\n >\n ⓘ\n </button>\n </Focusable>\n <Tooltip>{description}</Tooltip>\n </TooltipTrigger>\n );\n}\n\n// ── Row implementations ──────────────────────────────────────────────\n\n/** Props for `<SliderRow>`. */\nexport interface SliderRowProps {\n label: ReactNode;\n value: number;\n min: number;\n max: number;\n step?: number;\n onChange: (next: number) => void;\n /** Override how the value is rendered next to the label. Defaults to `value.toString()`. */\n format?: (value: number) => ReactNode;\n /**\n * Optional suffix rendered next to the readout. A string becomes a\n * baseline-aligned dim \"word\" unit (e.g. \"px\"); pass JSX like `<sup>°</sup>`\n * to get the browser's native super positioning for symbol units.\n */\n unit?: ReactNode;\n layout?: PropertyRowLayout;\n description?: string;\n /** Take the full width of the enclosing grid — see `<PropertyRow span>`. */\n span?: boolean;\n}\n\n/** A bounded number edited by dragging, with a live readout whose precision\n * follows `step`. */\nexport function SliderRow({\n label,\n value,\n min,\n max,\n step = 1,\n onChange,\n format,\n unit,\n layout,\n description,\n span,\n}: SliderRowProps) {\n // Default readout precision tracks `step`: integer steps → 0 decimals,\n // 0.1 → 1 decimal, 0.05/0.02/0.01 → 2 decimals, 0.005 → 3, etc. Callers\n // can still pass an explicit `format` to override (e.g. for a custom\n // unit string or a non-decimal display like fractions).\n const decimals = step >= 1 ? 0 : Math.min(6, Math.max(0, Math.ceil(-Math.log10(step))));\n const effectiveFormat =\n format ??\n ((n: number) =>\n formatNumber(n, {\n useGrouping: false,\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n }));\n return (\n <PropertyRow\n span={span}\n label={label}\n readout={\n <EditableReadout\n value={value}\n min={min}\n max={max}\n format={effectiveFormat}\n unit={unit}\n onCommit={onChange}\n />\n }\n layout={layout}\n description={description}\n >\n <input\n type=\"range\"\n className={shared.range}\n tabIndex={-1}\n min={min}\n max={max}\n step={step}\n value={value}\n onChange={(e) => {\n const v = Number(e.target.value);\n dlog('property-panel', 'slider', { label, value: v });\n onChange(v);\n }}\n />\n </PropertyRow>\n );\n}\n\ninterface EditableReadoutProps {\n value: number;\n min: number;\n max: number;\n format?: (value: number) => ReactNode;\n unit?: ReactNode;\n onCommit: (next: number) => void;\n}\n\n/**\n * Readout that swaps to a number input on click, commits on Enter/blur,\n * cancels on Escape. Clicks are stopped so the wrapping <label> doesn't\n * forward focus to the slider thumb.\n */\nfunction EditableReadout({ value, min, max, format, unit, onCommit }: EditableReadoutProps) {\n // Draft is non-null only while the input is focused. Live value mirrors\n // into the input otherwise. Pattern mirrors speech-balloons Lab.tsx:893-942.\n const [draft, setDraft] = useState<string | null>(null);\n const fmt = (n: number) =>\n format ? format(n) : formatNumber(n, { useGrouping: false, maximumFractionDigits: 20 });\n const displayValue =\n draft !== null\n ? draft\n : (() => {\n const formatted = fmt(value);\n return typeof formatted === 'string' ? formatted : String(formatted);\n })();\n\n const suffix =\n unit == null ? null : typeof unit === 'string' ? (\n <span className={s.readoutUnit}>{unit}</span>\n ) : (\n unit\n );\n\n const commit = () => {\n if (draft !== null) {\n const n = parseSignedNumber(draft);\n if (Number.isFinite(n)) onCommit(Math.min(max, Math.max(min, n)));\n }\n setDraft(null);\n };\n\n return (\n <span className={s.readoutGroup}>\n <input\n type=\"text\"\n inputMode=\"decimal\"\n className={s.readoutInput}\n value={displayValue}\n onFocus={(e) => {\n const formatted = fmt(value);\n setDraft(typeof formatted === 'string' ? formatted : String(formatted));\n e.currentTarget.select();\n }}\n onChange={(e) => setDraft(e.target.value.replace(/-/g, '−'))}\n onBlur={commit}\n onClick={(e) => {\n // Stop the click from reaching the wrapping <label> (which would\n // forward focus to the slider thumb).\n e.preventDefault();\n e.stopPropagation();\n e.currentTarget.focus();\n }}\n onMouseDown={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n commit();\n e.currentTarget.blur();\n } else if (e.key === 'Escape') {\n setDraft(null);\n e.currentTarget.blur();\n }\n }}\n />\n {suffix}\n </span>\n );\n}\n\n/** Props for `<ColorRow>`. */\nexport interface ColorRowProps {\n label: ReactNode;\n value: string;\n onChange: (next: string) => void;\n /** 0..1 alpha. When provided, a translucent slider renders beneath the swatch. */\n alpha?: number;\n onAlphaChange?: (next: number) => void;\n /**\n * Render the alpha track as inert (dimmed, no thumb, not-allowed cursor).\n * Use when the color's consumer drops alpha so the affordance reads dead.\n */\n alphaDisabled?: boolean;\n description?: string;\n /** Take the full width of the enclosing grid — see `<PropertyRow span>`. */\n span?: boolean;\n}\n\n/** A color swatch, optionally with an alpha slider beneath it. */\nexport function ColorRow({\n label,\n value,\n onChange,\n alpha,\n onAlphaChange,\n alphaDisabled,\n description,\n span,\n}: ColorRowProps) {\n const showAlpha = alpha != null;\n return (\n <PropertyRow span={span} label={label} variant=\"color\" description={description}>\n <input type=\"color\" value={value} onChange={(e) => onChange(e.target.value)} />\n {showAlpha && (\n <input\n type=\"range\"\n className={`${shared.range} ${shared.alpha} ${s.alpha}`}\n min={0}\n max={1}\n step={0.01}\n value={alpha}\n disabled={alphaDisabled}\n onChange={(e) => onAlphaChange?.(Number(e.target.value))}\n />\n )}\n </PropertyRow>\n );\n}\n\n/** Props for `<CheckboxRow>`. */\nexport interface CheckboxRowProps {\n label: ReactNode;\n value: boolean;\n onChange: (next: boolean) => void;\n description?: string;\n /** Take the full width of the enclosing grid — see `<PropertyRow span>`. */\n span?: boolean;\n}\n\n/** A boolean checkbox. */\nexport function CheckboxRow({ label, value, onChange, span, description }: CheckboxRowProps) {\n return (\n <PropertyRow span={span} label={label} variant=\"checkbox\" description={description}>\n <input type=\"checkbox\" checked={value} onChange={(e) => onChange(e.target.checked)} />\n </PropertyRow>\n );\n}\n\n/** Props for `<TextRow>`. */\nexport interface TextRowProps {\n label: ReactNode;\n value: string;\n onChange: (next: string) => void;\n placeholder?: string;\n maxLength?: number;\n layout?: PropertyRowLayout;\n description?: string;\n /** Take the full width of the enclosing grid — see `<PropertyRow span>`. */\n span?: boolean;\n}\n\n/** A single-line text input. */\nexport function TextRow({\n label,\n value,\n onChange,\n placeholder,\n maxLength,\n layout,\n description,\n span,\n}: TextRowProps) {\n return (\n <PropertyRow span={span} label={label} layout={layout} description={description}>\n <input\n type=\"text\"\n value={value}\n placeholder={placeholder}\n maxLength={maxLength}\n onChange={(e) => onChange(e.target.value)}\n />\n </PropertyRow>\n );\n}\n\n/** Props for `<NumberRow>`. */\nexport interface NumberRowProps {\n label: ReactNode;\n value: number;\n onChange: (next: number) => void;\n min?: number;\n max?: number;\n step?: number;\n placeholder?: string;\n /**\n * Optional suffix rendered after the field. A string becomes a dim \"word\"\n * unit (e.g. \"px\"); pass JSX like `<sup>°</sup>` for symbol units. Display\n * only — it never participates in parsing, and the value stays a number.\n */\n unit?: ReactNode;\n layout?: PropertyRowLayout;\n description?: string;\n /** Take the full width of the enclosing grid — see `<PropertyRow span>`. */\n span?: boolean;\n}\n\n/** A number typed directly. Reach for `<SliderRow>` when the range matters\n * more than the exact value. */\nexport function NumberRow({\n label,\n value,\n onChange,\n min,\n max,\n step,\n placeholder,\n unit,\n layout,\n description,\n span,\n}: NumberRowProps) {\n const input = (\n <input\n type=\"number\"\n value={value}\n min={min}\n max={max}\n step={step}\n placeholder={placeholder}\n onChange={(e) => {\n const raw = e.target.value;\n if (raw === '') return;\n const n = Number(raw);\n if (Number.isFinite(n)) onChange(n);\n }}\n />\n );\n return (\n <PropertyRow span={span} label={label} layout={layout} description={description}>\n {unit == null ? (\n input\n ) : (\n <span className={s.fieldUnitGroup}>\n {input}\n {typeof unit === 'string' ? <span className={s.readoutUnit}>{unit}</span> : unit}\n </span>\n )}\n </PropertyRow>\n );\n}\n\nexport interface PropertyOption<T extends string> {\n value: T;\n label: ReactNode;\n}\n\n/** Props for `<SelectRow>`. */\nexport interface SelectRowProps<T extends string> {\n label: ReactNode;\n value: T;\n options: ReadonlyArray<PropertyOption<T>>;\n onChange: (next: T) => void;\n layout?: PropertyRowLayout;\n description?: string;\n /** Take the full width of the enclosing grid — see `<PropertyRow span>`. */\n span?: boolean;\n}\n\n/** A dropdown over a fixed set of choices. Prefer `<ToggleRow>` when there\n * are only two or three and they should all stay visible. */\nexport function SelectRow<T extends string>({\n label,\n value,\n options,\n onChange,\n layout,\n description,\n span,\n}: SelectRowProps<T>) {\n return (\n <PropertyRow span={span} label={label} layout={layout} description={description}>\n <select\n value={value}\n onChange={(e) => {\n const v = e.target.value as T;\n dlog('property-panel', 'select', { label, value: v });\n onChange(v);\n }}\n >\n {options.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {/* HTML <option> only renders text; ReactNode → string coerce */}\n {opt.label as string}\n </option>\n ))}\n </select>\n </PropertyRow>\n );\n}\n\n/** Props for `<ToggleRow>`. */\nexport interface ToggleRowProps<T extends string> {\n label: ReactNode;\n value: T;\n options: ReadonlyArray<PropertyOption<T>>;\n onChange: (next: T) => void;\n layout?: PropertyRowLayout;\n description?: string;\n /** Take the full width of the enclosing grid — see `<PropertyRow span>`. */\n span?: boolean;\n}\n\n/** A segmented control: the same choice as a select, with every option\n * visible at once. */\nexport function ToggleRow<T extends string>({\n label,\n value,\n options,\n onChange,\n layout,\n description,\n span,\n}: ToggleRowProps<T>) {\n return (\n <PropertyRow span={span} label={label} layout={layout} description={description}>\n <div className={s.toggle}>\n {options.map((opt) => {\n const selected = opt.value === value;\n return (\n <button\n key={opt.value}\n type=\"button\"\n aria-pressed={selected}\n className={\n selected ? `${s.toggleButton} ${s.toggleButtonSelected}` : s.toggleButton\n }\n onClick={() => onChange(opt.value)}\n >\n {opt.label}\n </button>\n );\n })}\n </div>\n </PropertyRow>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Properties-BuNzy7Hb.js","names":[],"sources":["../../src/components/Properties/CurveField.module.css","../../src/components/Properties/CurveField.tsx","../../src/components/Properties/Properties.module.css","../../src/components/Properties/EffectCard.tsx","../../src/components/Properties/PropertyGroup.tsx","../../src/components/Properties/PropertyPanel.tsx"],"sourcesContent":[".field {\n display: grid;\n gap: 6px;\n grid-column: 1 / -1;\n box-sizing: border-box;\n}\n\n.field * {\n box-sizing: border-box;\n}\n\n.plot {\n position: relative;\n width: 100%;\n}\n\n.marks {\n position: absolute;\n inset: 0;\n pointer-events: none;\n}\n\n.actions {\n display: flex;\n gap: 6px;\n justify-self: start;\n}\n\n.action {\n background: transparent;\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n color: var(--wzl-fg);\n padding: 4px 10px;\n font-size: var(--wzl-font-size-sm);\n cursor: pointer;\n}\n\n.action:hover {\n background: var(--wzl-surface-raised);\n}\n","import { useCallback, useMemo } from 'react';\nimport { dlog } from '../../dlog';\nimport { type ControlPoint, CurveEditor } from '../CurveEditor';\nimport s from './CurveField.module.css';\n\n/** An annotation drawn over a curve plot: a shaded band across a range of x,\n * or a vertical line at one x. */\nexport type CurveMark =\n | { kind: 'band'; x: [number, number]; color?: string }\n | { kind: 'line'; x: number; color?: string };\n\n/** Props for `<CurveField>`. */\nexport interface CurveFieldProps {\n /** Flat [x0, y0, x1, y1, …] — matches how curve-as-array configs\n * serialize in JSON snapshots. */\n values: number[];\n min: number;\n max: number;\n step: number;\n /** Plot width in CSS px. Caller sizes (typically uses a ResizeObserver). */\n width: number;\n /** Plot height in CSS px. Default 110. */\n height?: number;\n /** Flat default curve. When provided, a Reset button restores it. */\n defaults?: readonly number[];\n onChange: (next: number[]) => void;\n /** Optional vertical marks overlaid on the plot (e.g. landmarks). */\n marks?: readonly CurveMark[];\n}\n\n/**\n * A function-domain (y = f(x), x ∈ [0,1]) curve editor with per-stop\n * numeric readouts and flip buttons. Wraps weasel-ui's\n * `CurveEditor`; consumers wanting the raw editor (2D paths,\n * custom anchors) should import that instead.\n */\nexport function CurveField({\n values,\n min,\n max,\n step,\n width,\n height = 110,\n defaults,\n onChange,\n marks,\n}: CurveFieldProps) {\n const points: ControlPoint[] = useMemo(() => {\n const out: ControlPoint[] = [];\n for (let i = 0; i + 1 < values.length; i += 2) out.push({ x: values[i], y: values[i + 1] });\n return out;\n }, [values]);\n\n const handleChange = useCallback(\n (next: ControlPoint[]) => {\n const flat: number[] = new Array(next.length * 2);\n for (let i = 0; i < next.length; i++) {\n const ySnap = Math.round(next[i].y / step) * step;\n const y = Math.max(min, Math.min(max, ySnap));\n flat[i * 2] = next[i].x;\n flat[i * 2 + 1] = y;\n }\n dlog('curve-field', 'handleChange', {\n nLen: next.length,\n flatLen: flat.length,\n sample: flat.slice(0, 4),\n });\n onChange(flat);\n },\n [onChange, min, max, step],\n );\n\n const handleFlip = useCallback(() => {\n const flipped: Array<{ x: number; y: number }> = [];\n for (let i = 0; i + 1 < values.length; i += 2) {\n flipped.push({ x: 1 - values[i], y: values[i + 1] });\n }\n flipped.sort((a, b) => a.x - b.x);\n const out: number[] = [];\n for (const p of flipped) out.push(p.x, p.y);\n onChange(out);\n }, [values, onChange]);\n\n const handleFlipVertical = useCallback(() => {\n const mid = (min + max) / 2;\n const flipped: number[] = new Array(values.length);\n for (let i = 0; i + 1 < values.length; i += 2) {\n flipped[i] = values[i];\n flipped[i + 1] = 2 * mid - values[i + 1];\n }\n onChange(flipped);\n }, [values, min, max, onChange]);\n\n return (\n <div className={s.field}>\n <div className={s.plot}>\n <CurveEditor\n value={points}\n onInput={handleChange}\n domain=\"1d\"\n constrain=\"function\"\n xRange={[0, 1]}\n yRange={[min, max]}\n width={width}\n height={height}\n endpoints=\"pinned-x\"\n addPointMode=\"click-curve\"\n minPoints={2}\n grid={{}}\n history={false}\n />\n {marks && marks.length > 0 && (\n <svg\n className={s.marks}\n width={width}\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n aria-hidden=\"true\"\n >\n {marks.map((m) => {\n const color = m.color ?? '#ffcc00';\n if (m.kind === 'band') {\n const x0 = Math.max(0, Math.min(1, m.x[0])) * width;\n const x1 = Math.max(0, Math.min(1, m.x[1])) * width;\n const left = Math.min(x0, x1);\n const w = Math.abs(x1 - x0);\n return (\n <g key={`band-${m.x[0]}-${m.x[1]}-${color}`}>\n <rect x={left} y={0} width={w} height={height} fill={color} fillOpacity={0.4} />\n <line\n x1={left}\n y1={0}\n x2={left}\n y2={height}\n stroke={color}\n strokeOpacity={0.85}\n strokeWidth={1}\n />\n <line\n x1={left + w}\n y1={0}\n x2={left + w}\n y2={height}\n stroke={color}\n strokeOpacity={0.85}\n strokeWidth={1}\n />\n </g>\n );\n }\n const x = Math.max(0, Math.min(1, m.x)) * width;\n return (\n <line\n key={`mark-${m.x}-${color}`}\n x1={x}\n y1={0}\n x2={x}\n y2={height}\n stroke={color}\n strokeWidth={1}\n />\n );\n })}\n </svg>\n )}\n </div>\n <div className={s.actions}>\n <button type=\"button\" className={s.action} onClick={handleFlip}>\n Flip horizontally\n </button>\n <button type=\"button\" className={s.action} onClick={handleFlipVertical}>\n Flip vertically\n </button>\n {defaults && (\n <button\n type=\"button\"\n className={s.action}\n onClick={() => onChange([...defaults])}\n >\n Reset\n </button>\n )}\n </div>\n </div>\n );\n}\n","/* One stylesheet for the whole property-panel family. PropertyList, PropertyRow,\n PropertyGroup and Subpanel/EffectCard are styled together because the grid\n rules genuinely cross components — `.listPairs > .subpanel` and\n `.groupBody > :not(.rowColor)` name two components in one selector, and a CSS\n Module can only compose local names within its own file. */\n\n/* Authored against a border-box reset (labkit's `.lk-root *`). weasel-ui has no\n global reset, so the family restates it over its own subtrees. */\n.panel,\n.list,\n.row,\n.group,\n.subpanel,\n.card,\n.cardList,\n.panel *,\n.list *,\n.row *,\n.group *,\n.subpanel *,\n.card *,\n.cardList * {\n box-sizing: border-box;\n}\n\n.panel {\n background: var(--wzl-surface-raised);\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-lg);\n padding: 14px 16px;\n backdrop-filter: blur(var(--wzl-glass-blur));\n -webkit-backdrop-filter: blur(var(--wzl-glass-blur));\n}\n\n.panelTitle {\n font-family: var(--wzl-font-display);\n font-weight: var(--wzl-font-weight-light);\n font-size: var(--wzl-font-size-lg);\n line-height: var(--wzl-leading-snug);\n letter-spacing: 0.01em;\n color: var(--wzl-fg);\n margin: 0 0 12px;\n}\n\n.list {\n display: grid;\n grid-template-columns: 1fr 1fr;\n column-gap: 10px;\n row-gap: 4px;\n align-items: stretch;\n /* A property panel is dense: its fields are shorter and tighter than a\n standalone Input. Set here, on the container, so one declaration moves\n every field in the panel. */\n --wzl-field-h: 20px;\n --wzl-field-pad-x: 6px;\n}\n\n/* Default packing: only color rows pair, everything else spans full width.\n Scoped to NOT apply in pairs mode (otherwise this 0,2,0 selector beats the\n pairs override below, which is only 0,1,0). */\n.list:not(.listPairs) > :not(.rowColor) {\n grid-column: 1 / -1;\n}\n\n/* `pack=\"pairs\"` flips that: every row auto-places into the 2-col grid two\n per row, and explicit full-width children opt in via the span helper class. */\n.listPairs > * {\n min-width: 0;\n}\n\n.listPairs > .span,\n.listPairs > .subpanel,\n.listPairs > h3 {\n grid-column: 1 / -1;\n}\n\n/* ── Row ────────────────────────────────────────────────────────\n Default shape: label on top (with optional right-aligned readout),\n control below. */\n\n.row {\n display: flex;\n flex-direction: column;\n /* No gap by default — controls sit flush under their label, matching\n speech-balloons `.field { gap: 0 }`. Slider rows pull tighter still\n via a negative margin on the label below. */\n gap: 0;\n min-width: 0;\n}\n\n/* Slider rows: pull the track up under the label so the row reads as one\n tight block rather than a label + a separate horizontal strip. Also\n bottom-align the track within the row so paired rows in a 2-col grid\n keep their tracks lined up even when one label wraps to two lines. */\n.row:has(> input[type='range']) {\n justify-content: flex-end;\n}\n\n.row:has(> input[type='range']) > .rowLabel {\n /* Baseline-align (not center) so the readout input doesn't inflate the\n label-group height past the text baseline. */\n align-items: baseline;\n justify-content: space-between;\n gap: 4px;\n margin-bottom: -4px;\n}\n\n.rowLabel {\n font-family: var(--wzl-font-display);\n font-weight: var(--wzl-font-weight-light);\n font-size: var(--wzl-font-size-sm);\n line-height: var(--wzl-leading-snug);\n color: var(--wzl-fg-muted);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.readout {\n font-style: normal;\n font-weight: var(--wzl-font-weight-medium);\n color: var(--wzl-accent);\n margin-left: auto;\n}\n\n/* ⓘ help affordance beside a row label. Every dimension is stated because a\n host app's bare-element defaults can be authored at zero specificity —\n labkit's `:where(.lk-root) :where(button)` sets `height: var(--wzl-control-h)`\n and horizontal padding, which would otherwise stretch a one-glyph button into\n a full-height control. */\n.help {\n flex: 0 0 auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.15em;\n height: 1.15em;\n padding: 0;\n border: 0;\n background: none;\n backdrop-filter: none;\n -webkit-backdrop-filter: none;\n color: var(--wzl-fg-subtle);\n font-size: var(--wzl-font-size-sm);\n line-height: 1;\n letter-spacing: normal;\n cursor: help;\n border-radius: var(--wzl-radius-sm);\n}\n\n.help:hover {\n color: var(--wzl-fg);\n border-color: transparent;\n}\n\n.help:focus-visible {\n outline: 2px solid var(--wzl-focus-ring);\n outline-offset: 1px;\n color: var(--wzl-fg);\n}\n\n/* Single always-rendered <input> styled to read as a text label until focused. */\n.readoutInput {\n font: inherit;\n font-weight: var(--wzl-font-weight-medium);\n /* Force leading-tight — the label parent inherits 1.2, which would make the\n input box ~3px taller than the speech-balloons reference. */\n line-height: var(--wzl-leading-tight);\n color: var(--wzl-accent);\n background: transparent;\n border: 1px solid transparent;\n border-radius: var(--wzl-radius-sm);\n padding: 1px 0;\n margin: 0;\n width: calc(5em - 24px);\n text-align: right;\n cursor: text;\n outline: none;\n transition: background 80ms ease, border-color 80ms ease;\n -moz-appearance: textfield;\n}\n\n.readoutInput:hover {\n background: color-mix(in srgb, currentColor 8%, transparent);\n}\n\n.readoutInput:focus {\n background: var(--wzl-surface-sunken);\n border-color: var(--wzl-focus-ring);\n}\n\n.readoutInput::-webkit-inner-spin-button,\n.readoutInput::-webkit-outer-spin-button {\n -webkit-appearance: none;\n margin: 0;\n}\n\n/* Inline-block (not flex) so the browser's native vertical-align for\n <sub>/<sup> still works inside — flex collapses baseline semantics\n and sinks superscripts onto the baseline. */\n.readoutGroup {\n display: inline-block;\n white-space: nowrap;\n}\n\n/* Word-style units (e.g. \"px\") read as a small dim suffix next to the digits. */\n.readoutUnit {\n color: var(--wzl-fg-muted);\n font-size: var(--wzl-font-size-2xs);\n line-height: var(--wzl-leading-tight);\n pointer-events: none;\n}\n\n.fieldUnitGroup {\n display: inline-block;\n white-space: nowrap;\n min-width: 0;\n}\n\n.fieldUnitGroup .readoutUnit {\n margin-left: 4px;\n}\n\n/* Symbol units (e.g. `<sup>°</sup>`) lean on the browser's native super\n positioning. Just dim them to match — do NOT set line-height: 0, that\n would re-collapse the glyph onto the baseline. */\n.readoutGroup sup,\n.fieldUnitGroup sup {\n color: var(--wzl-fg-muted);\n font-size: var(--wzl-font-size-2xs);\n pointer-events: none;\n}\n\n/* ── Inline layout: label sits to the left of the control. ────── */\n.rowInline {\n flex-direction: row;\n align-items: center;\n /* Label on the left edge, field on the right: fields of different widths\n still line up as one rail down the column. */\n justify-content: space-between;\n gap: 6px;\n}\n\n.rowInline .rowLabel {\n /* The label never gives: shrinking it breaks a name like `silhouette_width`\n mid-word, since it carries no break opportunity of its own. The field is\n what yields as the column narrows. */\n flex: 0 0 auto;\n}\n\n.rowInline > input,\n.rowInline > select,\n.rowInline > .toggle {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n/* Fields size to their content, not to the row they happen to sit in: a number\n is a handful of digits and a short string is a word. Stated as a width and\n not as a `min(100%, …)`, because an auto-width sidebar sizes to what its\n fields ask for and a percentage asks for an input's 20-character default. */\n.row input[type='number']:not(.readoutInput) {\n width: var(--wzl-prop-number-width, 9ch);\n max-width: 100%;\n}\n\n.row input[type='text']:not(.readoutInput) {\n width: var(--wzl-prop-text-width, 16ch);\n max-width: 100%;\n}\n\n/* An inline row stays on one line: the field gives up width to the label as the\n column narrows, down to a floor that still shows a value. */\n.rowInline > input:not(.readoutInput),\n.rowInline > select {\n flex: 0 1 auto;\n min-width: 4ch;\n}\n\n.rowInline input[type='range'] {\n width: auto;\n}\n\n.row input[type='text']:not(.readoutInput),\n.row input[type='number'] {\n background: var(--wzl-surface-sunken);\n color: var(--wzl-fg);\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n /* Height is stated rather than padded to, because the display face's line box\n is half again its font size — padding a field to size makes it 27px tall\n around 13px of text and no amount of trimming the padding fixes that. */\n height: var(--wzl-field-h, var(--wzl-control-h));\n padding: 0 var(--wzl-field-pad-x);\n font-family: var(--wzl-font-ui);\n font-weight: var(--wzl-font-weight-medium);\n font-size: var(--wzl-font-size);\n line-height: var(--wzl-leading-tight);\n}\n\n/* Numbers right-align so a column of them shares a decimal position; text does\n not, so this cannot live in the rule the two share. */\n.row input[type='number'] {\n text-align: right;\n}\n\n.row input[type='text']:not(.readoutInput):focus,\n.row input[type='number']:focus {\n outline: none;\n border-color: var(--wzl-focus-ring);\n box-shadow: 0 0 0 1px var(--wzl-focus-ring);\n}\n\n/* Selects in property rows render borderless — chevron only, transparent\n background, no chrome. Keep the native arrow via appearance: auto so users\n still see something clickable. */\n.row select {\n background: transparent;\n color: var(--wzl-fg);\n border: 0;\n border-radius: 0;\n height: var(--wzl-field-h, var(--wzl-control-h));\n padding: 0;\n font-family: var(--wzl-font-ui);\n font-weight: var(--wzl-font-weight-medium);\n font-size: var(--wzl-font-size);\n line-height: var(--wzl-leading-tight);\n cursor: pointer;\n}\n\n/* An outline rather than the boxed family's inset ring: a property-row select\n is borderless, so there is no border to recolour. */\n.row select:focus {\n outline: 1px solid var(--wzl-focus-ring);\n outline-offset: 2px;\n}\n\n/* ── Color variant: label and swatch side-by-side, two per row in the grid.\n Switches to a 2-row grid when an alpha slider is rendered beneath. */\n.rowColor {\n display: grid;\n grid-template-columns: 1fr auto;\n grid-template-rows: auto;\n align-content: end;\n column-gap: 8px;\n row-gap: 0;\n min-width: 0;\n}\n\n.rowColor .rowLabel {\n grid-column: 1;\n grid-row: 1;\n align-self: center;\n flex: initial;\n min-width: 0;\n}\n\n/* 36×24, hairline border, no padding. Padding: 0 is important — the native\n <input type=\"color\"> in some browsers (esp. Chrome) inflates beyond its\n declared box when given padding, which expands the grid row and pushes\n the paired slider track out of bottom alignment. */\n.rowColor input[type='color'] {\n grid-column: 2;\n grid-row: 1;\n align-self: center;\n box-sizing: border-box;\n width: 36px;\n height: 18px;\n background: transparent;\n border: 0;\n border-radius: var(--wzl-radius-sm);\n padding: 0;\n cursor: pointer;\n flex-shrink: 0;\n}\n\n/* Alpha slider: full-width second row beneath the color swatch. */\n.rowColor input[type='range'].alpha {\n grid-column: 1 / -1;\n grid-row: 2;\n height: 12px;\n width: 100%;\n margin: 0;\n}\n\n/* ── Checkbox variant: control-first inline layout, no uppercase label. */\n.rowCheckbox {\n flex-direction: row;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n}\n\n.rowCheckbox .rowLabel {\n text-transform: none;\n letter-spacing: normal;\n font-size: var(--wzl-font-size);\n color: var(--wzl-fg);\n /* Place the label after the checkbox visually. */\n order: 1;\n}\n\n.rowCheckbox input[type='checkbox'] {\n accent-color: var(--wzl-accent);\n width: 14px;\n height: 14px;\n cursor: pointer;\n}\n\n/* ── Segmented toggle group ───────────────────────────────────── */\n.toggle {\n display: flex;\n width: 100%;\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n overflow: hidden;\n}\n\n.toggleButton {\n flex: 1;\n height: 26px;\n padding: 0 8px;\n background: rgba(0, 0, 0, 0.25);\n color: var(--wzl-fg-muted);\n border: 0;\n border-radius: 0;\n font-family: var(--wzl-font-ui);\n font-weight: var(--wzl-font-weight-medium);\n font-size: var(--wzl-font-size-sm);\n line-height: var(--wzl-leading-tight);\n cursor: pointer;\n backdrop-filter: none;\n -webkit-backdrop-filter: none;\n}\n\n.toggleButton + .toggleButton {\n border-left: 1px solid var(--wzl-border);\n}\n\n.toggleButton:hover {\n color: var(--wzl-fg);\n border-color: var(--wzl-border);\n}\n\n.toggleButtonSelected {\n background: var(--wzl-accent);\n color: var(--wzl-fg-on-accent);\n font-weight: var(--wzl-font-weight-medium);\n}\n\n.toggleButtonSelected:hover {\n color: var(--wzl-fg-on-accent);\n}\n\n/* ── PropertyGroup ──────────────────────────────────────────────────\n Bordered, sunken section inside a list. */\n.group {\n grid-column: 1 / -1;\n background: var(--wzl-surface-sunken);\n border-radius: var(--wzl-radius-md);\n padding: 6px 10px 8px;\n}\n\n.groupTitle {\n display: grid;\n grid-template-columns: 1fr auto 1fr;\n align-items: center;\n gap: 8px;\n margin: 0 0 4px;\n font-family: var(--wzl-font-display);\n font-weight: var(--wzl-font-weight-light);\n font-size: var(--wzl-font-size-sm);\n letter-spacing: var(--wzl-tracking-wider);\n text-transform: uppercase;\n color: var(--wzl-fg-subtle);\n}\n\n.groupTitle hr {\n border: 0;\n border-top: 1px solid var(--wzl-border);\n margin: 0;\n}\n\n.groupBody {\n display: grid;\n grid-template-columns: 1fr 1fr;\n column-gap: 10px;\n row-gap: 4px;\n /* Stretch so paired rows match in height — slider rows then bottom-align\n their tracks (via `margin-top: auto`) and stay lined up even when one\n sibling's label wraps to two lines. */\n align-items: stretch;\n}\n\n.group:not(.groupPairs) .groupBody > :not(.rowColor) {\n grid-column: 1 / -1;\n}\n\n.groupPairs .groupBody > * {\n grid-column: auto;\n}\n\n.groupPairs .groupBody > h3,\n.groupPairs .groupBody > .span {\n grid-column: 1 / -1;\n}\n\n/* ── Subpanel ────────────────────────────────────────────────────────\n Headered group of controls inside a list. The header is a typographic\n label flanked by a horizontal rule — no card chrome, just the divider. */\n.subpanel {\n display: grid;\n grid-template-columns: 1fr 1fr;\n column-gap: 10px;\n row-gap: 3px;\n align-items: stretch;\n background: transparent;\n border: 0;\n padding: 0;\n margin: 0;\n}\n\n.subpanel > * {\n min-width: 0;\n}\n\n.subpanel > .subpanelTitle,\n.subpanel > .span {\n grid-column: 1 / -1;\n}\n\n.subpanelTitle {\n font-family: var(--wzl-font-display);\n font-weight: var(--wzl-font-weight-medium);\n font-size: var(--wzl-font-size-sm);\n line-height: var(--wzl-leading-tight);\n color: var(--wzl-fg);\n text-transform: uppercase;\n letter-spacing: var(--wzl-tracking-wider);\n margin: 0;\n padding: 6px 0 4px;\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.subpanelTitle > span {\n flex: 0 0 auto;\n}\n\n.subpanelTitle > hr {\n flex: 1 1 auto;\n border: 0;\n border-top: 1px solid rgba(255, 255, 255, 0.12);\n margin: 0;\n}\n\n/* ── EffectCard ──────────────────────────────────────────────────────\n Draggable, collapsible card. Optional --wzl-effect-card-accent re-binds the\n local --wzl-accent so every descendant control auto-tints. */\n.card {\n background: rgba(0, 0, 0, 0.25);\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n.cardDragging {\n opacity: 0.45;\n}\n\n.cardExpanded {\n background: rgba(0, 0, 0, 0.3);\n}\n\n.cardAccented {\n /* Re-bind --wzl-accent inside the card so every descendant style that paints\n with var(--wzl-accent) (readouts, slider thumb, etc.) picks up the\n per-card tint automatically. */\n --wzl-accent: var(--wzl-effect-card-accent);\n background: color-mix(in srgb, var(--wzl-effect-card-accent) 4%, rgba(0, 0, 0, 0.28));\n border-color: color-mix(in srgb, var(--wzl-effect-card-accent) 28%, var(--wzl-border));\n border-left-width: 4px;\n border-left-color: var(--wzl-effect-card-accent);\n}\n\n.cardAccented.cardExpanded {\n background: color-mix(in srgb, var(--wzl-effect-card-accent) 5%, rgba(0, 0, 0, 0.35));\n}\n\n.cardAccented .cardHead {\n background: color-mix(in srgb, var(--wzl-effect-card-accent) 38%, rgba(0, 0, 0, 0.4));\n}\n\n.cardAccented .cardHead:hover {\n background: color-mix(in srgb, var(--wzl-effect-card-accent) 48%, rgba(0, 0, 0, 0.35));\n}\n\n.cardHead {\n display: flex;\n align-items: baseline;\n gap: 8px;\n padding: 2px 10px;\n cursor: pointer;\n user-select: none;\n}\n\n.cardHead:hover {\n background: rgba(255, 255, 255, 0.03);\n}\n\n.cardHandle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--wzl-fg-muted);\n cursor: grab;\n padding: 0 2px;\n}\n\n.cardHandle:active {\n cursor: grabbing;\n}\n\n.cardIndexBadge {\n /* Rendered as a <button> (keyboard-operable drag/toggle handle); strip the\n native button chrome so it matches the surrounding badge styling. */\n appearance: none;\n border: 0;\n background: none;\n display: inline-flex;\n align-items: baseline;\n justify-content: center;\n min-width: 14px;\n padding: 0 2px;\n color: var(--wzl-effect-card-accent, var(--wzl-fg-muted));\n font-size: var(--wzl-font-size);\n font-weight: var(--wzl-font-weight-bold);\n font-family: var(--wzl-font-mono);\n line-height: 1;\n}\n\n.cardTitle {\n font-family: var(--wzl-font-display);\n font-weight: var(--wzl-font-weight-medium);\n font-size: var(--wzl-font-size);\n line-height: var(--wzl-leading-tight);\n color: var(--wzl-fg);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n.cardPrimary {\n font-family: var(--wzl-font-ui);\n font-weight: var(--wzl-font-weight-medium);\n font-size: var(--wzl-font-size-sm);\n line-height: var(--wzl-leading-tight);\n color: var(--wzl-fg);\n background: rgba(0, 0, 0, 0.3);\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n padding: 2px 6px;\n margin-left: auto;\n}\n\n/* Push the remove button to the right edge — whether it follows a primary\n chip or sits alone after the title. */\n.cardHead .cardRemove,\n.cardHead .cardPrimary + .cardRemove {\n margin-left: auto;\n}\n\n.cardHead .cardPrimary ~ .cardRemove {\n margin-left: 4px;\n}\n\n.cardRemove {\n background: transparent;\n color: var(--wzl-fg-muted);\n border: 1px solid transparent;\n border-radius: var(--wzl-radius-sm);\n width: 22px;\n height: 22px;\n font-size: var(--wzl-font-size);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.cardRemove:hover {\n color: var(--wzl-danger-fg, var(--wzl-danger));\n border-color: color-mix(in srgb, var(--wzl-danger) 40%, transparent);\n background: color-mix(in srgb, var(--wzl-danger) 8%, transparent);\n}\n\n.cardBody {\n padding: 4px 12px 12px;\n border-top: 1px solid var(--wzl-border);\n}\n\n/* ── EffectCardList ────────────────────────────────────────────────── */\n.cardList {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.cardListWrap {\n position: relative;\n}\n\n.cardDropHint {\n height: 0;\n border-top: 2px solid var(--wzl-accent);\n border-radius: var(--wzl-radius-sm);\n margin: 2px 0;\n box-shadow: 0 0 6px color-mix(in srgb, var(--wzl-accent) 60%, transparent);\n}\n\n.cardListEmpty {\n font-family: var(--wzl-font-display);\n font-weight: var(--wzl-font-weight-light);\n font-size: var(--wzl-font-size-sm);\n line-height: var(--wzl-leading);\n color: var(--wzl-fg-muted);\n text-align: center;\n padding: 16px 12px;\n border: 1px dashed var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n}\n","import { type CSSProperties, type ReactNode, useState } from 'react';\nimport s from './Properties.module.css';\n\n// ── Subpanel ─────────────────────────────────────────────────────────\n// Headered group of controls inside a wider panel/list. The header is a\n// label flanked by a horizontal rule — no background, padding, or border;\n// purely a typographic divider. Ported from speech-balloons styles.css:277-313.\n\n/** Props for `<Subpanel>`. */\nexport interface SubpanelProps {\n title: ReactNode;\n children: ReactNode;\n className?: string;\n}\n\n/** A typographic divider inside a property list: a small title flanked by a\n * rule. Purely a heading — use `<PropertyGroup>` for a bordered section. */\nexport function Subpanel({ title, children, className }: SubpanelProps) {\n const cls = className ? `${s.subpanel} ${className}` : s.subpanel;\n return (\n <div className={cls}>\n <h4 className={s.subpanelTitle}>\n <span>{title}</span>\n <hr />\n </h4>\n {children}\n </div>\n );\n}\n\n// ── EffectCard ───────────────────────────────────────────────────────\n// Draggable, collapsible card for stacked effect editors. Optional\n// --wzl-effect-card-accent per-instance recolors the title bar, border-left, and\n// (via re-binding --wzl-accent inside the card) every descendant control that\n// reads from the accent token.\n\n/** Props for `<EffectCard>`. */\nexport interface EffectCardProps {\n /** Card title rendered in the title bar (e.g. effect kind label or a select). */\n title: ReactNode;\n /** Optional badge/chip displayed before the remove button (e.g. primary value summary). */\n primary?: ReactNode;\n /** Optional accent color in any CSS color form. Sets --wzl-effect-card-accent on the card. */\n accent?: string;\n /** Optional ordinal badge that doubles as the drag handle (used by tail cards in SB). */\n index?: number;\n expanded?: boolean;\n onToggleExpanded?: () => void;\n onRemove?: () => void;\n /**\n * Drag-handle wiring. When provided, an inline handle (⋮⋮ icon or `index`)\n * activates HTML5 drag on press. Consumer owns the drag/drop coordination —\n * see EffectCardList for the batteries-included wrapper.\n */\n draggable?: boolean;\n onPressHandle?: () => void;\n onReleaseHandle?: () => void;\n onDragStart?: (e: React.DragEvent<HTMLDivElement>) => void;\n onDragEnd?: (e: React.DragEvent<HTMLDivElement>) => void;\n onDragOver?: (e: React.DragEvent<HTMLDivElement>) => void;\n onDragLeave?: (e: React.DragEvent<HTMLDivElement>) => void;\n onDrop?: (e: React.DragEvent<HTMLDivElement>) => void;\n dragging?: boolean;\n children?: ReactNode;\n className?: string;\n}\n\n/** An accented, collapsible card for one item in a list of like things —\n * effects, layers, tails. The accent color rebinds the theme accent token\n * within the card, so its controls pick it up too. */\nexport function EffectCard({\n title,\n primary,\n accent,\n index,\n expanded = true,\n onToggleExpanded,\n onRemove,\n draggable,\n onPressHandle,\n onReleaseHandle,\n onDragStart,\n onDragEnd,\n onDragOver,\n onDragLeave,\n onDrop,\n dragging,\n children,\n className,\n}: EffectCardProps) {\n const stateClass = `${expanded ? ` ${s.cardExpanded}` : ''}${dragging ? ` ${s.cardDragging}` : ''}${accent ? ` ${s.cardAccented}` : ''}`;\n const cls = `${s.card}${stateClass}${className ? ` ${className}` : ''}`;\n // Per-instance dynamic color → CSS custom property requires inline style.\n // The card's CSS re-binds --wzl-accent to it so descendants tint.\n const style = accent ? ({ '--wzl-effect-card-accent': accent } as CSSProperties) : undefined;\n\n // When a toggle handler is supplied the header acts as a disclosure button:\n // role + tab stop + Enter/Space. Bundled together so the interactive props are\n // applied as a set (no half-interactive element when onToggleExpanded is absent).\n const headProps = onToggleExpanded\n ? {\n role: 'button' as const,\n tabIndex: 0,\n onClick: onToggleExpanded,\n onKeyDown: (e: React.KeyboardEvent) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onToggleExpanded();\n }\n },\n }\n : {};\n\n return (\n // biome-ignore lint/a11y/noStaticElementInteractions: native HTML5 drag-and-drop container — drag events are not click/keyboard interactions and have no semantic element; the in-card handle and header carry the a11y affordances.\n <div\n className={cls}\n style={style}\n draggable={draggable}\n onDragStart={onDragStart}\n onDragEnd={onDragEnd}\n onDragOver={onDragOver}\n onDragLeave={onDragLeave}\n onDrop={onDrop}\n >\n <div className={s.cardHead} {...headProps}>\n {index != null ? (\n <button\n type=\"button\"\n className={`${s.cardIndexBadge} ${s.cardHandle}`}\n title=\"Drag to reorder · click to toggle\"\n aria-label={`Item ${index + 1} — drag to reorder, click to toggle`}\n onMouseDown={onPressHandle}\n onMouseUp={onReleaseHandle}\n onClick={(e) => {\n e.stopPropagation();\n onToggleExpanded?.();\n }}\n >\n {index + 1}\n </button>\n ) : (\n <span\n className={s.cardHandle}\n aria-hidden=\"true\"\n title=\"Drag to reorder\"\n onMouseDown={onPressHandle}\n onMouseUp={onReleaseHandle}\n >\n <DragHandleIcon />\n </span>\n )}\n <span className={s.cardTitle}>{title}</span>\n {primary != null && <span className={s.cardPrimary}>{primary}</span>}\n {onRemove && (\n <button\n type=\"button\"\n className={s.cardRemove}\n onClick={(e) => {\n e.stopPropagation();\n onRemove();\n }}\n title=\"Remove\"\n aria-label=\"Remove\"\n >\n ✕\n </button>\n )}\n </div>\n {expanded && <div className={s.cardBody}>{children}</div>}\n </div>\n );\n}\n\nfunction DragHandleIcon() {\n return (\n <svg width=\"10\" height=\"14\" viewBox=\"0 0 10 14\" aria-hidden=\"true\">\n <circle cx=\"3\" cy=\"3\" r=\"1.3\" fill=\"currentColor\" />\n <circle cx=\"7\" cy=\"3\" r=\"1.3\" fill=\"currentColor\" />\n <circle cx=\"3\" cy=\"7\" r=\"1.3\" fill=\"currentColor\" />\n <circle cx=\"7\" cy=\"7\" r=\"1.3\" fill=\"currentColor\" />\n <circle cx=\"3\" cy=\"11\" r=\"1.3\" fill=\"currentColor\" />\n <circle cx=\"7\" cy=\"11\" r=\"1.3\" fill=\"currentColor\" />\n </svg>\n );\n}\n\n// ── EffectCardList ───────────────────────────────────────────────────\n// Batteries-included list wrapper that coordinates HTML5 drag-and-drop across\n// a list of EffectCards. Consumer supplies items + a renderItem callback;\n// onReorder fires with (sourceId, targetId, position) on drop. Mirrors the\n// LayerStack pattern from speech-balloons Lab.tsx:630-707.\n\n/** The minimum an item must carry to appear in an `<EffectCardList>`. */\nexport interface EffectCardListItem {\n id: string | number;\n}\n\n/** Props for `<EffectCardList>`. */\nexport interface EffectCardListProps<T extends EffectCardListItem> {\n items: ReadonlyArray<T>;\n renderItem: (\n item: T,\n helpers: {\n isExpanded: boolean;\n isDragging: boolean;\n toggleExpanded: () => void;\n cardProps: Pick<\n EffectCardProps,\n | 'expanded'\n | 'dragging'\n | 'draggable'\n | 'onToggleExpanded'\n | 'onPressHandle'\n | 'onReleaseHandle'\n | 'onDragStart'\n | 'onDragEnd'\n | 'onDragOver'\n | 'onDragLeave'\n | 'onDrop'\n >;\n },\n ) => ReactNode;\n onReorder: (sourceId: T['id'], targetId: T['id'], position: 'before' | 'after') => void;\n /** Optional rendered when the list is empty. */\n empty?: ReactNode;\n /** IDs to render expanded on first mount. Defaults to all collapsed. */\n defaultExpandedIds?: ReadonlyArray<T['id']>;\n}\n\n/** A drag-reorderable list of `<EffectCard>`s. Owns the expanded and dragging\n * state and hands it back to `renderItem`, so the caller only supplies each\n * card's contents. */\nexport function EffectCardList<T extends EffectCardListItem>({\n items,\n renderItem,\n onReorder,\n empty,\n defaultExpandedIds,\n}: EffectCardListProps<T>) {\n const [expanded, setExpanded] = useState<Set<T['id']>>(() => new Set(defaultExpandedIds ?? []));\n const [draggingId, setDraggingId] = useState<T['id'] | null>(null);\n const [pressedId, setPressedId] = useState<T['id'] | null>(null);\n const [dropHint, setDropHint] = useState<{ id: T['id']; position: 'before' | 'after' } | null>(\n null,\n );\n\n if (items.length === 0) {\n return <>{empty ?? <div className={s.cardListEmpty}>No items.</div>}</>;\n }\n\n return (\n <div className={s.cardList}>\n {items.map((item) => {\n const isExpanded = expanded.has(item.id);\n const isDragging = draggingId === item.id;\n const isPressed = pressedId === item.id;\n const showHintBefore =\n dropHint && dropHint.id === item.id && dropHint.position === 'before';\n const showHintAfter = dropHint && dropHint.id === item.id && dropHint.position === 'after';\n\n const toggleExpanded = () =>\n setExpanded((prev) => {\n const next = new Set(prev);\n if (next.has(item.id)) next.delete(item.id);\n else next.add(item.id);\n return next;\n });\n\n return (\n <div key={String(item.id)} className={s.cardListWrap}>\n {showHintBefore && <div className={s.cardDropHint} />}\n {renderItem(item, {\n isExpanded,\n isDragging,\n toggleExpanded,\n cardProps: {\n expanded: isExpanded,\n dragging: isDragging,\n draggable: isPressed,\n onToggleExpanded: toggleExpanded,\n onPressHandle: () => setPressedId(item.id),\n onReleaseHandle: () => setPressedId(null),\n onDragStart: (e) => {\n e.dataTransfer.effectAllowed = 'move';\n e.dataTransfer.setData('text/plain', String(item.id));\n setDraggingId(item.id);\n },\n onDragEnd: () => {\n setDraggingId(null);\n setPressedId(null);\n setDropHint(null);\n },\n onDragOver: (e) => {\n if (draggingId === null || draggingId === item.id) return;\n e.preventDefault();\n e.dataTransfer.dropEffect = 'move';\n const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect();\n const position: 'before' | 'after' =\n e.clientY < rect.top + rect.height / 2 ? 'before' : 'after';\n setDropHint({ id: item.id, position });\n },\n onDragLeave: () => {\n setDropHint((h) => (h && h.id === item.id ? null : h));\n },\n onDrop: (e) => {\n e.preventDefault();\n if (draggingId === null || draggingId === item.id) {\n setDropHint(null);\n return;\n }\n const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect();\n const position: 'before' | 'after' =\n e.clientY < rect.top + rect.height / 2 ? 'before' : 'after';\n onReorder(draggingId, item.id, position);\n setDropHint(null);\n },\n },\n })}\n {showHintAfter && <div className={s.cardDropHint} />}\n </div>\n );\n })}\n </div>\n );\n}\n","import type { ReactNode } from 'react';\nimport s from './Properties.module.css';\n\n/** Props for `<PropertyGroup>`. */\nexport interface PropertyGroupProps {\n /** Title rendered between two rules at the top of the group. */\n title: ReactNode;\n /** When true the group renders nothing — useful for conditional sections. */\n hidden?: boolean;\n children: ReactNode;\n className?: string;\n /**\n * How rows pack into the 2-column grid.\n * - `'auto-color'` (default): only color rows pair side-by-side; everything\n * else spans the full width.\n * - `'pairs'`: every row auto-places into the 2-column grid two-per-row.\n */\n pack?: 'auto-color' | 'pairs';\n}\n\n/**\n * Visually-bordered group inside a PropertyList. Use to scope a set of\n * related rows under a heading (e.g. \"Aqua\", \"Bevel\", \"Dome\" sections\n * inside a fill effect's controls).\n */\nexport function PropertyGroup({\n title,\n hidden,\n children,\n className,\n pack = 'auto-color',\n}: PropertyGroupProps) {\n if (hidden) return null;\n const packClass = pack === 'pairs' ? ` ${s.groupPairs}` : '';\n const cls = `${s.group}${packClass}${className ? ` ${className}` : ''}`;\n return (\n <div className={cls}>\n <h3 className={s.groupTitle}>\n <hr />\n <span>{title}</span>\n <hr />\n </h3>\n <div className={s.groupBody}>{children}</div>\n </div>\n );\n}\n","import { type ReactNode, useState } from 'react';\nimport { Focusable } from 'react-aria-components';\nimport { dlog } from '../../dlog';\nimport { formatNumber, parseSignedNumber } from '../../format/number';\nimport { Tooltip, TooltipTrigger } from '../Tooltip';\nimport shared from '../range.module.css';\nimport s from './Properties.module.css';\n\n/** Props for `<PropertyPanel>`. */\nexport interface PropertyPanelProps {\n title?: ReactNode;\n children: ReactNode;\n className?: string;\n}\n\n/** A titled panel holding property rows — the sidebar container the rest of\n * this module's components fill. */\nexport function PropertyPanel({ title, children, className }: PropertyPanelProps) {\n const cls = className ? `${s.panel} ${className}` : s.panel;\n return (\n <div className={cls}>\n {title != null && <h2 className={s.panelTitle}>{title}</h2>}\n {children}\n </div>\n );\n}\n\n/** How a property list packs its rows into two columns. */\nexport type PropertyListPack = 'auto-color' | 'pairs';\n\n/** Props for `<PropertyList>`. */\nexport interface PropertyListProps {\n children: ReactNode;\n className?: string;\n /**\n * How rows pack into the 2-column grid.\n * - `'auto-color'` (default): only color rows pair side-by-side; everything\n * else spans the full width. Right for sparse top-level panels.\n * - `'pairs'`: every row auto-places into the 2-column grid two-per-row.\n * Headers and subpanels still span the full width; wrap any other\n * full-width child in `<PropertySpan>`. Right for dense effect bodies.\n */\n pack?: PropertyListPack;\n}\n\n/**\n * Grid container for PropertyRows. Use standalone for chrome-less layouts, or\n * nest inside <PropertyPanel/> for the standard glass card.\n */\nexport function PropertyList({ children, className, pack = 'auto-color' }: PropertyListProps) {\n const packClass = pack === 'pairs' ? ` ${s.listPairs}` : '';\n const cls = `${s.list}${packClass}${className ? ` ${className}` : ''}`;\n return <div className={cls}>{children}</div>;\n}\n\n/** Props for `<PropertySpan>`. */\nexport interface PropertySpanProps {\n children: ReactNode;\n className?: string;\n}\n\n/**\n * Wrapper that makes an arbitrary child span both columns of a\n * `<PropertyList pack=\"pairs\">`, a `<PropertyGroup pack=\"pairs\">` body, or a\n * `<Subpanel>`. `<PropertyRow span>` covers the row case; this covers\n * everything else a consumer puts in the grid.\n */\nexport function PropertySpan({ children, className }: PropertySpanProps) {\n return <div className={className ? `${s.span} ${className}` : s.span}>{children}</div>;\n}\n\n/** Which control shape a row holds, which decides its intrinsic layout. */\nexport type PropertyRowVariant = 'default' | 'color' | 'checkbox';\n/** Whether a row's label sits above its control or beside it. */\nexport type PropertyRowLayout = 'block' | 'inline';\n\n/** Props for `<PropertyRow>`. */\nexport interface PropertyRowProps {\n label: ReactNode;\n /** Right-aligned readout shown next to the label (e.g. current value). */\n readout?: ReactNode;\n /**\n * Help text for the row, shown in a tooltip off an ⓘ affordance beside the\n * label. Empty or absent renders no affordance.\n */\n description?: string;\n variant?: PropertyRowVariant;\n /**\n * Label position relative to the control. \"block\" (default) stacks\n * label above control; \"inline\" places the label to the left.\n * Color and checkbox variants are always inline by their own nature.\n */\n layout?: PropertyRowLayout;\n /**\n * Take the full width of the enclosing grid. Only has an effect inside a\n * `<PropertyList pack=\"pairs\">` or a `<Subpanel>`; elsewhere rows are already\n * full width.\n */\n span?: boolean;\n children: ReactNode;\n htmlFor?: string;\n className?: string;\n}\n\n/** The label-plus-control frame every typed row below is built from. Use it\n * directly for a control this module does not cover. */\nexport function PropertyRow({\n label,\n readout,\n description,\n variant = 'default',\n layout = 'block',\n span,\n children,\n htmlFor,\n className,\n}: PropertyRowProps) {\n const variantClass = variant === 'color' ? ` ${s.rowColor}` : variant === 'checkbox' ? ` ${s.rowCheckbox}` : '';\n // Layout only meaningfully applies to the default variant; color and\n // checkbox have their own intrinsic orientation.\n const layoutClass = variant === 'default' && layout === 'inline' ? ` ${s.rowInline}` : '';\n const spanClass = span ? ` ${s.span}` : '';\n const cls = `${s.row}${variantClass}${layoutClass}${spanClass}${className ? ` ${className}` : ''}`;\n return (\n <label className={cls} htmlFor={htmlFor}>\n <span className={s.rowLabel}>\n {label}\n {description ? <PropertyRowHelp label={label} description={description} /> : null}\n {readout != null && <em className={s.readout}>{readout}</em>}\n </span>\n {children}\n </label>\n );\n}\n\n/** Tooltip trigger for a row's `description`. A tooltip trigger has to be\n * interactive to be keyboard-reachable, so this is a real button. */\nfunction PropertyRowHelp({ label, description }: { label: ReactNode; description: string }) {\n const name = typeof label === 'string' ? label : 'this setting';\n return (\n <TooltipTrigger>\n <Focusable>\n <button\n type=\"button\"\n className={s.help}\n aria-label={`About ${name}`}\n onClick={(e) => {\n // The wrapping <label> would otherwise actuate the row's control.\n e.preventDefault();\n e.stopPropagation();\n }}\n onMouseDown={(e) => e.stopPropagation()}\n >\n ⓘ\n </button>\n </Focusable>\n <Tooltip>{description}</Tooltip>\n </TooltipTrigger>\n );\n}\n\n// ── Row implementations ──────────────────────────────────────────────\n\n/** Props for `<SliderRow>`. */\nexport interface SliderRowProps {\n label: ReactNode;\n value: number;\n min: number;\n max: number;\n step?: number;\n onChange: (next: number) => void;\n /** Override how the value is rendered next to the label. Defaults to `value.toString()`. */\n format?: (value: number) => ReactNode;\n /**\n * Optional suffix rendered next to the readout. A string becomes a\n * baseline-aligned dim \"word\" unit (e.g. \"px\"); pass JSX like `<sup>°</sup>`\n * to get the browser's native super positioning for symbol units.\n */\n unit?: ReactNode;\n layout?: PropertyRowLayout;\n description?: string;\n /** Take the full width of the enclosing grid — see `<PropertyRow span>`. */\n span?: boolean;\n}\n\n/** A bounded number edited by dragging, with a live readout whose precision\n * follows `step`. */\nexport function SliderRow({\n label,\n value,\n min,\n max,\n step = 1,\n onChange,\n format,\n unit,\n layout,\n description,\n span,\n}: SliderRowProps) {\n // Default readout precision tracks `step`: integer steps → 0 decimals,\n // 0.1 → 1 decimal, 0.05/0.02/0.01 → 2 decimals, 0.005 → 3, etc. Callers\n // can still pass an explicit `format` to override (e.g. for a custom\n // unit string or a non-decimal display like fractions).\n const decimals = step >= 1 ? 0 : Math.min(6, Math.max(0, Math.ceil(-Math.log10(step))));\n const effectiveFormat =\n format ??\n ((n: number) =>\n formatNumber(n, {\n useGrouping: false,\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n }));\n return (\n <PropertyRow\n span={span}\n label={label}\n readout={\n <EditableReadout\n value={value}\n min={min}\n max={max}\n format={effectiveFormat}\n unit={unit}\n onCommit={onChange}\n />\n }\n layout={layout}\n description={description}\n >\n <input\n type=\"range\"\n className={shared.range}\n tabIndex={-1}\n min={min}\n max={max}\n step={step}\n value={value}\n onChange={(e) => {\n const v = Number(e.target.value);\n dlog('property-panel', 'slider', { label, value: v });\n onChange(v);\n }}\n />\n </PropertyRow>\n );\n}\n\ninterface EditableReadoutProps {\n value: number;\n min: number;\n max: number;\n format?: (value: number) => ReactNode;\n unit?: ReactNode;\n onCommit: (next: number) => void;\n}\n\n/**\n * Readout that swaps to a number input on click, commits on Enter/blur,\n * cancels on Escape. Clicks are stopped so the wrapping <label> doesn't\n * forward focus to the slider thumb.\n */\nfunction EditableReadout({ value, min, max, format, unit, onCommit }: EditableReadoutProps) {\n // Draft is non-null only while the input is focused. Live value mirrors\n // into the input otherwise. Pattern mirrors speech-balloons Lab.tsx:893-942.\n const [draft, setDraft] = useState<string | null>(null);\n const fmt = (n: number) =>\n format ? format(n) : formatNumber(n, { useGrouping: false, maximumFractionDigits: 20 });\n const displayValue =\n draft !== null\n ? draft\n : (() => {\n const formatted = fmt(value);\n return typeof formatted === 'string' ? formatted : String(formatted);\n })();\n\n const suffix =\n unit == null ? null : typeof unit === 'string' ? (\n <span className={s.readoutUnit}>{unit}</span>\n ) : (\n unit\n );\n\n const commit = () => {\n if (draft !== null) {\n const n = parseSignedNumber(draft);\n if (Number.isFinite(n)) onCommit(Math.min(max, Math.max(min, n)));\n }\n setDraft(null);\n };\n\n return (\n <span className={s.readoutGroup}>\n <input\n type=\"text\"\n inputMode=\"decimal\"\n className={s.readoutInput}\n value={displayValue}\n onFocus={(e) => {\n const formatted = fmt(value);\n setDraft(typeof formatted === 'string' ? formatted : String(formatted));\n e.currentTarget.select();\n }}\n onChange={(e) => setDraft(e.target.value.replace(/-/g, '−'))}\n onBlur={commit}\n onClick={(e) => {\n // Stop the click from reaching the wrapping <label> (which would\n // forward focus to the slider thumb).\n e.preventDefault();\n e.stopPropagation();\n e.currentTarget.focus();\n }}\n onMouseDown={(e) => e.stopPropagation()}\n onKeyDown={(e) => {\n if (e.key === 'Enter') {\n commit();\n e.currentTarget.blur();\n } else if (e.key === 'Escape') {\n setDraft(null);\n e.currentTarget.blur();\n }\n }}\n />\n {suffix}\n </span>\n );\n}\n\n/** Props for `<ColorRow>`. */\nexport interface ColorRowProps {\n label: ReactNode;\n value: string;\n onChange: (next: string) => void;\n /** 0..1 alpha. When provided, a translucent slider renders beneath the swatch. */\n alpha?: number;\n onAlphaChange?: (next: number) => void;\n /**\n * Render the alpha track as inert (dimmed, no thumb, not-allowed cursor).\n * Use when the color's consumer drops alpha so the affordance reads dead.\n */\n alphaDisabled?: boolean;\n description?: string;\n /** Take the full width of the enclosing grid — see `<PropertyRow span>`. */\n span?: boolean;\n}\n\n/** A color swatch, optionally with an alpha slider beneath it. */\nexport function ColorRow({\n label,\n value,\n onChange,\n alpha,\n onAlphaChange,\n alphaDisabled,\n description,\n span,\n}: ColorRowProps) {\n const showAlpha = alpha != null;\n return (\n <PropertyRow span={span} label={label} variant=\"color\" description={description}>\n <input type=\"color\" value={value} onChange={(e) => onChange(e.target.value)} />\n {showAlpha && (\n <input\n type=\"range\"\n className={`${shared.range} ${shared.alpha} ${s.alpha}`}\n min={0}\n max={1}\n step={0.01}\n value={alpha}\n disabled={alphaDisabled}\n onChange={(e) => onAlphaChange?.(Number(e.target.value))}\n />\n )}\n </PropertyRow>\n );\n}\n\n/** Props for `<CheckboxRow>`. */\nexport interface CheckboxRowProps {\n label: ReactNode;\n value: boolean;\n onChange: (next: boolean) => void;\n description?: string;\n /** Take the full width of the enclosing grid — see `<PropertyRow span>`. */\n span?: boolean;\n}\n\n/** A boolean checkbox. */\nexport function CheckboxRow({ label, value, onChange, span, description }: CheckboxRowProps) {\n return (\n <PropertyRow span={span} label={label} variant=\"checkbox\" description={description}>\n <input type=\"checkbox\" checked={value} onChange={(e) => onChange(e.target.checked)} />\n </PropertyRow>\n );\n}\n\n/** Props for `<TextRow>`. */\nexport interface TextRowProps {\n label: ReactNode;\n value: string;\n onChange: (next: string) => void;\n placeholder?: string;\n maxLength?: number;\n layout?: PropertyRowLayout;\n description?: string;\n /** Take the full width of the enclosing grid — see `<PropertyRow span>`. */\n span?: boolean;\n}\n\n/** A single-line text input. */\nexport function TextRow({\n label,\n value,\n onChange,\n placeholder,\n maxLength,\n layout,\n description,\n span,\n}: TextRowProps) {\n return (\n <PropertyRow span={span} label={label} layout={layout} description={description}>\n <input\n type=\"text\"\n value={value}\n placeholder={placeholder}\n maxLength={maxLength}\n onChange={(e) => onChange(e.target.value)}\n />\n </PropertyRow>\n );\n}\n\n/** Props for `<NumberRow>`. */\nexport interface NumberRowProps {\n label: ReactNode;\n value: number;\n onChange: (next: number) => void;\n min?: number;\n max?: number;\n step?: number;\n placeholder?: string;\n /**\n * Optional suffix rendered after the field. A string becomes a dim \"word\"\n * unit (e.g. \"px\"); pass JSX like `<sup>°</sup>` for symbol units. Display\n * only — it never participates in parsing, and the value stays a number.\n */\n unit?: ReactNode;\n layout?: PropertyRowLayout;\n description?: string;\n /** Take the full width of the enclosing grid — see `<PropertyRow span>`. */\n span?: boolean;\n}\n\n/** A number typed directly. Reach for `<SliderRow>` when the range matters\n * more than the exact value. */\nexport function NumberRow({\n label,\n value,\n onChange,\n min,\n max,\n step,\n placeholder,\n unit,\n layout,\n description,\n span,\n}: NumberRowProps) {\n const input = (\n <input\n type=\"number\"\n value={value}\n min={min}\n max={max}\n step={step}\n placeholder={placeholder}\n onChange={(e) => {\n const raw = e.target.value;\n if (raw === '') return;\n const n = Number(raw);\n if (Number.isFinite(n)) onChange(n);\n }}\n />\n );\n return (\n <PropertyRow span={span} label={label} layout={layout} description={description}>\n {unit == null ? (\n input\n ) : (\n <span className={s.fieldUnitGroup}>\n {input}\n {typeof unit === 'string' ? <span className={s.readoutUnit}>{unit}</span> : unit}\n </span>\n )}\n </PropertyRow>\n );\n}\n\nexport interface PropertyOption<T extends string> {\n value: T;\n label: ReactNode;\n}\n\n/** Props for `<SelectRow>`. */\nexport interface SelectRowProps<T extends string> {\n label: ReactNode;\n value: T;\n options: ReadonlyArray<PropertyOption<T>>;\n onChange: (next: T) => void;\n layout?: PropertyRowLayout;\n description?: string;\n /** Take the full width of the enclosing grid — see `<PropertyRow span>`. */\n span?: boolean;\n}\n\n/** A dropdown over a fixed set of choices. Prefer `<ToggleRow>` when there\n * are only two or three and they should all stay visible. */\nexport function SelectRow<T extends string>({\n label,\n value,\n options,\n onChange,\n layout,\n description,\n span,\n}: SelectRowProps<T>) {\n return (\n <PropertyRow span={span} label={label} layout={layout} description={description}>\n <select\n value={value}\n onChange={(e) => {\n const v = e.target.value as T;\n dlog('property-panel', 'select', { label, value: v });\n onChange(v);\n }}\n >\n {options.map((opt) => (\n <option key={opt.value} value={opt.value}>\n {/* HTML <option> only renders text; ReactNode → string coerce */}\n {opt.label as string}\n </option>\n ))}\n </select>\n </PropertyRow>\n );\n}\n\n/** Props for `<ToggleRow>`. */\nexport interface ToggleRowProps<T extends string> {\n label: ReactNode;\n value: T;\n options: ReadonlyArray<PropertyOption<T>>;\n onChange: (next: T) => void;\n layout?: PropertyRowLayout;\n description?: string;\n /** Take the full width of the enclosing grid — see `<PropertyRow span>`. */\n span?: boolean;\n}\n\n/** A segmented control: the same choice as a select, with every option\n * visible at once. */\nexport function ToggleRow<T extends string>({\n label,\n value,\n options,\n onChange,\n layout,\n description,\n span,\n}: ToggleRowProps<T>) {\n return (\n <PropertyRow span={span} label={label} layout={layout} description={description}>\n <div className={s.toggle}>\n {options.map((opt) => {\n const selected = opt.value === value;\n return (\n <button\n key={opt.value}\n type=\"button\"\n aria-pressed={selected}\n className={\n selected ? `${s.toggleButton} ${s.toggleButtonSelected}` : s.toggleButton\n }\n onClick={() => onChange(opt.value)}\n >\n {opt.label}\n </button>\n );\n })}\n </div>\n </PropertyRow>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ import { jsx as e } from "react/jsx-runtime";
2
+ import { openPointerSession as t } from "@weasel-js/core";
3
+ import { useCallback as n, useEffect as r, useRef as i } from "react";
4
+ var a = {
5
+ handle: "_handle_c4v7n_1",
6
+ horizontal: "_horizontal_c4v7n_14"
7
+ }, o = (e, t, n) => Math.min(n, Math.max(t, e));
8
+ function s(e, t, n, r) {
9
+ let i = r > 0 ? Math.round(e / r) * r : e;
10
+ return o(Number.isInteger(r) ? i : Number(i.toFixed(10)), t, n);
11
+ }
12
+ function c(o) {
13
+ let { orientation: c = "vertical", value: l, min: u, max: d, invert: f, onInput: p, onChange: m, step: h = 1, ariaLabel: g, className: _ } = o, v = i(o);
14
+ v.current = o;
15
+ let y = i(null), b = i(null);
16
+ r(() => () => {
17
+ b.current?.cancel();
18
+ }, []);
19
+ let x = (e) => {
20
+ let t = y.current, n = v.current, r = e - t.origin;
21
+ return s(t.startValue + (n.invert ? -r : r), n.min, n.max, n.step ?? 1);
22
+ }, S = (e) => {
23
+ y.current = null, b.current = null, v.current.onChange?.(e);
24
+ }, C = n((e) => {
25
+ if (e.button !== 0 || b.current) return;
26
+ let n = v.current, r = (n.orientation ?? "vertical") === "vertical", i = (e) => r ? e.clientX : e.clientY;
27
+ y.current = {
28
+ origin: i(e),
29
+ startValue: n.value,
30
+ last: s(n.value, n.min, n.max, n.step ?? 1)
31
+ }, b.current = t(e.currentTarget, e, {
32
+ onMove: (e) => {
33
+ let t = x(i(e));
34
+ y.current.last = t, v.current.onInput(t);
35
+ },
36
+ onEnd: (e) => {
37
+ S(x(i(e)));
38
+ },
39
+ onCancel: () => {
40
+ S(y.current.last);
41
+ }
42
+ }), e.preventDefault();
43
+ }, []), w = n((e) => {
44
+ let t = c === "vertical" ? "ArrowRight" : "ArrowDown", n = c === "vertical" ? "ArrowLeft" : "ArrowUp", r = h * (e.shiftKey ? 8 : 1) * (f ? -1 : 1), i;
45
+ if (e.key === t ? i = l + r : e.key === n ? i = l - r : e.key === "Home" ? i = f ? d : u : e.key === "End" && (i = f ? u : d), i === void 0) return;
46
+ e.preventDefault();
47
+ let a = s(i, u, d, h);
48
+ p(a), m?.(a);
49
+ }, [
50
+ f,
51
+ d,
52
+ u,
53
+ p,
54
+ m,
55
+ c,
56
+ h,
57
+ l
58
+ ]);
59
+ return /* @__PURE__ */ e("div", {
60
+ className: [
61
+ a.handle,
62
+ c === "horizontal" && a.horizontal,
63
+ _
64
+ ].filter(Boolean).join(" "),
65
+ role: "separator",
66
+ tabIndex: 0,
67
+ "aria-label": g,
68
+ "aria-orientation": c,
69
+ "aria-valuenow": l,
70
+ "aria-valuemin": u,
71
+ "aria-valuemax": d,
72
+ onPointerDown: C,
73
+ onKeyDown: w
74
+ });
75
+ }
76
+ //#endregion
77
+ export { c as t };
78
+
79
+ //# sourceMappingURL=ResizeHandle-DfqqfaZz.js.map