@weasel-js/labkit 1.3.0-pre.0 → 1.3.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.
- package/dist/_dts/{index-DgcNdEdh.d.ts → types-BttTIent.d.ts} +2 -2
- package/dist/_dts/{types-AHlQxBNN.d.ts → types-DCGEcMO_.d.ts} +1 -1
- package/dist/canvas/index.d.ts +2 -2
- package/dist/canvas/index.js +3 -2
- package/dist/chrome/index.d.ts +3 -3
- package/dist/chrome/index.js +6 -5
- package/dist/chunk-3XXPU73K.js +138 -0
- package/dist/chunk-3XXPU73K.js.map +1 -0
- package/dist/{chunk-PO6L3NSH.js → chunk-6X5RBC6G.js} +17 -17
- package/dist/chunk-6X5RBC6G.js.map +1 -0
- package/dist/{chunk-NS54R52R.js → chunk-BDJXIBRZ.js} +5 -17
- package/dist/chunk-BDJXIBRZ.js.map +1 -0
- package/dist/{chunk-V7PRSIRQ.js → chunk-BMW4TDP5.js} +4 -4
- package/dist/chunk-BMW4TDP5.js.map +1 -0
- package/dist/{chunk-O7NVSCLN.js → chunk-JDROYQM3.js} +8 -8
- package/dist/chunk-JDROYQM3.js.map +1 -0
- package/dist/{chunk-HFDVGF4X.js → chunk-LGKRIUSW.js} +3 -3
- package/dist/{chunk-HFDVGF4X.js.map → chunk-LGKRIUSW.js.map} +1 -1
- package/dist/{chunk-Z6HJ5FEM.js → chunk-QCICIHEO.js} +19 -7
- package/dist/{chunk-Z6HJ5FEM.js.map → chunk-QCICIHEO.js.map} +1 -1
- package/dist/{chunk-RVNN4CHQ.js → chunk-QRYPXSGP.js} +5 -5
- package/dist/{chunk-RVNN4CHQ.js.map → chunk-QRYPXSGP.js.map} +1 -1
- package/dist/chunk-RGT4EJIB.js +6961 -0
- package/dist/chunk-RGT4EJIB.js.map +1 -0
- package/dist/chunk-SBXNQW4G.js +3 -0
- package/dist/chunk-SBXNQW4G.js.map +1 -0
- package/dist/chunk-T66HU3HX.js +17 -0
- package/dist/chunk-T66HU3HX.js.map +1 -0
- package/dist/chunk-WTI26YTM.js +266 -0
- package/dist/chunk-WTI26YTM.js.map +1 -0
- package/dist/controls/index.d.ts +2 -2
- package/dist/controls/index.js +3 -3
- package/dist/dragdrop/index.d.ts +44 -3
- package/dist/dragdrop/index.js +3 -1
- package/dist/index.d.ts +571 -541
- package/dist/index.js +69 -541
- package/dist/index.js.map +1 -1
- package/dist/layers/index.d.ts +3 -3
- package/dist/layers/index.js +4 -2
- package/dist/passthrough/weasel-canvas.js +1 -1
- package/dist/passthrough/weasel-ui.d.ts +15 -2
- package/dist/passthrough/weasel-ui.js +3 -2
- package/dist/primitives/index.js +5 -4
- package/dist/styles.css +1 -703
- package/dist/surface/index.js +2 -2
- package/dist/ui/layers/index.d.ts +8 -2
- package/dist/ui/layers/index.js +2 -4
- package/dist/undo/index.d.ts +2 -2
- package/package.json +5 -5
- package/src/config/builder.test.ts +7 -1
- package/src/config/builder.ts +1 -5
- package/src/config/resolve.test.ts +1 -2
- package/src/controls/ControlPanel.stories.tsx +41 -2
- package/src/controls/ControlPanel.test.tsx +31 -9
- package/src/controls/ControlPanel.tsx +56 -22
- package/src/dragdrop/index.ts +11 -0
- package/src/index.ts +49 -2
- package/src/lab/index.ts +1 -1
- package/src/layers/AGENTS.md +1 -1
- package/src/layers/LayerList.tsx +1 -1
- package/src/passthrough/weasel-ui.ts +22 -17
- package/src/styles.less +0 -4
- package/src/theme/Interstellar.stories.tsx +1 -1
- package/src/trial/Trial.config.test.tsx +3 -8
- package/src/ui/layers/index.ts +5 -1
- package/dist/chunk-NS54R52R.js.map +0 -1
- package/dist/chunk-O2BULFHX.js +0 -152
- package/dist/chunk-O2BULFHX.js.map +0 -1
- package/dist/chunk-O7NVSCLN.js.map +0 -1
- package/dist/chunk-PO6L3NSH.js.map +0 -1
- package/dist/chunk-PQJ5B2U2.js +0 -27
- package/dist/chunk-PQJ5B2U2.js.map +0 -1
- package/dist/chunk-TLGRYALP.js +0 -6151
- package/dist/chunk-TLGRYALP.js.map +0 -1
- package/dist/chunk-V7PRSIRQ.js.map +0 -1
- package/dist/chunk-ZZAYVX4X.js +0 -505
- package/dist/chunk-ZZAYVX4X.js.map +0 -1
- package/src/primitives/DragHandleGlyph.tsx +0 -29
- package/src/ui/format.test.ts +0 -33
- package/src/ui/format.ts +0 -30
- package/src/ui/layers/LayerStack.less +0 -143
- package/src/ui/layers/LayerStack.stories.tsx +0 -45
- package/src/ui/layers/LayerStack.test.tsx +0 -170
- package/src/ui/layers/LayerStack.tsx +0 -208
- package/src/ui/properties/CheckboxRow.stories.tsx +0 -27
- package/src/ui/properties/ColorRow.stories.tsx +0 -106
- package/src/ui/properties/CurveField.less +0 -48
- package/src/ui/properties/CurveField.stories.tsx +0 -20
- package/src/ui/properties/CurveField.test.tsx +0 -127
- package/src/ui/properties/CurveField.tsx +0 -184
- package/src/ui/properties/EffectCard.tsx +0 -324
- package/src/ui/properties/Gallery.stories.tsx +0 -178
- package/src/ui/properties/NumberRow.stories.tsx +0 -65
- package/src/ui/properties/PropertyGroup.less +0 -50
- package/src/ui/properties/PropertyGroup.stories.tsx +0 -34
- package/src/ui/properties/PropertyGroup.test.tsx +0 -33
- package/src/ui/properties/PropertyGroup.tsx +0 -45
- package/src/ui/properties/PropertyList.stories.tsx +0 -83
- package/src/ui/properties/PropertyPanel.less +0 -606
- package/src/ui/properties/PropertyPanel.stories.tsx +0 -190
- package/src/ui/properties/PropertyPanel.test.tsx +0 -193
- package/src/ui/properties/PropertyPanel.tsx +0 -469
- package/src/ui/properties/PropertyRow.stories.tsx +0 -53
- package/src/ui/properties/SelectRow.stories.tsx +0 -58
- package/src/ui/properties/SliderRow.stories.tsx +0 -74
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +0 -398
- package/src/ui/properties/TextRow.stories.tsx +0 -62
- package/src/ui/properties/ToggleRow.stories.tsx +0 -57
- package/src/ui/properties/index.ts +0 -39
- 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
|
-
};
|