@weasel-js/ui 1.0.2 → 1.0.3

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 (188) hide show
  1. package/dist/chunks/ActionBar-GThu74LC.js.map +1 -1
  2. package/dist/chunks/ActionsBar-9aR5N3lU.js.map +1 -1
  3. package/dist/chunks/Badge-DZQqAV4d.js.map +1 -1
  4. package/dist/chunks/BandEditor-13K91O9M.js +228 -0
  5. package/dist/chunks/BandEditor-13K91O9M.js.map +1 -0
  6. package/dist/chunks/Button-9QNIYugK.js.map +1 -1
  7. package/dist/chunks/Callout-CFqG5xho.js.map +1 -1
  8. package/dist/chunks/Checkbox-KQ5L8APP.js.map +1 -1
  9. package/dist/chunks/ColorField-ykpiD3XU.js.map +1 -1
  10. package/dist/chunks/ComboBox-DpxsFVBy.js.map +1 -1
  11. package/dist/chunks/{CurveEditor-fRctbmoV.js → CurveEditor-CqIt_Xph.js} +3 -3
  12. package/dist/chunks/CurveEditor-CqIt_Xph.js.map +1 -0
  13. package/dist/chunks/DataGrid-BQklXGRl.js.map +1 -1
  14. package/dist/chunks/Dialog-3FCd_gYi.js.map +1 -1
  15. package/dist/chunks/Field-VQwmrIna.js.map +1 -1
  16. package/dist/chunks/{GradientEditor-BL5kg7a_.js → GradientEditor-DeptQpWX.js} +4 -4
  17. package/dist/chunks/GradientEditor-DeptQpWX.js.map +1 -0
  18. package/dist/chunks/Input-DpkwYtUY.js.map +1 -1
  19. package/dist/chunks/Keycaps-CFX7UAj8.js.map +1 -1
  20. package/dist/chunks/NumberField-BbzI0GtU.js.map +1 -1
  21. package/dist/chunks/OptionsBar-CHvH4n4u.js.map +1 -1
  22. package/dist/chunks/Plot2D-CEnhzodm.js.map +1 -1
  23. package/dist/chunks/{PointPlotter-R8hguJPq.js → PointPlotter-DD-bWmRc.js} +2 -2
  24. package/dist/chunks/PointPlotter-DD-bWmRc.js.map +1 -0
  25. package/dist/chunks/Powerline-Dd9Ukhcl.js.map +1 -1
  26. package/dist/chunks/Prefs-lgI4x4jF.js.map +1 -1
  27. package/dist/chunks/RadioGroup-CIpHr3g6.js.map +1 -1
  28. package/dist/chunks/RangeSlider-CaDogGTj.js.map +1 -1
  29. package/dist/chunks/{ResizeHandle-CCQ2y87y.js → ResizeHandle-BOpmtOOj.js} +2 -2
  30. package/dist/chunks/ResizeHandle-BOpmtOOj.js.map +1 -0
  31. package/dist/chunks/Select-BE3Y1NrH.js.map +1 -1
  32. package/dist/chunks/SelectionPanel-Zaw8tYQH.js.map +1 -1
  33. package/dist/chunks/Sidebar-DP_nyJze.js.map +1 -1
  34. package/dist/chunks/SidebarPanel-BpD0ptyg.js.map +1 -1
  35. package/dist/chunks/{Slider-Di2kNqVy.js → Slider-CmPeFJ3x.js} +4 -4
  36. package/dist/chunks/Slider-CmPeFJ3x.js.map +1 -0
  37. package/dist/chunks/StatusBar-DBO0E0Yd.js.map +1 -1
  38. package/dist/chunks/Switch-JyVSSAzC.js.map +1 -1
  39. package/dist/chunks/Tabs-Df0sDomx.js.map +1 -1
  40. package/dist/chunks/Toast-DFw4IrD_.js.map +1 -1
  41. package/dist/chunks/ToggleBar-BP1zFcvg.js.map +1 -1
  42. package/dist/chunks/ToolButton-DpT-mukU.js.map +1 -1
  43. package/dist/chunks/ToolGroup-BTIwd4U4.js.map +1 -1
  44. package/dist/chunks/ToolOptionsBar-JvmY7iNe.js.map +1 -1
  45. package/dist/chunks/ToolPalette-DPn0owno.js.map +1 -1
  46. package/dist/chunks/Tooltip-Clgn4trm.js.map +1 -1
  47. package/dist/chunks/useRovingTabIndex-DBZtq4IH.js.map +1 -1
  48. package/dist/color/oklch.d.ts +12 -0
  49. package/dist/color/oklch.d.ts.map +1 -1
  50. package/dist/components/ActionBar/ActionBar.d.ts +1 -0
  51. package/dist/components/ActionBar/ActionBar.d.ts.map +1 -1
  52. package/dist/components/ActionsBar/ActionsBar.d.ts +15 -0
  53. package/dist/components/ActionsBar/ActionsBar.d.ts.map +1 -1
  54. package/dist/components/Badge/Badge.d.ts +14 -0
  55. package/dist/components/Badge/Badge.d.ts.map +1 -1
  56. package/dist/components/Badge/bases/edgeProfiles.d.ts +8 -0
  57. package/dist/components/Badge/bases/edgeProfiles.d.ts.map +1 -1
  58. package/dist/components/Badge/types.d.ts +11 -0
  59. package/dist/components/Badge/types.d.ts.map +1 -1
  60. package/dist/components/BandEditor/BandEditor.d.ts +56 -0
  61. package/dist/components/BandEditor/BandEditor.d.ts.map +1 -0
  62. package/dist/components/BandEditor/bands.d.ts +45 -0
  63. package/dist/components/BandEditor/bands.d.ts.map +1 -0
  64. package/dist/components/BandEditor/index.d.ts +4 -0
  65. package/dist/components/BandEditor/index.d.ts.map +1 -0
  66. package/dist/components/BandEditor/index.js +2 -0
  67. package/dist/components/BandEditor/scale.d.ts +25 -0
  68. package/dist/components/BandEditor/scale.d.ts.map +1 -0
  69. package/dist/components/Button/Button.d.ts +13 -0
  70. package/dist/components/Button/Button.d.ts.map +1 -1
  71. package/dist/components/Callout/Callout.d.ts +2 -0
  72. package/dist/components/Callout/Callout.d.ts.map +1 -1
  73. package/dist/components/Checkbox/Checkbox.d.ts +4 -0
  74. package/dist/components/Checkbox/Checkbox.d.ts.map +1 -1
  75. package/dist/components/ColorField/ColorField.d.ts +4 -0
  76. package/dist/components/ColorField/ColorField.d.ts.map +1 -1
  77. package/dist/components/ComboBox/ComboBox.d.ts +11 -0
  78. package/dist/components/ComboBox/ComboBox.d.ts.map +1 -1
  79. package/dist/components/CurveEditor/CurveEditor.d.ts +14 -2
  80. package/dist/components/CurveEditor/CurveEditor.d.ts.map +1 -1
  81. package/dist/components/CurveEditor/LayeredCurveEditor.d.ts +15 -0
  82. package/dist/components/CurveEditor/LayeredCurveEditor.d.ts.map +1 -1
  83. package/dist/components/CurveEditor/createFunctionLayer.d.ts +39 -0
  84. package/dist/components/CurveEditor/createFunctionLayer.d.ts.map +1 -1
  85. package/dist/components/CurveEditor/index.js +1 -1
  86. package/dist/components/CurveEditor/layerTypes.d.ts +5 -0
  87. package/dist/components/CurveEditor/layerTypes.d.ts.map +1 -1
  88. package/dist/components/CurveEditor/setCurveOp.d.ts +4 -0
  89. package/dist/components/CurveEditor/setCurveOp.d.ts.map +1 -1
  90. package/dist/components/DataGrid/DataGrid.d.ts +12 -0
  91. package/dist/components/DataGrid/DataGrid.d.ts.map +1 -1
  92. package/dist/components/Dialog/Dialog.d.ts +1 -0
  93. package/dist/components/Dialog/Dialog.d.ts.map +1 -1
  94. package/dist/components/Field/Field.d.ts +9 -0
  95. package/dist/components/Field/Field.d.ts.map +1 -1
  96. package/dist/components/GradientEditor/GradientEditor.d.ts +4 -0
  97. package/dist/components/GradientEditor/GradientEditor.d.ts.map +1 -1
  98. package/dist/components/GradientEditor/GradientHandles.d.ts +4 -0
  99. package/dist/components/GradientEditor/GradientHandles.d.ts.map +1 -1
  100. package/dist/components/GradientEditor/index.js +1 -1
  101. package/dist/components/Input/Input.d.ts +1 -0
  102. package/dist/components/Input/Input.d.ts.map +1 -1
  103. package/dist/components/Keycaps/Keycap.d.ts +10 -0
  104. package/dist/components/Keycaps/Keycap.d.ts.map +1 -1
  105. package/dist/components/Keycaps/Keycaps.d.ts +2 -0
  106. package/dist/components/Keycaps/Keycaps.d.ts.map +1 -1
  107. package/dist/components/Keycaps/keySpecsFromMods.d.ts +2 -0
  108. package/dist/components/Keycaps/keySpecsFromMods.d.ts.map +1 -1
  109. package/dist/components/NumberField/NumberField.d.ts +9 -0
  110. package/dist/components/NumberField/NumberField.d.ts.map +1 -1
  111. package/dist/components/OptionsBar/OptionsBar.d.ts +12 -0
  112. package/dist/components/OptionsBar/OptionsBar.d.ts.map +1 -1
  113. package/dist/components/Plot2D/Plot2D.d.ts +14 -0
  114. package/dist/components/Plot2D/Plot2D.d.ts.map +1 -1
  115. package/dist/components/Plot2D/geometry.d.ts +10 -0
  116. package/dist/components/Plot2D/geometry.d.ts.map +1 -1
  117. package/dist/components/PointPlotter/PointPlotter.d.ts +3 -2
  118. package/dist/components/PointPlotter/PointPlotter.d.ts.map +1 -1
  119. package/dist/components/PointPlotter/index.js +1 -1
  120. package/dist/components/Powerline/Powerline.d.ts +13 -0
  121. package/dist/components/Powerline/Powerline.d.ts.map +1 -1
  122. package/dist/components/Prefs/PrefsDialog.d.ts +4 -0
  123. package/dist/components/Prefs/PrefsDialog.d.ts.map +1 -1
  124. package/dist/components/Prefs/PrefsForm.d.ts +6 -0
  125. package/dist/components/Prefs/PrefsForm.d.ts.map +1 -1
  126. package/dist/components/Prefs/schema.d.ts +17 -0
  127. package/dist/components/Prefs/schema.d.ts.map +1 -1
  128. package/dist/components/RadioGroup/RadioGroup.d.ts +8 -0
  129. package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -1
  130. package/dist/components/RangeSlider/RangeSlider.d.ts +1 -0
  131. package/dist/components/RangeSlider/RangeSlider.d.ts.map +1 -1
  132. package/dist/components/ResizeHandle/ResizeHandle.d.ts +7 -3
  133. package/dist/components/ResizeHandle/ResizeHandle.d.ts.map +1 -1
  134. package/dist/components/ResizeHandle/index.js +1 -1
  135. package/dist/components/Select/Select.d.ts +11 -0
  136. package/dist/components/Select/Select.d.ts.map +1 -1
  137. package/dist/components/SelectionPanel/SelectionPanel.d.ts +6 -0
  138. package/dist/components/SelectionPanel/SelectionPanel.d.ts.map +1 -1
  139. package/dist/components/SelectionPanel/model.d.ts +12 -0
  140. package/dist/components/SelectionPanel/model.d.ts.map +1 -1
  141. package/dist/components/Sidebar/Sidebar.d.ts +1 -0
  142. package/dist/components/Sidebar/Sidebar.d.ts.map +1 -1
  143. package/dist/components/SidebarPanel/SidebarPanel.d.ts +1 -0
  144. package/dist/components/SidebarPanel/SidebarPanel.d.ts.map +1 -1
  145. package/dist/components/Slider/Slider.d.ts +43 -2
  146. package/dist/components/Slider/Slider.d.ts.map +1 -1
  147. package/dist/components/Slider/index.js +1 -1
  148. package/dist/components/StatusBar/StatusBar.d.ts +2 -0
  149. package/dist/components/StatusBar/StatusBar.d.ts.map +1 -1
  150. package/dist/components/Switch/Switch.d.ts +11 -0
  151. package/dist/components/Switch/Switch.d.ts.map +1 -1
  152. package/dist/components/Tabs/Tabs.d.ts +11 -0
  153. package/dist/components/Tabs/Tabs.d.ts.map +1 -1
  154. package/dist/components/Toast/Toast.d.ts +2 -0
  155. package/dist/components/Toast/Toast.d.ts.map +1 -1
  156. package/dist/components/Toast/queue.d.ts +7 -0
  157. package/dist/components/Toast/queue.d.ts.map +1 -1
  158. package/dist/components/ToggleBar/ToggleBar.d.ts +16 -0
  159. package/dist/components/ToggleBar/ToggleBar.d.ts.map +1 -1
  160. package/dist/components/ToolButton/ToolButton.d.ts +1 -0
  161. package/dist/components/ToolButton/ToolButton.d.ts.map +1 -1
  162. package/dist/components/ToolGroup/ToolGroup.d.ts +1 -0
  163. package/dist/components/ToolGroup/ToolGroup.d.ts.map +1 -1
  164. package/dist/components/ToolOptionsBar/ToolOptionsBar.d.ts +1 -0
  165. package/dist/components/ToolOptionsBar/ToolOptionsBar.d.ts.map +1 -1
  166. package/dist/components/ToolPalette/ToolPalette.d.ts +9 -0
  167. package/dist/components/ToolPalette/ToolPalette.d.ts.map +1 -1
  168. package/dist/components/Tooltip/Tooltip.d.ts +5 -0
  169. package/dist/components/Tooltip/Tooltip.d.ts.map +1 -1
  170. package/dist/format/number.d.ts +4 -0
  171. package/dist/format/number.d.ts.map +1 -1
  172. package/dist/index.d.ts +1 -0
  173. package/dist/index.d.ts.map +1 -1
  174. package/dist/index.js +12 -11
  175. package/dist/index.js.map +1 -1
  176. package/dist/paintGradientTrack.d.ts +13 -0
  177. package/dist/paintGradientTrack.d.ts.map +1 -1
  178. package/dist/style.css +1 -1
  179. package/dist/useReorderDragList.d.ts +23 -0
  180. package/dist/useReorderDragList.d.ts.map +1 -1
  181. package/dist/useRovingTabIndex.d.ts +5 -0
  182. package/dist/useRovingTabIndex.d.ts.map +1 -1
  183. package/package.json +2 -2
  184. package/dist/chunks/CurveEditor-fRctbmoV.js.map +0 -1
  185. package/dist/chunks/GradientEditor-BL5kg7a_.js.map +0 -1
  186. package/dist/chunks/PointPlotter-R8hguJPq.js.map +0 -1
  187. package/dist/chunks/ResizeHandle-CCQ2y87y.js.map +0 -1
  188. package/dist/chunks/Slider-Di2kNqVy.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CurveEditor-CqIt_Xph.js","names":[],"sources":["../../src/components/CurveEditor/catmullRom.ts","../../src/components/CurveEditor/monotone.ts","../../src/components/CurveEditor/interpolation.ts","../../src/components/CurveEditor/hitTest.ts","../../src/components/CurveEditor/CurveEditor.module.css","../../src/components/CurveEditor/createFunctionLayer.tsx","../../src/components/CurveEditor/LayeredCurveEditor.tsx","../../src/components/CurveEditor/CurveEditor.tsx","../../src/components/CurveEditor/setCurveOp.ts"],"sourcesContent":["/**\n * Catmull-Rom spline sampling, parameterized by α.\n *\n * α = 0 → uniform parameterization (original Catmull-Rom). Fast, can\n * produce cusps and self-intersections at sharp 2D angles.\n * α = 0.5 → centripetal (Yuksel/Schneider 2011). No cusps, no\n * self-intersection. The \"safe default\" for 2D.\n * α = 1 → chordal. Smoother than uniform, different artifacts.\n *\n * `sampleCurve(...)` is the convenience alias for centripetal (α=0.5).\n */\n\nexport interface Point {\n x: number;\n y: number;\n}\n\nfunction knot(a: Point, b: Point, alpha: number): number {\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n return Math.pow(Math.sqrt(dx * dx + dy * dy), alpha);\n}\n\n/**\n * Sample the Catmull-Rom segment between p1 and p2 at parameter\n * t ∈ [0, 1]. p0 and p3 are the surrounding control points (use phantom\n * reflection at endpoints — see sampleCatmullRom below).\n *\n * Returns p1 at t=0 and p2 at t=1. `alpha` controls parameterization.\n */\nexport function segmentSampleAt(\n p0: Point, p1: Point, p2: Point, p3: Point, t: number,\n alpha: number = 0.5,\n): Point {\n const t0 = 0;\n const t1 = t0 + knot(p0, p1, alpha);\n const t2 = t1 + knot(p1, p2, alpha);\n const t3 = t2 + knot(p2, p3, alpha);\n\n // Degenerate: coincident p1/p2 → linear interpolation fallback.\n if (t2 - t1 === 0) {\n return { x: p1.x + (p2.x - p1.x) * t, y: p1.y + (p2.y - p1.y) * t };\n }\n\n const u = t1 + t * (t2 - t1);\n\n const a1x = ((t1 - u) * p0.x + (u - t0) * p1.x) / (t1 - t0);\n const a1y = ((t1 - u) * p0.y + (u - t0) * p1.y) / (t1 - t0);\n const a2x = ((t2 - u) * p1.x + (u - t1) * p2.x) / (t2 - t1);\n const a2y = ((t2 - u) * p1.y + (u - t1) * p2.y) / (t2 - t1);\n const a3x = ((t3 - u) * p2.x + (u - t2) * p3.x) / (t3 - t2);\n const a3y = ((t3 - u) * p2.y + (u - t2) * p3.y) / (t3 - t2);\n\n const b1x = ((t2 - u) * a1x + (u - t0) * a2x) / (t2 - t0);\n const b1y = ((t2 - u) * a1y + (u - t0) * a2y) / (t2 - t0);\n const b2x = ((t3 - u) * a2x + (u - t1) * a3x) / (t3 - t1);\n const b2y = ((t3 - u) * a2y + (u - t1) * a3y) / (t3 - t1);\n\n const cx = ((t2 - u) * b1x + (u - t1) * b2x) / (t2 - t1);\n const cy = ((t2 - u) * b1y + (u - t1) * b2y) / (t2 - t1);\n return { x: cx, y: cy };\n}\n\n/**\n * Sample the whole curve through `anchors` using Catmull-Rom with the\n * given α. Returns `(n-1) * samplesPerSegment + 1` points. Phantom\n * endpoints by reflection so the first/last segments are tangent to\n * the chord into/out of the endpoints. Returns `[]` for fewer than 2\n * anchors.\n */\nexport function sampleCatmullRom(\n anchors: readonly Point[],\n samplesPerSegment: number,\n alpha: number = 0.5,\n): Point[] {\n if (anchors.length < 2) return [];\n if (samplesPerSegment < 1) samplesPerSegment = 1;\n\n const n = anchors.length;\n const out: Point[] = [];\n\n const reflectStart: Point = {\n x: 2 * anchors[0].x - anchors[1].x,\n y: 2 * anchors[0].y - anchors[1].y,\n };\n const reflectEnd: Point = {\n x: 2 * anchors[n - 1].x - anchors[n - 2].x,\n y: 2 * anchors[n - 1].y - anchors[n - 2].y,\n };\n\n for (let i = 0; i < n - 1; i++) {\n const p0 = i === 0 ? reflectStart : anchors[i - 1];\n const p1 = anchors[i];\n const p2 = anchors[i + 1];\n const p3 = i + 2 < n ? anchors[i + 2] : reflectEnd;\n const tStart = i === 0 ? 0 : 1;\n for (let s = tStart; s <= samplesPerSegment; s++) {\n if (s === 0) {\n out.push(p1);\n continue;\n }\n if (s === samplesPerSegment) {\n out.push(p2);\n continue;\n }\n const t = s / samplesPerSegment;\n out.push(segmentSampleAt(p0, p1, p2, p3, t, alpha));\n }\n }\n return out;\n}\n\n/** Convenience alias for centripetal Catmull-Rom (α=0.5). The most\n * common choice; safe default for 2D paths. */\nexport function sampleCurve(\n anchors: readonly Point[],\n samplesPerSegment: number,\n): Point[] {\n return sampleCatmullRom(anchors, samplesPerSegment, 0.5);\n}\n","/**\n * Monotone cubic interpolation (Fritsch-Carlson / Steffen variant).\n *\n * Passes through every anchor and guarantees the curve never\n * overshoots `min(neighbors)` / `max(neighbors)` along either axis.\n * The right choice for animation timing curves (no anticipation) or\n * audio envelopes (no out-of-range values).\n *\n * Parameterized by index — works for both 1D (function-shaped) and\n * 2D (parametric) anchor sequences. The x and y coordinates are\n * interpolated independently as functions of the anchor index.\n *\n * Reference: Fritsch & Carlson 1980, \"Monotone piecewise cubic\n * interpolation.\" Steffen's 1990 refinement is conceptually the same;\n * we use a min-clamp variant that's monotonicity-preserving by\n * construction.\n */\n\nimport type { Point } from './catmullRom';\n\n/** Per-axis tangent computation for a 1D series of y-values sampled at\n * unit intervals (index-parameterized). Returns one tangent per\n * anchor. */\nfunction tangents(ys: readonly number[]): number[] {\n const n = ys.length;\n if (n === 0) return [];\n if (n === 1) return [0];\n\n // Secant slopes (dx = 1 in index parameterization).\n const m: number[] = new Array(n - 1);\n for (let i = 0; i < n - 1; i++) m[i] = ys[i + 1] - ys[i];\n\n const t: number[] = new Array(n);\n t[0] = m[0];\n t[n - 1] = m[n - 2];\n\n for (let i = 1; i < n - 1; i++) {\n if (m[i - 1] * m[i] <= 0) {\n // Sign change or flat → local extremum; tangent is zero (forces\n // the curve to settle without overshoot).\n t[i] = 0;\n } else {\n // Weighted average of adjacent secants (with dx_i = dx_{i-1} = 1,\n // this is just the arithmetic mean). Clamped by Steffen's rule\n // so the cubic stays monotone within each segment.\n const avg = (m[i - 1] + m[i]) / 2;\n const mag = Math.min(Math.abs(m[i - 1]), Math.abs(m[i]), Math.abs(avg));\n t[i] = Math.sign(avg) * mag;\n }\n }\n return t;\n}\n\n/** Cubic Hermite basis at parameter t ∈ [0, 1] given endpoint values\n * p0, p1 and endpoint tangents m0, m1. */\nfunction hermite(\n p0: number, p1: number, m0: number, m1: number, t: number,\n): number {\n const t2 = t * t;\n const t3 = t2 * t;\n return (\n (2 * t3 - 3 * t2 + 1) * p0 +\n (t3 - 2 * t2 + t) * m0 +\n (-2 * t3 + 3 * t2) * p1 +\n (t3 - t2) * m1\n );\n}\n\n/**\n * Sample the monotone-cubic curve through `anchors`. Returns\n * `(n-1) * samplesPerSegment + 1` points (same shape as\n * `sampleCatmullRom`). `[]` for fewer than 2 anchors.\n */\nexport function sampleMonotone(\n anchors: readonly Point[],\n samplesPerSegment: number,\n): Point[] {\n if (anchors.length < 2) return [];\n if (samplesPerSegment < 1) samplesPerSegment = 1;\n\n const n = anchors.length;\n const xs = anchors.map((a) => a.x);\n const ys = anchors.map((a) => a.y);\n const tx = tangents(xs);\n const ty = tangents(ys);\n\n const out: Point[] = [];\n for (let i = 0; i < n - 1; i++) {\n const startStep = i === 0 ? 0 : 1; // skip duplicate boundary\n for (let s = startStep; s <= samplesPerSegment; s++) {\n if (s === 0) { out.push(anchors[i]); continue; }\n if (s === samplesPerSegment) { out.push(anchors[i + 1]); continue; }\n const t = s / samplesPerSegment;\n out.push({\n x: hermite(xs[i], xs[i + 1], tx[i], tx[i + 1], t),\n y: hermite(ys[i], ys[i + 1], ty[i], ty[i + 1], t),\n });\n }\n }\n return out;\n}\n","/**\n * Interpolation mode dispatcher for CurveEditor. The vocabulary of\n * supported algorithms and the unified `sample(...)` entry point that\n * picks the right one. All algorithms produce\n * `(n-1) * samplesPerSegment + 1` points and pass through every\n * anchor — CurveEditor is strictly interpolating, never approximating.\n */\n\nimport { sampleCatmullRom, type Point } from './catmullRom';\nimport { sampleMonotone } from './monotone';\n\n/** Supported interpolation modes. All are interpolating (pass through\n * every anchor); the differences are in tangent computation and\n * parameterization. */\nexport type InterpolationMode =\n | 'linear'\n | 'catmull-rom' // centripetal (α=0.5) — safe default\n | 'catmull-rom-uniform' // α=0; can cusp on sharp 2D angles\n | 'catmull-rom-chordal' // α=1; smoother than uniform\n | 'monotone'; // Fritsch-Carlson / Steffen; no overshoot\n\n/** Sample anchors as a polyline. Produces the same sample count shape\n * as the Catmull-Rom variants so downstream segment-slicing logic\n * (hit testing, fill path construction) works uniformly. */\nfunction sampleLinear(\n anchors: readonly Point[],\n samplesPerSegment: number,\n): Point[] {\n if (anchors.length < 2) return [];\n if (samplesPerSegment < 1) samplesPerSegment = 1;\n\n const out: Point[] = [];\n for (let i = 0; i < anchors.length - 1; i++) {\n const a = anchors[i];\n const b = anchors[i + 1];\n const startStep = i === 0 ? 0 : 1;\n for (let s = startStep; s <= samplesPerSegment; s++) {\n if (s === 0) { out.push(a); continue; }\n if (s === samplesPerSegment) { out.push(b); continue; }\n const t = s / samplesPerSegment;\n out.push({ x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t });\n }\n }\n return out;\n}\n\n/** Sample `anchors` using the chosen interpolation mode. */\nexport function sampleByInterpolation(\n anchors: readonly Point[],\n samplesPerSegment: number,\n mode: InterpolationMode,\n): Point[] {\n switch (mode) {\n case 'linear':\n return sampleLinear(anchors, samplesPerSegment);\n case 'catmull-rom-uniform':\n return sampleCatmullRom(anchors, samplesPerSegment, 0);\n case 'catmull-rom-chordal':\n return sampleCatmullRom(anchors, samplesPerSegment, 1);\n case 'monotone':\n return sampleMonotone(anchors, samplesPerSegment);\n case 'catmull-rom':\n default:\n return sampleCatmullRom(anchors, samplesPerSegment, 0.5);\n }\n}\n","/**\n * Curve-specific hit tests. Generic coord transforms live in\n * `../Plot2D/geometry`; what's here is curve-only (anchor / polyline\n * proximity).\n */\nimport type { Point } from '../Plot2D/geometry';\n\n/**\n * Nearest-anchor hit test. Anchors are already in plot/screen coords.\n * Returns the index of the nearest anchor whose squared distance to\n * the pointer is ≤ radius². Null if no anchor is within radius.\n */\nexport function hitTestAnchor(\n anchors: readonly Point[],\n pointer: Point,\n radius: number,\n): { index: number } | null {\n const r2 = radius * radius;\n let best: { index: number; d2: number } | null = null;\n for (let i = 0; i < anchors.length; i++) {\n const a = anchors[i];\n const dx = a.x - pointer.x;\n const dy = a.y - pointer.y;\n const d2 = dx * dx + dy * dy;\n if (d2 > r2) continue;\n if (best === null || d2 < best.d2) best = { index: i, d2 };\n }\n return best ? { index: best.index } : null;\n}\n\n/**\n * Hit test against a per-segment sampled polyline. Each entry in\n * `segments` is the sample list for one segment (inclusive of\n * endpoints). Returns segment index + t (∈ [0, 1]) of the closest\n * point on the polyline within radius. Null if nothing is in range.\n *\n * Distance is measured to the line-segment chords between adjacent\n * samples, not just to the samples themselves — so sparse sampling\n * still produces accurate hits along the chord.\n */\nexport function hitTestCurve(\n segments: readonly (readonly Point[])[],\n pointer: Point,\n radius: number,\n): { segIdx: number; t: number } | null {\n const r2 = radius * radius;\n let best: { segIdx: number; t: number; d2: number } | null = null;\n for (let segIdx = 0; segIdx < segments.length; segIdx++) {\n const samples = segments[segIdx];\n if (samples.length < 2) continue;\n const last = samples.length - 1;\n for (let i = 0; i < last; i++) {\n const a = samples[i];\n const b = samples[i + 1];\n const abx = b.x - a.x;\n const aby = b.y - a.y;\n const apx = pointer.x - a.x;\n const apy = pointer.y - a.y;\n const ab2 = abx * abx + aby * aby;\n let u = ab2 > 0 ? (apx * abx + apy * aby) / ab2 : 0;\n if (u < 0) u = 0;\n else if (u > 1) u = 1;\n const cx = a.x + u * abx;\n const cy = a.y + u * aby;\n const dx = cx - pointer.x;\n const dy = cy - pointer.y;\n const d2 = dx * dx + dy * dy;\n if (d2 > r2) continue;\n const t = (i + u) / last;\n if (best === null || d2 < best.d2) best = { segIdx, t, d2 };\n }\n }\n return best ? { segIdx: best.segIdx, t: best.t } : null;\n}\n",".root {\n /* Component-local vars layered over the theme tokens. Each var resolves\n * to a semantic token from @weasel-js/theme; consumers override\n * either the curve-* var (component scope) or the wzl-* token (app\n * scope) without touching this file.\n *\n * Background / grid / axis tokens live on Plot2D now (this component\n * composes Plot2D, which paints the background and chrome). */\n --curve-line: var(--wzl-curve-color);\n --curve-line-width: var(--wzl-curve-width);\n --curve-fill: color-mix(in srgb, var(--curve-line) 30%, transparent);\n --curve-anchor-fill: var(--wzl-surface);\n --curve-anchor-stroke: var(--curve-line);\n --curve-anchor-radius: 4;\n --curve-anchor-active-fill: var(--curve-line);\n --curve-pinned-fill: var(--wzl-fg-subtle);\n}\n\n.fill {\n fill: var(--curve-fill);\n stroke: none;\n pointer-events: none;\n}\n\n.curve {\n stroke: var(--curve-line);\n stroke-width: var(--curve-line-width);\n fill: none;\n pointer-events: stroke;\n}\n\n.anchor {\n fill: var(--curve-anchor-fill);\n stroke: var(--curve-anchor-stroke);\n stroke-width: 1.5;\n cursor: grab;\n}\n\n.anchor:hover {\n fill: var(--curve-anchor-active-fill);\n}\n\n.anchor.active {\n fill: var(--curve-anchor-active-fill);\n cursor: grabbing;\n}\n\n.anchor.pinned {\n fill: var(--curve-pinned-fill);\n cursor: default;\n}\n\n/* Caller-set locked anchors share the gray pinned fill but signal\n * \"not interactive at all\" via the not-allowed cursor. */\n.anchor.locked {\n cursor: not-allowed;\n}\n\n/* Endpoint vertices render as diamonds (rotated squares) instead of\n * circles, so they read as a different category of handle. The shape\n * is enforced inline via SVG <rect transform>; this class only adds\n * the hover/active styling. */\n.endpoint {\n /* No fill / stroke overrides — endpoint visuals reuse .anchor /\n * .anchor.pinned / .anchor.active styling. The class exists as a\n * marker for future per-endpoint styling needs. */\n}\n\n/* When the consumer opts into hiding non-interactive handles, pinned\n * endpoints disappear entirely. */\n.hidden {\n display: none;\n}\n","/**\n * Built-in function/curve layer for LayeredCurveEditor. Bundles the\n * curve-render + anchor-render + drag/insert/delete gestures that were\n * the entirety of the original `CurveEditor`. Multiple instances coexist\n * by id; consumers can compose several to plot e.g. a primary function\n * and a derived (read-only) curve on the same chart.\n */\nimport type { ReactNode } from 'react';\nimport { sampleByInterpolation, type InterpolationMode } from './interpolation';\nimport { hitTestAnchor, hitTestCurve } from './hitTest';\nimport { dlog } from '../../dlog';\nimport s from './CurveEditor.module.css';\nimport type {\n CurveLayer, LayerCtx, LayerGesture, LayerRenderCtx, ModelPoint, PlotPoint,\n EmptyDownArgs,\n} from './layerTypes';\n\n// ── Public types ─────────────────────────────────────────────────────\n\n/** One anchor of a curve, in model space. */\nexport interface ControlPoint {\n x: number;\n y: number;\n /** When true, this control point can't be moved or deleted by the\n * user. Render as a smaller diamond with locked styling. */\n locked?: boolean;\n}\n\n/**\n * Whether a curve's points keep their x order. In `'1d'` an anchor cannot be\n * dragged past its neighbors and a new point is inserted at its x position;\n * in `'2d'` points are free to move anywhere and keep their list order.\n */\nexport type CurveDomain = '1d' | '2d';\n/**\n * How much of a curve's first and last anchor the user may move: everything,\n * only their y, or nothing.\n */\nexport type EndpointMode = 'free' | 'pinned-x' | 'pinned-both';\n/**\n * Where a click adds a new anchor — on the curve itself, anywhere in the\n * plot, or nowhere.\n */\nexport type AddPointMode = 'click-curve' | 'click-empty' | 'never';\n\n/**\n * What a `renderAnchor` function is given for one anchor: the point, its\n * index, its position in plot space (`cx`/`cy`), and the states that change\n * how it should look.\n */\nexport interface AnchorRenderProps {\n point: ControlPoint;\n index: number;\n cx: number;\n cy: number;\n isActive: boolean;\n isLocked: boolean;\n isPinnedEndpoint: boolean;\n isEndpoint: boolean;\n}\n\n/**\n * Per-instance curve-rendering overrides. Currently empty — passing `{}`\n * means \"draw the curve\", and `false`/`null` means don't.\n */\nexport interface CurveSettings {\n // reserved for future per-instance overrides\n}\n\n/** Shades the region between the curve and one edge of the plot. */\nexport interface FillSettings {\n side: 'below' | 'above';\n color?: string;\n}\n\n/** Configuration for {@link createFunctionLayer}. */\nexport interface FunctionLayerConfig {\n /** Layer id; default `'function'`. Two instances on the same editor\n * must have distinct ids. */\n id?: string;\n domain?: CurveDomain;\n endpoints?: EndpointMode;\n interpolation?: InterpolationMode;\n constrain?: 'none' | 'function';\n addPointMode?: AddPointMode;\n minPoints?: number;\n maxPoints?: number;\n curve?: CurveSettings | false | null;\n fill?: FillSettings | false | null;\n hideNonInteractive?: boolean;\n renderAnchor?: (info: AnchorRenderProps) => ReactNode;\n /** When false, the layer renders the curve+fill but doesn't render\n * anchors and doesn't accept pointer input. For derived read-only\n * curves (e.g. plotting f'(x) alongside f(x)). Default true. */\n interactive?: boolean;\n /** Override x clamp range for drags and pinned endpoints. Defaults\n * to the editor's modelRange x. Use this to constrain a layer to a\n * sub-range of the plot (e.g. a bevel curve occupying x ∈ [0, b]\n * while the editor's xRange is [0, halfWidth]). */\n xClamp?: readonly [number, number];\n}\n\n/**\n * The state a function layer owns. Treat it as opaque and round-trip it\n * through `LayeredCurveEditor`'s `onLayerChange`; `points` is the part worth\n * reading.\n */\nexport interface FunctionLayerState {\n points: readonly ControlPoint[];\n /** Layer-internal: the index of the anchor currently being dragged.\n * Cleared between gestures. Consumers treat the whole state as\n * opaque and round-trip it via onLayerChange. */\n activeIndex: number | null;\n}\n\nconst SAMPLES_PER_SEGMENT = 16;\nconst ANCHOR_SNAP_PX = 12;\nconst CURVE_HIT_PX = 8;\n\n// ── Helpers ───────────────────────────────────────────────────────────\n\nfunction clampToModelRange(p: ModelPoint, ctx: LayerCtx): ModelPoint {\n const m = ctx.modelRange;\n const xLo = Math.min(m.xMin, m.xMax);\n const xHi = Math.max(m.xMin, m.xMax);\n const yLo = Math.min(m.yMin, m.yMax);\n const yHi = Math.max(m.yMin, m.yMax);\n return {\n x: Math.max(xLo, Math.min(xHi, p.x)),\n y: Math.max(yLo, Math.min(yHi, p.y)),\n };\n}\n\nfunction isPinnedEndpointAt(index: number, count: number, endpoints: EndpointMode): boolean {\n if (endpoints === 'free') return false;\n return index === 0 || index === count - 1;\n}\n\nfunction projectAnchors(points: readonly ControlPoint[], ctx: LayerCtx): PlotPoint[] {\n return points.map((p) => ctx.toPlot(p));\n}\n\nfunction segmentSamples(\n points: readonly ControlPoint[],\n interpolation: InterpolationMode,\n ctx: LayerCtx,\n): PlotPoint[][] {\n if (points.length < 2) return [];\n const all = sampleByInterpolation(points, SAMPLES_PER_SEGMENT, interpolation);\n const out: PlotPoint[][] = [];\n for (let i = 0; i < points.length - 1; i++) {\n const start = i * SAMPLES_PER_SEGMENT;\n const end = start + SAMPLES_PER_SEGMENT;\n const slice = all.slice(start, end + 1);\n out.push(slice.map((p) => ctx.toPlot(p)));\n }\n return out;\n}\n\nfunction resolveInterpolation(cfg: FunctionLayerConfig): InterpolationMode {\n return (cfg.constrain ?? 'none') === 'function'\n ? 'monotone'\n : (cfg.interpolation ?? 'catmull-rom');\n}\n\n// ── Factory ───────────────────────────────────────────────────────────\n\n/**\n * Builds the built-in curve layer for `LayeredCurveEditor`: it draws the\n * interpolated curve and its anchors, and handles dragging, inserting and\n * deleting them.\n *\n * Several can coexist on one editor as long as their ids differ — plotting a\n * derived read-only curve beside an editable one is the case it is shaped\n * for, via `interactive: false`.\n */\nexport function createFunctionLayer(cfg: FunctionLayerConfig = {}): CurveLayer<FunctionLayerState> {\n const id = cfg.id ?? 'function';\n const domain = cfg.domain ?? '2d';\n const endpoints = cfg.endpoints ?? 'free';\n const constrain = cfg.constrain ?? 'none';\n const addPointMode = cfg.addPointMode ?? 'click-curve';\n const interpolation = resolveInterpolation(cfg);\n const interactive = cfg.interactive !== false;\n const curveVisible = cfg.curve !== false && cfg.curve !== null;\n\n // ── render ──────────────────────────────────────────────────────────\n function render(state: FunctionLayerState, ctx: LayerRenderCtx): ReactNode {\n const { points, activeIndex } = state;\n const plotAnchors = projectAnchors(points, ctx);\n\n let pathD = '';\n let fillD: string | null = null;\n if (points.length >= 2) {\n const samples = sampleByInterpolation(points, SAMPLES_PER_SEGMENT, interpolation)\n .map((p) => ctx.toPlot(p));\n const parts: string[] = [`M${samples[0].x.toFixed(2)},${samples[0].y.toFixed(2)}`];\n for (let i = 1; i < samples.length; i++) {\n parts.push(`L${samples[i].x.toFixed(2)},${samples[i].y.toFixed(2)}`);\n }\n pathD = parts.join('');\n\n if (cfg.fill) {\n const closingY = cfg.fill.side === 'below' ? ctx.plotSize.height : 0;\n const first = samples[0];\n const last = samples[samples.length - 1];\n const fillParts = [...parts];\n fillParts.push(`L${last.x.toFixed(2)},${closingY.toFixed(2)}`);\n fillParts.push(`L${first.x.toFixed(2)},${closingY.toFixed(2)}`);\n fillParts.push('Z');\n fillD = fillParts.join('');\n }\n }\n\n return (\n <>\n {fillD && (\n <path\n data-curve-element=\"fill\"\n className={s.fill}\n d={fillD}\n fill={cfg.fill ? cfg.fill.color : undefined}\n />\n )}\n {pathD && curveVisible && (\n <path\n className={s.curve}\n d={pathD}\n fill=\"none\"\n />\n )}\n {interactive && plotAnchors.map((a, i) => {\n const pinned = isPinnedEndpointAt(i, points.length, endpoints);\n const locked = points[i]?.locked === true;\n if ((locked || pinned) && cfg.hideNonInteractive) return null;\n const active = activeIndex === i;\n const isEndpoint = curveVisible && (i === 0 || i === points.length - 1);\n if (cfg.renderAnchor) {\n const node = cfg.renderAnchor({\n point: points[i], index: i,\n cx: a.x, cy: a.y,\n isActive: active, isLocked: locked,\n isPinnedEndpoint: pinned, isEndpoint,\n });\n if (node !== null && node !== undefined) {\n return <g key={i} data-anchor-index={i}>{node}</g>;\n }\n }\n const anchorCls = [\n s.anchor,\n isEndpoint && s.endpoint,\n (pinned || locked) && s.pinned,\n locked && s.locked,\n active && s.active,\n ].filter(Boolean).join(' ');\n const renderAsDiamond = isEndpoint || locked;\n if (renderAsDiamond) {\n const half = locked ? 3.55 : 5;\n return (\n <rect\n key={i}\n className={anchorCls}\n x={a.x - half}\n y={a.y - half}\n width={half * 2}\n height={half * 2}\n transform={`rotate(45 ${a.x} ${a.y})`}\n data-anchor-index={i}\n />\n );\n }\n return (\n <circle\n key={i}\n className={anchorCls}\n cx={a.x}\n cy={a.y}\n r={4}\n data-anchor-index={i}\n />\n );\n })}\n </>\n );\n }\n\n // ── hit test ────────────────────────────────────────────────────────\n function hitTest(state: FunctionLayerState, plot: PlotPoint, ctx: LayerCtx) {\n if (!interactive) return null;\n const plotAnchors = projectAnchors(state.points, ctx);\n const a = hitTestAnchor(plotAnchors, plot, ANCHOR_SNAP_PX);\n if (a) return { kind: 'anchor' as const, payload: { index: a.index } };\n if (addPointMode === 'click-curve' && curveVisible) {\n const segs = segmentSamples(state.points, interpolation, ctx);\n const c = hitTestCurve(segs, plot, CURVE_HIT_PX);\n if (c) return { kind: 'curve' as const, payload: { segIdx: c.segIdx, t: c.t } };\n }\n return null;\n }\n\n // ── drag gesture ────────────────────────────────────────────────────\n function makeDragGesture(index: number): LayerGesture<FunctionLayerState> {\n return {\n onMove(state, model, _e, ctx) {\n const points = state.points;\n if (index < 0 || index >= points.length) return state;\n const mr = ctx.modelRange;\n const xMin = cfg.xClamp ? cfg.xClamp[0] : mr.xMin;\n const xMax = cfg.xClamp ? cfg.xClamp[1] : mr.xMax;\n let nx = model.x;\n let ny = model.y;\n const isEndpoint = index === 0 || index === points.length - 1;\n\n if (endpoints === 'pinned-both' && isEndpoint) {\n nx = index === 0 ? xMin : xMax;\n ny = index === 0 ? mr.yMin : mr.yMax;\n } else if (endpoints === 'pinned-x' && isEndpoint) {\n nx = index === 0 ? xMin : xMax;\n } else if (constrain === 'function' || domain === '1d') {\n const epsilon = constrain === 'function'\n ? (xMax - xMin) / 1000\n : 0;\n const left = index > 0 ? points[index - 1].x + epsilon : xMin;\n const right = index < points.length - 1 ? points[index + 1].x - epsilon : xMax;\n nx = Math.max(left, Math.min(right, nx));\n }\n\n // Always clamp x to the layer's own range (so free-mode\n // interior anchors of a sub-range layer can't escape it).\n // Normalize bounds for inverted ranges.\n {\n const lo = Math.min(xMin, xMax);\n const hi = Math.max(xMin, xMax);\n nx = Math.max(lo, Math.min(hi, nx));\n }\n const clamped = clampToModelRange({ x: nx, y: ny }, ctx);\n const next = points.slice();\n next[index] = { ...points[index], x: clamped.x, y: clamped.y };\n return { ...state, points: next, activeIndex: index };\n },\n onCommit(state) { return { ...state, activeIndex: null }; },\n onCancel(state) { return { ...state, activeIndex: null }; },\n };\n }\n\n // ── pointerdown on a positive hit ───────────────────────────────────\n function onPointerDown(\n state: FunctionLayerState,\n hit: { kind: string; payload?: unknown },\n e: PointerEvent,\n ctx: LayerCtx,\n extra: EmptyDownArgs<FunctionLayerState>,\n ): LayerGesture<FunctionLayerState> | void {\n if (hit.kind === 'anchor') {\n const { index } = hit.payload as { index: number };\n dlog('function-layer', 'anchor-down', { id, index, shift: ctx.modifiers.shift, button: e.button });\n if (state.points[index]?.locked) return;\n if (ctx.modifiers.shift) {\n if (isPinnedEndpointAt(index, state.points.length, endpoints)) return;\n if (cfg.minPoints !== undefined && state.points.length <= cfg.minPoints) return;\n extra.commit({\n ...state,\n points: state.points.filter((_, i) => i !== index),\n activeIndex: null,\n });\n return;\n }\n if (e.button === 2) return;\n extra.commit({ ...state, activeIndex: index });\n return makeDragGesture(index);\n }\n if (hit.kind === 'curve') {\n const { segIdx } = hit.payload as { segIdx: number; t: number };\n if (cfg.maxPoints !== undefined && state.points.length >= cfg.maxPoints) return;\n const insertIndex = segIdx + 1;\n const modelPt = clampToModelRange(extra.model, ctx);\n\n // function/1d: refuse insert that would be immediately clamped\n // against a neighbor (mouse jitter would otherwise drop a phantom\n // point directly above an existing anchor). Snap to dragging the\n // neighbor instead.\n if (constrain === 'function' || domain === '1d') {\n const epsilon = constrain === 'function'\n ? (ctx.modelRange.xMax - ctx.modelRange.xMin) / 1000\n : 0;\n const left = insertIndex > 0 ? state.points[insertIndex - 1] : null;\n const right = insertIndex < state.points.length ? state.points[insertIndex] : null;\n if (left && Math.abs(modelPt.x - left.x) < epsilon * 2) {\n extra.commit({ ...state, activeIndex: insertIndex - 1 });\n return makeDragGesture(insertIndex - 1);\n }\n if (right && Math.abs(modelPt.x - right.x) < epsilon * 2) {\n extra.commit({ ...state, activeIndex: insertIndex });\n return makeDragGesture(insertIndex);\n }\n }\n\n const nextPoints = [...state.points.slice(0, insertIndex), modelPt, ...state.points.slice(insertIndex)];\n extra.commit({ ...state, points: nextPoints, activeIndex: insertIndex });\n return makeDragGesture(insertIndex);\n }\n }\n\n // ── empty-space click ───────────────────────────────────────────────\n function onEmptyPointerDown(\n state: FunctionLayerState,\n model: ModelPoint,\n _e: PointerEvent,\n ctx: LayerCtx,\n extra: EmptyDownArgs<FunctionLayerState>,\n ): LayerGesture<FunctionLayerState> | void {\n if (!interactive) return;\n if (addPointMode !== 'click-empty') return;\n if (cfg.maxPoints !== undefined && state.points.length >= cfg.maxPoints) return;\n\n const clamped = clampToModelRange(model, ctx);\n let insertIndex = state.points.length;\n if (domain === '1d') {\n for (let i = 0; i < state.points.length; i++) {\n if (state.points[i].x > clamped.x) { insertIndex = i; break; }\n }\n }\n const nextPoints = [...state.points.slice(0, insertIndex), clamped, ...state.points.slice(insertIndex)];\n extra.commit({ ...state, points: nextPoints, activeIndex: null });\n return;\n }\n\n return {\n id,\n render,\n hitTest,\n onPointerDown,\n onEmptyPointerDown,\n };\n}\n\n/** Convenience: build a function-layer state from a points array. */\nexport function functionLayerState(points: readonly ControlPoint[]): FunctionLayerState {\n return { points, activeIndex: null };\n}\n","import {\n useCallback, useEffect, useMemo, useRef, useState,\n type CSSProperties,\n type KeyboardEvent as ReactKeyboardEvent,\n type PointerEvent as ReactPointerEvent,\n type ReactNode,\n} from 'react';\nimport {\n Plot2D,\n type Plot2DHandle,\n type GridSettings,\n type AxesSettings,\n} from '../Plot2D';\nimport {\n modelToPlot, plotToModel,\n type ModelRange,\n} from '../Plot2D/geometry';\nimport { dlog } from '../../dlog';\nimport s from './CurveEditor.module.css';\nimport type {\n CurveLayer, LayerCtx, LayerGesture, LayerModifiers, ModelPoint, PlotPoint,\n} from './layerTypes';\n\n/**\n * A layer paired with the state the consumer holds for it. The editor never\n * stores layer state itself.\n */\nexport interface LayerBinding<S = unknown> {\n layer: CurveLayer<S>;\n state: S;\n}\n\n/** Props for {@link LayeredCurveEditor}. */\nexport interface LayeredCurveEditorProps {\n /** Bottom-to-top render order: `layers[0]` paints first, `layers[N-1]`\n * paints on top. Hit-testing runs in reverse (topmost claim wins). */\n layers: ReadonlyArray<LayerBinding<any>>;\n\n /** Fires every frame during a gesture with the live in-flight state\n * for the layer that owns the gesture. */\n onLayerChange(id: string, next: unknown): void;\n\n /** Fires once per discrete user action (drag-end, one-shot insert,\n * delete) for the layer that produced the change. `prev` is the\n * layer's state at gesture start. */\n onLayerCommit?(id: string, next: unknown, prev: unknown): void;\n\n width: number;\n height: number;\n /** Model-space x range. Default [0, 1]. */\n xRange?: readonly [number, number];\n /** Model-space y range. Default [0, 1]. */\n yRange?: readonly [number, number];\n grid?: GridSettings | false | null;\n axes?: AxesSettings | false | null;\n\n /** Built-in undo/redo via Cmd/Ctrl+Z (Shift+Z or Y for redo) on the\n * focused SVG. Snapshots every layer's state on each commit; undo\n * fires `onLayerChange(id, prevState)` for each layer whose state\n * changed. Default `true`. */\n history?: boolean;\n\n className?: string;\n style?: CSSProperties;\n\n /** Extra SVG content rendered behind all layers (under the lowest\n * layer but above grid/axes). Use this for static chrome that isn't\n * worth modeling as a layer. */\n children?: ReactNode;\n}\n\ninterface ActiveGesture {\n layerId: string;\n pointerId: number;\n gesture: LayerGesture<any>;\n startStates: Map<string, unknown>;\n startSelfState: unknown;\n}\n\nfunction readModifiers(e: PointerEvent | ReactPointerEvent<any> | KeyboardEvent | ReactKeyboardEvent<any>): LayerModifiers {\n return {\n shift: e.shiftKey,\n alt: e.altKey,\n meta: e.metaKey,\n ctrl: e.ctrlKey,\n };\n}\n\n/**\n * A `Plot2D` that hosts a stack of composable curve layers. It owns the\n * pointer gestures and the coordinate mapping; each layer supplies its own\n * drawing, hit testing and gesture behavior.\n *\n * Layer state stays with the consumer: the editor reports changes through\n * `onLayerChange` during a gesture and `onLayerCommit` at its end, and reads\n * the state back from props each frame. That is what lets one layer's drag\n * update another layer's state mid-gesture.\n */\nexport function LayeredCurveEditor(props: LayeredCurveEditorProps) {\n const {\n layers, onLayerChange, onLayerCommit,\n width, height,\n xRange = [0, 1], yRange = [0, 1],\n grid, axes,\n className, style, children,\n } = props;\n\n const historyEnabled = props.history !== false;\n\n const modelRange: ModelRange = useMemo(\n () => ({ xMin: xRange[0], xMax: xRange[1], yMin: yRange[0], yMax: yRange[1] }),\n [xRange, yRange],\n );\n const plotSize = useMemo(() => ({ width, height }), [width, height]);\n\n // Refs that always hold the latest props — gesture handlers and\n // window listeners close over these refs, never over render-time\n // values, so mid-drag re-renders (including cross-layer state\n // updates) are visible to the next pointermove tick.\n const layersRef = useRef(layers);\n layersRef.current = layers;\n const modelRangeRef = useRef(modelRange);\n modelRangeRef.current = modelRange;\n const plotSizeRef = useRef(plotSize);\n plotSizeRef.current = plotSize;\n\n const plotRef = useRef<Plot2DHandle | null>(null);\n\n // ── Coord helpers ──────────────────────────────────────────────────\n const makeCtx = useCallback((modifiers: LayerModifiers): LayerCtx => {\n const mr = modelRangeRef.current;\n const ps = plotSizeRef.current;\n return {\n modelRange: mr,\n plotSize: ps,\n toPlot: (p) => modelToPlot(p, mr, ps),\n toModel: (p) => plotToModel(p, mr, ps),\n modifiers,\n };\n }, []);\n\n // ── Gesture routing ────────────────────────────────────────────────\n const activeRef = useRef(null as ActiveGesture | null);\n const [activeLayerId, setActiveLayerId] = useState<string | null>(null);\n\n // ── History (whole-state snapshots) ────────────────────────────────\n const pastRef = useRef<Map<string, unknown>[]>([]);\n const futureRef = useRef<Map<string, unknown>[]>([]);\n\n const snapshot = useCallback((): Map<string, unknown> => {\n const m = new Map<string, unknown>();\n for (const b of layersRef.current) m.set(b.layer.id, b.state);\n return m;\n }, []);\n\n const restore = useCallback((snap: Map<string, unknown>) => {\n const current = layersRef.current;\n for (const b of current) {\n const prev = snap.get(b.layer.id);\n if (prev !== undefined && prev !== b.state) {\n onLayerChange(b.layer.id, prev);\n }\n }\n }, [onLayerChange]);\n\n const undo = useCallback(() => {\n if (!historyEnabled) return;\n const past = pastRef.current;\n if (past.length === 0) return;\n const prev = past.pop()!;\n futureRef.current.push(snapshot());\n restore(prev);\n }, [historyEnabled, snapshot, restore]);\n\n const redo = useCallback(() => {\n if (!historyEnabled) return;\n const future = futureRef.current;\n if (future.length === 0) return;\n const next = future.pop()!;\n pastRef.current.push(snapshot());\n restore(next);\n }, [historyEnabled, snapshot, restore]);\n\n // ── Window-level pointer routing during an active gesture ──────────\n // Stable identity across renders so addEventListener / removeEventListener\n // see the same function. Delegate to refs that point at the latest\n // per-render impl.\n const onWindowMoveRef = useRef<(e: PointerEvent) => void>(() => {});\n const onWindowUpRef = useRef<(e: PointerEvent) => void>(() => {});\n const onWindowCancelRef = useRef<(e: PointerEvent) => void>(() => {});\n\n const stableMove = useRef((e: PointerEvent) => onWindowMoveRef.current(e)).current;\n const stableUp = useRef((e: PointerEvent) => onWindowUpRef.current(e)).current;\n const stableCancel = useRef((e: PointerEvent) => onWindowCancelRef.current(e)).current;\n\n const cleanupGesture = useCallback(() => {\n window.removeEventListener('pointermove', stableMove);\n window.removeEventListener('pointerup', stableUp);\n window.removeEventListener('pointercancel', stableCancel);\n activeRef.current = null;\n setActiveLayerId(null);\n }, [stableMove, stableUp, stableCancel]);\n\n const findLayerState = useCallback((id: string): unknown | undefined => {\n for (const b of layersRef.current) if (b.layer.id === id) return b.state;\n return undefined;\n }, []);\n\n onWindowMoveRef.current = (e: PointerEvent) => {\n const a = activeRef.current;\n if (!a || a.pointerId !== e.pointerId) return;\n const h = plotRef.current;\n if (!h) return;\n const model = h.clientToModel(e);\n const current = findLayerState(a.layerId);\n if (current === undefined) return;\n const ctx = makeCtx(readModifiers(e));\n const next = a.gesture.onMove(current, model, e, ctx);\n if (next !== current) onLayerChange(a.layerId, next);\n };\n\n onWindowUpRef.current = (e: PointerEvent) => {\n const a = activeRef.current;\n if (!a || a.pointerId !== e.pointerId) return;\n const current = findLayerState(a.layerId);\n if (current === undefined) { cleanupGesture(); return; }\n const ctx = makeCtx(readModifiers(e));\n const finalState = a.gesture.onCommit ? a.gesture.onCommit(current, ctx) : current;\n if (finalState !== current) onLayerChange(a.layerId, finalState);\n if (historyEnabled) {\n pastRef.current.push(a.startStates);\n futureRef.current = [];\n }\n if (onLayerCommit) onLayerCommit(a.layerId, finalState, a.startSelfState);\n cleanupGesture();\n };\n\n onWindowCancelRef.current = (e: PointerEvent) => {\n const a = activeRef.current;\n if (!a || a.pointerId !== e.pointerId) return;\n const current = findLayerState(a.layerId);\n if (current === undefined) { cleanupGesture(); return; }\n const ctx = makeCtx(readModifiers(e));\n const restored = a.gesture.onCancel ? a.gesture.onCancel(current, ctx) : a.startSelfState;\n if (restored !== current) onLayerChange(a.layerId, restored);\n cleanupGesture();\n };\n\n const installGesture = useCallback((layerId: string, pointerId: number, gesture: LayerGesture<any>) => {\n const startStates = snapshot();\n const startSelfState = startStates.get(layerId);\n activeRef.current = {\n layerId, pointerId, gesture,\n startStates,\n startSelfState,\n };\n setActiveLayerId(layerId);\n window.addEventListener('pointermove', stableMove);\n window.addEventListener('pointerup', stableUp);\n window.addEventListener('pointercancel', stableCancel);\n }, [snapshot, stableMove, stableUp, stableCancel]);\n\n // ── pointerdown dispatch ───────────────────────────────────────────\n const onSvgPointerDown = useCallback((\n e: ReactPointerEvent<SVGSVGElement>,\n coords: { plot: PlotPoint; model: ModelPoint },\n ) => {\n if (activeRef.current) return; // ignore secondary touches mid-gesture\n const modifiers = readModifiers(e);\n const ctx = makeCtx(modifiers);\n const ls = layersRef.current;\n const nativeEvent = e.nativeEvent;\n\n dlog('layered-curve-editor', 'pointerdown', { plot: coords.plot, model: coords.model, layers: ls.length });\n\n // Capture the pre-event snapshot once; reuse for history if any\n // path produces a commit (gesture-start commit or one-shot).\n const preEventSnapshot = snapshot();\n\n // Top-to-bottom hit test.\n for (let i = ls.length - 1; i >= 0; i--) {\n const b = ls[i];\n const hit = b.layer.hitTest?.(b.state, coords.plot, ctx);\n if (!hit) continue;\n // Found a claim. onPointerDown may start a gesture, absorb the\n // click, or commit a one-shot edit via extra.commit.\n const prevState = b.state;\n let committed: unknown = undefined;\n const gesture = b.layer.onPointerDown?.(\n prevState, hit, nativeEvent, ctx,\n {\n plot: coords.plot,\n model: coords.model,\n commit(next) { committed = next; onLayerChange(b.layer.id, next); },\n },\n );\n e.stopPropagation();\n if (gesture) {\n installGesture(b.layer.id, e.pointerId, gesture);\n return;\n }\n if (committed !== undefined) {\n if (historyEnabled) {\n pastRef.current.push(preEventSnapshot);\n futureRef.current = [];\n }\n onLayerCommit?.(b.layer.id, committed, prevState);\n }\n return;\n }\n\n // No hit — empty space dispatch, top-to-bottom.\n let pendingCommit: { id: string; next: unknown; prev: unknown } | null = null;\n for (let i = ls.length - 1; i >= 0; i--) {\n const b = ls[i];\n if (!b.layer.onEmptyPointerDown) continue;\n const prevState = b.state;\n let committed: unknown = undefined;\n const gesture = b.layer.onEmptyPointerDown(\n prevState, coords.model, nativeEvent, ctx,\n {\n plot: coords.plot,\n model: coords.model,\n commit(next) {\n committed = next;\n onLayerChange(b.layer.id, next);\n },\n },\n );\n if (gesture) {\n // If onEmptyPointerDown also published a commit (e.g. click-curve\n // insert publishes the new point then drags it), the gesture's\n // startStates already capture the post-insert state; the commit\n // for history is the *pre*-event snapshot, written when the\n // gesture eventually finishes via the gesture-commit path. We\n // override the gesture's startStates so undo restores correctly.\n installGesture(b.layer.id, e.pointerId, gesture);\n if (committed !== undefined && activeRef.current !== null) {\n (activeRef.current as ActiveGesture).startStates = preEventSnapshot;\n (activeRef.current as ActiveGesture).startSelfState = preEventSnapshot.get(b.layer.id);\n }\n return;\n }\n if (committed !== undefined) {\n pendingCommit = { id: b.layer.id, next: committed, prev: prevState };\n break;\n }\n }\n if (pendingCommit) {\n if (historyEnabled) {\n pastRef.current.push(preEventSnapshot);\n futureRef.current = [];\n }\n onLayerCommit?.(pendingCommit.id, pendingCommit.next, pendingCommit.prev);\n }\n }, [makeCtx, installGesture, onLayerChange, onLayerCommit, historyEnabled, snapshot]);\n\n // ── keyboard ───────────────────────────────────────────────────────\n const onSvgKeyDown = useCallback((e: ReactKeyboardEvent<SVGSVGElement>) => {\n const modifiers = readModifiers(e);\n const ctx = makeCtx(modifiers);\n const ls = layersRef.current;\n // History first.\n if (historyEnabled && (e.metaKey || e.ctrlKey)) {\n const k = e.key.toLowerCase();\n if (k === 'z') {\n if (e.shiftKey) redo();\n else undo();\n e.preventDefault();\n return;\n }\n if (k === 'y') { redo(); e.preventDefault(); return; }\n }\n // Then layers (top-to-bottom).\n for (let i = ls.length - 1; i >= 0; i--) {\n const b = ls[i];\n b.layer.onKeyDown?.(b.state, e.nativeEvent, ctx);\n if (e.isDefaultPrevented()) return;\n }\n }, [historyEnabled, undo, redo, makeCtx]);\n\n // Clean up listeners on unmount.\n useEffect(() => () => {\n window.removeEventListener('pointermove', stableMove);\n window.removeEventListener('pointerup', stableUp);\n window.removeEventListener('pointercancel', stableCancel);\n }, [stableMove, stableUp, stableCancel]);\n\n // ── render ─────────────────────────────────────────────────────────\n const renderCtxBase: Omit<LayerCtx, 'modifiers'> = useMemo(() => ({\n modelRange,\n plotSize,\n toPlot: (p) => modelToPlot(p, modelRange, plotSize),\n toModel: (p) => plotToModel(p, modelRange, plotSize),\n }), [modelRange, plotSize]);\n\n const cls = [s.root, className].filter(Boolean).join(' ');\n\n return (\n <Plot2D\n ref={plotRef}\n className={cls}\n style={style}\n width={width}\n height={height}\n xRange={xRange}\n yRange={yRange}\n grid={grid}\n axes={axes}\n tabIndex={historyEnabled ? 0 : undefined}\n onPointerDown={onSvgPointerDown}\n onKeyDown={onSvgKeyDown}\n >\n {children}\n {layers.map((b) => {\n const ctx = {\n ...renderCtxBase,\n modifiers: { shift: false, alt: false, meta: false, ctrl: false },\n isActive: activeLayerId === b.layer.id,\n };\n return (\n <g key={b.layer.id} data-layer-id={b.layer.id}>\n {b.layer.render(b.state, ctx)}\n </g>\n );\n })}\n </Plot2D>\n );\n}\n","/**\n * Back-compat wrapper around `LayeredCurveEditor` that preserves the\n * original single-curve API. New consumers wanting multi-layer\n * composition should reach for `LayeredCurveEditor` + `createFunctionLayer`\n * directly.\n */\nimport {\n useCallback, useEffect, useMemo, useRef, useState,\n type CSSProperties,\n type MouseEvent as ReactMouseEvent,\n type ReactNode,\n} from 'react';\nimport {\n type GridSettings,\n type AxesSettings,\n} from '../Plot2D';\nimport type { InterpolationMode } from './interpolation';\nimport {\n createFunctionLayer,\n type ControlPoint,\n type CurveDomain,\n type EndpointMode,\n type AddPointMode,\n type CurveSettings,\n type FillSettings,\n type AnchorRenderProps,\n type FunctionLayerState,\n} from './createFunctionLayer';\nimport { LayeredCurveEditor } from './LayeredCurveEditor';\n\nexport type { GridSettings, AxesSettings };\nexport type {\n ControlPoint, CurveDomain, EndpointMode, AddPointMode,\n CurveSettings, FillSettings, AnchorRenderProps,\n};\n\n/**\n * Props for {@link CurveEditor}. `onInput` fires throughout a gesture and\n * `onChange` once at its end, with the pre-gesture points as `prev`.\n */\nexport interface CurveEditorProps {\n value: readonly ControlPoint[];\n onInput: (next: ControlPoint[]) => void;\n onChange?: (next: ControlPoint[], prev: readonly ControlPoint[]) => void;\n domain?: CurveDomain;\n interpolation?: InterpolationMode;\n endpoints?: EndpointMode;\n xRange?: readonly [number, number];\n yRange?: readonly [number, number];\n width: number;\n height: number;\n grid?: GridSettings | false | null;\n axes?: AxesSettings | false | null;\n curve?: CurveSettings | false | null;\n fill?: FillSettings | false | null;\n hideNonInteractive?: boolean;\n constrain?: 'none' | 'function';\n history?: boolean;\n minPoints?: number;\n maxPoints?: number;\n addPointMode?: AddPointMode;\n renderAnchor?: (info: AnchorRenderProps) => ReactNode;\n /** Extra SVG content rendered behind the curve and anchors. */\n decorations?: ReactNode;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * Editable curve through a list of control points: drag anchors, click to\n * insert, right-click to delete, with optional built-in undo.\n *\n * This is the single-curve API, implemented as one `createFunctionLayer` on\n * a `LayeredCurveEditor`. Reach for those directly to stack several curves\n * on one plot.\n */\nexport function CurveEditor(props: CurveEditorProps) {\n const {\n value, onInput, onChange,\n domain, endpoints, interpolation, constrain, addPointMode,\n minPoints, maxPoints, curve, fill, hideNonInteractive, renderAnchor,\n width, height, xRange, yRange, grid, axes, history,\n className, style, decorations,\n } = props;\n\n // Layer is config-only; rebuild only when *config* changes.\n const layer = useMemo(() => createFunctionLayer({\n id: 'function',\n domain, endpoints, interpolation, constrain, addPointMode,\n minPoints, maxPoints, curve, fill, hideNonInteractive, renderAnchor,\n }), [\n domain, endpoints, interpolation, constrain, addPointMode,\n minPoints, maxPoints, curve, fill, hideNonInteractive, renderAnchor,\n ]);\n\n // Hybrid controlled-state: the wrapper holds the full FunctionLayerState\n // locally so that consumers using the original CurveEditor API don't\n // have to wire their `value` prop back through on every drag tick to\n // keep the in-flight gesture advancing. We adopt the consumer's\n // `value` prop when it changes externally (and no gesture is active).\n const [layerState, setLayerState] = useState<FunctionLayerState>(\n () => ({ points: value, activeIndex: null }),\n );\n // Ref mirror so the layerChange callback can compare against the\n // freshest state without re-binding on every render.\n const layerStateRef = useRef(layerState);\n layerStateRef.current = layerState;\n\n // Sync from controlled `value` when the consumer updates it externally\n // and we're idle. Skip the update during a gesture so drag-in-flight\n // points aren't clobbered by a stale `value`.\n useEffect(() => {\n if (layerStateRef.current.activeIndex !== null) return;\n if (layerStateRef.current.points === value) return;\n setLayerState({ points: value, activeIndex: null });\n }, [value]);\n\n const handleLayerChange = useCallback((_id: string, nextUnknown: unknown) => {\n const next = nextUnknown as FunctionLayerState;\n const prevPoints = layerStateRef.current.points;\n setLayerState(next);\n if (next.points !== prevPoints) {\n onInput(next.points as ControlPoint[]);\n }\n }, [onInput]);\n\n const handleLayerCommit = useCallback((_id: string, nextUnknown: unknown, prevUnknown: unknown) => {\n if (!onChange) return;\n const next = nextUnknown as FunctionLayerState;\n const prev = prevUnknown as FunctionLayerState;\n if (next.points !== prev.points) {\n onChange?.(next.points as ControlPoint[], prev.points);\n }\n }, [onChange]);\n\n // Right-click on an anchor → delete (preserves original CurveEditor\n // behavior). LayeredCurveEditor doesn't model contextmenu as a\n // gesture; we intercept at the wrapper level by walking the DOM\n // target for `data-anchor-index`.\n const onContextMenu = useCallback((e: ReactMouseEvent<HTMLDivElement>) => {\n const target = e.target as Element | null;\n const node = target?.closest?.('[data-anchor-index]') as Element | null;\n if (!node) return;\n e.preventDefault();\n e.stopPropagation();\n const idxAttr = node.getAttribute('data-anchor-index');\n if (idxAttr === null) return;\n const idx = Number(idxAttr);\n const points = layerStateRef.current.points;\n if (!Number.isInteger(idx) || idx < 0 || idx >= points.length) return;\n if (points[idx]?.locked) return;\n if (endpoints !== undefined && endpoints !== 'free' && (idx === 0 || idx === points.length - 1)) return;\n if (minPoints !== undefined && points.length <= minPoints) return;\n const next = points.filter((_, i) => i !== idx);\n setLayerState({ points: next, activeIndex: null });\n onInput(next as ControlPoint[]);\n onChange?.(next as ControlPoint[], points);\n }, [endpoints, minPoints, onInput, onChange]);\n\n const layers = useMemo(() => [{ layer, state: layerState }], [layer, layerState]);\n\n return (\n <div\n onContextMenu={onContextMenu}\n style={{ display: 'contents' }}\n >\n <LayeredCurveEditor\n layers={layers}\n onLayerChange={handleLayerChange}\n onLayerCommit={handleLayerCommit}\n width={width}\n height={height}\n xRange={xRange}\n yRange={yRange}\n grid={grid}\n axes={axes}\n history={history}\n className={className}\n style={style}\n >\n {decorations}\n </LayeredCurveEditor>\n </div>\n );\n}\n","import type { Op } from '@weasel-js/core';\nimport type { ControlPoint } from './CurveEditor';\n\n/**\n * The adapter contract for {@link createSetCurveOp}. Consumers wire\n * this to whatever state container holds the curve's value — typically\n * a small callback that calls a React state setter or dispatches a\n * Redux/Zustand action.\n */\nexport interface SetCurveAdapter {\n setValue(id: string, next: readonly ControlPoint[]): void;\n}\n\n/**\n * Arguments for {@link createSetCurveOp}. `coalesceKey` merges consecutive\n * edits sharing it into one history entry.\n */\nexport interface CreateSetCurveOpArgs {\n /** Stable id the consumer uses for this curve. Used so a single\n * adapter can handle multiple curves; the op's apply routes by id. */\n id: string;\n from: readonly ControlPoint[];\n to: readonly ControlPoint[];\n label?: string;\n coalesceKey?: string;\n}\n\n/**\n * Op factory for editing a CurveEditor's value through weasel-history.\n *\n * history.applyOps(\n * [createSetCurveOp({ id: 'easing', from, to, label: 'Edit easing' })],\n * 'Edit easing',\n * );\n *\n * The op's `apply(adapter)` calls `adapter.setValue(id, to)`. `invert()`\n * returns a mirror op with `from`/`to` swapped. When `from` and `to`\n * are structurally equal, `apply` returns `false` so history can skip\n * the entry — same convention as `createSetPathOp`.\n */\nexport function createSetCurveOp(args: CreateSetCurveOpArgs): Op {\n const { id, from, to, label, coalesceKey } = args;\n return {\n name: 'setCurve',\n args: { id, from, to, label, coalesceKey },\n label,\n coalesceKey,\n apply(adapter) {\n if (controlPointsEqual(from, to)) return false;\n (adapter as SetCurveAdapter).setValue(id, to);\n return undefined;\n },\n invert() {\n return createSetCurveOp({ id, from: to, to: from, label, coalesceKey });\n },\n };\n}\n\nfunction controlPointsEqual(\n a: readonly ControlPoint[],\n b: readonly ControlPoint[],\n): boolean {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (a[i].x !== b[i].x || a[i].y !== b[i].y) return false;\n }\n return true;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"DataGrid-BQklXGRl.js","names":[],"sources":["../../src/useReorderDragList.ts","../../src/components/DataGrid/DataGrid.module.css","../../src/components/DataGrid/DataGrid.tsx"],"sourcesContent":["import { useCallback, useRef, useState } from 'react';\nimport type { ReactNode, PointerEvent as ReactPointerEvent, RefCallback } from 'react';\n\nexport interface LayerListItem {\n id: string;\n label: ReactNode;\n /** Locked rows cannot be dragged, cannot be crossed by drops, and\n * never combine with other rows in a multi-selection. */\n locked?: boolean;\n /** Optional color swatch rendered before the label. Any CSS color string. */\n swatch?: string;\n}\n\nexport interface UseReorderDragListOptions {\n items: LayerListItem[];\n selectedIds: string[];\n onReorder(ids: string[], targetIndex: number): void;\n /** Pointer-move distance (px) before pending drag engages. Default 4. */\n threshold?: number;\n}\n\nexport interface ReorderDragState {\n draggedIds: string[] | null;\n targetIndex: number | null;\n}\n\nexport interface ReorderDragHandlers {\n rowProps(id: string, index: number): { onPointerDown(e: ReactPointerEvent): void };\n containerProps: {\n ref: RefCallback<HTMLElement>;\n onPointerMove(e: ReactPointerEvent): void;\n onPointerUp(e: ReactPointerEvent): void;\n onPointerCancel(e: ReactPointerEvent): void;\n };\n state: ReorderDragState;\n}\n\ninterface PendingState {\n id: string;\n sourceIndex: number;\n startX: number;\n startY: number;\n pointerId: number;\n}\n\ninterface ActiveState extends PendingState {\n draggedIds: string[];\n targetIndex: number;\n}\n\nexport function useReorderDragList(opts: UseReorderDragListOptions): ReorderDragHandlers {\n const { threshold = 4 } = opts;\n const optsRef = useRef(opts);\n optsRef.current = opts;\n const containerRef = useRef<HTMLElement | null>(null);\n const pendingRef = useRef<PendingState | null>(null);\n const activeRef = useRef<ActiveState | null>(null);\n const [state, setState] = useState<ReorderDragState>({ draggedIds: null, targetIndex: null });\n\n const computeTargetIndex = useCallback((clientY: number): number => {\n const items = optsRef.current.items;\n // Locked rows act as walls — drops cannot cross them. Cap at the\n // first locked row's index (or items.length if none are locked).\n const firstLocked = items.findIndex((it) => it.locked);\n const cap = firstLocked === -1 ? items.length : firstLocked;\n const c = containerRef.current;\n if (!c) return 0;\n const rows = Array.from(c.children) as HTMLElement[];\n let raw = rows.length;\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i];\n if (!row) continue;\n const r = row.getBoundingClientRect();\n if (clientY < r.bottom) { raw = i; break; }\n }\n return Math.min(raw, cap);\n }, []);\n\n const refCb = useCallback<RefCallback<HTMLElement>>((el) => {\n containerRef.current = el;\n }, []);\n\n const onPointerDownRow = useCallback((id: string, index: number, e: ReactPointerEvent) => {\n // Locked items cannot be dragged — skip recording the pending state so\n // pointer-move cannot engage. Plain click still works because LayerList\n // tracks click intent in its own ref, separate from drag candidacy.\n const item = optsRef.current.items[index];\n if (item?.locked) return;\n // Capture the pointer to the row so subsequent move/up events keep firing\n // (on the row, bubbling to the container) even when the user drags\n // outside the list's bounding box. Without capture, releasing outside\n // the container leaves the drag state stuck because pointerup fires on\n // the document, not the list.\n try { (e.currentTarget as Element).setPointerCapture?.(e.pointerId); } catch { /* unsupported / already captured */ }\n pendingRef.current = {\n id,\n sourceIndex: index,\n startX: e.clientX,\n startY: e.clientY,\n pointerId: e.pointerId,\n };\n }, []);\n\n const reset = useCallback(() => {\n pendingRef.current = null;\n activeRef.current = null;\n setState({ draggedIds: null, targetIndex: null });\n }, []);\n\n const onPointerMove = useCallback((e: ReactPointerEvent) => {\n const active = activeRef.current;\n if (active) {\n const targetIndex = computeTargetIndex(e.clientY);\n if (targetIndex !== active.targetIndex) {\n active.targetIndex = targetIndex;\n setState({ draggedIds: active.draggedIds, targetIndex });\n }\n return;\n }\n const pending = pendingRef.current;\n if (!pending) return;\n const dx = e.clientX - pending.startX;\n const dy = e.clientY - pending.startY;\n if (Math.hypot(dx, dy) < threshold) return;\n // Engage.\n const selected = optsRef.current.selectedIds;\n const inSelection = selected.includes(pending.id);\n const draggedIds = inSelection ? [...selected] : [pending.id];\n const targetIndex = computeTargetIndex(e.clientY);\n activeRef.current = { ...pending, draggedIds, targetIndex };\n setState({ draggedIds, targetIndex });\n }, [computeTargetIndex, threshold]);\n\n const onPointerUp = useCallback((e: ReactPointerEvent) => {\n const active = activeRef.current;\n if (!active) {\n reset();\n return;\n }\n const targetIndex = computeTargetIndex(e.clientY);\n // Skip no-op: target lands inside the dragged-ids contiguous block.\n const items = optsRef.current.items;\n const indices = active.draggedIds\n .map((id) => items.findIndex((it) => it.id === id))\n .filter((i) => i >= 0)\n .sort((a, b) => a - b);\n const isContiguous =\n indices.length > 0 &&\n indices.every((v, i) => {\n if (i === 0) return true;\n const prev = indices[i - 1];\n return prev !== undefined && v === prev + 1;\n });\n const first = indices[0];\n const last = indices[indices.length - 1];\n const wouldBeNoop =\n isContiguous &&\n first !== undefined &&\n last !== undefined &&\n targetIndex >= first &&\n targetIndex <= last + 1;\n if (!wouldBeNoop) {\n optsRef.current.onReorder(active.draggedIds, targetIndex);\n }\n reset();\n }, [computeTargetIndex, reset]);\n\n const onPointerCancel = useCallback(() => {\n reset();\n }, [reset]);\n\n return {\n rowProps: (id, index) => ({\n onPointerDown: (e) => onPointerDownRow(id, index, e),\n }),\n containerProps: {\n ref: refCb,\n onPointerMove,\n onPointerUp,\n onPointerCancel,\n },\n state,\n };\n}\n",".grid {\n position: relative;\n width: 100%;\n}\n\n.table {\n width: 100%;\n border-collapse: collapse;\n font-size: 11px;\n}\n\n.table th,\n.table td {\n text-align: left;\n padding: 3px 6px;\n vertical-align: middle;\n}\n.table tbody tr {\n /* Subtle row separator — was a full-weight border on every cell.\n Drastically toned down per visual-weight feedback. */\n border-bottom: 1px solid rgba(255, 255, 255, 0.04);\n}\n\n.table th {\n font-weight: 600;\n color: var(--wzl-fg-muted);\n position: sticky;\n top: 0;\n background: var(--wzl-surface, #2a2018);\n z-index: 1;\n border-bottom: 1px solid var(--wzl-border);\n}\n\n.sortable {\n cursor: pointer;\n user-select: none;\n}\n.sortable:hover { color: var(--wzl-fg, #e6e7e9); }\n.sortActive { color: var(--wzl-fg, #e6e7e9); }\n\n.empty {\n color: var(--wzl-fg-muted);\n font-style: italic;\n padding: 8px 0;\n text-align: center;\n}\n\n/* Drag-handle column. */\n.handleCol { width: 14px; }\n.handleCell {\n width: 14px;\n cursor: grab;\n color: var(--wzl-fg-muted);\n user-select: none;\n text-align: center;\n}\n.handleCell:active { cursor: grabbing; }\n\n.dragging { opacity: 0.4; }\n\n.dropIndicator {\n position: absolute;\n left: 0;\n right: 0;\n height: 2px;\n background: var(--wzl-accent, #7fb069);\n pointer-events: none;\n}\n","/**\n * Lightweight sortable data grid. Use for inspector tables — action\n * registries, route maps, telemetry rows. Intentionally narrow:\n *\n * - Sortable columns (click header to cycle asc → desc → none)\n * - Custom cell render via per-column `render`\n * - Optional drag handles for row reordering (when `onReorder` is set)\n *\n * Not: virtual scrolling, column resizing, multi-column sort, filtering,\n * inline editing. Reach for a real grid library when you need those.\n */\nimport { Fragment, useMemo, useState } from 'react';\nimport type { ReactNode } from 'react';\nimport { useReorderDragList } from '../../useReorderDragList';\nimport s from './DataGrid.module.css';\n\nexport interface DataGridColumn<Row> {\n id: string;\n header: ReactNode;\n /** Pull a sortable value from the row. Defaults to `(row as any)[column.id]`. */\n accessor?: (row: Row) => string | number | null | undefined;\n /** Render the cell. Defaults to `String(accessor(row) ?? '')`. */\n render?: (row: Row) => ReactNode;\n /** Default true. Set false to suppress the sort affordance on this column. */\n sortable?: boolean;\n /** Optional CSS class on every <td> in this column. */\n className?: string;\n}\n\nexport interface DataGridProps<Row extends { id: string }> {\n rows: readonly Row[];\n columns: readonly DataGridColumn<Row>[];\n /** Initial sort. Omit for unsorted (rows render in input order). */\n defaultSort?: { columnId: string; direction: 'asc' | 'desc' };\n /** Enable drag handles in a leading column. Receives the reorder spec\n * (the existing `useReorderDragList` semantics — see weasel-ui). */\n onReorder?: (ids: string[], targetIndex: number) => void;\n /** Empty-state row text. Default `'—'`. */\n empty?: ReactNode;\n className?: string;\n}\n\ntype SortState = { columnId: string; direction: 'asc' | 'desc' } | null;\n\nexport function DataGrid<Row extends { id: string }>(props: DataGridProps<Row>) {\n const { rows, columns, defaultSort, onReorder, empty = '—', className } = props;\n const [sort, setSort] = useState<SortState>(defaultSort ?? null);\n\n const sortedRows = useMemo(() => {\n if (!sort) return [...rows];\n const col = columns.find((c) => c.id === sort.columnId);\n if (!col) return [...rows];\n const get = col.accessor ?? ((row: Row) => (row as unknown as Record<string, unknown>)[col.id] as string | number | null | undefined);\n const dir = sort.direction === 'asc' ? 1 : -1;\n return [...rows].sort((a, b) => {\n const av = get(a);\n const bv = get(b);\n if (av == null && bv == null) return 0;\n if (av == null) return 1;\n if (bv == null) return -1;\n if (av < bv) return -1 * dir;\n if (av > bv) return 1 * dir;\n return 0;\n });\n }, [rows, columns, sort]);\n\n const cycleSort = (columnId: string) => {\n setSort((cur) => {\n if (!cur || cur.columnId !== columnId) return { columnId, direction: 'asc' };\n if (cur.direction === 'asc') return { columnId, direction: 'desc' };\n return null;\n });\n };\n\n // Drag handles via useReorderDragList. Adapter: the hook expects\n // `{ id, label }`-shaped items; we map our rows to that shape (label\n // unused — the hook only uses id and locked).\n const drag = useReorderDragList({\n items: sortedRows.map((r) => ({ id: r.id, label: '' })),\n selectedIds: [],\n onReorder: onReorder ?? (() => {}),\n });\n const dragEnabled = !!onReorder;\n\n const cls = [s.grid, className].filter(Boolean).join(' ');\n\n return (\n <div\n className={cls}\n ref={dragEnabled ? (drag.containerProps.ref as React.RefCallback<HTMLDivElement>) : undefined}\n onPointerMove={dragEnabled ? drag.containerProps.onPointerMove : undefined}\n onPointerUp={dragEnabled ? drag.containerProps.onPointerUp : undefined}\n onPointerCancel={dragEnabled ? drag.containerProps.onPointerCancel : undefined}\n >\n <table className={s.table}>\n <thead>\n <tr>\n {dragEnabled && <th className={s.handleCol} aria-label=\"drag\" />}\n {columns.map((col) => {\n const sortable = col.sortable !== false;\n const active = sort?.columnId === col.id;\n const indicator = active ? (sort.direction === 'asc' ? ' ↑' : ' ↓') : '';\n return (\n <th\n key={col.id}\n className={[col.className, sortable ? s.sortable : '', active ? s.sortActive : '']\n .filter(Boolean).join(' ')}\n onClick={sortable ? () => cycleSort(col.id) : undefined}\n >\n {col.header}{indicator}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sortedRows.length === 0 && (\n <tr>\n <td colSpan={columns.length + (dragEnabled ? 1 : 0)} className={s.empty}>\n {empty}\n </td>\n </tr>\n )}\n {sortedRows.map((row, i) => {\n const isDragging = drag.state.draggedIds?.includes(row.id) ?? false;\n return (\n <Fragment key={row.id}>\n <tr className={isDragging ? s.dragging : undefined}>\n {dragEnabled && (\n <td\n className={s.handleCell}\n onPointerDown={(e) => drag.rowProps(row.id, i).onPointerDown(e)}\n >\n <span aria-hidden=\"true\">⋮⋮</span>\n </td>\n )}\n {columns.map((col) => {\n const get = col.accessor ?? ((r: Row) => (r as unknown as Record<string, unknown>)[col.id] as string | number | null | undefined);\n const content = col.render ? col.render(row) : String(get(row) ?? '');\n return (\n <td key={col.id} className={col.className}>\n {content}\n </td>\n );\n })}\n </tr>\n </Fragment>\n );\n })}\n </tbody>\n </table>\n {dragEnabled && drag.state.targetIndex !== null && (\n <div\n className={s.dropIndicator}\n style={{ top: `${drag.state.targetIndex * 28}px` }}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DataGrid-BQklXGRl.js","names":[],"sources":["../../src/useReorderDragList.ts","../../src/components/DataGrid/DataGrid.module.css","../../src/components/DataGrid/DataGrid.tsx"],"sourcesContent":["import { useCallback, useRef, useState } from 'react';\nimport type { ReactNode, PointerEvent as ReactPointerEvent, RefCallback } from 'react';\n\n/** One row in a reorderable list. */\nexport interface LayerListItem {\n id: string;\n label: ReactNode;\n /** Locked rows cannot be dragged, cannot be crossed by drops, and\n * never combine with other rows in a multi-selection. */\n locked?: boolean;\n /** Optional color swatch rendered before the label. Any CSS color string. */\n swatch?: string;\n}\n\n/**\n * Options for {@link useReorderDragList}. `onReorder` receives the dragged ids\n * and the index they were dropped at, measured against the pre-drag `items`.\n */\nexport interface UseReorderDragListOptions {\n items: LayerListItem[];\n selectedIds: string[];\n onReorder(ids: string[], targetIndex: number): void;\n /** Pointer-move distance (px) before pending drag engages. Default 4. */\n threshold?: number;\n}\n\n/**\n * Live drag state for rendering feedback: which ids are being dragged and the\n * insertion index the drop would use. Both `null` when no drag is engaged.\n */\nexport interface ReorderDragState {\n draggedIds: string[] | null;\n targetIndex: number | null;\n}\n\n/**\n * Props to spread onto the list container and each row, plus the live\n * {@link ReorderDragState}.\n */\nexport interface ReorderDragHandlers {\n rowProps(id: string, index: number): { onPointerDown(e: ReactPointerEvent): void };\n containerProps: {\n ref: RefCallback<HTMLElement>;\n onPointerMove(e: ReactPointerEvent): void;\n onPointerUp(e: ReactPointerEvent): void;\n onPointerCancel(e: ReactPointerEvent): void;\n };\n state: ReorderDragState;\n}\n\ninterface PendingState {\n id: string;\n sourceIndex: number;\n startX: number;\n startY: number;\n pointerId: number;\n}\n\ninterface ActiveState extends PendingState {\n draggedIds: string[];\n targetIndex: number;\n}\n\n/**\n * Drag-to-reorder for a vertical list of rows. Dragging a row that is part of\n * the current selection drags the whole selection; dragging any other row\n * drags just that row. Locked rows can neither be dragged nor crossed by a\n * drop. A drop that would leave a contiguous block where it already is does\n * not call `onReorder`.\n *\n * The pointer is captured on the row, so a drag that leaves the list still\n * tracks and still releases cleanly.\n */\nexport function useReorderDragList(opts: UseReorderDragListOptions): ReorderDragHandlers {\n const { threshold = 4 } = opts;\n const optsRef = useRef(opts);\n optsRef.current = opts;\n const containerRef = useRef<HTMLElement | null>(null);\n const pendingRef = useRef<PendingState | null>(null);\n const activeRef = useRef<ActiveState | null>(null);\n const [state, setState] = useState<ReorderDragState>({ draggedIds: null, targetIndex: null });\n\n const computeTargetIndex = useCallback((clientY: number): number => {\n const items = optsRef.current.items;\n // Locked rows act as walls — drops cannot cross them. Cap at the\n // first locked row's index (or items.length if none are locked).\n const firstLocked = items.findIndex((it) => it.locked);\n const cap = firstLocked === -1 ? items.length : firstLocked;\n const c = containerRef.current;\n if (!c) return 0;\n const rows = Array.from(c.children) as HTMLElement[];\n let raw = rows.length;\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i];\n if (!row) continue;\n const r = row.getBoundingClientRect();\n if (clientY < r.bottom) { raw = i; break; }\n }\n return Math.min(raw, cap);\n }, []);\n\n const refCb = useCallback<RefCallback<HTMLElement>>((el) => {\n containerRef.current = el;\n }, []);\n\n const onPointerDownRow = useCallback((id: string, index: number, e: ReactPointerEvent) => {\n // Locked items cannot be dragged — skip recording the pending state so\n // pointer-move cannot engage. Plain click still works because LayerList\n // tracks click intent in its own ref, separate from drag candidacy.\n const item = optsRef.current.items[index];\n if (item?.locked) return;\n // Capture the pointer to the row so subsequent move/up events keep firing\n // (on the row, bubbling to the container) even when the user drags\n // outside the list's bounding box. Without capture, releasing outside\n // the container leaves the drag state stuck because pointerup fires on\n // the document, not the list.\n try { (e.currentTarget as Element).setPointerCapture?.(e.pointerId); } catch { /* unsupported / already captured */ }\n pendingRef.current = {\n id,\n sourceIndex: index,\n startX: e.clientX,\n startY: e.clientY,\n pointerId: e.pointerId,\n };\n }, []);\n\n const reset = useCallback(() => {\n pendingRef.current = null;\n activeRef.current = null;\n setState({ draggedIds: null, targetIndex: null });\n }, []);\n\n const onPointerMove = useCallback((e: ReactPointerEvent) => {\n const active = activeRef.current;\n if (active) {\n const targetIndex = computeTargetIndex(e.clientY);\n if (targetIndex !== active.targetIndex) {\n active.targetIndex = targetIndex;\n setState({ draggedIds: active.draggedIds, targetIndex });\n }\n return;\n }\n const pending = pendingRef.current;\n if (!pending) return;\n const dx = e.clientX - pending.startX;\n const dy = e.clientY - pending.startY;\n if (Math.hypot(dx, dy) < threshold) return;\n // Engage.\n const selected = optsRef.current.selectedIds;\n const inSelection = selected.includes(pending.id);\n const draggedIds = inSelection ? [...selected] : [pending.id];\n const targetIndex = computeTargetIndex(e.clientY);\n activeRef.current = { ...pending, draggedIds, targetIndex };\n setState({ draggedIds, targetIndex });\n }, [computeTargetIndex, threshold]);\n\n const onPointerUp = useCallback((e: ReactPointerEvent) => {\n const active = activeRef.current;\n if (!active) {\n reset();\n return;\n }\n const targetIndex = computeTargetIndex(e.clientY);\n // Skip no-op: target lands inside the dragged-ids contiguous block.\n const items = optsRef.current.items;\n const indices = active.draggedIds\n .map((id) => items.findIndex((it) => it.id === id))\n .filter((i) => i >= 0)\n .sort((a, b) => a - b);\n const isContiguous =\n indices.length > 0 &&\n indices.every((v, i) => {\n if (i === 0) return true;\n const prev = indices[i - 1];\n return prev !== undefined && v === prev + 1;\n });\n const first = indices[0];\n const last = indices[indices.length - 1];\n const wouldBeNoop =\n isContiguous &&\n first !== undefined &&\n last !== undefined &&\n targetIndex >= first &&\n targetIndex <= last + 1;\n if (!wouldBeNoop) {\n optsRef.current.onReorder(active.draggedIds, targetIndex);\n }\n reset();\n }, [computeTargetIndex, reset]);\n\n const onPointerCancel = useCallback(() => {\n reset();\n }, [reset]);\n\n return {\n rowProps: (id, index) => ({\n onPointerDown: (e) => onPointerDownRow(id, index, e),\n }),\n containerProps: {\n ref: refCb,\n onPointerMove,\n onPointerUp,\n onPointerCancel,\n },\n state,\n };\n}\n",".grid {\n position: relative;\n width: 100%;\n}\n\n.table {\n width: 100%;\n border-collapse: collapse;\n font-size: 11px;\n}\n\n.table th,\n.table td {\n text-align: left;\n padding: 3px 6px;\n vertical-align: middle;\n}\n.table tbody tr {\n /* Subtle row separator — was a full-weight border on every cell.\n Drastically toned down per visual-weight feedback. */\n border-bottom: 1px solid rgba(255, 255, 255, 0.04);\n}\n\n.table th {\n font-weight: 600;\n color: var(--wzl-fg-muted);\n position: sticky;\n top: 0;\n background: var(--wzl-surface, #2a2018);\n z-index: 1;\n border-bottom: 1px solid var(--wzl-border);\n}\n\n.sortable {\n cursor: pointer;\n user-select: none;\n}\n.sortable:hover { color: var(--wzl-fg, #e6e7e9); }\n.sortActive { color: var(--wzl-fg, #e6e7e9); }\n\n.empty {\n color: var(--wzl-fg-muted);\n font-style: italic;\n padding: 8px 0;\n text-align: center;\n}\n\n/* Drag-handle column. */\n.handleCol { width: 14px; }\n.handleCell {\n width: 14px;\n cursor: grab;\n color: var(--wzl-fg-muted);\n user-select: none;\n text-align: center;\n}\n.handleCell:active { cursor: grabbing; }\n\n.dragging { opacity: 0.4; }\n\n.dropIndicator {\n position: absolute;\n left: 0;\n right: 0;\n height: 2px;\n background: var(--wzl-accent, #7fb069);\n pointer-events: none;\n}\n","/**\n * Lightweight sortable data grid. Use for inspector tables — action\n * registries, route maps, telemetry rows. Intentionally narrow:\n *\n * - Sortable columns (click header to cycle asc → desc → none)\n * - Custom cell render via per-column `render`\n * - Optional drag handles for row reordering (when `onReorder` is set)\n *\n * Not: virtual scrolling, column resizing, multi-column sort, filtering,\n * inline editing. Reach for a real grid library when you need those.\n */\nimport { Fragment, useMemo, useState } from 'react';\nimport type { ReactNode } from 'react';\nimport { useReorderDragList } from '../../useReorderDragList';\nimport s from './DataGrid.module.css';\n\n/**\n * One column of a {@link DataGrid}. `id` doubles as the default property name\n * read off each row when no `accessor` is given.\n */\nexport interface DataGridColumn<Row> {\n id: string;\n header: ReactNode;\n /** Pull a sortable value from the row. Defaults to `(row as any)[column.id]`. */\n accessor?: (row: Row) => string | number | null | undefined;\n /** Render the cell. Defaults to `String(accessor(row) ?? '')`. */\n render?: (row: Row) => ReactNode;\n /** Default true. Set false to suppress the sort affordance on this column. */\n sortable?: boolean;\n /** Optional CSS class on every <td> in this column. */\n className?: string;\n}\n\n/** Props for {@link DataGrid}. */\nexport interface DataGridProps<Row extends { id: string }> {\n rows: readonly Row[];\n columns: readonly DataGridColumn<Row>[];\n /** Initial sort. Omit for unsorted (rows render in input order). */\n defaultSort?: { columnId: string; direction: 'asc' | 'desc' };\n /** Enable drag handles in a leading column. Receives the reorder spec\n * (the existing `useReorderDragList` semantics — see weasel-ui). */\n onReorder?: (ids: string[], targetIndex: number) => void;\n /** Empty-state row text. Default `'—'`. */\n empty?: ReactNode;\n className?: string;\n}\n\ntype SortState = { columnId: string; direction: 'asc' | 'desc' } | null;\n\n/**\n * Sortable table for inspector-style data. Rows are keyed by `id`. Clicking a\n * sortable header cycles ascending, descending, unsorted; nullish values sort\n * last regardless of direction. Passing `onReorder` adds a leading drag-handle\n * column — note that the drag indices it reports are into the *sorted* row\n * order, not the input order.\n */\nexport function DataGrid<Row extends { id: string }>(props: DataGridProps<Row>) {\n const { rows, columns, defaultSort, onReorder, empty = '—', className } = props;\n const [sort, setSort] = useState<SortState>(defaultSort ?? null);\n\n const sortedRows = useMemo(() => {\n if (!sort) return [...rows];\n const col = columns.find((c) => c.id === sort.columnId);\n if (!col) return [...rows];\n const get = col.accessor ?? ((row: Row) => (row as unknown as Record<string, unknown>)[col.id] as string | number | null | undefined);\n const dir = sort.direction === 'asc' ? 1 : -1;\n return [...rows].sort((a, b) => {\n const av = get(a);\n const bv = get(b);\n if (av == null && bv == null) return 0;\n if (av == null) return 1;\n if (bv == null) return -1;\n if (av < bv) return -1 * dir;\n if (av > bv) return 1 * dir;\n return 0;\n });\n }, [rows, columns, sort]);\n\n const cycleSort = (columnId: string) => {\n setSort((cur) => {\n if (!cur || cur.columnId !== columnId) return { columnId, direction: 'asc' };\n if (cur.direction === 'asc') return { columnId, direction: 'desc' };\n return null;\n });\n };\n\n // Drag handles via useReorderDragList. Adapter: the hook expects\n // `{ id, label }`-shaped items; we map our rows to that shape (label\n // unused — the hook only uses id and locked).\n const drag = useReorderDragList({\n items: sortedRows.map((r) => ({ id: r.id, label: '' })),\n selectedIds: [],\n onReorder: onReorder ?? (() => {}),\n });\n const dragEnabled = !!onReorder;\n\n const cls = [s.grid, className].filter(Boolean).join(' ');\n\n return (\n <div\n className={cls}\n ref={dragEnabled ? (drag.containerProps.ref as React.RefCallback<HTMLDivElement>) : undefined}\n onPointerMove={dragEnabled ? drag.containerProps.onPointerMove : undefined}\n onPointerUp={dragEnabled ? drag.containerProps.onPointerUp : undefined}\n onPointerCancel={dragEnabled ? drag.containerProps.onPointerCancel : undefined}\n >\n <table className={s.table}>\n <thead>\n <tr>\n {dragEnabled && <th className={s.handleCol} aria-label=\"drag\" />}\n {columns.map((col) => {\n const sortable = col.sortable !== false;\n const active = sort?.columnId === col.id;\n const indicator = active ? (sort.direction === 'asc' ? ' ↑' : ' ↓') : '';\n return (\n <th\n key={col.id}\n className={[col.className, sortable ? s.sortable : '', active ? s.sortActive : '']\n .filter(Boolean).join(' ')}\n onClick={sortable ? () => cycleSort(col.id) : undefined}\n >\n {col.header}{indicator}\n </th>\n );\n })}\n </tr>\n </thead>\n <tbody>\n {sortedRows.length === 0 && (\n <tr>\n <td colSpan={columns.length + (dragEnabled ? 1 : 0)} className={s.empty}>\n {empty}\n </td>\n </tr>\n )}\n {sortedRows.map((row, i) => {\n const isDragging = drag.state.draggedIds?.includes(row.id) ?? false;\n return (\n <Fragment key={row.id}>\n <tr className={isDragging ? s.dragging : undefined}>\n {dragEnabled && (\n <td\n className={s.handleCell}\n onPointerDown={(e) => drag.rowProps(row.id, i).onPointerDown(e)}\n >\n <span aria-hidden=\"true\">⋮⋮</span>\n </td>\n )}\n {columns.map((col) => {\n const get = col.accessor ?? ((r: Row) => (r as unknown as Record<string, unknown>)[col.id] as string | number | null | undefined);\n const content = col.render ? col.render(row) : String(get(row) ?? '');\n return (\n <td key={col.id} className={col.className}>\n {content}\n </td>\n );\n })}\n </tr>\n </Fragment>\n );\n })}\n </tbody>\n </table>\n {dragEnabled && drag.state.targetIndex !== null && (\n <div\n className={s.dropIndicator}\n style={{ top: `${drag.state.targetIndex * 28}px` }}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Dialog-3FCd_gYi.js","names":[],"sources":["../../src/components/Dialog/Dialog.module.css","../../src/components/Dialog/Dialog.tsx"],"sourcesContent":[".overlay {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.4);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n opacity: 0;\n transition: opacity var(--wzl-motion-fast);\n}\n\n.overlay[data-entering] {\n opacity: 0;\n}\n\n.overlay[data-entering][data-rac] {\n opacity: 1;\n}\n\n.overlay[data-exiting] {\n opacity: 0;\n}\n\n.overlay[data-rac]:not([data-entering]):not([data-exiting]) {\n opacity: 1;\n}\n\n.modal {\n background: var(--wzl-surface);\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-md);\n box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);\n color: var(--wzl-fg);\n font-family: var(--wzl-font-ui);\n max-width: min(900px, calc(100vw - 32px));\n max-height: calc(100vh - 32px);\n display: flex;\n flex-direction: column;\n outline: none;\n}\n\n.dialog {\n display: flex;\n flex-direction: column;\n min-height: 0;\n flex: 1;\n outline: none;\n}\n\n.header {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-bottom: 1px solid var(--wzl-border);\n flex: 0 0 auto;\n}\n\n.title {\n flex: 1;\n margin: 0;\n font-size: 14px;\n font-weight: 600;\n color: var(--wzl-fg);\n}\n\n.close {\n flex: 0 0 auto;\n width: 22px;\n height: 22px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n border: 1px solid transparent;\n border-radius: var(--wzl-radius-sm);\n color: var(--wzl-fg-muted);\n cursor: pointer;\n padding: 0;\n font-size: 16px;\n line-height: 1;\n}\n\n.close:hover {\n background: color-mix(in srgb, var(--wzl-fg) 10%, transparent);\n color: var(--wzl-fg);\n}\n\n.body {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 16px;\n}\n\n.footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 16px;\n border-top: 1px solid var(--wzl-border);\n flex: 0 0 auto;\n}\n","import type { ReactNode } from 'react';\nimport {\n Modal as RACModal,\n ModalOverlay,\n Dialog as RACDialog,\n Heading,\n type ModalOverlayProps,\n type DialogProps as RACDialogProps,\n} from 'react-aria-components';\nimport s from './Dialog.module.css';\n\nexport type DialogProps = Omit<ModalOverlayProps, 'children' | 'className'> & {\n /** Heading rendered in the dialog's default header. Omit when supplying\n * a custom `header` slot via children. */\n title?: ReactNode;\n /** Show the built-in close button. Defaults to true when `onOpenChange`\n * is wired so the user has an escape hatch. */\n showCloseButton?: boolean;\n /** Body content. */\n children?: ReactNode;\n /** Footer slot — typically action buttons. */\n footer?: ReactNode;\n /** ARIA role. Defaults to `dialog`. Use `alertdialog` for confirms. */\n role?: RACDialogProps['role'];\n /** Class applied to the modal box (inside the overlay). */\n className?: string;\n};\n\n/**\n * Modal dialog wrapping React Aria. Supplies a default header (title +\n * close), scrollable body, and footer slot. Pass `isOpen` + `onOpenChange`\n * for controlled visibility, or omit both and use a `<DialogTrigger>` from\n * `react-aria-components` upstream of this component.\n *\n * Escape, click-outside, focus trap, and scroll lock all come from the\n * underlying primitives.\n */\nexport function Dialog(props: DialogProps) {\n const {\n title,\n showCloseButton,\n children,\n footer,\n role = 'dialog',\n className,\n isOpen,\n onOpenChange,\n ...rest\n } = props;\n\n const showClose = showCloseButton ?? Boolean(onOpenChange);\n\n return (\n <ModalOverlay {...rest} isOpen={isOpen} onOpenChange={onOpenChange} className={s.overlay}>\n <RACModal className={[s.modal, className].filter(Boolean).join(' ')}>\n <RACDialog role={role} className={s.dialog}>\n {({ close }) => (\n <>\n {(title !== undefined || showClose) && (\n <header className={s.header}>\n {title !== undefined && (\n <Heading slot=\"title\" className={s.title}>{title}</Heading>\n )}\n {showClose && (\n <button\n type=\"button\"\n className={s.close}\n onClick={close}\n aria-label=\"Close dialog\"\n >\n ×\n </button>\n )}\n </header>\n )}\n <div className={s.body}>{children}</div>\n {footer !== undefined && <footer className={s.footer}>{footer}</footer>}\n </>\n )}\n </RACDialog>\n </RACModal>\n </ModalOverlay>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;ACqCA,SAAgB,EAAO,GAAoB;CACzC,IAAM,EACJ,UACA,oBACA,aACA,WACA,UAAO,UACP,cACA,WACA,iBACA,GAAG,MACD,GAEE,IAAY,KAAmB,EAAQ;AAE7C,QACE,kBAAC,GAAD;EAAc,GAAI;EAAc;EAAsB;EAAc,WAAW,EAAE;YAC/E,kBAAC,GAAD;GAAU,WAAW,CAAC,EAAE,OAAO,EAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI;aACjE,kBAAC,GAAD;IAAiB;IAAM,WAAW,EAAE;eAChC,EAAE,eACF,kBAAA,GAAA,EAAA,UAAA;MACI,MAAU,KAAA,KAAa,MACvB,kBAAC,UAAD;MAAQ,WAAW,EAAE;gBAArB,CACG,MAAU,KAAA,KACT,kBAAC,GAAD;OAAS,MAAK;OAAQ,WAAW,EAAE;iBAAQ;OAAgB,CAAA,EAE5D,KACC,kBAAC,UAAD;OACE,MAAK;OACL,WAAW,EAAE;OACb,SAAS;OACT,cAAW;iBACZ;OAEQ,CAAA,CAEJ;;KAEX,kBAAC,OAAD;MAAK,WAAW,EAAE;MAAO;MAAe,CAAA;KACvC,MAAW,KAAA,KAAa,kBAAC,UAAD;MAAQ,WAAW,EAAE;gBAAS;MAAgB,CAAA;KACtE,EAAA,CAAA;IAEK,CAAA;GACH,CAAA;EACE,CAAA"}
1
+ {"version":3,"file":"Dialog-3FCd_gYi.js","names":[],"sources":["../../src/components/Dialog/Dialog.module.css","../../src/components/Dialog/Dialog.tsx"],"sourcesContent":[".overlay {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.4);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1000;\n opacity: 0;\n transition: opacity var(--wzl-motion-fast);\n}\n\n.overlay[data-entering] {\n opacity: 0;\n}\n\n.overlay[data-entering][data-rac] {\n opacity: 1;\n}\n\n.overlay[data-exiting] {\n opacity: 0;\n}\n\n.overlay[data-rac]:not([data-entering]):not([data-exiting]) {\n opacity: 1;\n}\n\n.modal {\n background: var(--wzl-surface);\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-md);\n box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);\n color: var(--wzl-fg);\n font-family: var(--wzl-font-ui);\n max-width: min(900px, calc(100vw - 32px));\n max-height: calc(100vh - 32px);\n display: flex;\n flex-direction: column;\n outline: none;\n}\n\n.dialog {\n display: flex;\n flex-direction: column;\n min-height: 0;\n flex: 1;\n outline: none;\n}\n\n.header {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-bottom: 1px solid var(--wzl-border);\n flex: 0 0 auto;\n}\n\n.title {\n flex: 1;\n margin: 0;\n font-size: 14px;\n font-weight: 600;\n color: var(--wzl-fg);\n}\n\n.close {\n flex: 0 0 auto;\n width: 22px;\n height: 22px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n border: 1px solid transparent;\n border-radius: var(--wzl-radius-sm);\n color: var(--wzl-fg-muted);\n cursor: pointer;\n padding: 0;\n font-size: 16px;\n line-height: 1;\n}\n\n.close:hover {\n background: color-mix(in srgb, var(--wzl-fg) 10%, transparent);\n color: var(--wzl-fg);\n}\n\n.body {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 16px;\n}\n\n.footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 16px;\n border-top: 1px solid var(--wzl-border);\n flex: 0 0 auto;\n}\n","import type { ReactNode } from 'react';\nimport {\n Modal as RACModal,\n ModalOverlay,\n Dialog as RACDialog,\n Heading,\n type ModalOverlayProps,\n type DialogProps as RACDialogProps,\n} from 'react-aria-components';\nimport s from './Dialog.module.css';\n\n/** Props for {@link Dialog}, on top of React Aria's `ModalOverlay` props. */\nexport type DialogProps = Omit<ModalOverlayProps, 'children' | 'className'> & {\n /** Heading rendered in the dialog's default header. Omit when supplying\n * a custom `header` slot via children. */\n title?: ReactNode;\n /** Show the built-in close button. Defaults to true when `onOpenChange`\n * is wired so the user has an escape hatch. */\n showCloseButton?: boolean;\n /** Body content. */\n children?: ReactNode;\n /** Footer slot — typically action buttons. */\n footer?: ReactNode;\n /** ARIA role. Defaults to `dialog`. Use `alertdialog` for confirms. */\n role?: RACDialogProps['role'];\n /** Class applied to the modal box (inside the overlay). */\n className?: string;\n};\n\n/**\n * Modal dialog wrapping React Aria. Supplies a default header (title +\n * close), scrollable body, and footer slot. Pass `isOpen` + `onOpenChange`\n * for controlled visibility, or omit both and use a `<DialogTrigger>` from\n * `react-aria-components` upstream of this component.\n *\n * Escape, click-outside, focus trap, and scroll lock all come from the\n * underlying primitives.\n */\nexport function Dialog(props: DialogProps) {\n const {\n title,\n showCloseButton,\n children,\n footer,\n role = 'dialog',\n className,\n isOpen,\n onOpenChange,\n ...rest\n } = props;\n\n const showClose = showCloseButton ?? Boolean(onOpenChange);\n\n return (\n <ModalOverlay {...rest} isOpen={isOpen} onOpenChange={onOpenChange} className={s.overlay}>\n <RACModal className={[s.modal, className].filter(Boolean).join(' ')}>\n <RACDialog role={role} className={s.dialog}>\n {({ close }) => (\n <>\n {(title !== undefined || showClose) && (\n <header className={s.header}>\n {title !== undefined && (\n <Heading slot=\"title\" className={s.title}>{title}</Heading>\n )}\n {showClose && (\n <button\n type=\"button\"\n className={s.close}\n onClick={close}\n aria-label=\"Close dialog\"\n >\n ×\n </button>\n )}\n </header>\n )}\n <div className={s.body}>{children}</div>\n {footer !== undefined && <footer className={s.footer}>{footer}</footer>}\n </>\n )}\n </RACDialog>\n </RACModal>\n </ModalOverlay>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;ACsCA,SAAgB,EAAO,GAAoB;CACzC,IAAM,EACJ,UACA,oBACA,aACA,WACA,UAAO,UACP,cACA,WACA,iBACA,GAAG,MACD,GAEE,IAAY,KAAmB,EAAQ;AAE7C,QACE,kBAAC,GAAD;EAAc,GAAI;EAAc;EAAsB;EAAc,WAAW,EAAE;YAC/E,kBAAC,GAAD;GAAU,WAAW,CAAC,EAAE,OAAO,EAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI;aACjE,kBAAC,GAAD;IAAiB;IAAM,WAAW,EAAE;eAChC,EAAE,eACF,kBAAA,GAAA,EAAA,UAAA;MACI,MAAU,KAAA,KAAa,MACvB,kBAAC,UAAD;MAAQ,WAAW,EAAE;gBAArB,CACG,MAAU,KAAA,KACT,kBAAC,GAAD;OAAS,MAAK;OAAQ,WAAW,EAAE;iBAAQ;OAAgB,CAAA,EAE5D,KACC,kBAAC,UAAD;OACE,MAAK;OACL,WAAW,EAAE;OACb,SAAS;OACT,cAAW;iBACZ;OAEQ,CAAA,CAEJ;;KAEX,kBAAC,OAAD;MAAK,WAAW,EAAE;MAAO;MAAe,CAAA;KACvC,MAAW,KAAA,KAAa,kBAAC,UAAD;MAAQ,WAAW,EAAE;gBAAS;MAAgB,CAAA;KACtE,EAAA,CAAA;IAEK,CAAA;GACH,CAAA;EACE,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"Field-VQwmrIna.js","names":[],"sources":["../../src/components/Field/Field.module.css","../../src/components/Field/Field.tsx"],"sourcesContent":[".field {\n display: flex;\n flex-direction: column;\n gap: 4px;\n font-family: var(--wzl-font-ui);\n color: var(--wzl-fg);\n}\n\n.label {\n font-size: 12px;\n font-weight: 500;\n color: var(--wzl-fg);\n cursor: default;\n}\n\n.hint {\n font-size: 11px;\n color: var(--wzl-fg-subtle);\n line-height: 1.4;\n}\n\n.error {\n font-size: 11px;\n color: var(--wzl-danger, #ff5b5b);\n line-height: 1.4;\n}\n\n.row {\n flex-direction: row;\n align-items: center;\n gap: 8px;\n}\n\n.row .label {\n flex: 1;\n min-width: 0;\n}\n","import type { ReactNode } from 'react';\nimport s from './Field.module.css';\n\nexport type FieldOrientation = 'stacked' | 'row';\n\nexport type FieldProps = {\n orientation?: FieldOrientation;\n className?: string;\n children?: ReactNode;\n};\n\n/**\n * Layout primitive that arranges a label, control, hint, and error in the\n * canonical weasel-ui form-row shape. The control itself owns its semantics\n * (RAC `TextField`, `Checkbox`, etc.); this just paints the layout.\n *\n * Most users will instead reach for `Input` / `Checkbox` / `Switch`, which\n * compose Field internally. Use Field directly when you need a custom\n * control to sit in the same visual rhythm.\n */\nexport function Field({ orientation = 'stacked', className, children }: FieldProps) {\n const cls = [s.field, orientation === 'row' && s.row, className].filter(Boolean).join(' ');\n return <div className={cls}>{children}</div>;\n}\n\nexport const fieldClasses = {\n root: s.field,\n row: s.row,\n label: s.label,\n hint: s.hint,\n error: s.error,\n};\n"],"mappings":";;;;;;;;;;ACoBA,SAAgB,EAAM,EAAE,iBAAc,WAAW,cAAW,eAAwB;AAElF,QAAO,kBAAC,OAAD;EAAK,WADA;GAAC,EAAE;GAAO,MAAgB,SAAS,EAAE;GAAK;GAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAC/D;EAAM;EAAe,CAAA;;AAG9C,IAAa,IAAe;CAC1B,MAAM,EAAE;CACR,KAAK,EAAE;CACP,OAAO,EAAE;CACT,MAAM,EAAE;CACR,OAAO,EAAE;CACV"}
1
+ {"version":3,"file":"Field-VQwmrIna.js","names":[],"sources":["../../src/components/Field/Field.module.css","../../src/components/Field/Field.tsx"],"sourcesContent":[".field {\n display: flex;\n flex-direction: column;\n gap: 4px;\n font-family: var(--wzl-font-ui);\n color: var(--wzl-fg);\n}\n\n.label {\n font-size: 12px;\n font-weight: 500;\n color: var(--wzl-fg);\n cursor: default;\n}\n\n.hint {\n font-size: 11px;\n color: var(--wzl-fg-subtle);\n line-height: 1.4;\n}\n\n.error {\n font-size: 11px;\n color: var(--wzl-danger, #ff5b5b);\n line-height: 1.4;\n}\n\n.row {\n flex-direction: row;\n align-items: center;\n gap: 8px;\n}\n\n.row .label {\n flex: 1;\n min-width: 0;\n}\n","import type { ReactNode } from 'react';\nimport s from './Field.module.css';\n\n/**\n * Whether a {@link Field} stacks its label above the control or sits them\n * side by side.\n */\nexport type FieldOrientation = 'stacked' | 'row';\n\n/** Props for {@link Field}. */\nexport type FieldProps = {\n orientation?: FieldOrientation;\n className?: string;\n children?: ReactNode;\n};\n\n/**\n * Layout primitive that arranges a label, control, hint, and error in the\n * canonical weasel-ui form-row shape. The control itself owns its semantics\n * (RAC `TextField`, `Checkbox`, etc.); this just paints the layout.\n *\n * Most users will instead reach for `Input` / `Checkbox` / `Switch`, which\n * compose Field internally. Use Field directly when you need a custom\n * control to sit in the same visual rhythm.\n */\nexport function Field({ orientation = 'stacked', className, children }: FieldProps) {\n const cls = [s.field, orientation === 'row' && s.row, className].filter(Boolean).join(' ');\n return <div className={cls}>{children}</div>;\n}\n\n/**\n * The class names {@link Field} paints its slots with, so a control composing\n * its own layout can match the same form-row rhythm.\n */\nexport const fieldClasses = {\n root: s.field,\n row: s.row,\n label: s.label,\n hint: s.hint,\n error: s.error,\n};\n"],"mappings":";;;;;;;;;;ACyBA,SAAgB,EAAM,EAAE,iBAAc,WAAW,cAAW,eAAwB;AAElF,QAAO,kBAAC,OAAD;EAAK,WADA;GAAC,EAAE;GAAO,MAAgB,SAAS,EAAE;GAAK;GAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAC/D;EAAM;EAAe,CAAA;;AAO9C,IAAa,IAAe;CAC1B,MAAM,EAAE;CACR,KAAK,EAAE;CACP,OAAO,EAAE;CACT,MAAM,EAAE;CACR,OAAO,EAAE;CACV"}
@@ -1,4 +1,4 @@
1
- import { t as e } from "./Slider-Di2kNqVy.js";
1
+ import { t as e } from "./Slider-CmPeFJ3x.js";
2
2
  import { t } from "./ToggleBar-BP1zFcvg.js";
3
3
  import { t as n } from "./ColorField-ykpiD3XU.js";
4
4
  import { useCallback as r, useRef as i } from "react";
@@ -90,8 +90,8 @@ function _(i) {
90
90
  thumbs: b,
91
91
  ariaLabel: "Gradient stops",
92
92
  readoutPlacement: "none",
93
- onChange: (e) => s?.(y(x(e))),
94
- onCommit: (e) => l(y(x(e))),
93
+ onInput: (e) => s?.(y(x(e))),
94
+ onChange: (e) => l(y(x(e))),
95
95
  onAddThumb: (e) => ({
96
96
  value: e,
97
97
  color: o(v, e)
@@ -257,4 +257,4 @@ var w = 7;
257
257
  //#endregion
258
258
  export { _ as n, p as r, y as t };
259
259
 
260
- //# sourceMappingURL=GradientEditor-BL5kg7a_.js.map
260
+ //# sourceMappingURL=GradientEditor-DeptQpWX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GradientEditor-DeptQpWX.js","names":[],"sources":["../../src/paintGradientTrack.tsx","../../src/components/GradientEditor/GradientEditor.module.css","../../src/components/GradientEditor/GradientEditor.tsx","../../src/components/GradientEditor/GradientHandles.module.css","../../src/components/GradientEditor/GradientHandles.tsx"],"sourcesContent":["import type { ReactNode, CSSProperties } from 'react';\nimport type { TrackCtx } from './components/Slider';\n\n/**\n * Options for {@link paintGradientTrack}.\n *\n * `gradient` maps a normalized position along the track (0 to 1) to a CSS\n * color; `samples` is how many stops the resulting linear-gradient uses.\n * `activeRange`, given in the slider's own value units, keeps that span at\n * full strength and dims + hatches the rest.\n */\nexport type GradientTrackOpts = {\n gradient: (t: number) => string;\n samples?: number;\n activeRange?: [number, number];\n hatch?: {\n angleDeg?: number;\n stripe?: number;\n gap?: number;\n dim?: number;\n };\n};\n\nconst DEFAULT_HATCH = { angleDeg: 135, stripe: 2, gap: 4, dim: 75 };\n\n/**\n * Builds a `Slider` `renderTrack` function that paints the track as a\n * sampled color gradient, optionally dimming and hatching the portions\n * outside an active range.\n */\nexport function paintGradientTrack(opts: GradientTrackOpts): (ctx: TrackCtx) => ReactNode {\n const { gradient, samples = 16, activeRange, hatch } = opts;\n\n return (ctx: TrackCtx) => {\n const stops: string[] = [];\n for (let i = 0; i <= samples; i++) {\n const t = i / samples;\n stops.push(`${gradient(t)} ${(t * 100).toFixed(1)}%`);\n }\n const baseGradient = `linear-gradient(to right, ${stops.join(', ')})`;\n\n const layers: string[] = [];\n if (activeRange) {\n const lowPct = ctx.valueToFraction(activeRange[0]) * 100;\n const highPct = ctx.valueToFraction(activeRange[1]) * 100;\n const h = { ...DEFAULT_HATCH, ...hatch };\n const stripe = `repeating-linear-gradient(${h.angleDeg}deg, transparent 0 ${h.stripe}px, var(--wzl-surface) ${h.stripe}px ${h.stripe + h.gap}px)`;\n const dimColor = `color-mix(in srgb, var(--wzl-surface) ${h.dim}%, transparent)`;\n const dimOverlay = `linear-gradient(${dimColor}, ${dimColor})`;\n\n if (lowPct > 0) {\n layers.push(`${dimOverlay} left 0 / ${lowPct.toFixed(2)}% 100% no-repeat`);\n layers.push(`${stripe} left 0 / ${lowPct.toFixed(2)}% 100% no-repeat`);\n }\n if (highPct < 100) {\n const wR = (100 - highPct).toFixed(2);\n layers.push(`${dimOverlay} right 0 / ${wR}% 100% no-repeat`);\n layers.push(`${stripe} right 0 / ${wR}% 100% no-repeat`);\n }\n }\n layers.push(baseGradient);\n\n const style: CSSProperties = {\n position: 'absolute',\n inset: 0,\n background: layers.join(', '),\n };\n return <div style={style} />;\n };\n}\n",".root {\n display: flex;\n flex-direction: column;\n gap: 8px;\n min-inline-size: 0;\n}\n\n.swatches {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n/* Each stop's opacity slider would otherwise stretch the row to a width no\n * properties panel has; the chip stays legible, the slider yields. */\n.swatch {\n flex: 0 1 auto;\n min-inline-size: 0;\n}\n","import { useCallback, type ReactElement } from 'react';\nimport {\n sampleGradientStops,\n withGradientKind,\n type GradStop,\n type GradientFill,\n type GradientKind,\n} from '@weasel-js/core';\nimport { Slider, type Thumb } from '../Slider';\nimport { ColorField } from '../ColorField';\nimport { ToggleBar, type ToggleBarItem } from '../ToggleBar';\nimport { paintGradientTrack } from '../../paintGradientTrack';\nimport s from './GradientEditor.module.css';\n\nconst KINDS: readonly ToggleBarItem<GradientKind>[] = [\n { value: 'linear-gradient', label: 'Linear' },\n { value: 'radial-gradient', label: 'Radial' },\n { value: 'conic-gradient', label: 'Conic' },\n];\n\n/** Fewer than two stops is not a gradient any renderer can ramp between. */\nconst MIN_STOPS = 2;\n\n/**\n * Props for {@link GradientEditor}. `onInput` fires throughout a gesture and\n * `onChange` once at its end.\n */\nexport interface GradientEditorProps {\n /** The gradient being edited. */\n value: GradientFill;\n /**\n * Live value during a gesture — a stop drag, a color-picker scrub. Wire\n * it for preview; it fires many times per gesture and must not be\n * written to history.\n */\n onInput?: (next: GradientFill) => void;\n /** Committed value: one call per completed gesture. Pair with an\n * undoable write. */\n onChange: (next: GradientFill) => void;\n /** Show the linear / radial / conic switch. Default true; turn it off\n * when the surrounding UI already owns the kind. */\n kindSwitch?: boolean;\n className?: string;\n}\n\ntype StopThumb = Thumb & { color: string };\n\n/**\n * Editor for a gradient's kind and stop list.\n *\n * Geometry (`from`/`to`, `center`, `radius`, `angle`) is deliberately not\n * edited here — on a canvas that belongs on the artwork, via\n * `<GradientHandles>`. This component owns the parts with no spatial\n * meaning, so it composes into a properties panel at any width.\n *\n * Stops are addressed by their position in `value.stops`, which is never\n * reordered — dragging one stop past another leaves both indices alone, so\n * a drag can cross a neighbour without the two swapping under the pointer.\n * Rendering sorts a copy.\n */\nexport function GradientEditor(props: GradientEditorProps): ReactElement {\n const { value, onInput, onChange, kindSwitch = true, className } = props;\n const stops = value.stops;\n\n const withStops = useCallback(\n (next: GradStop[]): GradientFill => ({ ...value, stops: next }),\n [value],\n );\n\n const thumbs: StopThumb[] = stops.map((stop) => ({ value: stop.offset, color: stop.color }));\n\n const applyThumbs = (next: StopThumb[]): GradStop[] =>\n next.map((t) => ({ offset: t.value, color: t.color }));\n\n const setStopColor = (index: number, color: string): GradStop[] =>\n stops.map((stop, i) => (i === index ? { ...stop, color } : stop));\n\n // Sorted view for the swatch row, carrying each stop's real index so a\n // recolor writes back to the right entry.\n const ordered = stops\n .map((stop, index) => ({ stop, index }))\n .sort((a, b) => a.stop.offset - b.stop.offset);\n\n return (\n <div className={[s.root, className].filter(Boolean).join(' ')}>\n {kindSwitch && (\n <ToggleBar<GradientKind>\n items={KINDS}\n value={value.fill}\n size=\"sm\"\n ariaLabel=\"Gradient kind\"\n onChange={(kind) => kind && onChange(withGradientKind(value, kind))}\n />\n )}\n\n <Slider<StopThumb>\n min={0}\n max={1}\n step={0.005}\n constraint=\"free\"\n thumbs={thumbs}\n ariaLabel=\"Gradient stops\"\n readoutPlacement=\"none\"\n onInput={(next) => onInput?.(withStops(applyThumbs(next)))}\n onChange={(next) => onChange(withStops(applyThumbs(next)))}\n onAddThumb={(at) => ({ value: at, color: sampleGradientStops(stops, at) })}\n onRemoveThumb={() => stops.length > MIN_STOPS}\n renderTrack={paintGradientTrack({\n gradient: (t) => sampleGradientStops(stops, t),\n samples: 32,\n })}\n />\n\n <div className={s.swatches}>\n {ordered.map(({ stop, index }) => (\n <ColorField\n key={index}\n value={stop.color}\n alpha\n aria-label={`Stop ${index + 1} at ${Math.round(stop.offset * 100)}%`}\n className={s.swatch}\n onInput={(hex) => onInput?.(withStops(setStopColor(index, hex)))}\n onChange={(hex) => onChange(withStops(setStopColor(index, hex)))}\n />\n ))}\n </div>\n </div>\n );\n}\n","/* The overlay covers the canvas but must not intercept tool input; only the\n * handles opt back into hit-testing. */\n.overlay {\n position: absolute;\n inset: 0;\n pointer-events: none;\n}\n\n/* These sit over artwork of unknown color, not over the app's own surface,\n * so they are deliberately not themed: a themed handle disappears against\n * whatever the user painted underneath. White-on-dark reads on anything,\n * which is why every editor draws handles this way. */\n.guide {\n fill: none;\n stroke: #ffffff;\n stroke-opacity: 0.7;\n stroke-dasharray: 4 4;\n paint-order: stroke;\n}\n\n.handle {\n fill: #ffffff;\n stroke: #222222;\n stroke-width: 2;\n cursor: grab;\n pointer-events: auto;\n}\n\n.handle:active {\n cursor: grabbing;\n}\n\n.handle:focus-visible {\n outline: 2px solid var(--wzl-accent, #4a9eff);\n outline-offset: 2px;\n}\n","import { useRef, type ReactElement, type ReactNode } from 'react';\nimport { useHandleDrag, gradientGeometry, type GradientFill } from '@weasel-js/core';\nimport s from './GradientHandles.module.css';\n\n/** Structural, and deliberately not exported — `Plot2D` and `CurveEditor`\n * each publish their own `Point`, and a third would only be ambiguous at\n * the package barrel. Consumers pass any `{ x, y }`. */\ninterface Point {\n x: number;\n y: number;\n}\n\n/**\n * Props for {@link GradientHandles}. `onInput` fires throughout a drag and\n * `onChange` once at its end.\n */\nexport interface GradientHandlesProps {\n /**\n * The gradient whose geometry these handles move, in a **resolved,\n * isotropic** frame — one where `radius` is a length in the same units as\n * `center.x` and a right angle is a right angle.\n *\n * A `units: 'bounds'` gradient is not such a frame: `x` and `y` are\n * fractions of two different lengths, so a circle there is an ellipse on\n * screen and polar math silently mixes scales. Resolve it first with\n * `fillInPoseFrame(fill, box)` and convert edits back with\n * `fillToBoundsFrame(next, box)`.\n */\n value: GradientFill;\n /**\n * Gradient space → overlay pixels. For a `units: 'local'` gradient this\n * is the node's local-to-screen transform; for `'world'`, the view.\n */\n toScreen: (p: Point) => Point;\n /** Overlay pixels → gradient space. Must invert `toScreen`. */\n toLocal: (p: Point) => Point;\n /** Live during a drag — wire for preview, do not write to history. */\n onInput?: (next: GradientFill) => void;\n /** Committed at drag end: one call per gesture. */\n onChange: (next: GradientFill) => void;\n /** Overlay size in CSS pixels. */\n width: number;\n height: number;\n className?: string;\n}\n\n/**\n * Direct-manipulation handles for a gradient's geometry, drawn as an SVG\n * overlay above a canvas: endpoints for linear, center and radius for\n * radial, center and angle arm for conic.\n *\n * Positioning is entirely the consumer's `toScreen` / `toLocal` — this\n * component never sees a view or a scene node, so the same handles serve a\n * node-local gradient, a world-space one, and a plain unzoomed preview.\n *\n * The overlay ignores pointer events except on the handles themselves, so\n * it can sit over live canvas content without swallowing tool input.\n */\nexport function GradientHandles(props: GradientHandlesProps): ReactElement {\n const { value, toScreen, toLocal, onInput, onChange, width, height, className } = props;\n\n return (\n <svg\n className={[s.overlay, className].filter(Boolean).join(' ')}\n width={width}\n height={height}\n >\n {renderForKind(value, toScreen, toLocal, onInput, onChange)}\n </svg>\n );\n}\n\nfunction renderForKind(\n value: GradientFill,\n toScreen: (p: Point) => Point,\n toLocal: (p: Point) => Point,\n onInput: ((next: GradientFill) => void) | undefined,\n onChange: (next: GradientFill) => void,\n): ReactNode {\n const emit = (next: GradientFill, phase: 'input' | 'commit'): void => {\n if (phase === 'input') onInput?.(next);\n else onChange(next);\n };\n\n if (value.fill === 'linear-gradient') {\n const from = toScreen(value.from);\n const to = toScreen(value.to);\n return (\n <>\n <Guide x1={from.x} y1={from.y} x2={to.x} y2={to.y} />\n <DragPoint\n at={from}\n label=\"Gradient start\"\n onDrag={(p, phase) => emit({ ...value, from: toLocal(p) }, phase)}\n />\n <DragPoint\n at={to}\n label=\"Gradient end\"\n onDrag={(p, phase) => emit({ ...value, to: toLocal(p) }, phase)}\n />\n </>\n );\n }\n\n if (value.fill === 'radial-gradient') {\n const center = toScreen(value.center);\n // The radius handle rides the +x axis of gradient space, so it stays on\n // the drawn circle under a rotated or anisotropic transform.\n const edge = toScreen({ x: value.center.x + value.radius, y: value.center.y });\n const screenRadius = Math.hypot(edge.x - center.x, edge.y - center.y);\n return (\n <>\n <circle className={s.guide} cx={center.x} cy={center.y} r={screenRadius} />\n <DragPoint\n at={center}\n label=\"Gradient center\"\n onDrag={(p, phase) => emit({ ...value, center: toLocal(p) }, phase)}\n />\n <DragPoint\n at={edge}\n label=\"Gradient radius\"\n onDrag={(p, phase) => {\n const local = toLocal(p);\n const radius = Math.hypot(local.x - value.center.x, local.y - value.center.y);\n emit({ ...value, radius: Math.max(MIN_RADIUS, radius) }, phase);\n }}\n />\n </>\n );\n }\n\n const center = toScreen(value.center);\n const { radius } = gradientGeometry(value);\n const tip = toScreen({\n x: value.center.x + Math.cos(value.angle) * radius,\n y: value.center.y + Math.sin(value.angle) * radius,\n });\n return (\n <>\n <Guide x1={center.x} y1={center.y} x2={tip.x} y2={tip.y} />\n <DragPoint\n at={center}\n label=\"Gradient center\"\n onDrag={(p, phase) => emit({ ...value, center: toLocal(p) }, phase)}\n />\n <DragPoint\n at={tip}\n label=\"Gradient angle\"\n onDrag={(p, phase) => {\n const local = toLocal(p);\n const angle = Math.atan2(local.y - value.center.y, local.x - value.center.x);\n emit({ ...value, angle }, phase);\n }}\n />\n </>\n );\n}\n\n/** A radius of zero divides by zero in the shader's `t`; keep it off the floor. */\nconst MIN_RADIUS = 1;\n\nfunction Guide(props: { x1: number; y1: number; x2: number; y2: number }): ReactElement {\n return <line className={s.guide} {...props} />;\n}\n\nfunction DragPoint({\n at,\n label,\n onDrag,\n}: {\n at: Point;\n label: string;\n onDrag: (p: Point, phase: 'input' | 'commit') => void;\n}): ReactElement {\n // `useHandleDrag` reports the pointer on move but not on end, so the last\n // position is held here to commit with — otherwise releasing the pointer\n // would write whatever the gradient held before the drag began.\n const last = useRef<Point>(at);\n const drag = useHandleDrag<SVGCircleElement>({\n onMove: (p) => {\n last.current = p;\n onDrag(p, 'input');\n },\n onEnd: () => onDrag(last.current, 'commit'),\n });\n return (\n <circle\n className={s.handle}\n cx={at.x}\n cy={at.y}\n r={HANDLE_RADIUS}\n role=\"slider\"\n aria-label={label}\n tabIndex={0}\n {...drag}\n />\n );\n}\n\nconst HANDLE_RADIUS = 7;\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Input-DpkwYtUY.js","names":[],"sources":["../../src/components/Input/Input.module.css","../../src/components/Input/Input.tsx"],"sourcesContent":[".field {\n display: flex;\n flex-direction: column;\n width: 100%;\n min-width: 0;\n gap: 4px;\n}\n\n.frame {\n display: flex;\n align-items: center;\n gap: 6px;\n height: 24px;\n padding: 0 8px;\n background: var(--wzl-surface-sunken);\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n color: var(--wzl-fg);\n font-family: var(--wzl-font-ui);\n font-size: 13px;\n line-height: 1;\n transition: border-color var(--wzl-motion-fast), box-shadow var(--wzl-motion-fast);\n}\n\n.frame:focus-within {\n border-color: var(--wzl-focus-ring);\n box-shadow: 0 0 0 1px var(--wzl-focus-ring);\n}\n\n.field[data-invalid] .frame {\n border-color: var(--wzl-danger, #ff5b5b);\n}\n\n.field[data-disabled] .frame {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.frame input {\n flex: 1;\n min-width: 0;\n background: transparent;\n border: none;\n outline: none;\n color: inherit;\n font: inherit;\n padding: 0;\n}\n\n.frame input::placeholder {\n color: var(--wzl-fg-subtle);\n}\n\n.adornment {\n display: inline-flex;\n align-items: center;\n color: var(--wzl-fg-subtle);\n font-size: 12px;\n pointer-events: none;\n}\n","import { forwardRef, type ReactNode, type Ref } from 'react';\nimport {\n TextField,\n Label,\n Input as RACInput,\n Text,\n FieldError,\n type TextFieldProps as RACTextFieldProps,\n type ValidationResult,\n} from 'react-aria-components';\nimport { fieldClasses } from '../Field/Field';\nimport s from './Input.module.css';\n\nexport type InputProps = Omit<RACTextFieldProps, 'children' | 'className'> & {\n label?: ReactNode;\n description?: ReactNode;\n errorMessage?: ReactNode | ((v: ValidationResult) => ReactNode);\n placeholder?: string;\n leadingAdornment?: ReactNode;\n trailingAdornment?: ReactNode;\n className?: string;\n};\n\n/**\n * Single-line text input wrapping React Aria's TextField. Supplies a\n * default skin against the `--wzl-*` token system, and exposes the\n * familiar label / description / errorMessage / adornment slot shape.\n *\n * `ref` forwards to the underlying `<input>`.\n */\nexport const Input = forwardRef(function Input(\n props: InputProps,\n ref: Ref<HTMLInputElement>,\n) {\n const {\n label,\n description,\n errorMessage,\n placeholder,\n leadingAdornment,\n trailingAdornment,\n className,\n ...textFieldProps\n } = props;\n\n return (\n <TextField {...textFieldProps} className={[s.field, fieldClasses.root, className].filter(Boolean).join(' ')}>\n {label !== undefined && <Label className={fieldClasses.label}>{label}</Label>}\n <div className={s.frame}>\n {leadingAdornment !== undefined && <span className={s.adornment}>{leadingAdornment}</span>}\n <RACInput ref={ref} placeholder={placeholder} />\n {trailingAdornment !== undefined && <span className={s.adornment}>{trailingAdornment}</span>}\n </div>\n {description !== undefined && (\n <Text slot=\"description\" className={fieldClasses.hint}>\n {description}\n </Text>\n )}\n <FieldError className={fieldClasses.error}>{errorMessage}</FieldError>\n </TextField>\n );\n});\n"],"mappings":";;;;;;;;GC8Ba,IAAQ,EAAW,SAC9B,GACA,GACA;CACA,IAAM,EACJ,UACA,gBACA,iBACA,gBACA,qBACA,sBACA,cACA,GAAG,MACD;AAEJ,QACE,kBAAC,GAAD;EAAW,GAAI;EAAgB,WAAW;GAAC,EAAE;GAAO,EAAa;GAAM;GAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI;YAA3G;GACG,MAAU,KAAA,KAAa,kBAAC,GAAD;IAAO,WAAW,EAAa;cAAQ;IAAc,CAAA;GAC7E,kBAAC,OAAD;IAAK,WAAW,EAAE;cAAlB;KACG,MAAqB,KAAA,KAAa,kBAAC,QAAD;MAAM,WAAW,EAAE;gBAAY;MAAwB,CAAA;KAC1F,kBAAC,GAAD;MAAe;MAAkB;MAAe,CAAA;KAC/C,MAAsB,KAAA,KAAa,kBAAC,QAAD;MAAM,WAAW,EAAE;gBAAY;MAAyB,CAAA;KACxF;;GACL,MAAgB,KAAA,KACf,kBAAC,GAAD;IAAM,MAAK;IAAc,WAAW,EAAa;cAC9C;IACI,CAAA;GAET,kBAAC,GAAD;IAAY,WAAW,EAAa;cAAQ;IAA0B,CAAA;GAC5D;;EAEd"}
1
+ {"version":3,"file":"Input-DpkwYtUY.js","names":[],"sources":["../../src/components/Input/Input.module.css","../../src/components/Input/Input.tsx"],"sourcesContent":[".field {\n display: flex;\n flex-direction: column;\n width: 100%;\n min-width: 0;\n gap: 4px;\n}\n\n.frame {\n display: flex;\n align-items: center;\n gap: 6px;\n height: 24px;\n padding: 0 8px;\n background: var(--wzl-surface-sunken);\n border: 1px solid var(--wzl-border);\n border-radius: var(--wzl-radius-sm);\n color: var(--wzl-fg);\n font-family: var(--wzl-font-ui);\n font-size: 13px;\n line-height: 1;\n transition: border-color var(--wzl-motion-fast), box-shadow var(--wzl-motion-fast);\n}\n\n.frame:focus-within {\n border-color: var(--wzl-focus-ring);\n box-shadow: 0 0 0 1px var(--wzl-focus-ring);\n}\n\n.field[data-invalid] .frame {\n border-color: var(--wzl-danger, #ff5b5b);\n}\n\n.field[data-disabled] .frame {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.frame input {\n flex: 1;\n min-width: 0;\n background: transparent;\n border: none;\n outline: none;\n color: inherit;\n font: inherit;\n padding: 0;\n}\n\n.frame input::placeholder {\n color: var(--wzl-fg-subtle);\n}\n\n.adornment {\n display: inline-flex;\n align-items: center;\n color: var(--wzl-fg-subtle);\n font-size: 12px;\n pointer-events: none;\n}\n","import { forwardRef, type ReactNode, type Ref } from 'react';\nimport {\n TextField,\n Label,\n Input as RACInput,\n Text,\n FieldError,\n type TextFieldProps as RACTextFieldProps,\n type ValidationResult,\n} from 'react-aria-components';\nimport { fieldClasses } from '../Field/Field';\nimport s from './Input.module.css';\n\n/** Props for {@link Input}, on top of React Aria's `TextField` props. */\nexport type InputProps = Omit<RACTextFieldProps, 'children' | 'className'> & {\n label?: ReactNode;\n description?: ReactNode;\n errorMessage?: ReactNode | ((v: ValidationResult) => ReactNode);\n placeholder?: string;\n leadingAdornment?: ReactNode;\n trailingAdornment?: ReactNode;\n className?: string;\n};\n\n/**\n * Single-line text input wrapping React Aria's TextField. Supplies a\n * default skin against the `--wzl-*` token system, and exposes the\n * familiar label / description / errorMessage / adornment slot shape.\n *\n * `ref` forwards to the underlying `<input>`.\n */\nexport const Input = forwardRef(function Input(\n props: InputProps,\n ref: Ref<HTMLInputElement>,\n) {\n const {\n label,\n description,\n errorMessage,\n placeholder,\n leadingAdornment,\n trailingAdornment,\n className,\n ...textFieldProps\n } = props;\n\n return (\n <TextField {...textFieldProps} className={[s.field, fieldClasses.root, className].filter(Boolean).join(' ')}>\n {label !== undefined && <Label className={fieldClasses.label}>{label}</Label>}\n <div className={s.frame}>\n {leadingAdornment !== undefined && <span className={s.adornment}>{leadingAdornment}</span>}\n <RACInput ref={ref} placeholder={placeholder} />\n {trailingAdornment !== undefined && <span className={s.adornment}>{trailingAdornment}</span>}\n </div>\n {description !== undefined && (\n <Text slot=\"description\" className={fieldClasses.hint}>\n {description}\n </Text>\n )}\n <FieldError className={fieldClasses.error}>{errorMessage}</FieldError>\n </TextField>\n );\n});\n"],"mappings":";;;;;;;;GC+Ba,IAAQ,EAAW,SAC9B,GACA,GACA;CACA,IAAM,EACJ,UACA,gBACA,iBACA,gBACA,qBACA,sBACA,cACA,GAAG,MACD;AAEJ,QACE,kBAAC,GAAD;EAAW,GAAI;EAAgB,WAAW;GAAC,EAAE;GAAO,EAAa;GAAM;GAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI;YAA3G;GACG,MAAU,KAAA,KAAa,kBAAC,GAAD;IAAO,WAAW,EAAa;cAAQ;IAAc,CAAA;GAC7E,kBAAC,OAAD;IAAK,WAAW,EAAE;cAAlB;KACG,MAAqB,KAAA,KAAa,kBAAC,QAAD;MAAM,WAAW,EAAE;gBAAY;MAAwB,CAAA;KAC1F,kBAAC,GAAD;MAAe;MAAkB;MAAe,CAAA;KAC/C,MAAsB,KAAA,KAAa,kBAAC,QAAD;MAAM,WAAW,EAAE;gBAAY;MAAyB,CAAA;KACxF;;GACL,MAAgB,KAAA,KACf,kBAAC,GAAD;IAAM,MAAK;IAAc,WAAW,EAAa;cAC9C;IACI,CAAA;GAET,kBAAC,GAAD;IAAY,WAAW,EAAa;cAAQ;IAA0B,CAAA;GAC5D;;EAEd"}
@@ -1 +1 @@
1
- {"version":3,"file":"Keycaps-CFX7UAj8.js","names":[],"sources":["../../src/components/Keycaps/Keycaps.module.css","../../src/components/Keycaps/Keycap.tsx","../../src/components/Keycaps/Keycaps.tsx","../../src/components/Keycaps/keySpecsFromMods.ts"],"sourcesContent":["/* Keystroke \"key cap\" styling — each modifier and key in its own bordered chip. */\n.keys {\n display: inline-flex;\n gap: 3px;\n /* Bottom-align so chips with text labels and chips with icon glyphs\n (arrows, modifier symbols) line up at the same bottom edge. Baseline\n drifts because text baselines sit above icon centers. */\n align-items: flex-end;\n /* When this whole sequence sits inline in surrounding text (e.g.\n \"Press <KeySequence> to confirm.\"), the row anchors to the\n surrounding line's bottom edge, not its baseline. */\n vertical-align: bottom;\n flex-wrap: wrap;\n}\n\n.keysEmpty {\n color: var(--wzl-fg-muted);\n}\n\n.sep {\n color: var(--wzl-fg-muted);\n font-size: 11px;\n line-height: 1;\n}\n\n.key {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n height: 18px;\n padding: 0 4px;\n /* Light face, slightly-darker-than-face border, dark glyph — reads as\n a real keycap rather than a recessed slot in the panel. */\n background: var(--wzl-fg, #e6e7e9);\n border: 1px solid #25272c;\n border-bottom-width: 2px;\n border-radius: 3px;\n color: var(--wzl-surface);\n /* Keycaps are a deliberate exception to the site theme — the design-\n system UI font (often a display face like Oswald) is wrong for\n keyboard chips. Use a neutral sans stack so labels read like real\n key legends. Consumers wanting a specific face per chip pass the\n `font` prop on KeyCap. */\n font-family: ui-sans-serif, system-ui, sans-serif;\n font-size: 11px;\n font-weight: 700;\n line-height: 1;\n}\n\n/* Default chip is square — letters, digits, and single glyphs (⎋ ⌫ ↑ etc.)\n sit in a uniform key-cap box. */\n.key[data-kind='square'] {\n width: 18px;\n padding: 0;\n}\n\n/* Modifiers (⌘ ⇪ ⌥ ⌃) are wider to match the visual weight of how they\n appear on a real keyboard — and to differentiate them from the keys\n they modify in a stack. */\n.key[data-kind='modifier'] {\n min-width: 26px;\n padding: 0 6px;\n}\n\n/* Tab / Enter / Space — actual wide keys on a real keyboard. */\n.key[data-kind='wide'] {\n min-width: 32px;\n padding: 0 8px;\n}\n\n/* Optional/inverted (default variant): face flips to dark; legend drops\n to 60% opacity; dotted border drops to 40% opacity. Three soft cues\n stacked for \"may be held but isn't required.\" */\n.key[data-inverted] {\n background: var(--wzl-surface);\n color: color-mix(in srgb, var(--wzl-fg, #e6e7e9) 60%, transparent);\n border-style: dotted;\n border-color: color-mix(in srgb, var(--wzl-fg, #e6e7e9) 40%, transparent);\n border-width: 1.5px;\n border-bottom-width: 1.5px;\n}\n\n/* Minimal variant — no fill, borders and legend follow currentColor.\n Useful inline in colored prose / next to colored badges where the\n chip should take the surrounding text color. */\n.key[data-variant='minimal'] {\n background: transparent;\n border-color: currentColor;\n color: currentColor;\n}\n\n/* Optional + minimal: dotted border at full currentColor, legend at full\n currentColor. The dotted pattern alone carries the \"optional\" cue here\n — minimal's whole point is to defer color to the surrounding context,\n so dimming would fight that. (Default variant uses graded opacity as a\n stacked cue; minimal keeps it crisp.) */\n.key[data-variant='minimal'][data-inverted] {\n border-style: dotted;\n border-color: currentColor;\n color: currentColor;\n border-width: 1.5px;\n border-bottom-width: 1.5px;\n}\n","import type { CSSProperties } from 'react';\nimport s from './Keycaps.module.css';\n\nconst MODIFIER_GLYPHS = new Set(['⌘', '⇪', '⌥', '⌃', '⇧']);\nconst WIDE_GLYPHS = new Set(['⇥', '↵', '␣']);\n\nexport type KeycapKind = 'modifier' | 'wide' | 'square';\n\nexport function inferKeycapKind(label: string): KeycapKind {\n if (MODIFIER_GLYPHS.has(label)) return 'modifier';\n if (WIDE_GLYPHS.has(label)) return 'wide';\n if (label.length > 1) return 'wide';\n return 'square';\n}\n\nexport type KeyCapVariant = 'default' | 'minimal';\n\nexport interface KeyCapProps {\n /** Glyph rendered in the chip (modifier or key). */\n label: string;\n /** Marks the chip as not required to trigger the action. In the default\n * variant the chip face inverts (dark face / light glyph) for\n * emphasis. In the `'minimal'` variant the chip's border becomes\n * dotted to read as \"may be held, but isn't required.\" */\n inverted?: boolean;\n /** Visual style. `'default'` (default) renders a filled chip; `'minimal'`\n * renders an unfilled chip whose border and legend are `currentColor`\n * — useful inline in colored prose / next to colored badges where the\n * chip should take the surrounding text color. */\n variant?: KeyCapVariant;\n className?: string;\n /** Optional inline style. Useful for one-off width overrides (e.g.\n * building a keyboard-layout where the space bar spans several\n * standard-key widths). The component sets no inline styles of its\n * own — everything else comes from the CSS module. */\n style?: CSSProperties;\n /** Override the chip's font-family. Accepts any CSS font-family\n * string. When omitted, the chip inherits from the design-system\n * UI font token (`--wzl-font-ui`). Useful for one-off cases where\n * a different face is desired — though the canonical move is to\n * override the CSS variable at the consumer's scope, not the prop. */\n font?: string;\n}\n\n/** Single bordered keycap chip for one glyph (modifier or key). Use\n * `KeySequence` to render a full shortcut. */\nexport function KeyCap({ label, inverted = false, variant = 'default', className, style, font }: KeyCapProps) {\n const resolvedStyle = font ? { ...style, fontFamily: font } : style;\n return (\n <kbd\n className={[s.key, className].filter(Boolean).join(' ')}\n data-kind={inferKeycapKind(label)}\n data-variant={variant === 'default' ? undefined : variant}\n data-inverted={inverted || undefined}\n style={resolvedStyle}\n >\n {label}\n </kbd>\n );\n}\n","import { Fragment } from 'react';\nimport s from './Keycaps.module.css';\nimport { KeyCap, inferKeycapKind, type KeyCapVariant } from './Keycap';\n\nexport interface KeySpec {\n /** Glyph rendered in the chip (modifier or key). */\n label: string;\n /** When true, the chip renders inverted to mark it as not required to\n * trigger the action (e.g. an optional modifier). Defaults to false. */\n optional?: boolean;\n}\n\nexport interface KeySequenceProps {\n /** Keys to render. `undefined` or empty renders a muted em-dash.\n * Modifiers are always rendered first regardless of input order; relative\n * order within each group is preserved. */\n keys: readonly KeySpec[] | undefined;\n /** Character inserted between the trailing modifier chip and the first\n * non-modifier chip (e.g. `'+'` renders `⌘ + K`). `null` or `''`\n * suppresses it. Defaults to `'+'`. */\n separator?: string | null;\n /** Forwarded to every `KeyCap` in the sequence. `'minimal'` renders\n * unfilled chips whose border + legend are `currentColor`. */\n variant?: KeyCapVariant;\n /** Forwarded to every `KeyCap` in the sequence. Overrides the design-\n * system UI font for that chip only. */\n font?: string;\n className?: string;\n}\n\n/** Renders a shortcut as a row of `KeyCap` chips, one per key. Optional\n * keys render inverted to distinguish them from required ones. */\nexport function KeySequence({ keys, separator = '+', variant = 'default', font, className }: KeySequenceProps) {\n if (!keys || keys.length === 0) {\n return <span className={[s.keysEmpty, className].filter(Boolean).join(' ')}>—</span>;\n }\n const ordered = keys\n .map((k, i) => ({ k, i }))\n .sort((a, b) => {\n const am = inferKeycapKind(a.k.label) === 'modifier' ? 0 : 1;\n const bm = inferKeycapKind(b.k.label) === 'modifier' ? 0 : 1;\n return am - bm || a.i - b.i;\n })\n .map(({ k }) => k);\n const sepIdx = ordered.findIndex((k) => inferKeycapKind(k.label) !== 'modifier');\n const showSep = !!separator && sepIdx > 0 && sepIdx < ordered.length;\n return (\n <span className={[s.keys, className].filter(Boolean).join(' ')}>\n {ordered.map((k, i) => (\n <Fragment key={i}>\n {showSep && i === sepIdx ? <span className={s.sep}>{separator}</span> : null}\n <KeyCap label={k.label} inverted={k.optional ?? false} variant={variant} font={font} />\n </Fragment>\n ))}\n </span>\n );\n}\n","import type { KeySpec } from './Keycaps';\n\n/** Logical modifier name. Matches the kit's `ModifierKey` in shape — kept as a\n * local type so `weasel-ui` stays independent of `@weasel-js/core`. */\nexport type LogicalMod = 'mod' | 'shift' | 'alt' | 'ctrl' | 'meta';\n\n/** Detected (or overridden) platform. `'mod'` resolves to ⌘ on macOS,\n * Ctrl on Windows / Linux. */\nexport type Platform = 'macos' | 'windows' | 'linux';\n\n/** Visual form of a modifier or named-key label.\n *\n * - `'auto'` (default): the per-entry, per-platform default — whatever\n * label is actually printed on the physical key. macOS keys show the\n * Apple glyphs (⌘ ⌥ ⌃ ⇧ ⎋ ↵ ⇥). Windows keys mostly show text\n * (Ctrl, Alt, Shift) BUT the Win key shows its logo (⊞) and the\n * context-menu key shows ▤. Linux mirrors Windows with Super (⊞).\n * - `'symbol'` forces the Apple-style glyph everywhere a symbolic form\n * exists. Entries without a widely-recognized symbol still fall\n * back to text.\n * - `'text'` always spells the label out (Cmd / Option / Esc / Enter). */\nexport type LegendStyle = 'auto' | 'symbol' | 'text';\n\nexport interface LogicalModSpec {\n name: LogicalMod;\n /** Marks this modifier as optional (may be held but isn't required).\n * Forwarded as the resulting `KeySpec.optional`. */\n optional?: boolean;\n}\n\nexport interface KeySpecsFromModsOptions {\n /** Override OS detection. Defaults to the detected platform, or\n * `'macos'` when detection is unavailable (e.g. in tests, or when\n * `navigator` is missing). */\n platform?: Platform;\n /** Defaults to `'auto'` (symbol on macOS, text everywhere else). Pass\n * `'symbol'` or `'text'` to force a specific style regardless of\n * platform. */\n legend?: LegendStyle;\n}\n\nconst MOD_BY_PLATFORM: Record<LogicalMod, Record<Platform, Record<LegendStyle, string>>> = {\n mod: {\n macos: { auto: '⌘', symbol: '⌘', text: 'Cmd' },\n windows: { auto: 'Ctrl', symbol: 'Ctrl', text: 'Ctrl' },\n linux: { auto: 'Ctrl', symbol: 'Ctrl', text: 'Ctrl' },\n },\n shift: {\n macos: { auto: '⇧', symbol: '⇧', text: 'Shift' },\n windows: { auto: '⇧', symbol: '⇧', text: 'Shift' },\n linux: { auto: '⇧', symbol: '⇧', text: 'Shift' },\n },\n alt: {\n macos: { auto: '⌥', symbol: '⌥', text: 'Option' },\n windows: { auto: 'Alt', symbol: 'Alt', text: 'Alt' },\n linux: { auto: 'Alt', symbol: 'Alt', text: 'Alt' },\n },\n ctrl: {\n macos: { auto: '⌃', symbol: '⌃', text: 'Control' },\n windows: { auto: 'Ctrl', symbol: 'Ctrl', text: 'Ctrl' },\n linux: { auto: 'Ctrl', symbol: 'Ctrl', text: 'Ctrl' },\n },\n // Windows/Linux Meta has a recognized symbol on the physical key (⊞ Win\n // logo / Super logo), so `auto` shows it. Use `text` for Win / Super.\n meta: {\n macos: { auto: '⌘', symbol: '⌘', text: 'Cmd' },\n windows: { auto: '⊞', symbol: '⊞', text: 'Win' },\n linux: { auto: '⊞', symbol: '⊞', text: 'Super' },\n },\n};\n\n/** Detect the user's OS from the browser environment. UA sniffing — not\n * 100% reliable, but adequate for picking a modifier-glyph convention.\n * Falls back to `'macos'` (matches the kit's docs convention) when\n * `navigator` is unavailable. */\nexport function detectPlatform(): Platform {\n if (typeof navigator === 'undefined') return 'macos';\n // Prefer the modern userAgentData when available (Chromium-only today).\n const uaData = (navigator as Navigator & { userAgentData?: { platform?: string } }).userAgentData;\n const raw = (uaData?.platform ?? navigator.userAgent ?? '').toLowerCase();\n if (raw.includes('mac') || raw.includes('darwin') || raw.includes('iphone') || raw.includes('ipad')) return 'macos';\n if (raw.includes('win')) return 'windows';\n if (raw.includes('linux') || raw.includes('x11')) return 'linux';\n return 'macos';\n}\n\n/**\n * Map a list of **logical** modifiers (`'mod'`, `'shift'`, …) to **visual**\n * `KeySpec`s appropriate for the current platform.\n *\n * The kit-side gesture grammar uses logical names so the same route reads\n * correctly on every OS — `'mod'` means \"Cmd on Mac, Ctrl on Win/Linux\".\n * `KeySequence` is purely visual and renders the labels you give it; this\n * helper is the bridge.\n *\n * ```ts\n * <KeySequence\n * keys={keySpecsFromMods([{ name: 'mod' }, { name: 'shift', optional: true }])}\n * />\n * ```\n *\n * On a Mac: renders `⌘ ⇧`. On Windows: renders `Ctrl Shift`. Pass an\n * explicit `platform` to override detection (e.g. for docs that always\n * show the macOS form). Pass `legend: 'text'` to force \"Cmd\" over \"⌘\"\n * even on macOS.\n */\nexport function keySpecsFromMods(\n mods: readonly LogicalModSpec[],\n opts: KeySpecsFromModsOptions = {},\n): readonly KeySpec[] {\n const platform = opts.platform ?? detectPlatform();\n const legend = opts.legend ?? 'auto';\n return mods.map((m) => {\n const label = MOD_BY_PLATFORM[m.name][platform][legend];\n return m.optional ? { label, optional: true } : { label };\n });\n}\n\n/** Per-platform × per-legend table for named, non-modifier keys. `auto`\n * is whatever's printed on the physical key for that platform. Arrows\n * are universal symbols on every platform. */\nconst NAMED_KEY: Record<string, Record<Platform, Record<LegendStyle, string>>> = {\n Escape: { macos: { auto: '⎋', symbol: '⎋', text: 'Esc' }, windows: { auto: 'Esc', symbol: 'Esc', text: 'Esc' }, linux: { auto: 'Esc', symbol: 'Esc', text: 'Esc' } },\n Enter: { macos: { auto: '↵', symbol: '↵', text: 'Return' }, windows: { auto: 'Enter', symbol: 'Enter', text: 'Enter' }, linux: { auto: 'Enter', symbol: 'Enter', text: 'Enter' } },\n Return: { macos: { auto: '↵', symbol: '↵', text: 'Return' }, windows: { auto: 'Enter', symbol: 'Enter', text: 'Enter' }, linux: { auto: 'Enter', symbol: 'Enter', text: 'Enter' } },\n Tab: { macos: { auto: '⇥', symbol: '⇥', text: 'Tab' }, windows: { auto: 'Tab', symbol: 'Tab', text: 'Tab' }, linux: { auto: 'Tab', symbol: 'Tab', text: 'Tab' } },\n Space: { macos: { auto: '␣', symbol: '␣', text: 'Space' }, windows: { auto: 'Space', symbol: 'Space', text: 'Space' }, linux: { auto: 'Space', symbol: 'Space', text: 'Space' } },\n ' ': { macos: { auto: '␣', symbol: '␣', text: 'Space' }, windows: { auto: 'Space', symbol: 'Space', text: 'Space' }, linux: { auto: 'Space', symbol: 'Space', text: 'Space' } },\n Backspace: { macos: { auto: '⌫', symbol: '⌫', text: 'Backspace' }, windows: { auto: 'Backspace', symbol: 'Backspace', text: 'Backspace' }, linux: { auto: 'Backspace', symbol: 'Backspace', text: 'Backspace' } },\n Delete: { macos: { auto: '⌦', symbol: '⌦', text: 'Delete' }, windows: { auto: 'Delete', symbol: 'Delete', text: 'Delete' }, linux: { auto: 'Delete', symbol: 'Delete', text: 'Delete' } },\n // Context-menu key (the right-Ctrl-area key on Windows / Linux keyboards\n // with a ▤ icon). Mac has no equivalent; falls back to text 'Menu'.\n ContextMenu: { macos: { auto: 'Menu', symbol: 'Menu', text: 'Menu' }, windows: { auto: '▤', symbol: '▤', text: 'Menu' }, linux: { auto: '▤', symbol: '▤', text: 'Menu' } },\n ArrowUp: { macos: { auto: '↑', symbol: '↑', text: '↑' }, windows: { auto: '↑', symbol: '↑', text: '↑' }, linux: { auto: '↑', symbol: '↑', text: '↑' } },\n ArrowDown: { macos: { auto: '↓', symbol: '↓', text: '↓' }, windows: { auto: '↓', symbol: '↓', text: '↓' }, linux: { auto: '↓', symbol: '↓', text: '↓' } },\n ArrowLeft: { macos: { auto: '←', symbol: '←', text: '←' }, windows: { auto: '←', symbol: '←', text: '←' }, linux: { auto: '←', symbol: '←', text: '←' } },\n ArrowRight: { macos: { auto: '→', symbol: '→', text: '→' }, windows: { auto: '→', symbol: '→', text: '→' }, linux: { auto: '→', symbol: '→', text: '→' } },\n};\n\n/**\n * Render a raw `KeyboardEvent.key` value as a platform-appropriate\n * `KeySpec`. Named non-modifier keys (Esc, Enter, Tab, Space, Backspace,\n * Delete, arrows) get the platform's conventional form. Letter / digit /\n * other keys pass through upper-cased.\n *\n * ```ts\n * keySpecFromKey('Escape', { platform: 'macos' }) // { label: '⎋' }\n * keySpecFromKey('Escape', { platform: 'windows' }) // { label: 'Esc' }\n * keySpecFromKey('p') // { label: 'P' }\n * ```\n *\n * Symbol mode is only meaningful on macOS — Windows / Linux keyboards\n * print the text form on the physical key, so symbol mode there falls\n * back to text.\n */\nexport function keySpecFromKey(\n raw: string,\n opts: KeySpecsFromModsOptions & { optional?: boolean } = {},\n): KeySpec {\n const platform = opts.platform ?? detectPlatform();\n const legend = opts.legend ?? 'auto';\n const entry = NAMED_KEY[raw];\n const label = entry ? entry[platform][legend] : raw.length === 1 ? raw.toUpperCase() : raw;\n return opts.optional ? { label, optional: true } : { label };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"Keycaps-CFX7UAj8.js","names":[],"sources":["../../src/components/Keycaps/Keycaps.module.css","../../src/components/Keycaps/Keycap.tsx","../../src/components/Keycaps/Keycaps.tsx","../../src/components/Keycaps/keySpecsFromMods.ts"],"sourcesContent":["/* Keystroke \"key cap\" styling — each modifier and key in its own bordered chip. */\n.keys {\n display: inline-flex;\n gap: 3px;\n /* Bottom-align so chips with text labels and chips with icon glyphs\n (arrows, modifier symbols) line up at the same bottom edge. Baseline\n drifts because text baselines sit above icon centers. */\n align-items: flex-end;\n /* When this whole sequence sits inline in surrounding text (e.g.\n \"Press <KeySequence> to confirm.\"), the row anchors to the\n surrounding line's bottom edge, not its baseline. */\n vertical-align: bottom;\n flex-wrap: wrap;\n}\n\n.keysEmpty {\n color: var(--wzl-fg-muted);\n}\n\n.sep {\n color: var(--wzl-fg-muted);\n font-size: 11px;\n line-height: 1;\n}\n\n.key {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n height: 18px;\n padding: 0 4px;\n /* Light face, slightly-darker-than-face border, dark glyph — reads as\n a real keycap rather than a recessed slot in the panel. */\n background: var(--wzl-fg, #e6e7e9);\n border: 1px solid #25272c;\n border-bottom-width: 2px;\n border-radius: 3px;\n color: var(--wzl-surface);\n /* Keycaps are a deliberate exception to the site theme — the design-\n system UI font (often a display face like Oswald) is wrong for\n keyboard chips. Use a neutral sans stack so labels read like real\n key legends. Consumers wanting a specific face per chip pass the\n `font` prop on KeyCap. */\n font-family: ui-sans-serif, system-ui, sans-serif;\n font-size: 11px;\n font-weight: 700;\n line-height: 1;\n}\n\n/* Default chip is square — letters, digits, and single glyphs (⎋ ⌫ ↑ etc.)\n sit in a uniform key-cap box. */\n.key[data-kind='square'] {\n width: 18px;\n padding: 0;\n}\n\n/* Modifiers (⌘ ⇪ ⌥ ⌃) are wider to match the visual weight of how they\n appear on a real keyboard — and to differentiate them from the keys\n they modify in a stack. */\n.key[data-kind='modifier'] {\n min-width: 26px;\n padding: 0 6px;\n}\n\n/* Tab / Enter / Space — actual wide keys on a real keyboard. */\n.key[data-kind='wide'] {\n min-width: 32px;\n padding: 0 8px;\n}\n\n/* Optional/inverted (default variant): face flips to dark; legend drops\n to 60% opacity; dotted border drops to 40% opacity. Three soft cues\n stacked for \"may be held but isn't required.\" */\n.key[data-inverted] {\n background: var(--wzl-surface);\n color: color-mix(in srgb, var(--wzl-fg, #e6e7e9) 60%, transparent);\n border-style: dotted;\n border-color: color-mix(in srgb, var(--wzl-fg, #e6e7e9) 40%, transparent);\n border-width: 1.5px;\n border-bottom-width: 1.5px;\n}\n\n/* Minimal variant — no fill, borders and legend follow currentColor.\n Useful inline in colored prose / next to colored badges where the\n chip should take the surrounding text color. */\n.key[data-variant='minimal'] {\n background: transparent;\n border-color: currentColor;\n color: currentColor;\n}\n\n/* Optional + minimal: dotted border at full currentColor, legend at full\n currentColor. The dotted pattern alone carries the \"optional\" cue here\n — minimal's whole point is to defer color to the surrounding context,\n so dimming would fight that. (Default variant uses graded opacity as a\n stacked cue; minimal keeps it crisp.) */\n.key[data-variant='minimal'][data-inverted] {\n border-style: dotted;\n border-color: currentColor;\n color: currentColor;\n border-width: 1.5px;\n border-bottom-width: 1.5px;\n}\n","import type { CSSProperties } from 'react';\nimport s from './Keycaps.module.css';\n\nconst MODIFIER_GLYPHS = new Set(['⌘', '⇪', '⌥', '⌃', '⇧']);\nconst WIDE_GLYPHS = new Set(['⇥', '↵', '␣']);\n\n/**\n * Width class of a keycap: a modifier glyph, a multi-character legend that\n * needs a wider chip, or a single character in a square one.\n */\nexport type KeycapKind = 'modifier' | 'wide' | 'square';\n\n/**\n * Picks the chip width for a legend — modifier glyphs and anything longer\n * than one character get the wider forms.\n */\nexport function inferKeycapKind(label: string): KeycapKind {\n if (MODIFIER_GLYPHS.has(label)) return 'modifier';\n if (WIDE_GLYPHS.has(label)) return 'wide';\n if (label.length > 1) return 'wide';\n return 'square';\n}\n\n/** Visual style of a keycap chip. */\nexport type KeyCapVariant = 'default' | 'minimal';\n\n/** Props for {@link KeyCap}. */\nexport interface KeyCapProps {\n /** Glyph rendered in the chip (modifier or key). */\n label: string;\n /** Marks the chip as not required to trigger the action. In the default\n * variant the chip face inverts (dark face / light glyph) for\n * emphasis. In the `'minimal'` variant the chip's border becomes\n * dotted to read as \"may be held, but isn't required.\" */\n inverted?: boolean;\n /** Visual style. `'default'` (default) renders a filled chip; `'minimal'`\n * renders an unfilled chip whose border and legend are `currentColor`\n * — useful inline in colored prose / next to colored badges where the\n * chip should take the surrounding text color. */\n variant?: KeyCapVariant;\n className?: string;\n /** Optional inline style. Useful for one-off width overrides (e.g.\n * building a keyboard-layout where the space bar spans several\n * standard-key widths). The component sets no inline styles of its\n * own — everything else comes from the CSS module. */\n style?: CSSProperties;\n /** Override the chip's font-family. Accepts any CSS font-family\n * string. When omitted, the chip inherits from the design-system\n * UI font token (`--wzl-font-ui`). Useful for one-off cases where\n * a different face is desired — though the canonical move is to\n * override the CSS variable at the consumer's scope, not the prop. */\n font?: string;\n}\n\n/** Single bordered keycap chip for one glyph (modifier or key). Use\n * `KeySequence` to render a full shortcut. */\nexport function KeyCap({ label, inverted = false, variant = 'default', className, style, font }: KeyCapProps) {\n const resolvedStyle = font ? { ...style, fontFamily: font } : style;\n return (\n <kbd\n className={[s.key, className].filter(Boolean).join(' ')}\n data-kind={inferKeycapKind(label)}\n data-variant={variant === 'default' ? undefined : variant}\n data-inverted={inverted || undefined}\n style={resolvedStyle}\n >\n {label}\n </kbd>\n );\n}\n","import { Fragment } from 'react';\nimport s from './Keycaps.module.css';\nimport { KeyCap, inferKeycapKind, type KeyCapVariant } from './Keycap';\n\n/** One key in a shortcut. */\nexport interface KeySpec {\n /** Glyph rendered in the chip (modifier or key). */\n label: string;\n /** When true, the chip renders inverted to mark it as not required to\n * trigger the action (e.g. an optional modifier). Defaults to false. */\n optional?: boolean;\n}\n\n/** Props for {@link KeySequence}. */\nexport interface KeySequenceProps {\n /** Keys to render. `undefined` or empty renders a muted em-dash.\n * Modifiers are always rendered first regardless of input order; relative\n * order within each group is preserved. */\n keys: readonly KeySpec[] | undefined;\n /** Character inserted between the trailing modifier chip and the first\n * non-modifier chip (e.g. `'+'` renders `⌘ + K`). `null` or `''`\n * suppresses it. Defaults to `'+'`. */\n separator?: string | null;\n /** Forwarded to every `KeyCap` in the sequence. `'minimal'` renders\n * unfilled chips whose border + legend are `currentColor`. */\n variant?: KeyCapVariant;\n /** Forwarded to every `KeyCap` in the sequence. Overrides the design-\n * system UI font for that chip only. */\n font?: string;\n className?: string;\n}\n\n/** Renders a shortcut as a row of `KeyCap` chips, one per key. Optional\n * keys render inverted to distinguish them from required ones. */\nexport function KeySequence({ keys, separator = '+', variant = 'default', font, className }: KeySequenceProps) {\n if (!keys || keys.length === 0) {\n return <span className={[s.keysEmpty, className].filter(Boolean).join(' ')}>—</span>;\n }\n const ordered = keys\n .map((k, i) => ({ k, i }))\n .sort((a, b) => {\n const am = inferKeycapKind(a.k.label) === 'modifier' ? 0 : 1;\n const bm = inferKeycapKind(b.k.label) === 'modifier' ? 0 : 1;\n return am - bm || a.i - b.i;\n })\n .map(({ k }) => k);\n const sepIdx = ordered.findIndex((k) => inferKeycapKind(k.label) !== 'modifier');\n const showSep = !!separator && sepIdx > 0 && sepIdx < ordered.length;\n return (\n <span className={[s.keys, className].filter(Boolean).join(' ')}>\n {ordered.map((k, i) => (\n <Fragment key={i}>\n {showSep && i === sepIdx ? <span className={s.sep}>{separator}</span> : null}\n <KeyCap label={k.label} inverted={k.optional ?? false} variant={variant} font={font} />\n </Fragment>\n ))}\n </span>\n );\n}\n","import type { KeySpec } from './Keycaps';\n\n/** Logical modifier name. Matches the kit's `ModifierKey` in shape — kept as a\n * local type so `weasel-ui` stays independent of `@weasel-js/core`. */\nexport type LogicalMod = 'mod' | 'shift' | 'alt' | 'ctrl' | 'meta';\n\n/** Detected (or overridden) platform. `'mod'` resolves to ⌘ on macOS,\n * Ctrl on Windows / Linux. */\nexport type Platform = 'macos' | 'windows' | 'linux';\n\n/** Visual form of a modifier or named-key label.\n *\n * - `'auto'` (default): the per-entry, per-platform default — whatever\n * label is actually printed on the physical key. macOS keys show the\n * Apple glyphs (⌘ ⌥ ⌃ ⇧ ⎋ ↵ ⇥). Windows keys mostly show text\n * (Ctrl, Alt, Shift) BUT the Win key shows its logo (⊞) and the\n * context-menu key shows ▤. Linux mirrors Windows with Super (⊞).\n * - `'symbol'` forces the Apple-style glyph everywhere a symbolic form\n * exists. Entries without a widely-recognized symbol still fall\n * back to text.\n * - `'text'` always spells the label out (Cmd / Option / Esc / Enter). */\nexport type LegendStyle = 'auto' | 'symbol' | 'text';\n\n/** A modifier to render, named platform-independently. */\nexport interface LogicalModSpec {\n name: LogicalMod;\n /** Marks this modifier as optional (may be held but isn't required).\n * Forwarded as the resulting `KeySpec.optional`. */\n optional?: boolean;\n}\n\n/** Options for {@link keySpecsFromMods}. */\nexport interface KeySpecsFromModsOptions {\n /** Override OS detection. Defaults to the detected platform, or\n * `'macos'` when detection is unavailable (e.g. in tests, or when\n * `navigator` is missing). */\n platform?: Platform;\n /** Defaults to `'auto'` (symbol on macOS, text everywhere else). Pass\n * `'symbol'` or `'text'` to force a specific style regardless of\n * platform. */\n legend?: LegendStyle;\n}\n\nconst MOD_BY_PLATFORM: Record<LogicalMod, Record<Platform, Record<LegendStyle, string>>> = {\n mod: {\n macos: { auto: '⌘', symbol: '⌘', text: 'Cmd' },\n windows: { auto: 'Ctrl', symbol: 'Ctrl', text: 'Ctrl' },\n linux: { auto: 'Ctrl', symbol: 'Ctrl', text: 'Ctrl' },\n },\n shift: {\n macos: { auto: '⇧', symbol: '⇧', text: 'Shift' },\n windows: { auto: '⇧', symbol: '⇧', text: 'Shift' },\n linux: { auto: '⇧', symbol: '⇧', text: 'Shift' },\n },\n alt: {\n macos: { auto: '⌥', symbol: '⌥', text: 'Option' },\n windows: { auto: 'Alt', symbol: 'Alt', text: 'Alt' },\n linux: { auto: 'Alt', symbol: 'Alt', text: 'Alt' },\n },\n ctrl: {\n macos: { auto: '⌃', symbol: '⌃', text: 'Control' },\n windows: { auto: 'Ctrl', symbol: 'Ctrl', text: 'Ctrl' },\n linux: { auto: 'Ctrl', symbol: 'Ctrl', text: 'Ctrl' },\n },\n // Windows/Linux Meta has a recognized symbol on the physical key (⊞ Win\n // logo / Super logo), so `auto` shows it. Use `text` for Win / Super.\n meta: {\n macos: { auto: '⌘', symbol: '⌘', text: 'Cmd' },\n windows: { auto: '⊞', symbol: '⊞', text: 'Win' },\n linux: { auto: '⊞', symbol: '⊞', text: 'Super' },\n },\n};\n\n/** Detect the user's OS from the browser environment. UA sniffing — not\n * 100% reliable, but adequate for picking a modifier-glyph convention.\n * Falls back to `'macos'` (matches the kit's docs convention) when\n * `navigator` is unavailable. */\nexport function detectPlatform(): Platform {\n if (typeof navigator === 'undefined') return 'macos';\n // Prefer the modern userAgentData when available (Chromium-only today).\n const uaData = (navigator as Navigator & { userAgentData?: { platform?: string } }).userAgentData;\n const raw = (uaData?.platform ?? navigator.userAgent ?? '').toLowerCase();\n if (raw.includes('mac') || raw.includes('darwin') || raw.includes('iphone') || raw.includes('ipad')) return 'macos';\n if (raw.includes('win')) return 'windows';\n if (raw.includes('linux') || raw.includes('x11')) return 'linux';\n return 'macos';\n}\n\n/**\n * Map a list of **logical** modifiers (`'mod'`, `'shift'`, …) to **visual**\n * `KeySpec`s appropriate for the current platform.\n *\n * The kit-side gesture grammar uses logical names so the same route reads\n * correctly on every OS — `'mod'` means \"Cmd on Mac, Ctrl on Win/Linux\".\n * `KeySequence` is purely visual and renders the labels you give it; this\n * helper is the bridge.\n *\n * ```ts\n * <KeySequence\n * keys={keySpecsFromMods([{ name: 'mod' }, { name: 'shift', optional: true }])}\n * />\n * ```\n *\n * On a Mac: renders `⌘ ⇧`. On Windows: renders `Ctrl Shift`. Pass an\n * explicit `platform` to override detection (e.g. for docs that always\n * show the macOS form). Pass `legend: 'text'` to force \"Cmd\" over \"⌘\"\n * even on macOS.\n */\nexport function keySpecsFromMods(\n mods: readonly LogicalModSpec[],\n opts: KeySpecsFromModsOptions = {},\n): readonly KeySpec[] {\n const platform = opts.platform ?? detectPlatform();\n const legend = opts.legend ?? 'auto';\n return mods.map((m) => {\n const label = MOD_BY_PLATFORM[m.name][platform][legend];\n return m.optional ? { label, optional: true } : { label };\n });\n}\n\n/** Per-platform × per-legend table for named, non-modifier keys. `auto`\n * is whatever's printed on the physical key for that platform. Arrows\n * are universal symbols on every platform. */\nconst NAMED_KEY: Record<string, Record<Platform, Record<LegendStyle, string>>> = {\n Escape: { macos: { auto: '⎋', symbol: '⎋', text: 'Esc' }, windows: { auto: 'Esc', symbol: 'Esc', text: 'Esc' }, linux: { auto: 'Esc', symbol: 'Esc', text: 'Esc' } },\n Enter: { macos: { auto: '↵', symbol: '↵', text: 'Return' }, windows: { auto: 'Enter', symbol: 'Enter', text: 'Enter' }, linux: { auto: 'Enter', symbol: 'Enter', text: 'Enter' } },\n Return: { macos: { auto: '↵', symbol: '↵', text: 'Return' }, windows: { auto: 'Enter', symbol: 'Enter', text: 'Enter' }, linux: { auto: 'Enter', symbol: 'Enter', text: 'Enter' } },\n Tab: { macos: { auto: '⇥', symbol: '⇥', text: 'Tab' }, windows: { auto: 'Tab', symbol: 'Tab', text: 'Tab' }, linux: { auto: 'Tab', symbol: 'Tab', text: 'Tab' } },\n Space: { macos: { auto: '␣', symbol: '␣', text: 'Space' }, windows: { auto: 'Space', symbol: 'Space', text: 'Space' }, linux: { auto: 'Space', symbol: 'Space', text: 'Space' } },\n ' ': { macos: { auto: '␣', symbol: '␣', text: 'Space' }, windows: { auto: 'Space', symbol: 'Space', text: 'Space' }, linux: { auto: 'Space', symbol: 'Space', text: 'Space' } },\n Backspace: { macos: { auto: '⌫', symbol: '⌫', text: 'Backspace' }, windows: { auto: 'Backspace', symbol: 'Backspace', text: 'Backspace' }, linux: { auto: 'Backspace', symbol: 'Backspace', text: 'Backspace' } },\n Delete: { macos: { auto: '⌦', symbol: '⌦', text: 'Delete' }, windows: { auto: 'Delete', symbol: 'Delete', text: 'Delete' }, linux: { auto: 'Delete', symbol: 'Delete', text: 'Delete' } },\n // Context-menu key (the right-Ctrl-area key on Windows / Linux keyboards\n // with a ▤ icon). Mac has no equivalent; falls back to text 'Menu'.\n ContextMenu: { macos: { auto: 'Menu', symbol: 'Menu', text: 'Menu' }, windows: { auto: '▤', symbol: '▤', text: 'Menu' }, linux: { auto: '▤', symbol: '▤', text: 'Menu' } },\n ArrowUp: { macos: { auto: '↑', symbol: '↑', text: '↑' }, windows: { auto: '↑', symbol: '↑', text: '↑' }, linux: { auto: '↑', symbol: '↑', text: '↑' } },\n ArrowDown: { macos: { auto: '↓', symbol: '↓', text: '↓' }, windows: { auto: '↓', symbol: '↓', text: '↓' }, linux: { auto: '↓', symbol: '↓', text: '↓' } },\n ArrowLeft: { macos: { auto: '←', symbol: '←', text: '←' }, windows: { auto: '←', symbol: '←', text: '←' }, linux: { auto: '←', symbol: '←', text: '←' } },\n ArrowRight: { macos: { auto: '→', symbol: '→', text: '→' }, windows: { auto: '→', symbol: '→', text: '→' }, linux: { auto: '→', symbol: '→', text: '→' } },\n};\n\n/**\n * Render a raw `KeyboardEvent.key` value as a platform-appropriate\n * `KeySpec`. Named non-modifier keys (Esc, Enter, Tab, Space, Backspace,\n * Delete, arrows) get the platform's conventional form. Letter / digit /\n * other keys pass through upper-cased.\n *\n * ```ts\n * keySpecFromKey('Escape', { platform: 'macos' }) // { label: '⎋' }\n * keySpecFromKey('Escape', { platform: 'windows' }) // { label: 'Esc' }\n * keySpecFromKey('p') // { label: 'P' }\n * ```\n *\n * Symbol mode is only meaningful on macOS — Windows / Linux keyboards\n * print the text form on the physical key, so symbol mode there falls\n * back to text.\n */\nexport function keySpecFromKey(\n raw: string,\n opts: KeySpecsFromModsOptions & { optional?: boolean } = {},\n): KeySpec {\n const platform = opts.platform ?? detectPlatform();\n const legend = opts.legend ?? 'auto';\n const entry = NAMED_KEY[raw];\n const label = entry ? entry[platform][legend] : raw.length === 1 ? raw.toUpperCase() : raw;\n return opts.optional ? { label, optional: true } : { label };\n}\n"],"mappings":";;;;;;;GCGM,IAAkB,IAAI,IAAI;CAAC;CAAK;CAAK;CAAK;CAAK;CAAI,CAAC,EACpD,IAAc,IAAI,IAAI;CAAC;CAAK;CAAK;CAAI,CAAC;AAY5C,SAAgB,EAAgB,GAA2B;AAIzD,QAHI,EAAgB,IAAI,EAAM,GAAS,aACnC,EAAY,IAAI,EAAM,IACtB,EAAM,SAAS,IAAU,SACtB;;AAoCT,SAAgB,EAAO,EAAE,UAAO,cAAW,IAAO,aAAU,WAAW,cAAW,UAAO,WAAqB;CAC5G,IAAM,IAAgB,IAAO;EAAE,GAAG;EAAO,YAAY;EAAM,GAAG;AAC9D,QACE,kBAAC,OAAD;EACE,WAAW,CAAC,EAAE,KAAK,EAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI;EACvD,aAAW,EAAgB,EAAM;EACjC,gBAAc,MAAY,YAAY,KAAA,IAAY;EAClD,iBAAe,KAAY,KAAA;EAC3B,OAAO;YAEN;EACG,CAAA;;;;ACjCV,SAAgB,EAAY,EAAE,SAAM,eAAY,KAAK,aAAU,WAAW,SAAM,gBAA+B;AAC7G,KAAI,CAAC,KAAQ,EAAK,WAAW,EAC3B,QAAO,kBAAC,QAAD;EAAM,WAAW,CAAC,EAAE,WAAW,EAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI;YAAE;EAAQ,CAAA;CAEtF,IAAM,IAAU,EACb,KAAK,GAAG,OAAO;EAAE;EAAG;EAAG,EAAE,CACzB,MAAM,GAAG,OACG,EAAgB,EAAE,EAAE,MAAM,KAAK,aAAa,IAAI,MAChD,EAAgB,EAAE,EAAE,MAAM,KAAK,aAAa,IAAI,MACzC,EAAE,IAAI,EAAE,EAC1B,CACD,KAAK,EAAE,WAAQ,EAAE,EACd,IAAS,EAAQ,WAAW,MAAM,EAAgB,EAAE,MAAM,KAAK,WAAW,EAC1E,IAAU,CAAC,CAAC,KAAa,IAAS,KAAK,IAAS,EAAQ;AAC9D,QACE,kBAAC,QAAD;EAAM,WAAW,CAAC,EAAE,MAAM,EAAU,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI;YAC3D,EAAQ,KAAK,GAAG,MACf,kBAAC,GAAD,EAAA,UAAA,CACG,KAAW,MAAM,IAAS,kBAAC,QAAD;GAAM,WAAW,EAAE;aAAM;GAAiB,CAAA,GAAG,MACxE,kBAAC,GAAD;GAAQ,OAAO,EAAE;GAAO,UAAU,EAAE,YAAY;GAAgB;GAAe;GAAQ,CAAA,CAC9E,EAAA,EAHI,EAGJ,CACX;EACG,CAAA;;;;ACbX,IAAM,IAAqF;CACzF,KAAK;EACH,OAAS;GAAE,MAAM;GAAQ,QAAQ;GAAQ,MAAM;GAAO;EACtD,SAAS;GAAE,MAAM;GAAQ,QAAQ;GAAQ,MAAM;GAAQ;EACvD,OAAS;GAAE,MAAM;GAAQ,QAAQ;GAAQ,MAAM;GAAQ;EACxD;CACD,OAAO;EACL,OAAS;GAAE,MAAM;GAAK,QAAQ;GAAK,MAAM;GAAS;EAClD,SAAS;GAAE,MAAM;GAAK,QAAQ;GAAK,MAAM;GAAS;EAClD,OAAS;GAAE,MAAM;GAAK,QAAQ;GAAK,MAAM;GAAS;EACnD;CACD,KAAK;EACH,OAAS;GAAE,MAAM;GAAO,QAAQ;GAAO,MAAM;GAAU;EACvD,SAAS;GAAE,MAAM;GAAO,QAAQ;GAAO,MAAM;GAAO;EACpD,OAAS;GAAE,MAAM;GAAO,QAAQ;GAAO,MAAM;GAAO;EACrD;CACD,MAAM;EACJ,OAAS;GAAE,MAAM;GAAQ,QAAQ;GAAQ,MAAM;GAAW;EAC1D,SAAS;GAAE,MAAM;GAAQ,QAAQ;GAAQ,MAAM;GAAQ;EACvD,OAAS;GAAE,MAAM;GAAQ,QAAQ;GAAQ,MAAM;GAAQ;EACxD;CAGD,MAAM;EACJ,OAAS;GAAE,MAAM;GAAK,QAAQ;GAAK,MAAM;GAAO;EAChD,SAAS;GAAE,MAAM;GAAK,QAAQ;GAAK,MAAM;GAAO;EAChD,OAAS;GAAE,MAAM;GAAK,QAAQ;GAAK,MAAM;GAAS;EACnD;CACF;AAMD,SAAgB,IAA2B;AACzC,KAAI,OAAO,YAAc,IAAa,QAAO;CAG7C,IAAM,KADU,UAAoE,eAC/D,YAAY,UAAU,aAAa,IAAI,aAAa;AAIzE,QAHI,EAAI,SAAS,MAAM,IAAI,EAAI,SAAS,SAAS,IAAI,EAAI,SAAS,SAAS,IAAI,EAAI,SAAS,OAAO,GAAS,UACxG,EAAI,SAAS,MAAM,GAAS,YAC5B,EAAI,SAAS,QAAQ,IAAI,EAAI,SAAS,MAAM,GAAS,UAClD;;AAuBT,SAAgB,EACd,GACA,IAAgC,EAAE,EACd;CACpB,IAAM,IAAW,EAAK,YAAY,GAAgB,EAC5C,IAAS,EAAK,UAAU;AAC9B,QAAO,EAAK,KAAK,MAAM;EACrB,IAAM,IAAQ,EAAgB,EAAE,MAAM,GAAU;AAChD,SAAO,EAAE,WAAW;GAAE;GAAO,UAAU;GAAM,GAAG,EAAE,UAAO;GACzD;;AAMJ,IAAM,IAA2E;CAC/E,QAAa;EAAE,OAAO;GAAE,MAAM;GAAQ,QAAQ;GAAY,MAAM;GAAO;EAAQ,SAAS;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAO;EAAQ,OAAO;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAO;EAAE;CACvN,OAAa;EAAE,OAAO;GAAE,MAAM;GAAQ,QAAQ;GAAY,MAAM;GAAU;EAAK,SAAS;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAS;EAAM,OAAO;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAS;EAAE;CACzN,QAAa;EAAE,OAAO;GAAE,MAAM;GAAQ,QAAQ;GAAY,MAAM;GAAU;EAAK,SAAS;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAS;EAAM,OAAO;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAS;EAAE;CACzN,KAAa;EAAE,OAAO;GAAE,MAAM;GAAQ,QAAQ;GAAY,MAAM;GAAO;EAAQ,SAAS;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAO;EAAQ,OAAO;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAO;EAAE;CACvN,OAAa;EAAE,OAAO;GAAE,MAAM;GAAQ,QAAQ;GAAY,MAAM;GAAS;EAAM,SAAS;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAS;EAAM,OAAO;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAS;EAAE;CACzN,KAAa;EAAE,OAAO;GAAE,MAAM;GAAQ,QAAQ;GAAY,MAAM;GAAS;EAAM,SAAS;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAS;EAAM,OAAO;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAS;EAAE;CACzN,WAAa;EAAE,OAAO;GAAE,MAAM;GAAQ,QAAQ;GAAY,MAAM;GAAa;EAAE,SAAS;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAa;EAAE,OAAO;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAa;EAAE;CAC7N,QAAa;EAAE,OAAO;GAAE,MAAM;GAAQ,QAAQ;GAAY,MAAM;GAAU;EAAK,SAAS;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAU;EAAK,OAAO;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAU;EAAE;CAG1N,aAAa;EAAE,OAAO;GAAE,MAAM;GAAQ,QAAQ;GAAY,MAAM;GAAQ;EAAO,SAAS;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAQ;EAAO,OAAO;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAQ;EAAE;CACxN,SAAa;EAAE,OAAO;GAAE,MAAM;GAAQ,QAAQ;GAAY,MAAM;GAAK;EAAU,SAAS;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAK;EAAU,OAAO;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAK;EAAE;CACrN,WAAa;EAAE,OAAO;GAAE,MAAM;GAAQ,QAAQ;GAAY,MAAM;GAAK;EAAU,SAAS;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAK;EAAU,OAAO;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAK;EAAE;CACrN,WAAa;EAAE,OAAO;GAAE,MAAM;GAAQ,QAAQ;GAAY,MAAM;GAAK;EAAU,SAAS;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAK;EAAU,OAAO;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAK;EAAE;CACrN,YAAa;EAAE,OAAO;GAAE,MAAM;GAAQ,QAAQ;GAAY,MAAM;GAAK;EAAU,SAAS;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAK;EAAU,OAAO;GAAE,MAAM;GAAa,QAAQ;GAAa,MAAM;GAAK;EAAE;CACtN;AAkBD,SAAgB,EACd,GACA,IAAyD,EAAE,EAClD;CACT,IAAM,IAAW,EAAK,YAAY,GAAgB,EAC5C,IAAS,EAAK,UAAU,QACxB,IAAQ,EAAU,IAClB,IAAQ,IAAQ,EAAM,GAAU,KAAU,EAAI,WAAW,IAAI,EAAI,aAAa,GAAG;AACvF,QAAO,EAAK,WAAW;EAAE;EAAO,UAAU;EAAM,GAAG,EAAE,UAAO"}