@weasel-js/labkit 1.3.0-pre.0 → 1.4.0-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (259) hide show
  1. package/dist/_dts/CanvasStackContext-DtAn_3O-.d.ts +42 -0
  2. package/dist/_dts/DrawCommand-B3bskUsC.d.ts +564 -0
  3. package/dist/_dts/{PrefsForm-BYa6cWnO.d.ts → PrefsForm-BkUJZx0A.d.ts} +4 -1
  4. package/dist/_dts/fitViewToBounds-dZ2UDB6e.d.ts +21 -0
  5. package/dist/_dts/frac-Cp3NivlC.d.ts +728 -0
  6. package/dist/_dts/index-BDVzvRzQ.d.ts +236 -0
  7. package/dist/_dts/shapeKinds-Cx_rxwsa.d.ts +87 -0
  8. package/dist/_dts/{DrawCommand-DBB45NfN.d.ts → types-C-gh9Ap-.d.ts} +1 -582
  9. package/dist/_dts/{types-AHlQxBNN.d.ts → types-D6s4b7if.d.ts} +2 -2
  10. package/dist/_dts/{types-1Sdxy_Pv.d.ts → types-DYMaEvM5.d.ts} +26 -1
  11. package/dist/_dts/{useTrialState-CsGMjhu9.d.ts → useTrialState-gmMvZqPc.d.ts} +7 -2
  12. package/dist/canvas/index.d.ts +43 -25
  13. package/dist/canvas/index.js +5 -3
  14. package/dist/chrome/index.d.ts +9 -6
  15. package/dist/chrome/index.js +7 -6
  16. package/dist/chunk-2AYGEN57.js +32 -0
  17. package/dist/chunk-2AYGEN57.js.map +1 -0
  18. package/dist/{chunk-RVNN4CHQ.js → chunk-3P5TTTA7.js} +16 -8
  19. package/dist/chunk-3P5TTTA7.js.map +1 -0
  20. package/dist/{chunk-NS54R52R.js → chunk-3ZELRRBV.js} +45 -40
  21. package/dist/chunk-3ZELRRBV.js.map +1 -0
  22. package/dist/{chunk-HFDVGF4X.js → chunk-4EY67BKJ.js} +32 -7
  23. package/dist/chunk-4EY67BKJ.js.map +1 -0
  24. package/dist/chunk-5TCEVMPI.js +140 -0
  25. package/dist/chunk-5TCEVMPI.js.map +1 -0
  26. package/dist/{chunk-O7NVSCLN.js → chunk-6JMU56CU.js} +133 -134
  27. package/dist/chunk-6JMU56CU.js.map +1 -0
  28. package/dist/{chunk-CPUJ3QXL.js → chunk-CQLQPQ4P.js} +2 -2
  29. package/dist/chunk-CQLQPQ4P.js.map +1 -0
  30. package/dist/chunk-CTRKTLYZ.js +33 -0
  31. package/dist/chunk-CTRKTLYZ.js.map +1 -0
  32. package/dist/chunk-EQIB2PGC.js +458 -0
  33. package/dist/chunk-EQIB2PGC.js.map +1 -0
  34. package/dist/chunk-HEORBLVT.js +7128 -0
  35. package/dist/chunk-HEORBLVT.js.map +1 -0
  36. package/dist/{chunk-V7PRSIRQ.js → chunk-JDDIZSLL.js} +4 -4
  37. package/dist/chunk-JDDIZSLL.js.map +1 -0
  38. package/dist/chunk-LITE5YFB.js +669 -0
  39. package/dist/chunk-LITE5YFB.js.map +1 -0
  40. package/dist/{chunk-Z6HJ5FEM.js → chunk-ROQDRXKW.js} +732 -315
  41. package/dist/chunk-ROQDRXKW.js.map +1 -0
  42. package/dist/chunk-SBXNQW4G.js +3 -0
  43. package/dist/chunk-SBXNQW4G.js.map +1 -0
  44. package/dist/{chunk-4TMMVIDM.js → chunk-XJ6N32QP.js} +50 -6
  45. package/dist/chunk-XJ6N32QP.js.map +1 -0
  46. package/dist/chunk-YQJNT4XQ.js +293 -0
  47. package/dist/chunk-YQJNT4XQ.js.map +1 -0
  48. package/dist/controls/index.d.ts +4 -28
  49. package/dist/controls/index.js +3 -3
  50. package/dist/dragdrop/index.d.ts +50 -5
  51. package/dist/dragdrop/index.js +3 -1
  52. package/dist/index.d.ts +660 -302
  53. package/dist/index.js +1793 -587
  54. package/dist/index.js.map +1 -1
  55. package/dist/layers/index.d.ts +6 -5
  56. package/dist/layers/index.js +4 -2
  57. package/dist/loupe/index.d.ts +187 -0
  58. package/dist/loupe/index.js +7 -0
  59. package/dist/loupe/index.js.map +1 -0
  60. package/dist/passthrough/weasel-canvas.d.ts +3 -1
  61. package/dist/passthrough/weasel-canvas.js +1 -1
  62. package/dist/passthrough/weasel-ui.d.ts +151 -91
  63. package/dist/passthrough/weasel-ui.js +3 -2
  64. package/dist/primitives/index.d.ts +3 -1
  65. package/dist/primitives/index.js +6 -5
  66. package/dist/state/index.d.ts +3 -3
  67. package/dist/state/index.js +2 -2
  68. package/dist/styles.css +229 -703
  69. package/dist/surface/index.d.ts +18 -2
  70. package/dist/surface/index.js +2 -2
  71. package/dist/ui/layers/index.d.ts +8 -2
  72. package/dist/ui/layers/index.js +2 -4
  73. package/dist/undo/index.d.ts +5 -4
  74. package/package.json +14 -7
  75. package/src/annotations/AnnotationOverlay.tsx +205 -0
  76. package/src/annotations/AnnotationTargets.tsx +37 -0
  77. package/src/annotations/Annotations.less +129 -0
  78. package/src/annotations/Annotations.meaning.test.tsx +111 -0
  79. package/src/annotations/Annotations.overlay.test.tsx +142 -0
  80. package/src/annotations/AnnotationsContext.ts +49 -0
  81. package/src/annotations/ExportMenu.test.tsx +86 -0
  82. package/src/annotations/ExportMenu.tsx +182 -0
  83. package/src/annotations/MarkList.tsx +75 -0
  84. package/src/annotations/capture.test.ts +140 -0
  85. package/src/annotations/capture.ts +237 -0
  86. package/src/annotations/drawOne.test.ts +56 -0
  87. package/src/annotations/drawOne.ts +47 -0
  88. package/src/annotations/frac.test.ts +59 -0
  89. package/src/annotations/frac.ts +66 -0
  90. package/src/annotations/history.test.ts +82 -0
  91. package/src/annotations/history.ts +118 -0
  92. package/src/annotations/index.ts +37 -0
  93. package/src/annotations/paint.test.ts +125 -0
  94. package/src/annotations/paint.ts +130 -0
  95. package/src/annotations/staleness.test.ts +56 -0
  96. package/src/annotations/staleness.ts +41 -0
  97. package/src/annotations/store.test.ts +175 -0
  98. package/src/annotations/store.ts +317 -0
  99. package/src/annotations/svgNodes.test.ts +77 -0
  100. package/src/annotations/svgNodes.ts +74 -0
  101. package/src/annotations/toolMap.test.ts +36 -0
  102. package/src/annotations/toolMap.ts +54 -0
  103. package/src/annotations/types.ts +222 -0
  104. package/src/annotations/view.test.ts +37 -0
  105. package/src/annotations/view.ts +40 -0
  106. package/src/canvas/AGENTS.md +55 -5
  107. package/src/canvas/CanvasStack.test.tsx +27 -1
  108. package/src/canvas/CanvasStack.tsx +29 -4
  109. package/src/canvas/CanvasStackContext.ts +23 -4
  110. package/src/canvas/camera.test.ts +78 -0
  111. package/src/canvas/camera.ts +62 -0
  112. package/src/canvas/canvasCoords.test.ts +26 -0
  113. package/src/canvas/canvasCoords.ts +20 -7
  114. package/src/canvas/index.ts +5 -1
  115. package/src/canvas/useLayerScheduler.ts +7 -3
  116. package/src/canvas/usePanZoom.test.ts +36 -5
  117. package/src/canvas/usePanZoom.ts +20 -15
  118. package/src/canvas/worldSpec.test.ts +63 -0
  119. package/src/canvas/worldSpec.ts +48 -0
  120. package/src/chrome/builtins.test.ts +31 -1
  121. package/src/chrome/builtins.tsx +63 -19
  122. package/src/chrome/regions/PaletteRegion.test.tsx +22 -1
  123. package/src/chrome/regions/PaletteRegion.tsx +4 -0
  124. package/src/chrome/regions/SidebarRegion.tsx +37 -13
  125. package/src/chrome/regions/ToolbarRegion.tsx +2 -1
  126. package/src/chrome/regions/ViewportRegion.tsx +9 -1
  127. package/src/chrome/regions/regions.test.tsx +6 -1
  128. package/src/chrome/types.ts +20 -0
  129. package/src/config/builder.test.ts +7 -1
  130. package/src/config/builder.ts +1 -5
  131. package/src/config/resolve.test.ts +1 -2
  132. package/src/controls/ControlPanel.stories.tsx +41 -2
  133. package/src/controls/ControlPanel.test.tsx +69 -9
  134. package/src/controls/ControlPanel.tsx +106 -25
  135. package/src/dragdrop/DragDropRuntime.tsx +8 -2
  136. package/src/dragdrop/dragDrop.test.tsx +29 -2
  137. package/src/dragdrop/index.ts +11 -0
  138. package/src/index.test.ts +22 -0
  139. package/src/index.ts +136 -4
  140. package/src/instrument/SineWave.smoke.test.tsx +1 -0
  141. package/src/instrument/types.ts +24 -1
  142. package/src/lab/Lab.surface.test.tsx +162 -0
  143. package/src/lab/Lab.tsx +102 -17
  144. package/src/lab/Lab.undock.test.tsx +85 -0
  145. package/src/lab/LabFullChrome.stories.tsx +20 -0
  146. package/src/lab/LabShell.less +11 -0
  147. package/src/lab/Workspace.less +73 -0
  148. package/src/lab/Workspace.surface.test.tsx +2 -0
  149. package/src/lab/Workspace.tsx +97 -8
  150. package/src/lab/index.ts +1 -1
  151. package/src/lab/panelHost.ts +37 -0
  152. package/src/layers/AGENTS.md +1 -1
  153. package/src/layers/LayerList.tsx +1 -1
  154. package/src/loupe/AGENTS.md +49 -0
  155. package/src/loupe/CanvasLoupe.tsx +83 -0
  156. package/src/loupe/DomLoupe.tsx +62 -0
  157. package/src/loupe/Loupe.less +38 -0
  158. package/src/loupe/LoupeBubble.tsx +32 -0
  159. package/src/loupe/TrialLoupe.tsx +93 -0
  160. package/src/loupe/canvasLens.test.ts +205 -0
  161. package/src/loupe/canvasLens.ts +141 -0
  162. package/src/loupe/index.ts +19 -0
  163. package/src/loupe/types.test.ts +29 -0
  164. package/src/loupe/types.ts +90 -0
  165. package/src/loupe/useHostSize.ts +30 -0
  166. package/src/loupe/useLoupe.test.tsx +197 -0
  167. package/src/loupe/useLoupe.ts +189 -0
  168. package/src/passthrough/weasel-ui.ts +25 -17
  169. package/src/primitives/FloatingPanel.test.tsx +76 -1
  170. package/src/primitives/FloatingPanel.tsx +19 -3
  171. package/src/primitives/ScaleIndicator.test.tsx +7 -2
  172. package/src/primitives/Sidebar.less +18 -0
  173. package/src/primitives/Toolbar.less +8 -0
  174. package/src/primitives/Toolbar.tsx +5 -0
  175. package/src/primitives/useRovingTabIndex.test.ts +21 -0
  176. package/src/primitives/useRovingTabIndex.ts +26 -17
  177. package/src/state/document.test.ts +23 -0
  178. package/src/state/document.ts +14 -2
  179. package/src/state/index.ts +2 -0
  180. package/src/state/store.test.ts +24 -0
  181. package/src/state/store.ts +35 -1
  182. package/src/state/types.ts +11 -0
  183. package/src/state/undock.test.ts +40 -0
  184. package/src/state/undock.ts +39 -0
  185. package/src/styles.less +2 -4
  186. package/src/surface/SurfaceContext.ts +4 -0
  187. package/src/surface/index.ts +8 -3
  188. package/src/surface/useSurfaceTile.test.tsx +2 -0
  189. package/src/surface/useSurfaceTile.ts +7 -1
  190. package/src/surface/useTiledSurface.test.tsx +58 -0
  191. package/src/surface/useTiledSurface.ts +38 -3
  192. package/src/theme/Interstellar.stories.tsx +1 -1
  193. package/src/trial/Trial.annotations.persist.test.tsx +166 -0
  194. package/src/trial/Trial.annotations.test.tsx +121 -0
  195. package/src/trial/Trial.canvas.test.tsx +76 -1
  196. package/src/trial/Trial.config.test.tsx +3 -8
  197. package/src/trial/Trial.less +19 -0
  198. package/src/trial/Trial.loupe.test.tsx +128 -0
  199. package/src/trial/Trial.stories.tsx +1 -0
  200. package/src/trial/Trial.test.tsx +12 -0
  201. package/src/trial/Trial.trialId.test.tsx +36 -0
  202. package/src/trial/Trial.tsx +215 -40
  203. package/src/trial/TrialChrome.tsx +35 -0
  204. package/src/trial/UndockedSections.tsx +65 -0
  205. package/src/trial/trialOps.test.ts +21 -0
  206. package/src/trial/trialOps.ts +5 -3
  207. package/src/ui/layers/index.ts +5 -1
  208. package/dist/_dts/index-DgcNdEdh.d.ts +0 -314
  209. package/dist/chunk-4TMMVIDM.js.map +0 -1
  210. package/dist/chunk-CPUJ3QXL.js.map +0 -1
  211. package/dist/chunk-HFDVGF4X.js.map +0 -1
  212. package/dist/chunk-NS54R52R.js.map +0 -1
  213. package/dist/chunk-O2BULFHX.js +0 -152
  214. package/dist/chunk-O2BULFHX.js.map +0 -1
  215. package/dist/chunk-O7NVSCLN.js.map +0 -1
  216. package/dist/chunk-PO6L3NSH.js +0 -366
  217. package/dist/chunk-PO6L3NSH.js.map +0 -1
  218. package/dist/chunk-PQJ5B2U2.js +0 -27
  219. package/dist/chunk-PQJ5B2U2.js.map +0 -1
  220. package/dist/chunk-RVNN4CHQ.js.map +0 -1
  221. package/dist/chunk-TLGRYALP.js +0 -6151
  222. package/dist/chunk-TLGRYALP.js.map +0 -1
  223. package/dist/chunk-V7PRSIRQ.js.map +0 -1
  224. package/dist/chunk-Z6HJ5FEM.js.map +0 -1
  225. package/dist/chunk-ZZAYVX4X.js +0 -505
  226. package/dist/chunk-ZZAYVX4X.js.map +0 -1
  227. package/src/primitives/DragHandleGlyph.tsx +0 -29
  228. package/src/ui/format.test.ts +0 -33
  229. package/src/ui/format.ts +0 -30
  230. package/src/ui/layers/LayerStack.less +0 -143
  231. package/src/ui/layers/LayerStack.stories.tsx +0 -45
  232. package/src/ui/layers/LayerStack.test.tsx +0 -170
  233. package/src/ui/layers/LayerStack.tsx +0 -208
  234. package/src/ui/properties/CheckboxRow.stories.tsx +0 -27
  235. package/src/ui/properties/ColorRow.stories.tsx +0 -106
  236. package/src/ui/properties/CurveField.less +0 -48
  237. package/src/ui/properties/CurveField.stories.tsx +0 -20
  238. package/src/ui/properties/CurveField.test.tsx +0 -127
  239. package/src/ui/properties/CurveField.tsx +0 -184
  240. package/src/ui/properties/EffectCard.tsx +0 -324
  241. package/src/ui/properties/Gallery.stories.tsx +0 -178
  242. package/src/ui/properties/NumberRow.stories.tsx +0 -65
  243. package/src/ui/properties/PropertyGroup.less +0 -50
  244. package/src/ui/properties/PropertyGroup.stories.tsx +0 -34
  245. package/src/ui/properties/PropertyGroup.test.tsx +0 -33
  246. package/src/ui/properties/PropertyGroup.tsx +0 -45
  247. package/src/ui/properties/PropertyList.stories.tsx +0 -83
  248. package/src/ui/properties/PropertyPanel.less +0 -606
  249. package/src/ui/properties/PropertyPanel.stories.tsx +0 -190
  250. package/src/ui/properties/PropertyPanel.test.tsx +0 -193
  251. package/src/ui/properties/PropertyPanel.tsx +0 -469
  252. package/src/ui/properties/PropertyRow.stories.tsx +0 -53
  253. package/src/ui/properties/SelectRow.stories.tsx +0 -58
  254. package/src/ui/properties/SliderRow.stories.tsx +0 -74
  255. package/src/ui/properties/SpeechBalloonPanels.stories.tsx +0 -398
  256. package/src/ui/properties/TextRow.stories.tsx +0 -62
  257. package/src/ui/properties/ToggleRow.stories.tsx +0 -57
  258. package/src/ui/properties/index.ts +0 -39
  259. package/src/ui/properties/storyLayouts.tsx +0 -36
