@weasel-js/ui 1.4.3 → 1.4.4

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 (53) hide show
  1. package/dist/chunks/Button-CExdkQcU.js +90 -0
  2. package/dist/chunks/{Button-Cq5LBUMD.js.map → Button-CExdkQcU.js.map} +1 -1
  3. package/dist/chunks/{ColorField-D0OxUlvu.js → ColorField-C20aRvCg.js} +2 -2
  4. package/dist/chunks/{ColorField-D0OxUlvu.js.map → ColorField-C20aRvCg.js.map} +1 -1
  5. package/dist/chunks/{DetentSlider-BVWKdRyC.js → DetentSlider-BpD7bgjN.js} +2 -2
  6. package/dist/chunks/{DetentSlider-BVWKdRyC.js.map → DetentSlider-BpD7bgjN.js.map} +1 -1
  7. package/dist/chunks/{GradientEditor-C5NVILLk.js → GradientEditor-BLpAtMDK.js} +3 -3
  8. package/dist/chunks/{GradientEditor-C5NVILLk.js.map → GradientEditor-BLpAtMDK.js.map} +1 -1
  9. package/dist/chunks/{InlineRange-50ulExCL.js → InlineRange-CcfzY80y.js} +2 -2
  10. package/dist/chunks/{InlineRange-50ulExCL.js.map → InlineRange-CcfzY80y.js.map} +1 -1
  11. package/dist/chunks/{PaintInput-67U-COE4.js → PaintInput-B7AScmHN.js} +3 -3
  12. package/dist/chunks/{PaintInput-67U-COE4.js.map → PaintInput-B7AScmHN.js.map} +1 -1
  13. package/dist/chunks/{Prefs-D0l0PXLF.js → Prefs-4JSEtuU-.js} +3 -3
  14. package/dist/chunks/{Prefs-D0l0PXLF.js.map → Prefs-4JSEtuU-.js.map} +1 -1
  15. package/dist/chunks/{Properties-CMLua9mU.js → Properties-BpUYKi7-.js} +125 -113
  16. package/dist/chunks/Properties-BpUYKi7-.js.map +1 -0
  17. package/dist/chunks/{RangeSlider-C3to8A52.js → RangeSlider-S2R3zA8N.js} +8 -8
  18. package/dist/chunks/RangeSlider-S2R3zA8N.js.map +1 -0
  19. package/dist/chunks/{SelectionPanel-BWXU8BqW.js → SelectionPanel-C59wavdA.js} +4 -4
  20. package/dist/chunks/{SelectionPanel-BWXU8BqW.js.map → SelectionPanel-C59wavdA.js.map} +1 -1
  21. package/dist/chunks/{Slider-Bk_G5sSW.js → Slider-Bs-KSYgQ.js} +14 -14
  22. package/dist/chunks/Slider-Bs-KSYgQ.js.map +1 -0
  23. package/dist/chunks/{Timeline-BmYwYjXd.js → Timeline-Bz806JbE.js} +2 -2
  24. package/dist/chunks/{Timeline-BmYwYjXd.js.map → Timeline-Bz806JbE.js.map} +1 -1
  25. package/dist/chunks/range.module-CvSfABmu.js +8 -0
  26. package/dist/chunks/range.module-CvSfABmu.js.map +1 -0
  27. package/dist/components/Button/Button.d.ts +5 -0
  28. package/dist/components/Button/Button.d.ts.map +1 -1
  29. package/dist/components/Button/index.js +1 -1
  30. package/dist/components/ColorField/index.js +1 -1
  31. package/dist/components/DetentSlider/index.js +1 -1
  32. package/dist/components/GradientEditor/index.js +1 -1
  33. package/dist/components/InlineRange/index.js +1 -1
  34. package/dist/components/PaintInput/index.js +1 -1
  35. package/dist/components/Prefs/index.js +1 -1
  36. package/dist/components/Properties/PropertyPanel.d.ts +27 -2
  37. package/dist/components/Properties/PropertyPanel.d.ts.map +1 -1
  38. package/dist/components/Properties/index.js +1 -1
  39. package/dist/components/RangeSlider/RangeSlider.d.ts +5 -0
  40. package/dist/components/RangeSlider/RangeSlider.d.ts.map +1 -1
  41. package/dist/components/RangeSlider/index.js +1 -1
  42. package/dist/components/SelectionPanel/index.js +1 -1
  43. package/dist/components/Slider/index.js +1 -1
  44. package/dist/components/Timeline/index.js +1 -1
  45. package/dist/index.js +12 -12
  46. package/dist/style.css +1 -1
  47. package/package.json +6 -6
  48. package/dist/chunks/Button-Cq5LBUMD.js +0 -87
  49. package/dist/chunks/Properties-CMLua9mU.js.map +0 -1
  50. package/dist/chunks/RangeSlider-C3to8A52.js.map +0 -1
  51. package/dist/chunks/Slider-Bk_G5sSW.js.map +0 -1
  52. package/dist/chunks/range.module-CgURq-Ua.js +0 -8
  53. package/dist/chunks/range.module-CgURq-Ua.js.map +0 -1
