@charcuterie/ui 3.32.0 → 3.34.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 +30 -0
- package/dist/AdaptiveGrid/useAdaptiveColumns.d.ts +16 -0
- package/dist/AdaptiveGrid/useAdaptiveColumns.d.ts.map +1 -1
- package/dist/AdaptiveGrid/useAdaptiveColumns.js +1 -0
- package/dist/AdaptiveGrid/useAdaptiveColumns.js.map +1 -1
- package/dist/Board/Board.d.ts.map +1 -1
- package/dist/Board/Board.js +2 -2
- package/dist/Board/Board.js.map +1 -1
- package/dist/RangeSlider/RangeSlider.d.ts +117 -0
- package/dist/RangeSlider/RangeSlider.d.ts.map +1 -0
- package/dist/RangeSlider/RangeSlider.js +242 -0
- package/dist/RangeSlider/RangeSlider.js.map +1 -0
- package/dist/RangeSlider/rangeSliderValue.d.ts +102 -0
- package/dist/RangeSlider/rangeSliderValue.d.ts.map +1 -0
- package/dist/RangeSlider/rangeSliderValue.js +116 -0
- package/dist/RangeSlider/rangeSliderValue.js.map +1 -0
- package/dist/SegmentedControl/SegmentedControl.d.ts +12 -1
- package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedControl.js +3 -3
- package/dist/SegmentedControl/SegmentedControl.js.map +1 -1
- package/dist/SegmentedControl/SegmentedOption.d.ts +2 -1
- package/dist/SegmentedControl/SegmentedOption.d.ts.map +1 -1
- package/dist/SegmentedControl/SegmentedOption.js +7 -2
- package/dist/SegmentedControl/SegmentedOption.js.map +1 -1
- package/dist/Slider/Slider.d.ts +6 -1
- package/dist/Slider/Slider.d.ts.map +1 -1
- package/dist/Slider/Slider.js +8 -22
- package/dist/Slider/Slider.js.map +1 -1
- package/dist/TimecodeInput/TimecodeInput.d.ts +88 -0
- package/dist/TimecodeInput/TimecodeInput.d.ts.map +1 -0
- package/dist/TimecodeInput/TimecodeInput.js +258 -0
- package/dist/TimecodeInput/TimecodeInput.js.map +1 -0
- package/dist/TimecodeInput/timecode.d.ts +132 -0
- package/dist/TimecodeInput/timecode.d.ts.map +1 -0
- package/dist/TimecodeInput/timecode.js +246 -0
- package/dist/TimecodeInput/timecode.js.map +1 -0
- package/dist/VirtualizedGrid/VirtualizedGrid.d.ts +35 -0
- package/dist/VirtualizedGrid/VirtualizedGrid.d.ts.map +1 -1
- package/dist/VirtualizedGrid/VirtualizedGrid.js +37 -1
- package/dist/VirtualizedGrid/VirtualizedGrid.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/sliderStyles.d.ts +39 -0
- package/dist/sliderStyles.d.ts.map +1 -0
- package/dist/sliderStyles.js +50 -0
- package/dist/sliderStyles.js.map +1 -0
- package/package.json +1 -1
- package/src/AdaptiveGrid/useAdaptiveColumns.ts +17 -0
- package/src/Board/Board.test.tsx +8 -0
- package/src/Board/Board.tsx +7 -1
- package/src/RangeSlider/RangeSlider.mdx +85 -0
- package/src/RangeSlider/RangeSlider.stories.tsx +346 -0
- package/src/RangeSlider/RangeSlider.test.tsx +452 -0
- package/src/RangeSlider/RangeSlider.tsx +595 -0
- package/src/RangeSlider/rangeSliderValue.test.ts +217 -0
- package/src/RangeSlider/rangeSliderValue.ts +185 -0
- package/src/SegmentedControl/SegmentedControl.stories.tsx +50 -0
- package/src/SegmentedControl/SegmentedControl.test.tsx +36 -1
- package/src/SegmentedControl/SegmentedControl.tsx +14 -0
- package/src/SegmentedControl/SegmentedOption.tsx +7 -0
- package/src/Slider/Slider.tsx +23 -29
- package/src/TimecodeInput/TimecodeInput.mdx +155 -0
- package/src/TimecodeInput/TimecodeInput.stories.tsx +304 -0
- package/src/TimecodeInput/TimecodeInput.test.tsx +279 -0
- package/src/TimecodeInput/TimecodeInput.tsx +493 -0
- package/src/TimecodeInput/timecode.test.ts +236 -0
- package/src/TimecodeInput/timecode.ts +391 -0
- package/src/VirtualizedGrid/VirtualizedGrid.mdx +21 -0
- package/src/VirtualizedGrid/VirtualizedGrid.stories.tsx +64 -0
- package/src/VirtualizedGrid/VirtualizedGrid.test.tsx +122 -3
- package/src/VirtualizedGrid/VirtualizedGrid.tsx +37 -1
- package/src/index.ts +18 -0
- package/src/sliderStyles.ts +69 -0
- package/src/sourceRules.test.ts +25 -1
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
2
|
+
import { useState } from "react"
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
controlSizeArgType,
|
|
6
|
+
intentArgType,
|
|
7
|
+
} from "../argTypes.storyHelpers.ts"
|
|
8
|
+
import {
|
|
9
|
+
ContainerBoard,
|
|
10
|
+
StoryCell,
|
|
11
|
+
StoryGrid,
|
|
12
|
+
StorySection,
|
|
13
|
+
} from "../board.storyHelpers.tsx"
|
|
14
|
+
import { Slider } from "../Slider/Slider.tsx"
|
|
15
|
+
import { RangeSlider } from "./RangeSlider.tsx"
|
|
16
|
+
import type { RangeSliderValue } from "./rangeSliderValue.ts"
|
|
17
|
+
|
|
18
|
+
/** mm:ss, which is the app's job and not the component's. */
|
|
19
|
+
const toClock = (seconds: number): string => {
|
|
20
|
+
const whole = Math.max(0, Math.round(seconds))
|
|
21
|
+
|
|
22
|
+
const minutes = Math.floor(whole / 60)
|
|
23
|
+
|
|
24
|
+
const rest = whole % 60
|
|
25
|
+
|
|
26
|
+
return `${minutes}:${String(rest).padStart(2, "0")}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** An invented chapter list for a 45-minute programme. */
|
|
30
|
+
const chapters = [
|
|
31
|
+
{ label: "Cold open", value: 0 },
|
|
32
|
+
{ label: "Act one", value: 600 },
|
|
33
|
+
{ label: "Act two", value: 1_500 },
|
|
34
|
+
{ label: "Credits", value: 2_460 },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The same marks with the words taken off, which is what a narrow
|
|
39
|
+
* container wants: a label is centred on its mark, so two marks closer
|
|
40
|
+
* together than a label box overlap. The component draws what it is
|
|
41
|
+
* given.
|
|
42
|
+
*/
|
|
43
|
+
const chapterMarks = [
|
|
44
|
+
...chapters.map(({ value }) => ({ value })),
|
|
45
|
+
{ value: 240 },
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A controlled `RangeSlider` that owns its own value, which is what
|
|
50
|
+
* every real call site is. The stories that only paint use the plain
|
|
51
|
+
* component.
|
|
52
|
+
*/
|
|
53
|
+
const LiveRangeSlider = ({
|
|
54
|
+
initialValue = { end: 1_500, start: 600 },
|
|
55
|
+
...rangeSliderProps
|
|
56
|
+
}: {
|
|
57
|
+
initialValue?: RangeSliderValue
|
|
58
|
+
} & Omit<
|
|
59
|
+
Parameters<typeof RangeSlider>[0],
|
|
60
|
+
"onChange" | "value"
|
|
61
|
+
>): ReturnType<typeof RangeSlider> => {
|
|
62
|
+
const [value, setValue] = useState(initialValue)
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<RangeSlider
|
|
66
|
+
{...rangeSliderProps}
|
|
67
|
+
onChange={setValue}
|
|
68
|
+
value={value}
|
|
69
|
+
/>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const meta = {
|
|
74
|
+
title: "Components/Controls/RangeSlider",
|
|
75
|
+
component: RangeSlider,
|
|
76
|
+
parameters: { layout: "padded" },
|
|
77
|
+
argTypes: {
|
|
78
|
+
intent: intentArgType,
|
|
79
|
+
size: controlSizeArgType,
|
|
80
|
+
},
|
|
81
|
+
// The component's own defaults, restated — Storybook does not seed
|
|
82
|
+
// `args` from docgen, so an unstated default shows in the props
|
|
83
|
+
// table with nothing selected in its control.
|
|
84
|
+
args: {
|
|
85
|
+
intent: "accent",
|
|
86
|
+
isDisabled: false,
|
|
87
|
+
isLabelVisible: false,
|
|
88
|
+
isReadOnly: false,
|
|
89
|
+
isValueShown: false,
|
|
90
|
+
max: 100,
|
|
91
|
+
min: 0,
|
|
92
|
+
size: "md",
|
|
93
|
+
step: 1,
|
|
94
|
+
},
|
|
95
|
+
} satisfies Meta<typeof RangeSlider>
|
|
96
|
+
|
|
97
|
+
export default meta
|
|
98
|
+
|
|
99
|
+
type Story = StoryObj<typeof meta>
|
|
100
|
+
|
|
101
|
+
export const Default: Story = {
|
|
102
|
+
args: {
|
|
103
|
+
label: "Clip",
|
|
104
|
+
max: 2_700,
|
|
105
|
+
value: { end: 1_500, start: 600 },
|
|
106
|
+
valueFormat: toClock,
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const AllVariants: Story = {
|
|
111
|
+
args: { label: "Clip" },
|
|
112
|
+
render: (controlProps) => (
|
|
113
|
+
<StorySection title="Three heights off the density axis, and the six intents on the span.">
|
|
114
|
+
<StoryGrid columns={3}>
|
|
115
|
+
{(["sm", "md", "lg"] as const).map((size) => (
|
|
116
|
+
<StoryCell
|
|
117
|
+
align="stretch"
|
|
118
|
+
key={size}
|
|
119
|
+
label={size}
|
|
120
|
+
>
|
|
121
|
+
<RangeSlider
|
|
122
|
+
{...controlProps}
|
|
123
|
+
label={`Clip at ${size}`}
|
|
124
|
+
size={size}
|
|
125
|
+
value={{ end: 75, start: 25 }}
|
|
126
|
+
/>
|
|
127
|
+
</StoryCell>
|
|
128
|
+
))}
|
|
129
|
+
</StoryGrid>
|
|
130
|
+
|
|
131
|
+
<StoryGrid columns={3}>
|
|
132
|
+
{(
|
|
133
|
+
[
|
|
134
|
+
"accent",
|
|
135
|
+
"success",
|
|
136
|
+
"warning",
|
|
137
|
+
"danger",
|
|
138
|
+
"info",
|
|
139
|
+
"neutral",
|
|
140
|
+
] as const
|
|
141
|
+
).map((intent) => (
|
|
142
|
+
<StoryCell
|
|
143
|
+
align="stretch"
|
|
144
|
+
key={intent}
|
|
145
|
+
label={intent}
|
|
146
|
+
>
|
|
147
|
+
<RangeSlider
|
|
148
|
+
{...controlProps}
|
|
149
|
+
intent={intent}
|
|
150
|
+
label={`Clip, ${intent}`}
|
|
151
|
+
value={{ end: 75, start: 25 }}
|
|
152
|
+
/>
|
|
153
|
+
</StoryCell>
|
|
154
|
+
))}
|
|
155
|
+
</StoryGrid>
|
|
156
|
+
</StorySection>
|
|
157
|
+
),
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The two states worth looking at twice are the collapsed range and
|
|
162
|
+
* the read-only one.
|
|
163
|
+
*
|
|
164
|
+
* A collapsed range is what the crossing rule produces: a thumb
|
|
165
|
+
* dragged past its partner stops **on** it rather than swapping with
|
|
166
|
+
* it, so both handles sit on one value and the span has zero width. It
|
|
167
|
+
* is reachable, it is legal, and it opens again from whichever side
|
|
168
|
+
* the next press lands on.
|
|
169
|
+
*
|
|
170
|
+
* `isReadOnly` is not `isDisabled`. A read-only range keeps full
|
|
171
|
+
* contrast and stays focusable, so both ends are still readable by a
|
|
172
|
+
* screen reader.
|
|
173
|
+
*/
|
|
174
|
+
export const AllStates: Story = {
|
|
175
|
+
args: { label: "Clip" },
|
|
176
|
+
render: (controlProps) => (
|
|
177
|
+
<StorySection title="What varies is the span, and whether it can be changed.">
|
|
178
|
+
<StoryGrid columns={2}>
|
|
179
|
+
<StoryCell align="stretch" label="the whole range">
|
|
180
|
+
<RangeSlider
|
|
181
|
+
{...controlProps}
|
|
182
|
+
label="Everything"
|
|
183
|
+
value={{ end: 100, start: 0 }}
|
|
184
|
+
/>
|
|
185
|
+
</StoryCell>
|
|
186
|
+
|
|
187
|
+
<StoryCell
|
|
188
|
+
align="stretch"
|
|
189
|
+
label="collapsed — both thumbs on one value"
|
|
190
|
+
>
|
|
191
|
+
<RangeSlider
|
|
192
|
+
{...controlProps}
|
|
193
|
+
label="Collapsed"
|
|
194
|
+
value={{ end: 50, start: 50 }}
|
|
195
|
+
/>
|
|
196
|
+
</StoryCell>
|
|
197
|
+
|
|
198
|
+
<StoryCell align="stretch" label="disabled">
|
|
199
|
+
<RangeSlider
|
|
200
|
+
{...controlProps}
|
|
201
|
+
isDisabled
|
|
202
|
+
label="Disabled"
|
|
203
|
+
value={{ end: 70, start: 30 }}
|
|
204
|
+
/>
|
|
205
|
+
</StoryCell>
|
|
206
|
+
|
|
207
|
+
<StoryCell align="stretch" label="read-only">
|
|
208
|
+
<RangeSlider
|
|
209
|
+
{...controlProps}
|
|
210
|
+
isReadOnly
|
|
211
|
+
label="Read-only"
|
|
212
|
+
value={{ end: 70, start: 30 }}
|
|
213
|
+
/>
|
|
214
|
+
</StoryCell>
|
|
215
|
+
|
|
216
|
+
<StoryCell
|
|
217
|
+
align="stretch"
|
|
218
|
+
label="label and span shown"
|
|
219
|
+
>
|
|
220
|
+
<RangeSlider
|
|
221
|
+
{...controlProps}
|
|
222
|
+
isLabelVisible
|
|
223
|
+
isValueShown
|
|
224
|
+
label="Section"
|
|
225
|
+
max={2_700}
|
|
226
|
+
value={{ end: 1_500, start: 600 }}
|
|
227
|
+
valueFormat={toClock}
|
|
228
|
+
/>
|
|
229
|
+
</StoryCell>
|
|
230
|
+
|
|
231
|
+
<StoryCell align="stretch" label="with tick marks">
|
|
232
|
+
<RangeSlider
|
|
233
|
+
{...controlProps}
|
|
234
|
+
isLabelVisible
|
|
235
|
+
isValueShown
|
|
236
|
+
label="Chapters"
|
|
237
|
+
max={2_700}
|
|
238
|
+
ticks={chapters}
|
|
239
|
+
value={{ end: 1_500, start: 600 }}
|
|
240
|
+
valueFormat={toClock}
|
|
241
|
+
/>
|
|
242
|
+
</StoryCell>
|
|
243
|
+
</StoryGrid>
|
|
244
|
+
</StorySection>
|
|
245
|
+
),
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The bar fills whatever box it is given and both thumbs stay on their
|
|
250
|
+
* values at every width. Nothing here is a media query — a range
|
|
251
|
+
* slider in a 240px rail and one in a 544px panel are the same
|
|
252
|
+
* component.
|
|
253
|
+
*/
|
|
254
|
+
export const Responsive: Story = {
|
|
255
|
+
args: { label: "Clip" },
|
|
256
|
+
render: (controlProps) => (
|
|
257
|
+
<ContainerBoard>
|
|
258
|
+
{(width) => (
|
|
259
|
+
<RangeSlider
|
|
260
|
+
{...controlProps}
|
|
261
|
+
isLabelVisible
|
|
262
|
+
isValueShown
|
|
263
|
+
label={`Section at ${width}`}
|
|
264
|
+
max={2_700}
|
|
265
|
+
ticks={chapterMarks}
|
|
266
|
+
value={{ end: 1_500, start: 600 }}
|
|
267
|
+
valueFormat={toClock}
|
|
268
|
+
/>
|
|
269
|
+
)}
|
|
270
|
+
</ContainerBoard>
|
|
271
|
+
),
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The complete keyboard path. Tab reaches **each** thumb — the start
|
|
276
|
+
* first, then the end — and on either of them:
|
|
277
|
+
*
|
|
278
|
+
* - **Arrow Left / Down** and **Arrow Right / Up** move one `step`
|
|
279
|
+
* - **Page Down / Page Up** move one `largeStep`, ten steps by default
|
|
280
|
+
* - **Home** and **End** go as far as this thumb may go, which is the
|
|
281
|
+
* range's own end or the other thumb, whichever comes first
|
|
282
|
+
*
|
|
283
|
+
* A press anywhere on the bar picks up the **nearer** thumb, moves it
|
|
284
|
+
* there and starts a drag. The drag keeps reporting after the pointer
|
|
285
|
+
* leaves the bar vertically, and the thumb it picked up is the thumb
|
|
286
|
+
* it keeps moving — even once the pointer has travelled past the other
|
|
287
|
+
* one.
|
|
288
|
+
*/
|
|
289
|
+
export const Interactive: Story = {
|
|
290
|
+
args: { label: "Clip" },
|
|
291
|
+
render: (controlProps) => (
|
|
292
|
+
<StorySection title="Drive either thumb with the keyboard, or press anywhere on the bar.">
|
|
293
|
+
<LiveRangeSlider
|
|
294
|
+
{...controlProps}
|
|
295
|
+
initialValue={{ end: 1_500, start: 600 }}
|
|
296
|
+
isLabelVisible
|
|
297
|
+
isValueShown
|
|
298
|
+
label="Clip"
|
|
299
|
+
max={2_700}
|
|
300
|
+
step={30}
|
|
301
|
+
ticks={chapters}
|
|
302
|
+
valueFormat={toClock}
|
|
303
|
+
/>
|
|
304
|
+
</StorySection>
|
|
305
|
+
),
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* The two side by side, which is the claim `../sliderStyles.ts` makes
|
|
310
|
+
* in prose: one bar, painted once.
|
|
311
|
+
*
|
|
312
|
+
* What differs is the focus model, and it is the reason these are two
|
|
313
|
+
* components rather than one with a boolean. `Slider` puts
|
|
314
|
+
* `role="slider"`, the tab stop and the pointer target on the
|
|
315
|
+
* **track**, because one track reports one value. Two values cannot
|
|
316
|
+
* live on one accessible object, so here the role moves to the thumbs
|
|
317
|
+
* and the track becomes a `group` that carries the name.
|
|
318
|
+
*/
|
|
319
|
+
export const BesideTheSlider: Story = {
|
|
320
|
+
args: { label: "Clip" },
|
|
321
|
+
render: (controlProps) => (
|
|
322
|
+
<StorySection title="The same bar. One value on the track, or two on their own handles.">
|
|
323
|
+
<StoryGrid columns={2}>
|
|
324
|
+
<StoryCell align="stretch" label="Slider">
|
|
325
|
+
<Slider
|
|
326
|
+
intent={controlProps.intent}
|
|
327
|
+
isLabelVisible
|
|
328
|
+
label="Position"
|
|
329
|
+
size={controlProps.size}
|
|
330
|
+
value={55}
|
|
331
|
+
/>
|
|
332
|
+
</StoryCell>
|
|
333
|
+
|
|
334
|
+
<StoryCell align="stretch" label="RangeSlider">
|
|
335
|
+
<RangeSlider
|
|
336
|
+
intent={controlProps.intent}
|
|
337
|
+
isLabelVisible
|
|
338
|
+
label="Section"
|
|
339
|
+
size={controlProps.size}
|
|
340
|
+
value={{ end: 75, start: 25 }}
|
|
341
|
+
/>
|
|
342
|
+
</StoryCell>
|
|
343
|
+
</StoryGrid>
|
|
344
|
+
</StorySection>
|
|
345
|
+
),
|
|
346
|
+
}
|