@@ -1,469 +0,0 @@
1
- import { type ReactNode, useState } from 'react';
2
- import { dlog } from '../../passthrough/weasel-ui';
3
- import { formatNumber, parseSignedNumber } from '../format';
4
-
5
- /** Props for `<PropertyPanel>`. */
6
- export interface PropertyPanelProps {
7
- title?: ReactNode;
8
- children: ReactNode;
9
- className?: string;
10
- }
11
-
12
- /** A titled panel holding property rows — the sidebar container the rest of
13
- * this module's components fill. */
14
- export function PropertyPanel({ title, children, className }: PropertyPanelProps) {
15
- const cls = className ? `lk-property-panel ${className}` : 'lk-property-panel';
16
- return (
17
- <div className={cls}>
18
- {title != null && <h2 className="lk-property-panel__title">{title}</h2>}
19
- {children}
20
- </div>
21
- );
22
- }
23
-
24
- /** How a property list packs its rows into two columns. */
25
- export type PropertyListPack = 'auto-color' | 'pairs';
26
-
27
- /** Props for `<PropertyList>`. */
28
- export interface PropertyListProps {
29
- children: ReactNode;
30
- className?: string;
31
- /**
32
- * How rows pack into the 2-column grid.
33
- * - `'auto-color'` (default): only color rows pair side-by-side; everything
34
- * else spans the full width. Right for sparse top-level panels.
35
- * - `'pairs'`: every row auto-places into the 2-column grid two-per-row.
36
- * Headers, subpanels, and full-width children (`<hr>`, curve blocks)
37
- * still span via `lk-property-list__span`. Right for dense effect bodies.
38
- */
39
- pack?: PropertyListPack;
40
- }
41
-
42
- /**
43
- * Grid container for PropertyRows. Use standalone for chrome-less layouts, or
44
- * nest inside <PropertyPanel/> for the standard glass card.
45
- */
46
- export function PropertyList({ children, className, pack = 'auto-color' }: PropertyListProps) {
47
- const packClass = pack === 'pairs' ? ' lk-property-list--pairs' : '';
48
- const cls = `lk-property-list${packClass}${className ? ` ${className}` : ''}`;
49
- return <div className={cls}>{children}</div>;
50
- }
51
-
52
- /** Which control shape a row holds, which decides its intrinsic layout. */
53
- export type PropertyRowVariant = 'default' | 'color' | 'checkbox';
54
- /** Whether a row's label sits above its control or beside it. */
55
- export type PropertyRowLayout = 'block' | 'inline';
56
-
57
- /** Props for `<PropertyRow>`. */
58
- export interface PropertyRowProps {
59
- label: ReactNode;
60
- /** Right-aligned readout shown next to the label (e.g. current value). */
61
- readout?: ReactNode;
62
- variant?: PropertyRowVariant;
63
- /**
64
- * Label position relative to the control. "block" (default) stacks
65
- * label above control; "inline" places the label to the left.
66
- * Color and checkbox variants are always inline by their own nature.
67
- */
68
- layout?: PropertyRowLayout;
69
- children: ReactNode;
70
- htmlFor?: string;
71
- className?: string;
72
- }
73
-
74
- /** The label-plus-control frame every typed row below is built from. Use it
75
- * directly for a control this module does not cover. */
76
- export function PropertyRow({
77
- label,
78
- readout,
79
- variant = 'default',
80
- layout = 'block',
81
- children,
82
- htmlFor,
83
- className,
84
- }: PropertyRowProps) {
85
- const variantClass = variant === 'default' ? '' : ` lk-property-row--${variant}`;
86
- // Layout only meaningfully applies to the default variant; color and
87
- // checkbox have their own intrinsic orientation.
88
- const layoutClass =
89
- variant === 'default' && layout === 'inline' ? ' lk-property-row--inline' : '';
90
- const cls = `lk-property-row${variantClass}${layoutClass}${className ? ` ${className}` : ''}`;
91
- return (
92
- <label className={cls} htmlFor={htmlFor}>
93
- <span className="lk-property-row__label">
94
- {label}
95
- {readout != null && <em className="lk-property-row__readout">{readout}</em>}
96
- </span>
97
- {children}
98
- </label>
99
- );
100
- }
101
-
102
- // ── Row implementations ──────────────────────────────────────────────
103
-
104
- /** Props for `<SliderRow>`. */
105
- export interface SliderRowProps {
106
- label: ReactNode;
107
- value: number;
108
- min: number;
109
- max: number;
110
- step?: number;
111
- onChange: (next: number) => void;
112
- /** Override how the value is rendered next to the label. Defaults to `value.toString()`. */
113
- format?: (value: number) => ReactNode;
114
- /**
115
- * Optional suffix rendered next to the readout. A string becomes a
116
- * baseline-aligned dim "word" unit (e.g. "px"); pass JSX like `<sup>°</sup>`
117
- * to get the browser's native super positioning for symbol units.
118
- */
119
- unit?: ReactNode;
120
- layout?: PropertyRowLayout;
121
- }
122
-
123
- /** A bounded number edited by dragging, with a live readout whose precision
124
- * follows `step`. */
125
- export function SliderRow({
126
- label,
127
- value,
128
- min,
129
- max,
130
- step = 1,
131
- onChange,
132
- format,
133
- unit,
134
- layout,
135
- }: SliderRowProps) {
136
- // Default readout precision tracks `step`: integer steps → 0 decimals,
137
- // 0.1 → 1 decimal, 0.05/0.02/0.01 → 2 decimals, 0.005 → 3, etc. Callers
138
- // can still pass an explicit `format` to override (e.g. for a custom
139
- // unit string or a non-decimal display like fractions).
140
- const decimals = step >= 1 ? 0 : Math.min(6, Math.max(0, Math.ceil(-Math.log10(step))));
141
- const effectiveFormat = format ?? ((n: number) => formatNumber(n, decimals));
142
- return (
143
- <PropertyRow
144
- label={label}
145
- readout={
146
- <EditableReadout
147
- value={value}
148
- min={min}
149
- max={max}
150
- format={effectiveFormat}
151
- unit={unit}
152
- onCommit={onChange}
153
- />
154
- }
155
- layout={layout}
156
- >
157
- <input
158
- type="range"
159
- tabIndex={-1}
160
- min={min}
161
- max={max}
162
- step={step}
163
- value={value}
164
- onChange={(e) => {
165
- const v = Number(e.target.value);
166
- dlog('property-panel', 'slider', { label, value: v });
167
- onChange(v);
168
- }}
169
- />
170
- </PropertyRow>
171
- );
172
- }
173
-
174
- interface EditableReadoutProps {
175
- value: number;
176
- min: number;
177
- max: number;
178
- format?: (value: number) => ReactNode;
179
- unit?: ReactNode;
180
- onCommit: (next: number) => void;
181
- }
182
-
183
- /**
184
- * Readout that swaps to a number input on click, commits on Enter/blur,
185
- * cancels on Escape. Clicks are stopped so the wrapping <label> doesn't
186
- * forward focus to the slider thumb.
187
- */
188
- function EditableReadout({ value, min, max, format, unit, onCommit }: EditableReadoutProps) {
189
- // Draft is non-null only while the input is focused. Live value mirrors
190
- // into the input otherwise. Pattern mirrors speech-balloons Lab.tsx:893-942.
191
- const [draft, setDraft] = useState<string | null>(null);
192
- const fmt = (n: number) => (format ? format(n) : formatNumber(n));
193
- const displayValue =
194
- draft !== null
195
- ? draft
196
- : (() => {
197
- const formatted = fmt(value);
198
- return typeof formatted === 'string' ? formatted : String(formatted);
199
- })();
200
-
201
- const suffix =
202
- unit == null ? null : typeof unit === 'string' ? (
203
- <span className="lk-property-row__readout-unit">{unit}</span>
204
- ) : (
205
- unit
206
- );
207
-
208
- const commit = () => {
209
- if (draft !== null) {
210
- const n = parseSignedNumber(draft);
211
- if (Number.isFinite(n)) onCommit(Math.min(max, Math.max(min, n)));
212
- }
213
- setDraft(null);
214
- };
215
-
216
- return (
217
- <span className="lk-property-row__readout-group">
218
- <input
219
- type="text"
220
- inputMode="decimal"
221
- className="lk-property-row__readout-input"
222
- value={displayValue}
223
- onFocus={(e) => {
224
- const formatted = fmt(value);
225
- setDraft(typeof formatted === 'string' ? formatted : String(formatted));
226
- e.currentTarget.select();
227
- }}
228
- onChange={(e) => setDraft(e.target.value.replace(/-/g, '−'))}
229
- onBlur={commit}
230
- onClick={(e) => {
231
- // Stop the click from reaching the wrapping <label> (which would
232
- // forward focus to the slider thumb).
233
- e.preventDefault();
234
- e.stopPropagation();
235
- e.currentTarget.focus();
236
- }}
237
- onMouseDown={(e) => e.stopPropagation()}
238
- onKeyDown={(e) => {
239
- if (e.key === 'Enter') {
240
- commit();
241
- e.currentTarget.blur();
242
- } else if (e.key === 'Escape') {
243
- setDraft(null);
244
- e.currentTarget.blur();
245
- }
246
- }}
247
- />
248
- {suffix}
249
- </span>
250
- );
251
- }
252
-
253
- /** Props for `<ColorRow>`. */
254
- export interface ColorRowProps {
255
- label: ReactNode;
256
- value: string;
257
- onChange: (next: string) => void;
258
- /** 0..1 alpha. When provided, a translucent slider renders beneath the swatch. */
259
- alpha?: number;
260
- onAlphaChange?: (next: number) => void;
261
- /**
262
- * Render the alpha track as inert (dimmed, no thumb, not-allowed cursor).
263
- * Use when the color's consumer drops alpha so the affordance reads dead.
264
- */
265
- alphaDisabled?: boolean;
266
- }
267
-
268
- /** A color swatch, optionally with an alpha slider beneath it. */
269
- export function ColorRow({
270
- label,
271
- value,
272
- onChange,
273
- alpha,
274
- onAlphaChange,
275
- alphaDisabled,
276
- }: ColorRowProps) {
277
- const showAlpha = alpha != null;
278
- const className = alphaDisabled ? 'lk-property-row--alpha-disabled' : undefined;
279
- return (
280
- <PropertyRow label={label} variant="color" className={className}>
281
- <input type="color" value={value} onChange={(e) => onChange(e.target.value)} />
282
- {showAlpha && (
283
- <input
284
- type="range"
285
- className="lk-property-row__alpha"
286
- min={0}
287
- max={1}
288
- step={0.01}
289
- value={alpha}
290
- disabled={alphaDisabled}
291
- onChange={(e) => onAlphaChange?.(Number(e.target.value))}
292
- />
293
- )}
294
- </PropertyRow>
295
- );
296
- }
297
-
298
- /** Props for `<CheckboxRow>`. */
299
- export interface CheckboxRowProps {
300
- label: ReactNode;
301
- value: boolean;
302
- onChange: (next: boolean) => void;
303
- }
304
-
305
- /** A boolean checkbox. */
306
- export function CheckboxRow({ label, value, onChange }: CheckboxRowProps) {
307
- return (
308
- <PropertyRow label={label} variant="checkbox">
309
- <input type="checkbox" checked={value} onChange={(e) => onChange(e.target.checked)} />
310
- </PropertyRow>
311
- );
312
- }
313
-
314
- /** Props for `<TextRow>`. */
315
- export interface TextRowProps {
316
- label: ReactNode;
317
- value: string;
318
- onChange: (next: string) => void;
319
- placeholder?: string;
320
- maxLength?: number;
321
- layout?: PropertyRowLayout;
322
- }
323
-
324
- /** A single-line text input. */
325
- export function TextRow({ label, value, onChange, placeholder, maxLength, layout }: TextRowProps) {
326
- return (
327
- <PropertyRow label={label} layout={layout}>
328
- <input
329
- type="text"
330
- value={value}
331
- placeholder={placeholder}
332
- maxLength={maxLength}
333
- onChange={(e) => onChange(e.target.value)}
334
- />
335
- </PropertyRow>
336
- );
337
- }
338
-
339
- /** Props for `<NumberRow>`. */
340
- export interface NumberRowProps {
341
- label: ReactNode;
342
- value: number;
343
- onChange: (next: number) => void;
344
- min?: number;
345
- max?: number;
346
- step?: number;
347
- placeholder?: string;
348
- layout?: PropertyRowLayout;
349
- }
350
-
351
- /** A number typed directly. Reach for `<SliderRow>` when the range matters
352
- * more than the exact value. */
353
- export function NumberRow({
354
- label,
355
- value,
356
- onChange,
357
- min,
358
- max,
359
- step,
360
- placeholder,
361
- layout,
362
- }: NumberRowProps) {
363
- return (
364
- <PropertyRow label={label} layout={layout}>
365
- <input
366
- type="number"
367
- value={value}
368
- min={min}
369
- max={max}
370
- step={step}
371
- placeholder={placeholder}
372
- onChange={(e) => {
373
- const raw = e.target.value;
374
- if (raw === '') return;
375
- const n = Number(raw);
376
- if (Number.isFinite(n)) onChange(n);
377
- }}
378
- />
379
- </PropertyRow>
380
- );
381
- }
382
-
383
- export interface SelectOption<T extends string> {
384
- value: T;
385
- label: ReactNode;
386
- }
387
-
388
- /** Props for `<SelectRow>`. */
389
- export interface SelectRowProps<T extends string> {
390
- label: ReactNode;
391
- value: T;
392
- options: ReadonlyArray<SelectOption<T>>;
393
- onChange: (next: T) => void;
394
- layout?: PropertyRowLayout;
395
- }
396
-
397
- /** A dropdown over a fixed set of choices. Prefer `<ToggleRow>` when there
398
- * are only two or three and they should all stay visible. */
399
- export function SelectRow<T extends string>({
400
- label,
401
- value,
402
- options,
403
- onChange,
404
- layout,
405
- }: SelectRowProps<T>) {
406
- return (
407
- <PropertyRow label={label} layout={layout}>
408
- <select
409
- value={value}
410
- onChange={(e) => {
411
- const v = e.target.value as T;
412
- dlog('property-panel', 'select', { label, value: v });
413
- onChange(v);
414
- }}
415
- >
416
- {options.map((opt) => (
417
- <option key={opt.value} value={opt.value}>
418
- {/* HTML <option> only renders text; ReactNode → string coerce */}
419
- {opt.label as string}
420
- </option>
421
- ))}
422
- </select>
423
- </PropertyRow>
424
- );
425
- }
426
-
427
- /** Props for `<ToggleRow>`. */
428
- export interface ToggleRowProps<T extends string> {
429
- label: ReactNode;
430
- value: T;
431
- options: ReadonlyArray<SelectOption<T>>;
432
- onChange: (next: T) => void;
433
- layout?: PropertyRowLayout;
434
- }
435
-
436
- /** A segmented control: the same choice as a select, with every option
437
- * visible at once. */
438
- export function ToggleRow<T extends string>({
439
- label,
440
- value,
441
- options,
442
- onChange,
443
- layout,
444
- }: ToggleRowProps<T>) {
445
- return (
446
- <PropertyRow label={label} layout={layout}>
447
- <div className="lk-property-row__toggle">
448
- {options.map((opt) => {
449
- const selected = opt.value === value;
450
- return (
451
- <button
452
- key={opt.value}
453
- type="button"
454
- aria-pressed={selected}
455
- className={
456
- selected
457
- ? 'lk-property-row__toggle-button lk-property-row__toggle-button--selected'
458
- : 'lk-property-row__toggle-button'
459
- }
460
- onClick={() => onChange(opt.value)}
461
- >
462
- {opt.label}
463
- </button>
464
- );
465
- })}
466
- </div>
467
- </PropertyRow>
468
- );
469
- }
@@ -1,53 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { PropertyRow } from './PropertyPanel';
3
- import { SideBySide } from './storyLayouts';
4
-
5
- const meta: Meta<typeof PropertyRow> = {
6
- title: 'labkit/UI/Properties/Rows/PropertyRow',
7
- component: PropertyRow,
8
- };
9
- export default meta;
10
- type Story = StoryObj<typeof PropertyRow>;
11
-
12
- export const BothLayouts: Story = {
13
- render: () => (
14
- <SideBySide
15
- block={
16
- <PropertyRow label="Custom control">
17
- <input type="text" placeholder="anything goes" defaultValue="foo" />
18
- </PropertyRow>
19
- }
20
- inline={
21
- <PropertyRow label="Custom control" layout="inline">
22
- <input type="text" placeholder="anything goes" defaultValue="foo" />
23
- </PropertyRow>
24
- }
25
- />
26
- ),
27
- };
28
-
29
- export const WithReadout: Story = {
30
- render: () => (
31
- <div style={{ width: 280 }}>
32
- <PropertyRow label="Tempo" readout="120 bpm">
33
- <button type="button">Tap</button>
34
- </PropertyRow>
35
- </div>
36
- ),
37
- };
38
-
39
- export const Variants: Story = {
40
- render: () => (
41
- <div style={{ width: 280, display: 'flex', flexDirection: 'column', gap: 10 }}>
42
- <PropertyRow label="Default" readout="42">
43
- <input type="text" defaultValue="hello" />
44
- </PropertyRow>
45
- <PropertyRow label="Color variant" variant="color">
46
- <input type="color" defaultValue="#b08adb" />
47
- </PropertyRow>
48
- <PropertyRow label="Checkbox variant" variant="checkbox">
49
- <input type="checkbox" defaultChecked />
50
- </PropertyRow>
51
- </div>
52
- ),
53
- };
@@ -1,58 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { useState } from 'react';
3
- import { SelectRow } from './PropertyPanel';
4
- import { SideBySide } from './storyLayouts';
5
-
6
- const meta: Meta<typeof SelectRow> = {
7
- title: 'labkit/UI/Properties/Rows/SelectRow',
8
- component: SelectRow,
9
- };
10
- export default meta;
11
- type Story = StoryObj<typeof SelectRow>;
12
-
13
- const MODE_OPTIONS = [
14
- { value: 'fill', label: 'Fill only' },
15
- { value: 'stroke', label: 'Stroke only' },
16
- { value: 'both', label: 'Fill + stroke' },
17
- ] as const;
18
-
19
- function Modes({ layout }: { layout?: 'block' | 'inline' }) {
20
- const [value, setValue] = useState<'fill' | 'stroke' | 'both'>('both');
21
- return (
22
- <SelectRow
23
- label="Render mode"
24
- value={value}
25
- onChange={setValue}
26
- options={MODE_OPTIONS}
27
- layout={layout}
28
- />
29
- );
30
- }
31
-
32
- export const BothLayouts: Story = {
33
- render: () => <SideBySide block={<Modes />} inline={<Modes layout="inline" />} />,
34
- };
35
-
36
- function ManyOptions() {
37
- const [value, setValue] = useState('round');
38
- return (
39
- <SelectRow
40
- label="Line cap"
41
- value={value}
42
- onChange={setValue}
43
- options={[
44
- { value: 'butt', label: 'Butt' },
45
- { value: 'round', label: 'Round' },
46
- { value: 'square', label: 'Square' },
47
- ]}
48
- />
49
- );
50
- }
51
-
52
- export const LineCap: Story = {
53
- render: () => (
54
- <div style={{ width: 280 }}>
55
- <ManyOptions />
56
- </div>
57
- ),
58
- };
@@ -1,74 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react-vite';
2
- import { type ReactNode, useState } from 'react';
3
- import { SliderRow } from './PropertyPanel';
4
- import { SideBySide } from './storyLayouts';
5
-
6
- const meta: Meta<typeof SliderRow> = {
7
- title: 'labkit/UI/Properties/Rows/SliderRow',
8
- component: SliderRow,
9
- };
10
- export default meta;
11
- type Story = StoryObj<typeof SliderRow>;
12
-
13
- function Controlled({
14
- initial,
15
- layout,
16
- ...rest
17
- }: {
18
- initial: number;
19
- label: string;
20
- min: number;
21
- max: number;
22
- step?: number;
23
- format?: (v: number) => string;
24
- unit?: ReactNode;
25
- layout?: 'block' | 'inline';
26
- }) {
27
- const [value, setValue] = useState(initial);
28
- return <SliderRow value={value} onChange={setValue} layout={layout} {...rest} />;
29
- }
30
-
31
- export const BothLayouts: Story = {
32
- render: () => (
33
- <SideBySide
34
- block={
35
- <Controlled
36
- initial={0.65}
37
- label="Opacity"
38
- min={0}
39
- max={1}
40
- step={0.01}
41
- format={(v) => v.toFixed(2)}
42
- />
43
- }
44
- inline={
45
- <Controlled
46
- initial={0.65}
47
- label="Opacity"
48
- min={0}
49
- max={1}
50
- step={0.01}
51
- format={(v) => v.toFixed(2)}
52
- layout="inline"
53
- />
54
- }
55
- />
56
- ),
57
- };
58
-
59
- export const WithUnit: Story = {
60
- render: () => (
61
- <div style={{ width: 280 }}>
62
- <Controlled initial={12} label="Radius" min={0} max={64} format={(v) => `${v}px`} />
63
- </div>
64
- ),
65
- };
66
-
67
- export const WithUnitSuffix: Story = {
68
- render: () => (
69
- <div style={{ width: 280, display: 'grid', gap: 12 }}>
70
- <Controlled initial={12} label="Radius" min={0} max={64} unit="px" />
71
- <Controlled initial={45} label="Angle" min={-180} max={180} unit={<sup>°</sup>} />
72
- </div>
73
- ),
74
- };