@@ -1 +0,0 @@
1
- {"version":3,"file":"Properties-CMLua9mU.js","names":[],"sources":["../../src/components/Properties/CurveField.module.css","../../src/components/Properties/CurveField.tsx","../../src/components/Properties/Properties.module.css","../../src/components/Properties/PropertyPanel.tsx","../../src/components/Properties/EffectCard.tsx","../../src/components/Properties/PropertyGroup.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/* ── Container metrics ──────────────────────────────────────────────\n Every spacing number the family lays out with is read from a `--wzl-prop-*`\n custom property with its own value as the fallback, so a consumer can move\n any of them from an ancestor without selector surgery. The `density` classes\n below set them in preset bundles; nothing in this file declares one, so an\n inherited value always reaches the use site. */\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: var(--wzl-prop-panel-pad, 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 var(--wzl-prop-panel-title-gap, 12px);\n}\n\n.list {\n display: grid;\n grid-template-columns: 1fr 1fr;\n column-gap: var(--wzl-prop-column-gap, 10px);\n row-gap: var(--wzl-prop-row-gap, 4px);\n align-items: stretch;\n /* A property panel is dense: its fields are shorter and tighter than a\n standalone Input. Mapped here, on the container, so one declaration moves\n every field in the panel — and read from the container-level name so a\n density set further up is not shadowed by this declaration. */\n --wzl-field-h: var(--wzl-prop-field-h, 20px);\n --wzl-field-pad-x: var(--wzl-prop-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/* `pack=\"one-up\"` gives every row the full width, color rows included — the\n one packing a palette can be built from. */\n.listOneUp > * {\n grid-column: 1 / -1;\n}\n\n/* ── density ────────────────────────────────────────────────────────\n Preset bundles for the container metrics above. Set on whichever container\n the consumer puts `density` on; every metric inherits down from there, so a\n panel sets the whole family and a single group can still differ. `normal`\n states its values rather than omitting them, so a group can go back to the\n default inside a tighter panel. */\n\n.densityTight {\n --wzl-prop-row-gap: 2px;\n --wzl-prop-subpanel-row-gap: 2px;\n --wzl-prop-column-gap: 8px;\n --wzl-prop-panel-pad: 10px 12px;\n --wzl-prop-panel-title-gap: 8px;\n --wzl-prop-group-pad: 4px 8px 6px;\n --wzl-prop-group-title-gap: 2px;\n --wzl-prop-field-h: 18px;\n --wzl-prop-field-pad-x: 5px;\n}\n\n.densityNormal {\n --wzl-prop-row-gap: 4px;\n --wzl-prop-subpanel-row-gap: 3px;\n --wzl-prop-column-gap: 10px;\n --wzl-prop-panel-pad: 14px 16px;\n --wzl-prop-panel-title-gap: 12px;\n --wzl-prop-group-pad: 6px 10px 8px;\n --wzl-prop-group-title-gap: 4px;\n --wzl-prop-field-h: 20px;\n --wzl-prop-field-pad-x: 6px;\n}\n\n.densityRoomy {\n --wzl-prop-row-gap: 8px;\n --wzl-prop-subpanel-row-gap: 7px;\n --wzl-prop-column-gap: 14px;\n --wzl-prop-panel-pad: 18px 20px;\n --wzl-prop-panel-title-gap: 16px;\n --wzl-prop-group-pad: 10px 14px 12px;\n --wzl-prop-group-title-gap: 6px;\n --wzl-prop-field-h: 24px;\n --wzl-prop-field-pad-x: 8px;\n}\n\n/* ── align ──────────────────────────────────────────────────────────\n Where an inline row's label and control sit on the row's cross axis.\n Inherits like density does, so a column of color rows lines its swatches up\n on one edge from a single declaration on the list.\n\n Two names, because a color row aligns twice: its items within their grid row,\n and the whole grid within the row's box. They agree except at `baseline`,\n which `align-content` does not take — there the grid goes to the top and the\n items find the label's baseline inside it. */\n\n.alignStart {\n --wzl-prop-row-align: start;\n --wzl-prop-row-align-content: start;\n}\n\n.alignCenter {\n --wzl-prop-row-align: center;\n --wzl-prop-row-align-content: center;\n}\n\n.alignEnd {\n --wzl-prop-row-align: end;\n --wzl-prop-row-align-content: end;\n}\n\n.alignBaseline {\n --wzl-prop-row-align: baseline;\n --wzl-prop-row-align-content: start;\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: var(--wzl-prop-row-align, 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 -moz-appearance: textfield;\n appearance: textfield;\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/* Chrome reserves the spin-button gutter at the right edge whether or not the\n arrows are painted, so a right-aligned value stops ~20px short of its own\n border and the row reads as if something is missing from that space. The\n panel drives numbers by typing and dragging, never by the steppers.\n `.readoutInput` has always dropped them; this is the same removal for the\n typed field beside it. */\n.row input[type='number']::-webkit-inner-spin-button,\n.row input[type='number']::-webkit-outer-spin-button {\n -webkit-appearance: none;\n margin: 0;\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/* `align-content: end` sinks the whole grid so a color row paired with a\n taller sibling keeps its alpha track on the neighbour's bottom edge. An\n explicit `align` overrides both that and the per-item alignment below, which\n is what a column of swatches read as a group needs. */\n.rowColor {\n display: grid;\n grid-template-columns: 1fr auto;\n grid-template-rows: auto;\n align-content: var(--wzl-prop-row-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: var(--wzl-prop-row-align, 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: var(--wzl-prop-row-align, 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/* `layout=\"block\"` stacks the color row: label, then swatch, then alpha. */\n.rowColor.rowBlock {\n grid-template-columns: 1fr;\n}\n\n.rowColor.rowBlock .rowLabel {\n grid-column: 1;\n grid-row: 1;\n}\n\n.rowColor.rowBlock input[type='color'] {\n grid-column: 1;\n grid-row: 2;\n justify-self: start;\n}\n\n.rowColor.rowBlock input[type='range'].alpha {\n grid-row: 3;\n}\n\n/* ── Checkbox variant: control-first inline layout, no uppercase label. */\n.rowCheckbox {\n flex-direction: row;\n align-items: var(--wzl-prop-row-align, 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/* `layout=\"block\"` stacks the checkbox row: label above, box beneath, both on\n the leading edge — a block row's cross axis is the horizontal one. */\n.rowCheckbox.rowBlock {\n flex-direction: column;\n align-items: var(--wzl-prop-row-align, start);\n}\n\n.rowCheckbox.rowBlock .rowLabel {\n order: 0;\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: var(--wzl-prop-group-pad, 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 var(--wzl-prop-group-title-gap, 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/* A twisty leads the title, so the head takes over the title's bottom gap and\n the two sit on one line. */\n.groupHead {\n margin-bottom: var(--wzl-prop-group-title-gap, 4px);\n}\n\n.groupHead .groupTitle {\n margin-bottom: 0;\n}\n\n/* `hidden` on its own loses to the body's `display: grid` below. */\n.groupBody[hidden] {\n display: none;\n}\n\n.groupBody {\n display: grid;\n grid-template-columns: 1fr 1fr;\n column-gap: var(--wzl-prop-column-gap, 10px);\n row-gap: var(--wzl-prop-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.groupOneUp .groupBody > * {\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: var(--wzl-prop-column-gap, 10px);\n row-gap: var(--wzl-prop-subpanel-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 ReactNode, useEffect, useRef, 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/**\n * How much room a container gives its rows — gaps, padding, and field height,\n * moved together. Set on any container in the family; it reaches every\n * descendant, so an inner group can differ from the panel around it.\n */\nexport type PropertyDensity = 'tight' | 'normal' | 'roomy';\n\n/** Where an inline row's label and control sit on the row's cross axis. Set on\n * a container to line a whole column of them up. */\nexport type PropertyAlign = 'start' | 'center' | 'end' | 'baseline';\n\n/** Metric props every container in the family takes. Both reach descendants,\n * so the nearest container that states one wins. */\nexport interface PropertyMetricProps {\n /** Room the rows get. Unset inherits from an outer container, else `normal`. */\n density?: PropertyDensity;\n /**\n * Cross-axis alignment of an inline row's label and control. `baseline` sits\n * the control on the label's first-line baseline, which is what lines a\n * column of swatches up against labels of different heights. Unset keeps each\n * variant's own alignment — a color row centers its label and swatch and\n * sinks the pair to the row's bottom edge, which keeps a paired alpha track\n * level with its neighbour.\n */\n align?: PropertyAlign;\n}\n\nconst DENSITY_CLASS: Record<PropertyDensity, string> = {\n tight: s.densityTight,\n normal: s.densityNormal,\n roomy: s.densityRoomy,\n};\n\nconst ALIGN_CLASS: Record<PropertyAlign, string> = {\n start: s.alignStart,\n center: s.alignCenter,\n end: s.alignEnd,\n baseline: s.alignBaseline,\n};\n\n/** Joins a container's base class with its metric classes and the consumer's.\n * Exported for the family's other files, which share the same two props. */\nexport function propertyMetricClass(\n base: string,\n { density, align }: PropertyMetricProps,\n className?: string,\n): string {\n return [base, density && DENSITY_CLASS[density], align && ALIGN_CLASS[align], className]\n .filter(Boolean)\n .join(' ');\n}\n\n/** Props for `<PropertyPanel>`. */\nexport interface PropertyPanelProps extends PropertyMetricProps {\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({\n title,\n children,\n className,\n density,\n align,\n}: PropertyPanelProps) {\n return (\n <div className={propertyMetricClass(s.panel, { density, align }, className)}>\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' | 'one-up';\n\n/** Props for `<PropertyList>`. */\nexport interface PropertyListProps extends PropertyMetricProps {\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 * - `'one-up'`: every row spans the full width, color rows included. Right\n * for a palette — a column of swatches read as a group.\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({\n children,\n className,\n pack = 'auto-color',\n density,\n align,\n}: PropertyListProps) {\n const base = `${s.list}${pack === 'pairs' ? ` ${s.listPairs}` : pack === 'one-up' ? ` ${s.listOneUp}` : ''}`;\n return <div className={propertyMetricClass(base, { density, align }, className)}>{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 extends PropertyMetricProps {\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. Unset takes the variant's own\n * orientation: `block` — label above control — for the default variant, and\n * `inline` for the color and checkbox variants, which read as a row.\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,\n span,\n children,\n htmlFor,\n className,\n density,\n align,\n}: PropertyRowProps) {\n const variantClass = variant === 'color' ? s.rowColor : variant === 'checkbox' ? s.rowCheckbox : '';\n // Each variant already lays out one way; a class is only needed for the\n // other one. The default variant stacks, so it needs `.rowInline`; color and\n // checkbox read as a row, so they need `.rowBlock`.\n const intrinsic: PropertyRowLayout = variant === 'default' ? 'block' : 'inline';\n const resolved = layout ?? intrinsic;\n const layoutClass =\n resolved === intrinsic ? '' : resolved === 'inline' ? s.rowInline : s.rowBlock;\n const cls = propertyMetricClass(\n [s.row, variantClass, layoutClass, span && s.span].filter(Boolean).join(' '),\n { density, align },\n className,\n );\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 extends PropertyMetricProps {\n label: ReactNode;\n value: number;\n min: number;\n max: number;\n step?: number;\n /**\n * The committed value. Given `onInput` as well, it fires once a drag ends\n * or a typed readout is accepted; on its own it fires on every move, which\n * is what a row with one callback has always done.\n */\n onChange: (next: number) => void;\n /**\n * The live value, fired continuously through a drag. Pass it alongside\n * `onChange` when the write is expensive — cheap state here, the costly\n * work there. Mirrors `<Slider>`'s `onInput` / `onChange` pair.\n */\n onInput?: (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 onInput,\n format,\n unit,\n layout,\n description,\n span,\n density,\n align,\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 live = onInput ?? onChange;\n const commit = onInput ? onChange : undefined;\n const range = useRef<HTMLInputElement>(null);\n const latestCommit = useRef(commit);\n useEffect(() => {\n latestCommit.current = commit;\n });\n useEffect(() => {\n const el = range.current;\n if (!el) return;\n // The commit half is the platform's: a range input fires `input` through\n // the drag and `change` once, on release. React's synthetic onChange sees\n // only the first, since its value tracker drops the unchanged second.\n const onCommit = () => latestCommit.current?.(Number(el.value));\n el.addEventListener('change', onCommit);\n return () => el.removeEventListener('change', onCommit);\n }, []);\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={(next) => {\n live(next);\n commit?.(next);\n }}\n />\n }\n layout={layout}\n description={description}\n density={density}\n align={align}\n >\n <input\n ref={range}\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 live(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 extends PropertyMetricProps {\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 /** Label beside the swatch (default) or above it. */\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 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 layout,\n description,\n span,\n density,\n align,\n}: ColorRowProps) {\n const showAlpha = alpha != null;\n return (\n <PropertyRow\n span={span}\n label={label}\n variant=\"color\"\n layout={layout}\n description={description}\n density={density}\n align={align}\n >\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 extends PropertyMetricProps {\n label: ReactNode;\n value: boolean;\n onChange: (next: boolean) => void;\n /** Label beside the box (default) or above it. */\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 boolean checkbox. */\nexport function CheckboxRow({\n label,\n value,\n onChange,\n layout,\n span,\n description,\n density,\n align,\n}: CheckboxRowProps) {\n return (\n <PropertyRow\n span={span}\n label={label}\n variant=\"checkbox\"\n layout={layout}\n description={description}\n density={density}\n align={align}\n >\n <input type=\"checkbox\" checked={value} onChange={(e) => onChange(e.target.checked)} />\n </PropertyRow>\n );\n}\n\n/** Props for `<TextRow>`. */\nexport interface TextRowProps extends PropertyMetricProps {\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 density,\n align,\n}: TextRowProps) {\n return (\n <PropertyRow\n span={span}\n label={label}\n layout={layout}\n description={description}\n density={density}\n align={align}\n >\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 extends PropertyMetricProps {\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 density,\n align,\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\n span={span}\n label={label}\n layout={layout}\n description={description}\n density={density}\n align={align}\n >\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> extends PropertyMetricProps {\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 density,\n align,\n}: SelectRowProps<T>) {\n return (\n <PropertyRow\n span={span}\n label={label}\n layout={layout}\n description={description}\n density={density}\n align={align}\n >\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> extends PropertyMetricProps {\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 density,\n align,\n}: ToggleRowProps<T>) {\n return (\n <PropertyRow\n span={span}\n label={label}\n layout={layout}\n description={description}\n density={density}\n align={align}\n >\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","import { type CSSProperties, type ReactNode, useState } from 'react';\nimport s from './Properties.module.css';\nimport { type PropertyMetricProps, propertyMetricClass } from './PropertyPanel';\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 extends PropertyMetricProps {\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, density, align }: SubpanelProps) {\n const cls = propertyMetricClass(s.subpanel, { density, align }, className);\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, useId, useState } from 'react';\nimport { DisclosureRow } from '../Disclosure';\nimport s from './Properties.module.css';\nimport {\n type PropertyListPack,\n type PropertyMetricProps,\n propertyMetricClass,\n} from './PropertyPanel';\n\n/** Props for `<PropertyGroup>`. */\nexport interface PropertyGroupProps extends PropertyMetricProps {\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 /**\n * Give the title a twisty that folds the rows away. Implied by any of the\n * three props below, so it is only needed for a group that starts open and\n * keeps its own state.\n */\n collapsible?: boolean;\n /** Start folded. Read once; the group owns the state from then on. */\n defaultCollapsed?: boolean;\n /**\n * Folded or not, as the consumer holds it. Given, the group keeps no state\n * of its own and every toggle arrives at `onCollapsedChange` instead — which\n * is what a panel that remembers its sections past a reload needs.\n */\n collapsed?: boolean;\n onCollapsedChange?: (next: boolean) => void;\n children: ReactNode;\n className?: string;\n /** How rows pack into the 2-column grid — see `<PropertyList pack>`. */\n pack?: PropertyListPack;\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 *\n * A collapsible group keeps its rows mounted and hides them, so a control's\n * local state survives being folded away.\n */\nexport function PropertyGroup({\n title,\n hidden,\n collapsible,\n defaultCollapsed,\n collapsed,\n onCollapsedChange,\n children,\n className,\n pack = 'auto-color',\n density,\n align,\n}: PropertyGroupProps) {\n const bodyId = useId();\n const [own, setOwn] = useState(defaultCollapsed ?? false);\n if (hidden) return null;\n\n const folds =\n collapsible ??\n (defaultCollapsed !== undefined || collapsed !== undefined || onCollapsedChange !== undefined);\n const folded = folds && (collapsed ?? own);\n const toggle = () => {\n if (collapsed === undefined) setOwn(!folded);\n onCollapsedChange?.(!folded);\n };\n\n const base = `${s.group}${pack === 'pairs' ? ` ${s.groupPairs}` : pack === 'one-up' ? ` ${s.groupOneUp}` : ''}`;\n const cls = propertyMetricClass(base, { density, align }, className);\n const heading = (\n <h3 className={s.groupTitle}>\n <hr />\n <span>{title}</span>\n <hr />\n </h3>\n );\n return (\n <div className={cls}>\n {folds ? (\n <DisclosureRow\n className={s.groupHead}\n open={!folded}\n onToggle={toggle}\n label={typeof title === 'string' ? title : 'this section'}\n controls={bodyId}\n >\n {heading}\n </DisclosureRow>\n ) : (\n heading\n )}\n <div id={bodyId} className={s.groupBody} hidden={folded}>\n {children}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"RangeSlider-C3to8A52.js","names":[],"sources":["../../src/components/RangeSlider/RangeSlider.module.css","../../src/components/RangeSlider/RangeSlider.tsx"],"sourcesContent":[".slider {\n display: flex;\n flex-direction: column;\n width: 100%;\n gap: 4px;\n font-family: var(--wzl-font-ui);\n color: var(--wzl-fg);\n}\n\n.slider[data-orientation=\"vertical\"] {\n flex-direction: row;\n align-items: stretch;\n width: 28px;\n height: 160px;\n}\n\n.head {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 8px;\n}\n\n.label {\n font-size: var(--wzl-font-size-sm);\n font-weight: var(--wzl-font-weight-medium);\n color: var(--wzl-fg);\n}\n\n.output {\n font-size: var(--wzl-font-size-sm);\n color: var(--wzl-fg-subtle);\n font-variant-numeric: tabular-nums;\n}\n\n.track {\n position: relative;\n height: 6px;\n min-width: 80px;\n background: var(--wzl-surface-sunken);\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n}\n\n.slider[data-orientation=\"vertical\"] .track {\n width: 6px;\n height: 100%;\n}\n\n.fill {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n background: var(--wzl-accent-strong);\n border-radius: var(--wzl-radius-sm);\n}\n\n.thumb {\n width: 14px;\n height: 14px;\n background: var(--wzl-fg-muted);\n border: 1px solid var(--wzl-border-strong);\n border-radius: 50%;\n cursor: ew-resize;\n outline: none;\n transition: box-shadow var(--wzl-motion-fast);\n}\n\n.slider[data-orientation=\"vertical\"] .thumb {\n cursor: ns-resize;\n}\n\n.thumb[data-focus-visible] {\n box-shadow: 0 0 0 2px var(--wzl-focus-ring);\n}\n\n.thumb[data-dragging] {\n background: var(--wzl-accent-strong);\n}\n\n.slider[data-disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n","import type { ReactNode } from 'react';\nimport {\n Slider as RACSlider,\n SliderOutput,\n SliderTrack,\n SliderThumb,\n Label,\n type SliderProps as RACSliderProps,\n} from 'react-aria-components';\nimport s from './RangeSlider.module.css';\n\n/** Props for {@link RangeSlider}, on top of React Aria's `Slider` props. */\nexport type RangeSliderProps = Omit<RACSliderProps, 'children' | 'className'> & {\n label?: ReactNode;\n /** Show the current value next to the label. Defaults to true when a\n * label is supplied. */\n showOutput?: boolean;\n /** Format the rendered value(s). Useful for units (`v => `${v}%``). */\n formatOutput?: (v: number | number[]) => ReactNode;\n className?: string;\n};\n\n/**\n * Linear range slider on React Aria. Single-value by default; pass an array\n * to `value` / `defaultValue` for multi-thumb. Distinct from `Slider`, which\n * is fully controlled and gives each thumb its own bounds and readout.\n */\nexport function RangeSlider(props: RangeSliderProps) {\n const { label, showOutput, formatOutput, className, ...rest } = props;\n const wantOutput = showOutput ?? label !== undefined;\n return (\n <RACSlider {...rest} className={[s.slider, className].filter(Boolean).join(' ')}>\n {(label !== undefined || wantOutput) && (\n <div className={s.head}>\n {label !== undefined && <Label className={s.label}>{label}</Label>}\n {wantOutput && (\n <SliderOutput className={s.output}>\n {({ state }) => {\n const v = state.values.length === 1 ? state.values[0] : state.values;\n return formatOutput ? formatOutput(v) : Array.isArray(v) ? v.join(' – ') : String(v);\n }}\n </SliderOutput>\n )}\n </div>\n )}\n <SliderTrack className={s.track}>\n {({ state }) => {\n // Single-thumb: fill from start to thumb. Multi-thumb: fill\n // between adjacent thumbs (simplest = between first and last).\n const fillStart = state.values.length > 1 ? state.getThumbPercent(0) * 100 : 0;\n const fillEnd = state.getThumbPercent(state.values.length - 1) * 100;\n return (\n <>\n <div\n className={s.fill}\n style={{ left: `${fillStart}%`, right: `${100 - fillEnd}%` }}\n />\n {state.values.map((_, i) => (\n <SliderThumb key={i} index={i} className={s.thumb} />\n ))}\n </>\n );\n }}\n </SliderTrack>\n </RACSlider>\n );\n}\n"],"mappings":";;;;;;;;;;;;;AC2BA,SAAgB,EAAY,GAAyB;CACnD,IAAM,EAAE,UAAO,eAAY,iBAAc,cAAW,GAAG,MAAS,GAC1D,IAAa,KAAc,MAAU,KAAA;AAC3C,QACE,kBAAC,GAAD;EAAW,GAAI;EAAM,WAAW,CAAC,EAAE,QAAQ,EAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI;YAA/E,EACI,MAAU,KAAA,KAAa,MACvB,kBAAC,OAAD;GAAK,WAAW,EAAE;aAAlB,CACG,MAAU,KAAA,KAAa,kBAAC,GAAD;IAAO,WAAW,EAAE;cAAQ;IAAc,CAAA,EACjE,KACC,kBAAC,GAAD;IAAc,WAAW,EAAE;eACvB,EAAE,eAAY;KACd,IAAM,IAAI,EAAM,OAAO,WAAW,IAAI,EAAM,OAAO,KAAK,EAAM;AAC9D,YAAO,IAAe,EAAa,EAAE,GAAG,MAAM,QAAQ,EAAE,GAAG,EAAE,KAAK,MAAM,GAAG,OAAO,EAAE;;IAEzE,CAAA,CAEb;MAER,kBAAC,GAAD;GAAa,WAAW,EAAE;cACtB,EAAE,eAAY;IAGd,IAAM,IAAY,EAAM,OAAO,SAAS,IAAI,EAAM,gBAAgB,EAAE,GAAG,MAAM,GACvE,IAAU,EAAM,gBAAgB,EAAM,OAAO,SAAS,EAAE,GAAG;AACjE,WACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;KACE,WAAW,EAAE;KACb,OAAO;MAAE,MAAM,GAAG,EAAU;MAAI,OAAO,GAAG,MAAM,EAAQ;MAAI;KAC5D,CAAA,EACD,EAAM,OAAO,KAAK,GAAG,MACpB,kBAAC,GAAD;KAAqB,OAAO;KAAG,WAAW,EAAE;KAAS,EAAnC,EAAmC,CACrD,CACD,EAAA,CAAA;;GAGK,CAAA,CACJ"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"Slider-Bk_G5sSW.js","names":[],"sources":["../../src/components/Slider/Slider.module.css","../../src/components/Slider/Slider.tsx"],"sourcesContent":[".root {\n position: relative;\n user-select: none;\n -webkit-user-select: none;\n touch-action: none;\n}\n\n.row {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.track {\n position: relative;\n flex: 1;\n min-width: 0;\n height: var(--rp-track-height, 24px);\n background: var(--wzl-surface-sunken);\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n cursor: var(--rp-track-cursor, crosshair);\n /* No `overflow: hidden` here — thumbs at extreme values would get\n * clipped at the track edges. The gradient/track-paint clip is\n * applied to `.trackInner` instead, which lets thumbs spill freely. */\n}\n\n.trackInner {\n position: absolute;\n inset: 0;\n border-radius: var(--wzl-radius-sm);\n overflow: hidden;\n}\n\n.thumb {\n position: absolute;\n /* Thumb extends 2px above + below the track for grabbability + visual\n * weight. Track itself stays its natural height; the thumb just spills. */\n top: -2px;\n bottom: -2px;\n width: var(--rp-thumb-size, 14px);\n margin-left: calc(var(--rp-thumb-size, 14px) / -2);\n display: flex;\n align-items: center;\n justify-content: center;\n background: color-mix(in srgb, var(--wzl-fg-muted) 70%, transparent);\n border: 1px solid var(--wzl-border-strong);\n border-radius: var(--wzl-radius-sm);\n cursor: ew-resize;\n /* Frosted-glass over the (often colorful) track. Cheap on modern GPUs;\n * gracefully degrades to just the partial-alpha background on engines\n * without backdrop-filter support. */\n backdrop-filter: blur(3px);\n -webkit-backdrop-filter: blur(3px);\n /* Subtle raise — sells the \"thumb is on top of the track\" hierarchy. */\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n font: var(--wzl-font-weight-medium) var(--wzl-font-size-xs)/var(--wzl-leading-tight) ui-sans-serif, system-ui, sans-serif;\n font-variant-numeric: tabular-nums;\n color: var(--wzl-fg-inverse);\n /* White halo so labels stay legible over wildly varying gradient\n * backgrounds (e.g. labels 'T' / 'P' / 'B' over teal, hue 200° over\n * yellow-green, etc.). */\n text-shadow: 0 0 2px rgba(255, 255, 255, 0.7);\n}\n\n/* A slim slider's thumb is round and flush with the track, matching the\n property rows. The default thumb keeps its 2px spill and rounded-rect face. */\n.slim .thumb {\n top: 50%;\n bottom: auto;\n height: var(--rp-thumb-size);\n transform: translateY(-50%);\n border-radius: 50%;\n border: none;\n backdrop-filter: none;\n -webkit-backdrop-filter: none;\n box-shadow: none;\n background: color-mix(in srgb, var(--wzl-accent) var(--wzl-slider-thumb-tint), transparent);\n}\n\n.thumb:focus-visible {\n outline: 2px solid var(--wzl-accent);\n outline-offset: 1px;\n}\n\n.thumbActive {\n /* Visual marker for the dragging/focused thumb; subclass-overridable. */\n z-index: 1;\n}\n\n/* The row holds absolutely-positioned labels, so it has to state its own\n height: the text's, not the thumb's. It said 14px — the default thumb's\n size — which left 4px of dead space under the labels at either density. */\n.readoutsBelow {\n position: relative;\n height: calc(var(--wzl-font-size-xs) * var(--wzl-leading-tight));\n margin-top: 4px;\n}\n\n.readoutBelow {\n position: absolute;\n transform: translateX(-50%);\n font: var(--wzl-font-weight-medium) var(--wzl-font-size-xs)/var(--wzl-leading-tight) ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n color: var(--wzl-fg-muted);\n white-space: nowrap;\n}\n\n.readoutInline {\n display: inline-block;\n margin-left: 8px;\n font: var(--wzl-font-weight-medium) var(--wzl-font-size-sm)/var(--wzl-leading-tight) ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n color: var(--wzl-fg-muted);\n vertical-align: middle;\n}\n\n.notched {\n background: var(--thumb-svg, none);\n background-size: 100% 100%;\n background-repeat: no-repeat;\n background-color: transparent;\n border: none;\n /* Notched thumbs need to disable the .thumb defaults that paint to\n * the bounding box: backdrop-filter and box-shadow both ignore the\n * SVG's polygon/notch and would render through the notch cutout.\n * Use filter: drop-shadow instead — it traces the actual rendered\n * alpha (i.e. the polygon shape including the cut). */\n backdrop-filter: none;\n -webkit-backdrop-filter: none;\n box-shadow: none;\n filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.25));\n /* Default notched SVG (down-pointing pentagon, matches the perceptual-color experiment). */\n --thumb-svg: url(\"data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 30' preserveAspectRatio='none'%3E%3Cpolygon points='0,0 2.52,0 7,10.3 11.48,0 14,0 14,30 0,30' fill='rgba(255,255,255,0.62)' stroke='rgba(0,0,0,0.55)' stroke-width='0.75' stroke-linejoin='miter'/%3E%3C/svg%3E\");\n}\n\n.ticks {\n position: absolute;\n inset: 0;\n /* A mark must never eat the track click that adds a thumb, nor the\n * pointerdown that starts a drag through it. */\n pointer-events: none;\n}\n\n.tick {\n position: absolute;\n top: var(--rp-tick-inset, 20%);\n bottom: var(--rp-tick-inset, 20%);\n width: var(--rp-tick-width, 1px);\n margin-left: calc(var(--rp-tick-width, 1px) / -2);\n border-radius: var(--wzl-radius-pill);\n background: var(--rp-tick-color, var(--wzl-border-strong));\n}\n","import { useCallback, useEffect, useRef, type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactElement, type ReactNode } from 'react';\nimport { openPointerSession, type PointerSession } from '@weasel-js/core';\nimport s from './Slider.module.css';\nimport { formatNumber } from '../../format/number';\n\n/**\n * Passed to a custom thumb renderer: the thumb box in CSS px, and whether\n * this thumb is the one being dragged.\n */\nexport type ThumbRenderCtx = {\n width: number;\n height: number;\n isActive: boolean;\n};\n\n/**\n * A thumb's appearance — one of the two built-in shapes, or a custom\n * renderer.\n */\nexport type ThumbShape =\n | 'round'\n | 'notched'\n | { render: (ctx: ThumbRenderCtx) => ReactNode };\n\n/**\n * One handle on a {@link Slider}. `bounds` narrows the range this particular\n * thumb may move within, either fixed or computed from the current thumb\n * list.\n */\nexport type Thumb = {\n value: number;\n label?: string;\n shape?: ThumbShape;\n bounds?: [number, number] | ((ctx: BoundsCtx) => [number, number]);\n /**\n * Spoken form of `value`, published as `aria-valuetext`. Required by ARIA\n * whenever `value` is not the quantity the user is choosing — an index into\n * a value list, a log-scaled position — since `aria-valuenow` alone then\n * announces a number that means nothing.\n */\n valueText?: string;\n};\n\n/**\n * Passed to a thumb's `bounds` function: the full thumb list and this thumb's\n * index in it, so a bound can be expressed relative to its neighbors.\n */\nexport type BoundsCtx = {\n thumbs: readonly Thumb[];\n index: number;\n};\n\n/**\n * Passed to `renderTrack`: the track's width in CSS px and a mapping from a\n * slider value to its 0..1 position along the track.\n */\nexport type TrackCtx = {\n trackWidth: number;\n valueToFraction: (v: number) => number;\n};\n\n/**\n * Props for {@link Slider}.\n *\n * `onInput` fires continuously through a drag; `onChange` fires once when it\n * ends and is the one to write to history.\n *\n * `stops` are attractors: a drag that passes within a few pixels of one lands\n * on it, and the arrow keys move stop to stop. `step` still quantizes the\n * values between them. Each one is drawn on the track as a mark; pass\n * `showStops: false` for a track whose own paint already reads as the stops.\n *\n * `trackClick: 'move-nearest'` makes a press on bare track send the closest\n * thumb there and continue as a drag. It is off by default because on a\n * multi-thumb editor a stray click would yank a stop the user was not aiming\n * at; `onAddThumb`, where it is set, keeps the track press.\n *\n * `constraint: 'ordered'` keeps thumbs from crossing each other. Supplying\n * `onAddThumb` makes a click on empty track create a thumb, and supplying\n * `onRemoveThumb` lets a right-click or a drag off the track remove one —\n * both callbacks can decline by returning `null`/`false`. `allowShiftAll`\n * makes shift-drag translate every thumb together.\n */\nexport type SliderProps<T extends Thumb = Thumb> = {\n thumbs: readonly T[];\n onInput: (next: T[]) => void;\n onChange?: (next: T[]) => void;\n min: number;\n max: number;\n step?: number;\n stops?: number[];\n showStops?: boolean;\n trackClick?: 'none' | 'move-nearest';\n constraint?: 'free' | 'ordered';\n onAddThumb?: (atValue: number) => T | null;\n onRemoveThumb?: (index: number) => boolean;\n allowShiftAll?: boolean;\n renderTrack?: (ctx: TrackCtx) => ReactNode;\n trackHeight?: number;\n /** `'slim'` drives the track and thumb from the kit's slider tokens, so a\n * Slider matches the property rows. `trackHeight` still wins if given. */\n density?: 'default' | 'slim';\n renderReadout?: (thumb: T, index: number) => ReactNode;\n readoutPlacement?: 'none' | 'inline-after' | 'below-thumb';\n ariaLabel?: string;\n className?: string;\n};\n\nfunction snap(v: number, step: number | undefined, min: number): number {\n if (step === undefined || step <= 0) return v;\n return Math.round((v - min) / step) * step + min;\n}\n\nfunction clamp(v: number, lo: number, hi: number): number {\n return Math.max(lo, Math.min(hi, v));\n}\n\n/** How close a drag has to come to a stop, in track pixels, to land on it. */\nconst STOP_SNAP_PX = 8;\n\n/** A custom `ThumbShape.render` may put interactive content inside the thumb,\n * and capture would retarget pointerup and kill the click on it. */\nconst NO_CAPTURE = { capture: false } as const;\n\n/** The stops that are actually reachable: inside the range, deduped, ascending. */\nfunction usableStops(stops: number[] | undefined, min: number, max: number): number[] {\n if (!stops || stops.length === 0) return [];\n return [...new Set(stops.filter(v => v >= min && v <= max))].sort((a, b) => a - b);\n}\n\n/** Pull `v` onto the nearest stop within `tolerance`, or leave it where it is. */\nfunction attract(v: number, stops: number[], tolerance: number): number {\n let best = v;\n let bestGap = tolerance;\n for (const stop of stops) {\n const gap = Math.abs(stop - v);\n if (gap <= bestGap) {\n best = stop;\n bestGap = gap;\n }\n }\n return best;\n}\n\n/** The stop `count` places past `v` in `direction`, saturating at either end. */\nfunction stepStops(stops: number[], v: number, direction: 1 | -1, count: number): number {\n if (direction > 0) {\n const first = stops.findIndex(stop => stop > v);\n if (first === -1) return stops[stops.length - 1];\n return stops[Math.min(stops.length - 1, first + count - 1)];\n }\n let first = -1;\n for (let i = stops.length - 1; i >= 0; i--) {\n if (stops[i] < v) {\n first = i;\n break;\n }\n }\n if (first === -1) return stops[0];\n return stops[Math.max(0, first - (count - 1))];\n}\n\nfunction defaultStep(step: number | undefined, min: number, max: number): number {\n if (step !== undefined && step > 0) return step;\n return (max - min) / 100;\n}\n\n/** Keep a thumb inside its neighbors when `constraint` is `'ordered'`. */\nfunction clampOrdered(\n v: number,\n thumbs: readonly Thumb[],\n index: number,\n min: number,\n max: number,\n step: number | undefined,\n): number {\n const gap = step !== undefined && step > 0 ? step : (max - min) / 1000;\n const lower = index > 0 ? thumbs[index - 1].value + gap : min;\n const upper = index < thumbs.length - 1 ? thumbs[index + 1].value - gap : max;\n return clamp(v, lower, upper);\n}\n\nfunction resolveBounds(thumb: Thumb, ctx: BoundsCtx, fallbackMin: number, fallbackMax: number): [number, number] {\n if (!thumb.bounds) return [fallbackMin, fallbackMax];\n const tuple = typeof thumb.bounds === 'function' ? thumb.bounds(ctx) : thumb.bounds;\n return [tuple[0], tuple[1]];\n}\n\nfunction defaultReadout(thumb: Thumb): string {\n return formatNumber(thumb.value, { minimumFractionDigits: 3, maximumFractionDigits: 3 });\n}\n\n/**\n * Multi-thumb slider over a shared track. The thumb list is fully controlled:\n * every change, live or committed, arrives as a whole new array.\n *\n * Thumbs are draggable, and arrow/Home/End move the focused thumb — those\n * keystrokes fire `onInput` and `onChange` together, since there is no\n * in-flight state to buffer.\n */\nexport function Slider<T extends Thumb = Thumb>(props: SliderProps<T>): ReactElement {\n const { thumbs, onInput, onChange, min, max, step, constraint, trackHeight, density, ariaLabel, className } = props;\n\n const stops = usableStops(props.stops, min, max);\n\n const trackRef = useRef<HTMLDivElement | null>(null);\n // In-flight thumb buffer during a drag; null when not dragging.\n const dragBufferRef = useRef<T[] | null>(null);\n const sessionRef = useRef<PointerSession | null>(null);\n\n useEffect(() => () => { sessionRef.current?.cancel(); }, []);\n\n const valueToFraction = useCallback(\n (v: number): number => (max === min ? 0 : clamp((v - min) / (max - min), 0, 1)),\n [min, max],\n );\n\n const fractionToValue = useCallback(\n (f: number): number => min + clamp(f, 0, 1) * (max - min),\n [min, max],\n );\n\n // `seed` replaces the thumb list the drag starts from. A track press moves a\n // thumb before the drag begins, and without seeding, a release with no\n // movement would commit the stale buffer and undo that move.\n const beginThumbDrag = useCallback(\n (origin: Element, down: ReactPointerEvent, index: number, seed?: readonly T[]) => {\n sessionRef.current?.cancel();\n const buf: T[] = (seed ?? thumbs).map(t => ({ ...t }));\n dragBufferRef.current = buf;\n let droppedOff = false;\n\n const onMove = (ev: PointerEvent) => {\n const track = trackRef.current;\n const buffer = dragBufferRef.current;\n if (!track || !buffer) return;\n const rect = track.getBoundingClientRect();\n const f = clamp((ev.clientX - rect.left) / rect.width, 0, 1);\n let v = fractionToValue(f);\n v = snap(v, step, min);\n v = clamp(v, min, max);\n if (stops.length > 0) v = attract(v, stops, (STOP_SNAP_PX / rect.width) * (max - min));\n\n const [bLo, bHi] = resolveBounds(buffer[index], { thumbs: buffer, index }, min, max);\n v = clamp(v, bLo, bHi);\n\n if (constraint === 'ordered') v = clampOrdered(v, buffer, index, min, max, step);\n\n // Drop-off detection: pointer exits the track vertically by more than trackHeight.\n const bandHeight = rect.height;\n if (props.onRemoveThumb) {\n if (ev.clientY < rect.top - bandHeight || ev.clientY > rect.bottom + bandHeight) {\n droppedOff = true;\n } else {\n droppedOff = false;\n }\n }\n\n buffer[index] = { ...buffer[index], value: v };\n onInput(buffer.map(t => ({ ...t })));\n };\n\n // A drag that ends without a release — cancelled pointer, lost capture,\n // an unmount — drops the buffer without committing.\n const onCancel = () => {\n sessionRef.current = null;\n dragBufferRef.current = null;\n };\n\n const onEnd = () => {\n sessionRef.current = null;\n const buffer = dragBufferRef.current;\n dragBufferRef.current = null;\n if (!buffer) return;\n\n if (droppedOff && props.onRemoveThumb) {\n const accepted = props.onRemoveThumb(index);\n if (accepted) {\n const next = buffer.filter((_, i) => i !== index).map(t => ({ ...t })) as T[];\n onInput(next);\n onChange?.(next);\n return;\n }\n }\n\n onChange?.(buffer.map(t => ({ ...t })));\n };\n\n sessionRef.current = openPointerSession(origin, down, { onMove, onEnd, onCancel }, NO_CAPTURE);\n },\n [thumbs, onInput, onChange, fractionToValue, min, max, step, stops, constraint, props],\n );\n\n const beginShiftAllDrag = useCallback(\n (origin: Element, down: ReactPointerEvent, anchorX: number) => {\n sessionRef.current?.cancel();\n const buf: T[] = thumbs.map(t => ({ ...t }));\n const startValues = buf.map(t => t.value);\n dragBufferRef.current = buf;\n\n const onMove = (ev: PointerEvent) => {\n const track = trackRef.current;\n const buffer = dragBufferRef.current;\n if (!track || !buffer) return;\n const rect = track.getBoundingClientRect();\n const dxFraction = (ev.clientX - anchorX) / rect.width;\n let dValue = dxFraction * (max - min);\n dValue = snap(dValue, step, 0);\n\n // Clamp delta so no thumb leaves [min, max] (per-thumb bounds intentionally\n // not enforced — matches the experiment's hue-band shift-translate semantics).\n let allowedNeg = -Infinity;\n let allowedPos = Infinity;\n for (let i = 0; i < startValues.length; i++) {\n allowedNeg = Math.max(allowedNeg, min - startValues[i]);\n allowedPos = Math.min(allowedPos, max - startValues[i]);\n }\n dValue = clamp(dValue, allowedNeg, allowedPos);\n\n for (let i = 0; i < buffer.length; i++) {\n buffer[i] = { ...buffer[i], value: clamp(startValues[i] + dValue, min, max) };\n }\n onInput(buffer.map(t => ({ ...t })));\n };\n\n const onCancel = () => {\n sessionRef.current = null;\n dragBufferRef.current = null;\n };\n\n const onEnd = () => {\n sessionRef.current = null;\n const buffer = dragBufferRef.current;\n dragBufferRef.current = null;\n if (buffer) onChange?.(buffer.map(t => ({ ...t })));\n };\n\n sessionRef.current = openPointerSession(origin, down, { onMove, onEnd, onCancel }, NO_CAPTURE);\n },\n [thumbs, onInput, onChange, min, max, step],\n );\n\n const onThumbPointerDown = (index: number) => (e: ReactPointerEvent) => {\n // Only bail on explicit non-primary buttons (button > 0). jsdom's PointerEvent\n // leaves `button` undefined; treat that as primary so tests can drive drags.\n if (typeof e.button === 'number' && e.button > 0) return;\n // preventDefault below suppresses the focus the press would otherwise\n // give the thumb, and the arrow keys are on the thumb.\n (e.currentTarget as HTMLElement).focus?.();\n e.preventDefault();\n e.stopPropagation();\n const origin = e.currentTarget as HTMLElement;\n if (e.shiftKey && props.allowShiftAll) {\n beginShiftAllDrag(origin, e, e.clientX);\n } else {\n beginThumbDrag(origin, e, index);\n }\n };\n\n const onThumbContextMenu = (index: number) => (e: ReactMouseEvent) => {\n if (!props.onRemoveThumb) return;\n e.preventDefault();\n const accepted = props.onRemoveThumb(index);\n if (!accepted) return;\n const next = thumbs.filter((_, i) => i !== index).map(t => ({ ...t })) as T[];\n onInput(next);\n onChange?.(next);\n };\n\n const onTrackPointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {\n if (typeof e.button === 'number' && e.button > 0) return;\n const wantsMove = (props.trackClick ?? 'none') === 'move-nearest' && thumbs.length > 0;\n if (!props.onAddThumb && !wantsMove) return;\n // If the event originated on a thumb, the thumb's own handler ran first; this is a track click.\n if ((e.target as HTMLElement).closest(`.${s.thumb}`)) return;\n e.preventDefault();\n const track = trackRef.current;\n if (!track) return;\n const rect = track.getBoundingClientRect();\n const f = clamp((e.clientX - rect.left) / rect.width, 0, 1);\n let v = fractionToValue(f);\n v = snap(v, step, min);\n v = clamp(v, min, max);\n if (stops.length > 0) v = attract(v, stops, (STOP_SNAP_PX / rect.width) * (max - min));\n\n if (props.onAddThumb) {\n const created = props.onAddThumb(v);\n if (!created) return;\n const next = [...thumbs.map(t => ({ ...t })), created] as T[];\n onInput(next);\n onChange?.(next);\n return;\n }\n\n let index = 0;\n for (let i = 1; i < thumbs.length; i++) {\n if (Math.abs(thumbs[i].value - v) < Math.abs(thumbs[index].value - v)) index = i;\n }\n const next = thumbs.map(t => ({ ...t })) as T[];\n const [bLo, bHi] = resolveBounds(next[index], { thumbs: next, index }, min, max);\n let moved = clamp(v, bLo, bHi);\n if (constraint === 'ordered') moved = clampOrdered(moved, next, index, min, max, step);\n next[index] = { ...next[index], value: moved };\n onInput(next);\n beginThumbDrag(track, e, index, next);\n };\n\n const onThumbKeyDown = (index: number) => (e: ReactKeyboardEvent) => {\n const stepSize = defaultStep(step, min, max);\n let delta = 0;\n // With stops, a keystroke moves by whole stops rather than by value.\n let stopDelta = 0;\n let snapTo: 'home' | 'end' | null = null;\n\n switch (e.key) {\n case 'ArrowRight':\n case 'ArrowUp':\n delta = e.shiftKey ? stepSize * 10 : stepSize;\n stopDelta = e.shiftKey ? 10 : 1;\n break;\n case 'ArrowLeft':\n case 'ArrowDown':\n delta = e.shiftKey ? -stepSize * 10 : -stepSize;\n stopDelta = e.shiftKey ? -10 : -1;\n break;\n case 'PageUp':\n delta = stepSize * 10;\n stopDelta = 10;\n break;\n case 'PageDown':\n delta = -stepSize * 10;\n stopDelta = -10;\n break;\n case 'Home':\n snapTo = 'home';\n break;\n case 'End':\n snapTo = 'end';\n break;\n default:\n return;\n }\n\n e.preventDefault();\n const next = thumbs.map(t => ({ ...t }));\n const [bLo, bHi] = resolveBounds(next[index], { thumbs: next, index }, min, max);\n const lo = Math.max(min, bLo);\n const hi = Math.min(max, bHi);\n let v: number;\n if (snapTo === 'home') v = lo;\n else if (snapTo === 'end') v = hi;\n else if (stops.length > 0) {\n v = stepStops(stops, next[index].value, stopDelta > 0 ? 1 : -1, Math.abs(stopDelta));\n } else {\n v = snap(next[index].value + delta, step, min);\n }\n v = clamp(v, lo, hi);\n if (constraint === 'ordered') v = clampOrdered(v, next, index, lo, hi, step);\n next[index] = { ...next[index], value: v };\n onInput(next);\n onChange?.(next);\n };\n\n const placement = props.readoutPlacement ?? 'none';\n const renderReadout = props.renderReadout;\n\n const slim = density === 'slim';\n const rootVars: Record<string, string> = {};\n if (slim) {\n rootVars['--rp-track-height'] = 'var(--wzl-slider-track-h)';\n rootVars['--rp-thumb-size'] = 'var(--wzl-slider-thumb-size)';\n }\n // An explicit trackHeight wins: a caller who named a number meant it.\n if (trackHeight !== undefined) rootVars['--rp-track-height'] = `${trackHeight}px`;\n const rootClass = [s.root, slim && s.slim, className].filter(Boolean).join(' ');\n\n return (\n <div\n className={rootClass}\n style={Object.keys(rootVars).length > 0 ? (rootVars as CSSProperties) : undefined}\n >\n <div className={s.row}>\n <div className={s.track} ref={trackRef} onPointerDown={onTrackPointerDown}>\n {props.renderTrack && (\n <div className={s.trackInner}>\n {props.renderTrack({\n trackWidth: trackRef.current?.getBoundingClientRect().width ?? 0,\n valueToFraction,\n })}\n </div>\n )}\n {(props.showStops ?? true) && stops.length > 0 && (\n <div className={s.ticks} data-slider-ticks aria-hidden=\"true\">\n {stops.map(v => {\n const f = valueToFraction(v);\n return (\n <span\n key={v}\n className={s.tick}\n data-slider-tick\n data-fraction={String(f)}\n style={{ left: `${f * 100}%` }}\n />\n );\n })}\n </div>\n )}\n {thumbs.map((thumb, i) => {\n const isNotched = thumb.shape === 'notched';\n const customRender = typeof thumb.shape === 'object' && thumb.shape !== null ? thumb.shape.render : null;\n const cls = `${s.thumb}${isNotched ? ` ${s.notched}` : ''}`;\n return (\n <div\n key={i}\n role=\"slider\"\n tabIndex={0}\n aria-orientation=\"horizontal\"\n aria-valuemin={min}\n aria-valuemax={max}\n aria-valuenow={thumb.value}\n aria-valuetext={thumb.valueText}\n aria-label={[ariaLabel, thumb.label].filter(Boolean).join(' ') || undefined}\n className={cls}\n style={{ left: `${valueToFraction(thumb.value) * 100}%` }}\n onPointerDown={onThumbPointerDown(i)}\n onKeyDown={onThumbKeyDown(i)}\n onContextMenu={onThumbContextMenu(i)}\n >\n {customRender ? customRender({ width: 14, height: 24, isActive: false }) : (thumb.label ?? '')}\n </div>\n );\n })}\n </div>\n {placement === 'inline-after' && (\n <span data-readout=\"inline\" className={s.readoutInline}>\n {thumbs.map((t, i) => (\n <span key={i}>{i > 0 ? ' / ' : ''}{renderReadout ? renderReadout(t, i) : defaultReadout(t)}</span>\n ))}\n </span>\n )}\n </div>\n {placement === 'below-thumb' && (\n <div className={s.readoutsBelow}>\n {thumbs.map((t, i) => (\n <span\n key={i}\n data-readout=\"below\"\n className={s.readoutBelow}\n style={{ left: `${valueToFraction(t.value) * 100}%` }}\n >\n {renderReadout ? renderReadout(t, i) : defaultReadout(t)}\n </span>\n ))}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1,8 +0,0 @@
1
- var e = {
2
- range: "_range_1uep0_9",
3
- alpha: "_alpha_1uep0_74"
4
- };
5
- //#endregion
6
- export { e as t };
7
-
8
- //# sourceMappingURL=range.module-CgURq-Ua.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"range.module-CgURq-Ua.js","names":[],"sources":["../../src/components/range.module.css"],"sourcesContent":["/* The one skin for a native <input type=\"range\">. Imported by InlineRange and\n by the property rows, which render the same DOM and used to carry three\n independently authored copies of these rules.\n\n The track is painted here rather than left to `accent-color`: a UA fills the\n left of the track with the accent but draws the remainder near-white on a\n dark surface whatever `color-scheme` says, and overriding the track's\n background removes the accent fill instead of recolouring the remainder. */\n.range {\n appearance: none;\n -webkit-appearance: none;\n width: 100%;\n min-inline-size: 48px;\n /* The box is taller than the track so the thumb has room to spill; a surface\n where the element itself IS the track overrides this. */\n height: var(--wzl-range-box-h, 12px);\n margin: 0;\n padding: 0;\n /* Transparent so the track pseudo-element shows; a surface that paints the element itself overrides it. */\n background: var(--wzl-range-bg, transparent);\n cursor: pointer;\n}\n\n.range::-webkit-slider-runnable-track {\n height: var(--wzl-slider-track-h);\n border-radius: var(--wzl-radius-sm);\n background: var(\n --wzl-range-track-bg,\n color-mix(in srgb, var(--wzl-accent) var(--wzl-slider-track-tint), transparent)\n );\n}\n\n.range::-moz-range-track {\n height: var(--wzl-slider-track-h);\n border-radius: var(--wzl-radius-sm);\n background: var(\n --wzl-range-track-bg,\n color-mix(in srgb, var(--wzl-accent) var(--wzl-slider-track-tint), transparent)\n );\n}\n\n.range::-webkit-slider-thumb {\n appearance: none;\n -webkit-appearance: none;\n width: var(--wzl-slider-thumb-size);\n height: var(--wzl-slider-thumb-size);\n border: none;\n border-radius: 50%;\n background: color-mix(in srgb, var(--wzl-accent) var(--wzl-slider-thumb-tint), transparent);\n /* Centres an 8px thumb on a 4px track. Webkit does not centre it for you. */\n margin-top: calc((var(--wzl-slider-track-h) - var(--wzl-slider-thumb-size)) / 2);\n}\n\n.range::-moz-range-thumb {\n width: var(--wzl-slider-thumb-size);\n height: var(--wzl-slider-thumb-size);\n border: none;\n border-radius: 50%;\n background: color-mix(in srgb, var(--wzl-accent) var(--wzl-slider-thumb-tint), transparent);\n}\n\n.range:focus-visible {\n outline: 2px solid var(--wzl-focus-ring);\n outline-offset: 2px;\n}\n\n.range:disabled {\n cursor: not-allowed;\n opacity: 0.55;\n}\n\n/* Alpha variant: a fainter track, used where the row already carries a colour\n swatch and the slider must not compete with it. */\n.alpha::-webkit-slider-runnable-track {\n background: color-mix(in srgb, var(--wzl-accent) 10%, transparent);\n}\n\n.alpha::-moz-range-track {\n background: color-mix(in srgb, var(--wzl-accent) 10%, transparent);\n}\n\n/* A dead alpha track keeps its groove but loses its thumb, so the affordance\n reads inert rather than merely dimmed. Keyed off `:disabled`, which ColorRow\n already sets on the input — not off a class on the row. */\n.alpha:disabled::-webkit-slider-runnable-track,\n.alpha:disabled::-moz-range-track {\n background: color-mix(in srgb, var(--wzl-fg) 5%, transparent);\n}\n\n.alpha:disabled::-webkit-slider-thumb,\n.alpha:disabled::-moz-range-thumb {\n background: transparent;\n}\n"],"mappings":""}