@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.
Files changed (76) hide show
  1. package/README.md +30 -0
  2. package/dist/AdaptiveGrid/useAdaptiveColumns.d.ts +16 -0
  3. package/dist/AdaptiveGrid/useAdaptiveColumns.d.ts.map +1 -1
  4. package/dist/AdaptiveGrid/useAdaptiveColumns.js +1 -0
  5. package/dist/AdaptiveGrid/useAdaptiveColumns.js.map +1 -1
  6. package/dist/Board/Board.d.ts.map +1 -1
  7. package/dist/Board/Board.js +2 -2
  8. package/dist/Board/Board.js.map +1 -1
  9. package/dist/RangeSlider/RangeSlider.d.ts +117 -0
  10. package/dist/RangeSlider/RangeSlider.d.ts.map +1 -0
  11. package/dist/RangeSlider/RangeSlider.js +242 -0
  12. package/dist/RangeSlider/RangeSlider.js.map +1 -0
  13. package/dist/RangeSlider/rangeSliderValue.d.ts +102 -0
  14. package/dist/RangeSlider/rangeSliderValue.d.ts.map +1 -0
  15. package/dist/RangeSlider/rangeSliderValue.js +116 -0
  16. package/dist/RangeSlider/rangeSliderValue.js.map +1 -0
  17. package/dist/SegmentedControl/SegmentedControl.d.ts +12 -1
  18. package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
  19. package/dist/SegmentedControl/SegmentedControl.js +3 -3
  20. package/dist/SegmentedControl/SegmentedControl.js.map +1 -1
  21. package/dist/SegmentedControl/SegmentedOption.d.ts +2 -1
  22. package/dist/SegmentedControl/SegmentedOption.d.ts.map +1 -1
  23. package/dist/SegmentedControl/SegmentedOption.js +7 -2
  24. package/dist/SegmentedControl/SegmentedOption.js.map +1 -1
  25. package/dist/Slider/Slider.d.ts +6 -1
  26. package/dist/Slider/Slider.d.ts.map +1 -1
  27. package/dist/Slider/Slider.js +8 -22
  28. package/dist/Slider/Slider.js.map +1 -1
  29. package/dist/TimecodeInput/TimecodeInput.d.ts +88 -0
  30. package/dist/TimecodeInput/TimecodeInput.d.ts.map +1 -0
  31. package/dist/TimecodeInput/TimecodeInput.js +258 -0
  32. package/dist/TimecodeInput/TimecodeInput.js.map +1 -0
  33. package/dist/TimecodeInput/timecode.d.ts +132 -0
  34. package/dist/TimecodeInput/timecode.d.ts.map +1 -0
  35. package/dist/TimecodeInput/timecode.js +246 -0
  36. package/dist/TimecodeInput/timecode.js.map +1 -0
  37. package/dist/VirtualizedGrid/VirtualizedGrid.d.ts +35 -0
  38. package/dist/VirtualizedGrid/VirtualizedGrid.d.ts.map +1 -1
  39. package/dist/VirtualizedGrid/VirtualizedGrid.js +37 -1
  40. package/dist/VirtualizedGrid/VirtualizedGrid.js.map +1 -1
  41. package/dist/index.d.ts +7 -0
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +3 -0
  44. package/dist/index.js.map +1 -1
  45. package/dist/sliderStyles.d.ts +39 -0
  46. package/dist/sliderStyles.d.ts.map +1 -0
  47. package/dist/sliderStyles.js +50 -0
  48. package/dist/sliderStyles.js.map +1 -0
  49. package/package.json +1 -1
  50. package/src/AdaptiveGrid/useAdaptiveColumns.ts +17 -0
  51. package/src/Board/Board.test.tsx +8 -0
  52. package/src/Board/Board.tsx +7 -1
  53. package/src/RangeSlider/RangeSlider.mdx +85 -0
  54. package/src/RangeSlider/RangeSlider.stories.tsx +346 -0
  55. package/src/RangeSlider/RangeSlider.test.tsx +452 -0
  56. package/src/RangeSlider/RangeSlider.tsx +595 -0
  57. package/src/RangeSlider/rangeSliderValue.test.ts +217 -0
  58. package/src/RangeSlider/rangeSliderValue.ts +185 -0
  59. package/src/SegmentedControl/SegmentedControl.stories.tsx +50 -0
  60. package/src/SegmentedControl/SegmentedControl.test.tsx +36 -1
  61. package/src/SegmentedControl/SegmentedControl.tsx +14 -0
  62. package/src/SegmentedControl/SegmentedOption.tsx +7 -0
  63. package/src/Slider/Slider.tsx +23 -29
  64. package/src/TimecodeInput/TimecodeInput.mdx +155 -0
  65. package/src/TimecodeInput/TimecodeInput.stories.tsx +304 -0
  66. package/src/TimecodeInput/TimecodeInput.test.tsx +279 -0
  67. package/src/TimecodeInput/TimecodeInput.tsx +493 -0
  68. package/src/TimecodeInput/timecode.test.ts +236 -0
  69. package/src/TimecodeInput/timecode.ts +391 -0
  70. package/src/VirtualizedGrid/VirtualizedGrid.mdx +21 -0
  71. package/src/VirtualizedGrid/VirtualizedGrid.stories.tsx +64 -0
  72. package/src/VirtualizedGrid/VirtualizedGrid.test.tsx +122 -3
  73. package/src/VirtualizedGrid/VirtualizedGrid.tsx +37 -1
  74. package/src/index.ts +18 -0
  75. package/src/sliderStyles.ts +69 -0
  76. 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
+ }