@weasel-js/labkit 0.1.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/README.md +72 -0
- package/dist/_dts/fitViewToBounds-rWrBI4a6.d.ts +870 -0
- package/dist/_dts/index-C6Yze7sQ.d.ts +93 -0
- package/dist/_dts/types-Si4Fw-1F.d.ts +48 -0
- package/dist/_dts/useExperimentState-eSvpwV_P.d.ts +102 -0
- package/dist/canvas/index.d.ts +34 -0
- package/dist/canvas/index.js +4 -0
- package/dist/canvas/index.js.map +1 -0
- package/dist/chunk-2QNYYL3V.js +86 -0
- package/dist/chunk-2QNYYL3V.js.map +1 -0
- package/dist/chunk-54IQ2DX7.js +222 -0
- package/dist/chunk-54IQ2DX7.js.map +1 -0
- package/dist/chunk-6FO7G5BB.js +6095 -0
- package/dist/chunk-6FO7G5BB.js.map +1 -0
- package/dist/chunk-7BKDG73Z.js +54 -0
- package/dist/chunk-7BKDG73Z.js.map +1 -0
- package/dist/chunk-B5ZUYRKN.js +23330 -0
- package/dist/chunk-B5ZUYRKN.js.map +1 -0
- package/dist/chunk-HXZHVU4G.js +126 -0
- package/dist/chunk-HXZHVU4G.js.map +1 -0
- package/dist/chunk-PWC7AQZM.js +8 -0
- package/dist/chunk-PWC7AQZM.js.map +1 -0
- package/dist/chunk-SD3HDXR7.js +161 -0
- package/dist/chunk-SD3HDXR7.js.map +1 -0
- package/dist/chunk-SSMHPMDV.js +350 -0
- package/dist/chunk-SSMHPMDV.js.map +1 -0
- package/dist/chunk-VLAHRJOC.js +201 -0
- package/dist/chunk-VLAHRJOC.js.map +1 -0
- package/dist/controls/index.d.ts +13 -0
- package/dist/controls/index.js +3 -0
- package/dist/controls/index.js.map +1 -0
- package/dist/dragdrop/index.d.ts +3 -0
- package/dist/dragdrop/index.js +3 -0
- package/dist/dragdrop/index.js.map +1 -0
- package/dist/fonts/oswald-latin-variable.woff2 +0 -0
- package/dist/index.d.ts +410 -0
- package/dist/index.js +1425 -0
- package/dist/index.js.map +1 -0
- package/dist/layers/index.d.ts +17 -0
- package/dist/layers/index.js +3 -0
- package/dist/layers/index.js.map +1 -0
- package/dist/passthrough/weasel-canvas.d.ts +258 -0
- package/dist/passthrough/weasel-canvas.js +3 -0
- package/dist/passthrough/weasel-canvas.js.map +1 -0
- package/dist/passthrough/weasel-ui.d.ts +2826 -0
- package/dist/passthrough/weasel-ui.js +4 -0
- package/dist/passthrough/weasel-ui.js.map +1 -0
- package/dist/primitives/index.d.ts +57 -0
- package/dist/primitives/index.js +4 -0
- package/dist/primitives/index.js.map +1 -0
- package/dist/state/index.d.ts +33 -0
- package/dist/state/index.js +34 -0
- package/dist/state/index.js.map +1 -0
- package/dist/styles.css +1224 -0
- package/dist/theme-interstellar.css +25 -0
- package/dist/theme-light.css +13 -0
- package/dist/ui/layers/index.d.ts +42 -0
- package/dist/ui/layers/index.js +5 -0
- package/dist/ui/layers/index.js.map +1 -0
- package/dist/undo/index.d.ts +30 -0
- package/dist/undo/index.js +3 -0
- package/dist/undo/index.js.map +1 -0
- package/package.json +99 -0
- package/src/canvas/AGENTS.md +72 -0
- package/src/canvas/CanvasStack.less +22 -0
- package/src/canvas/CanvasStack.stories.tsx +63 -0
- package/src/canvas/CanvasStack.test.tsx +82 -0
- package/src/canvas/CanvasStack.tsx +108 -0
- package/src/canvas/CanvasStackContext.ts +8 -0
- package/src/canvas/canvasCoords.test.ts +30 -0
- package/src/canvas/canvasCoords.ts +15 -0
- package/src/canvas/index.ts +6 -0
- package/src/canvas/useLayerScheduler.ts +72 -0
- package/src/canvas/usePanZoom.ts +98 -0
- package/src/controls/ControlPanel.less +61 -0
- package/src/controls/ControlPanel.stories.tsx +77 -0
- package/src/controls/ControlPanel.test.tsx +149 -0
- package/src/controls/ControlPanel.tsx +249 -0
- package/src/controls/index.ts +12 -0
- package/src/controls/types.ts +61 -0
- package/src/dragdrop/DragDropRuntime.tsx +135 -0
- package/src/dragdrop/DragGhost.less +17 -0
- package/src/dragdrop/DragGhost.tsx +24 -0
- package/src/dragdrop/Palette.less +34 -0
- package/src/dragdrop/Palette.tsx +27 -0
- package/src/dragdrop/dragDrop.test.tsx +145 -0
- package/src/dragdrop/index.ts +1 -0
- package/src/fonts/oswald-latin-variable.woff2 +0 -0
- package/src/index.ts +70 -0
- package/src/instrument/SineWave.smoke.test.tsx +94 -0
- package/src/instrument/capabilityDetector.test.ts +64 -0
- package/src/instrument/capabilityDetector.ts +17 -0
- package/src/instrument/defineInstrument.test.ts +27 -0
- package/src/instrument/defineInstrument.ts +5 -0
- package/src/instrument/index.ts +19 -0
- package/src/instrument/types.ts +65 -0
- package/src/instrument/validateConfigSchema.test.ts +125 -0
- package/src/instrument/validateConfigSchema.ts +65 -0
- package/src/lab/Lab.less +3 -0
- package/src/lab/Lab.stories.tsx +51 -0
- package/src/lab/Lab.test.tsx +126 -0
- package/src/lab/Lab.tsx +196 -0
- package/src/lab/LabContext.test.ts +11 -0
- package/src/lab/LabContext.ts +28 -0
- package/src/lab/LabShell.less +45 -0
- package/src/lab/LabShell.stories.tsx +39 -0
- package/src/lab/LabShell.test.tsx +59 -0
- package/src/lab/LabShell.tsx +33 -0
- package/src/lab/WorkspaceGrid.less +8 -0
- package/src/lab/WorkspaceGrid.stories.tsx +61 -0
- package/src/lab/WorkspaceGrid.test.tsx +40 -0
- package/src/lab/WorkspaceGrid.tsx +20 -0
- package/src/lab/gridDims.test.ts +35 -0
- package/src/lab/gridDims.ts +11 -0
- package/src/lab/index.ts +10 -0
- package/src/layers/AGENTS.md +65 -0
- package/src/layers/LayerList.less +53 -0
- package/src/layers/LayerList.test.tsx +49 -0
- package/src/layers/LayerList.tsx +105 -0
- package/src/layers/index.ts +3 -0
- package/src/passthrough/weasel-canvas.test.ts +30 -0
- package/src/passthrough/weasel-canvas.ts +23 -0
- package/src/passthrough/weasel-ui.test.ts +79 -0
- package/src/passthrough/weasel-ui.ts +150 -0
- package/src/primitives/FpsMeter.less +14 -0
- package/src/primitives/FpsMeter.stories.tsx +14 -0
- package/src/primitives/FpsMeter.test.tsx +38 -0
- package/src/primitives/FpsMeter.tsx +36 -0
- package/src/primitives/ScaleIndicator.less +25 -0
- package/src/primitives/ScaleIndicator.stories.tsx +16 -0
- package/src/primitives/ScaleIndicator.test.tsx +46 -0
- package/src/primitives/ScaleIndicator.tsx +48 -0
- package/src/primitives/Sidebar.less +57 -0
- package/src/primitives/Sidebar.stories.tsx +38 -0
- package/src/primitives/Sidebar.test.tsx +74 -0
- package/src/primitives/Sidebar.tsx +32 -0
- package/src/primitives/StatusBar.less +21 -0
- package/src/primitives/StatusBar.stories.tsx +24 -0
- package/src/primitives/StatusBar.test.tsx +25 -0
- package/src/primitives/StatusBar.tsx +18 -0
- package/src/primitives/Toolbar.less +43 -0
- package/src/primitives/Toolbar.stories.tsx +36 -0
- package/src/primitives/Toolbar.test.tsx +65 -0
- package/src/primitives/Toolbar.tsx +44 -0
- package/src/primitives/fpsAverage.test.ts +21 -0
- package/src/primitives/fpsAverage.ts +5 -0
- package/src/primitives/index.ts +9 -0
- package/src/state/SingletonExperiment.test.tsx +101 -0
- package/src/state/SingletonExperiment.tsx +52 -0
- package/src/state/adapters.test.ts +94 -0
- package/src/state/adapters.ts +102 -0
- package/src/state/context.tsx +42 -0
- package/src/state/helpers.test.ts +89 -0
- package/src/state/helpers.ts +61 -0
- package/src/state/index.ts +40 -0
- package/src/state/store.test.ts +243 -0
- package/src/state/store.ts +225 -0
- package/src/state/types.ts +53 -0
- package/src/state/useExperimentState.test.tsx +102 -0
- package/src/state/useExperimentState.ts +28 -0
- package/src/styles.less +20 -0
- package/src/test-setup.ts +17 -0
- package/src/theme/Interstellar.stories.less +92 -0
- package/src/theme/Interstellar.stories.tsx +243 -0
- package/src/theme/base.less +122 -0
- package/src/theme/interstellar.less +34 -0
- package/src/theme/light.less +14 -0
- package/src/theme/tokens.less +85 -0
- package/src/ui/format.ts +19 -0
- package/src/ui/layers/LayerStack.less +143 -0
- package/src/ui/layers/LayerStack.stories.tsx +45 -0
- package/src/ui/layers/LayerStack.test.tsx +170 -0
- package/src/ui/layers/LayerStack.tsx +215 -0
- package/src/ui/layers/index.ts +1 -0
- package/src/ui/properties/CheckboxRow.stories.tsx +27 -0
- package/src/ui/properties/ColorRow.stories.tsx +106 -0
- package/src/ui/properties/CurveField.less +48 -0
- package/src/ui/properties/CurveField.stories.tsx +20 -0
- package/src/ui/properties/CurveField.test.tsx +127 -0
- package/src/ui/properties/CurveField.tsx +181 -0
- package/src/ui/properties/EffectCard.tsx +312 -0
- package/src/ui/properties/Gallery.stories.tsx +178 -0
- package/src/ui/properties/NumberRow.stories.tsx +65 -0
- package/src/ui/properties/PropertyGroup.less +50 -0
- package/src/ui/properties/PropertyGroup.stories.tsx +34 -0
- package/src/ui/properties/PropertyGroup.test.tsx +33 -0
- package/src/ui/properties/PropertyGroup.tsx +44 -0
- package/src/ui/properties/PropertyList.stories.tsx +83 -0
- package/src/ui/properties/PropertyPanel.less +592 -0
- package/src/ui/properties/PropertyPanel.stories.tsx +191 -0
- package/src/ui/properties/PropertyPanel.test.tsx +193 -0
- package/src/ui/properties/PropertyPanel.tsx +441 -0
- package/src/ui/properties/PropertyRow.stories.tsx +53 -0
- package/src/ui/properties/SelectRow.stories.tsx +58 -0
- package/src/ui/properties/SliderRow.stories.tsx +74 -0
- package/src/ui/properties/SpeechBalloonPanels.stories.tsx +396 -0
- package/src/ui/properties/TextRow.stories.tsx +62 -0
- package/src/ui/properties/ToggleRow.stories.tsx +57 -0
- package/src/ui/properties/index.ts +39 -0
- package/src/ui/properties/storyLayouts.tsx +36 -0
- package/src/undo/eventBus.test.ts +49 -0
- package/src/undo/eventBus.ts +32 -0
- package/src/undo/index.ts +5 -0
- package/src/undo/undoStack.test.ts +70 -0
- package/src/undo/undoStack.ts +38 -0
- package/src/workspace/DefaultSidebar.tsx +26 -0
- package/src/workspace/DefaultStatusBar.tsx +15 -0
- package/src/workspace/DefaultToolbar.tsx +88 -0
- package/src/workspace/Workspace.less +58 -0
- package/src/workspace/Workspace.stories.tsx +78 -0
- package/src/workspace/Workspace.test.tsx +186 -0
- package/src/workspace/Workspace.tsx +255 -0
- package/src/workspace/WorkspaceChrome.tsx +150 -0
- package/src/workspace/index.ts +24 -0
- package/src/workspace/slotTypes.ts +44 -0
- package/src/workspace/workspaceOps.test.ts +142 -0
- package/src/workspace/workspaceOps.ts +78 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { type ReactNode, useState } from 'react';
|
|
2
|
+
import { dlog } from '../../passthrough/weasel-ui';
|
|
3
|
+
import { formatNumber, parseSignedNumber } from '../format';
|
|
4
|
+
|
|
5
|
+
export interface PropertyPanelProps {
|
|
6
|
+
title?: ReactNode;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function PropertyPanel({ title, children, className }: PropertyPanelProps) {
|
|
12
|
+
const cls = className ? `lk-property-panel ${className}` : 'lk-property-panel';
|
|
13
|
+
return (
|
|
14
|
+
<div className={cls}>
|
|
15
|
+
{title != null && <h2 className="lk-property-panel__title">{title}</h2>}
|
|
16
|
+
{children}
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type PropertyListPack = 'auto-color' | 'pairs';
|
|
22
|
+
|
|
23
|
+
export interface PropertyListProps {
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
className?: string;
|
|
26
|
+
/**
|
|
27
|
+
* How rows pack into the 2-column grid.
|
|
28
|
+
* - `'auto-color'` (default): only color rows pair side-by-side; everything
|
|
29
|
+
* else spans the full width. Right for sparse top-level panels.
|
|
30
|
+
* - `'pairs'`: every row auto-places into the 2-column grid two-per-row.
|
|
31
|
+
* Headers, subpanels, and full-width children (`<hr>`, curve blocks)
|
|
32
|
+
* still span via `lk-property-list__span`. Right for dense effect bodies.
|
|
33
|
+
*/
|
|
34
|
+
pack?: PropertyListPack;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Grid container for PropertyRows. Use standalone for chrome-less layouts, or
|
|
39
|
+
* nest inside <PropertyPanel/> for the standard glass card.
|
|
40
|
+
*/
|
|
41
|
+
export function PropertyList({ children, className, pack = 'auto-color' }: PropertyListProps) {
|
|
42
|
+
const packClass = pack === 'pairs' ? ' lk-property-list--pairs' : '';
|
|
43
|
+
const cls = `lk-property-list${packClass}${className ? ` ${className}` : ''}`;
|
|
44
|
+
return <div className={cls}>{children}</div>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type PropertyRowVariant = 'default' | 'color' | 'checkbox';
|
|
48
|
+
export type PropertyRowLayout = 'block' | 'inline';
|
|
49
|
+
|
|
50
|
+
export interface PropertyRowProps {
|
|
51
|
+
label: ReactNode;
|
|
52
|
+
/** Right-aligned readout shown next to the label (e.g. current value). */
|
|
53
|
+
readout?: ReactNode;
|
|
54
|
+
variant?: PropertyRowVariant;
|
|
55
|
+
/**
|
|
56
|
+
* Label position relative to the control. "block" (default) stacks
|
|
57
|
+
* label above control; "inline" places the label to the left.
|
|
58
|
+
* Color and checkbox variants are always inline by their own nature.
|
|
59
|
+
*/
|
|
60
|
+
layout?: PropertyRowLayout;
|
|
61
|
+
children: ReactNode;
|
|
62
|
+
htmlFor?: string;
|
|
63
|
+
className?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function PropertyRow({
|
|
67
|
+
label,
|
|
68
|
+
readout,
|
|
69
|
+
variant = 'default',
|
|
70
|
+
layout = 'block',
|
|
71
|
+
children,
|
|
72
|
+
htmlFor,
|
|
73
|
+
className,
|
|
74
|
+
}: PropertyRowProps) {
|
|
75
|
+
const variantClass = variant === 'default' ? '' : ` lk-property-row--${variant}`;
|
|
76
|
+
// Layout only meaningfully applies to the default variant; color and
|
|
77
|
+
// checkbox have their own intrinsic orientation.
|
|
78
|
+
const layoutClass =
|
|
79
|
+
variant === 'default' && layout === 'inline' ? ' lk-property-row--inline' : '';
|
|
80
|
+
const cls = `lk-property-row${variantClass}${layoutClass}${className ? ` ${className}` : ''}`;
|
|
81
|
+
return (
|
|
82
|
+
<label className={cls} htmlFor={htmlFor}>
|
|
83
|
+
<span className="lk-property-row__label">
|
|
84
|
+
{label}
|
|
85
|
+
{readout != null && <em className="lk-property-row__readout">{readout}</em>}
|
|
86
|
+
</span>
|
|
87
|
+
{children}
|
|
88
|
+
</label>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Row implementations ──────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
export interface SliderRowProps {
|
|
95
|
+
label: ReactNode;
|
|
96
|
+
value: number;
|
|
97
|
+
min: number;
|
|
98
|
+
max: number;
|
|
99
|
+
step?: number;
|
|
100
|
+
onChange: (next: number) => void;
|
|
101
|
+
/** Override how the value is rendered next to the label. Defaults to `value.toString()`. */
|
|
102
|
+
format?: (value: number) => ReactNode;
|
|
103
|
+
/**
|
|
104
|
+
* Optional suffix rendered next to the readout. A string becomes a
|
|
105
|
+
* baseline-aligned dim "word" unit (e.g. "px"); pass JSX like `<sup>°</sup>`
|
|
106
|
+
* to get the browser's native super positioning for symbol units.
|
|
107
|
+
*/
|
|
108
|
+
unit?: ReactNode;
|
|
109
|
+
layout?: PropertyRowLayout;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function SliderRow({
|
|
113
|
+
label,
|
|
114
|
+
value,
|
|
115
|
+
min,
|
|
116
|
+
max,
|
|
117
|
+
step = 1,
|
|
118
|
+
onChange,
|
|
119
|
+
format,
|
|
120
|
+
unit,
|
|
121
|
+
layout,
|
|
122
|
+
}: SliderRowProps) {
|
|
123
|
+
// Default readout precision tracks `step`: integer steps → 0 decimals,
|
|
124
|
+
// 0.1 → 1 decimal, 0.05/0.02/0.01 → 2 decimals, 0.005 → 3, etc. Callers
|
|
125
|
+
// can still pass an explicit `format` to override (e.g. for a custom
|
|
126
|
+
// unit string or a non-decimal display like fractions).
|
|
127
|
+
const decimals = step >= 1 ? 0 : Math.min(6, Math.max(0, Math.ceil(-Math.log10(step))));
|
|
128
|
+
const effectiveFormat = format ?? ((n: number) => formatNumber(n, decimals));
|
|
129
|
+
return (
|
|
130
|
+
<PropertyRow
|
|
131
|
+
label={label}
|
|
132
|
+
readout={
|
|
133
|
+
<EditableReadout
|
|
134
|
+
value={value}
|
|
135
|
+
min={min}
|
|
136
|
+
max={max}
|
|
137
|
+
format={effectiveFormat}
|
|
138
|
+
unit={unit}
|
|
139
|
+
onCommit={onChange}
|
|
140
|
+
/>
|
|
141
|
+
}
|
|
142
|
+
layout={layout}
|
|
143
|
+
>
|
|
144
|
+
<input
|
|
145
|
+
type="range"
|
|
146
|
+
tabIndex={-1}
|
|
147
|
+
min={min}
|
|
148
|
+
max={max}
|
|
149
|
+
step={step}
|
|
150
|
+
value={value}
|
|
151
|
+
onChange={(e) => {
|
|
152
|
+
const v = Number(e.target.value);
|
|
153
|
+
dlog('property-panel', 'slider', { label, value: v });
|
|
154
|
+
onChange(v);
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
</PropertyRow>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface EditableReadoutProps {
|
|
162
|
+
value: number;
|
|
163
|
+
min: number;
|
|
164
|
+
max: number;
|
|
165
|
+
format?: (value: number) => ReactNode;
|
|
166
|
+
unit?: ReactNode;
|
|
167
|
+
onCommit: (next: number) => void;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Readout that swaps to a number input on click, commits on Enter/blur,
|
|
172
|
+
* cancels on Escape. Clicks are stopped so the wrapping <label> doesn't
|
|
173
|
+
* forward focus to the slider thumb.
|
|
174
|
+
*/
|
|
175
|
+
function EditableReadout({ value, min, max, format, unit, onCommit }: EditableReadoutProps) {
|
|
176
|
+
// Draft is non-null only while the input is focused. Live value mirrors
|
|
177
|
+
// into the input otherwise. Pattern mirrors speech-balloons Lab.tsx:893-942.
|
|
178
|
+
const [draft, setDraft] = useState<string | null>(null);
|
|
179
|
+
const fmt = (n: number) => (format ? format(n) : formatNumber(n));
|
|
180
|
+
const displayValue =
|
|
181
|
+
draft !== null
|
|
182
|
+
? draft
|
|
183
|
+
: (() => {
|
|
184
|
+
const formatted = fmt(value);
|
|
185
|
+
return typeof formatted === 'string' ? formatted : String(formatted);
|
|
186
|
+
})();
|
|
187
|
+
|
|
188
|
+
const suffix =
|
|
189
|
+
unit == null ? null : typeof unit === 'string' ? (
|
|
190
|
+
<span className="lk-property-row__readout-unit">{unit}</span>
|
|
191
|
+
) : (
|
|
192
|
+
unit
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const commit = () => {
|
|
196
|
+
if (draft !== null) {
|
|
197
|
+
const n = parseSignedNumber(draft);
|
|
198
|
+
if (Number.isFinite(n)) onCommit(Math.min(max, Math.max(min, n)));
|
|
199
|
+
}
|
|
200
|
+
setDraft(null);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<span className="lk-property-row__readout-group">
|
|
205
|
+
<input
|
|
206
|
+
type="text"
|
|
207
|
+
inputMode="decimal"
|
|
208
|
+
className="lk-property-row__readout-input"
|
|
209
|
+
value={displayValue}
|
|
210
|
+
onFocus={(e) => {
|
|
211
|
+
const formatted = fmt(value);
|
|
212
|
+
setDraft(typeof formatted === 'string' ? formatted : String(formatted));
|
|
213
|
+
e.currentTarget.select();
|
|
214
|
+
}}
|
|
215
|
+
onChange={(e) => setDraft(e.target.value.replace(/-/g, '−'))}
|
|
216
|
+
onBlur={commit}
|
|
217
|
+
onClick={(e) => {
|
|
218
|
+
// Stop the click from reaching the wrapping <label> (which would
|
|
219
|
+
// forward focus to the slider thumb).
|
|
220
|
+
e.preventDefault();
|
|
221
|
+
e.stopPropagation();
|
|
222
|
+
e.currentTarget.focus();
|
|
223
|
+
}}
|
|
224
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
225
|
+
onKeyDown={(e) => {
|
|
226
|
+
if (e.key === 'Enter') {
|
|
227
|
+
commit();
|
|
228
|
+
e.currentTarget.blur();
|
|
229
|
+
} else if (e.key === 'Escape') {
|
|
230
|
+
setDraft(null);
|
|
231
|
+
e.currentTarget.blur();
|
|
232
|
+
}
|
|
233
|
+
}}
|
|
234
|
+
/>
|
|
235
|
+
{suffix}
|
|
236
|
+
</span>
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export interface ColorRowProps {
|
|
241
|
+
label: ReactNode;
|
|
242
|
+
value: string;
|
|
243
|
+
onChange: (next: string) => void;
|
|
244
|
+
/** 0..1 alpha. When provided, a translucent slider renders beneath the swatch. */
|
|
245
|
+
alpha?: number;
|
|
246
|
+
onAlphaChange?: (next: number) => void;
|
|
247
|
+
/**
|
|
248
|
+
* Render the alpha track as inert (dimmed, no thumb, not-allowed cursor).
|
|
249
|
+
* Use when the color's consumer drops alpha so the affordance reads dead.
|
|
250
|
+
*/
|
|
251
|
+
alphaDisabled?: boolean;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function ColorRow({
|
|
255
|
+
label,
|
|
256
|
+
value,
|
|
257
|
+
onChange,
|
|
258
|
+
alpha,
|
|
259
|
+
onAlphaChange,
|
|
260
|
+
alphaDisabled,
|
|
261
|
+
}: ColorRowProps) {
|
|
262
|
+
const showAlpha = alpha != null;
|
|
263
|
+
const className = alphaDisabled ? 'lk-property-row--alpha-disabled' : undefined;
|
|
264
|
+
return (
|
|
265
|
+
<PropertyRow label={label} variant="color" className={className}>
|
|
266
|
+
<input type="color" value={value} onChange={(e) => onChange(e.target.value)} />
|
|
267
|
+
{showAlpha && (
|
|
268
|
+
<input
|
|
269
|
+
type="range"
|
|
270
|
+
className="lk-property-row__alpha"
|
|
271
|
+
min={0}
|
|
272
|
+
max={1}
|
|
273
|
+
step={0.01}
|
|
274
|
+
value={alpha}
|
|
275
|
+
disabled={alphaDisabled}
|
|
276
|
+
onChange={(e) => onAlphaChange?.(Number(e.target.value))}
|
|
277
|
+
/>
|
|
278
|
+
)}
|
|
279
|
+
</PropertyRow>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface CheckboxRowProps {
|
|
284
|
+
label: ReactNode;
|
|
285
|
+
value: boolean;
|
|
286
|
+
onChange: (next: boolean) => void;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function CheckboxRow({ label, value, onChange }: CheckboxRowProps) {
|
|
290
|
+
return (
|
|
291
|
+
<PropertyRow label={label} variant="checkbox">
|
|
292
|
+
<input type="checkbox" checked={value} onChange={(e) => onChange(e.target.checked)} />
|
|
293
|
+
</PropertyRow>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface TextRowProps {
|
|
298
|
+
label: ReactNode;
|
|
299
|
+
value: string;
|
|
300
|
+
onChange: (next: string) => void;
|
|
301
|
+
placeholder?: string;
|
|
302
|
+
maxLength?: number;
|
|
303
|
+
layout?: PropertyRowLayout;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function TextRow({ label, value, onChange, placeholder, maxLength, layout }: TextRowProps) {
|
|
307
|
+
return (
|
|
308
|
+
<PropertyRow label={label} layout={layout}>
|
|
309
|
+
<input
|
|
310
|
+
type="text"
|
|
311
|
+
value={value}
|
|
312
|
+
placeholder={placeholder}
|
|
313
|
+
maxLength={maxLength}
|
|
314
|
+
onChange={(e) => onChange(e.target.value)}
|
|
315
|
+
/>
|
|
316
|
+
</PropertyRow>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export interface NumberRowProps {
|
|
321
|
+
label: ReactNode;
|
|
322
|
+
value: number;
|
|
323
|
+
onChange: (next: number) => void;
|
|
324
|
+
min?: number;
|
|
325
|
+
max?: number;
|
|
326
|
+
step?: number;
|
|
327
|
+
placeholder?: string;
|
|
328
|
+
layout?: PropertyRowLayout;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function NumberRow({
|
|
332
|
+
label,
|
|
333
|
+
value,
|
|
334
|
+
onChange,
|
|
335
|
+
min,
|
|
336
|
+
max,
|
|
337
|
+
step,
|
|
338
|
+
placeholder,
|
|
339
|
+
layout,
|
|
340
|
+
}: NumberRowProps) {
|
|
341
|
+
return (
|
|
342
|
+
<PropertyRow label={label} layout={layout}>
|
|
343
|
+
<input
|
|
344
|
+
type="number"
|
|
345
|
+
value={value}
|
|
346
|
+
min={min}
|
|
347
|
+
max={max}
|
|
348
|
+
step={step}
|
|
349
|
+
placeholder={placeholder}
|
|
350
|
+
onChange={(e) => {
|
|
351
|
+
const raw = e.target.value;
|
|
352
|
+
if (raw === '') return;
|
|
353
|
+
const n = Number(raw);
|
|
354
|
+
if (Number.isFinite(n)) onChange(n);
|
|
355
|
+
}}
|
|
356
|
+
/>
|
|
357
|
+
</PropertyRow>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export interface SelectOption<T extends string> {
|
|
362
|
+
value: T;
|
|
363
|
+
label: ReactNode;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export interface SelectRowProps<T extends string> {
|
|
367
|
+
label: ReactNode;
|
|
368
|
+
value: T;
|
|
369
|
+
options: ReadonlyArray<SelectOption<T>>;
|
|
370
|
+
onChange: (next: T) => void;
|
|
371
|
+
layout?: PropertyRowLayout;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function SelectRow<T extends string>({
|
|
375
|
+
label,
|
|
376
|
+
value,
|
|
377
|
+
options,
|
|
378
|
+
onChange,
|
|
379
|
+
layout,
|
|
380
|
+
}: SelectRowProps<T>) {
|
|
381
|
+
return (
|
|
382
|
+
<PropertyRow label={label} layout={layout}>
|
|
383
|
+
<select
|
|
384
|
+
value={value}
|
|
385
|
+
onChange={(e) => {
|
|
386
|
+
const v = e.target.value as T;
|
|
387
|
+
dlog('property-panel', 'select', { label, value: v });
|
|
388
|
+
onChange(v);
|
|
389
|
+
}}
|
|
390
|
+
>
|
|
391
|
+
{options.map((opt) => (
|
|
392
|
+
<option key={opt.value} value={opt.value}>
|
|
393
|
+
{/* HTML <option> only renders text; ReactNode → string coerce */}
|
|
394
|
+
{opt.label as string}
|
|
395
|
+
</option>
|
|
396
|
+
))}
|
|
397
|
+
</select>
|
|
398
|
+
</PropertyRow>
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export interface ToggleRowProps<T extends string> {
|
|
403
|
+
label: ReactNode;
|
|
404
|
+
value: T;
|
|
405
|
+
options: ReadonlyArray<SelectOption<T>>;
|
|
406
|
+
onChange: (next: T) => void;
|
|
407
|
+
layout?: PropertyRowLayout;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function ToggleRow<T extends string>({
|
|
411
|
+
label,
|
|
412
|
+
value,
|
|
413
|
+
options,
|
|
414
|
+
onChange,
|
|
415
|
+
layout,
|
|
416
|
+
}: ToggleRowProps<T>) {
|
|
417
|
+
return (
|
|
418
|
+
<PropertyRow label={label} layout={layout}>
|
|
419
|
+
<div className="lk-property-row__toggle">
|
|
420
|
+
{options.map((opt) => {
|
|
421
|
+
const selected = opt.value === value;
|
|
422
|
+
return (
|
|
423
|
+
<button
|
|
424
|
+
key={opt.value}
|
|
425
|
+
type="button"
|
|
426
|
+
aria-pressed={selected}
|
|
427
|
+
className={
|
|
428
|
+
selected
|
|
429
|
+
? 'lk-property-row__toggle-button lk-property-row__toggle-button--selected'
|
|
430
|
+
: 'lk-property-row__toggle-button'
|
|
431
|
+
}
|
|
432
|
+
onClick={() => onChange(opt.value)}
|
|
433
|
+
>
|
|
434
|
+
{opt.label}
|
|
435
|
+
</button>
|
|
436
|
+
);
|
|
437
|
+
})}
|
|
438
|
+
</div>
|
|
439
|
+
</PropertyRow>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
};
|