@charcuterie/ui 3.33.0 → 4.0.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/Board/Board.d.ts.map +1 -1
- package/dist/Board/Board.js +2 -2
- package/dist/Board/Board.js.map +1 -1
- package/dist/Main/Main.d.ts +18 -19
- package/dist/Main/Main.d.ts.map +1 -1
- package/dist/Main/Main.js +21 -10
- package/dist/Main/Main.js.map +1 -1
- package/dist/Main/ScrollMemoryProvider.d.ts +38 -0
- package/dist/Main/ScrollMemoryProvider.d.ts.map +1 -0
- package/dist/Main/ScrollMemoryProvider.js +30 -0
- package/dist/Main/ScrollMemoryProvider.js.map +1 -0
- package/dist/Main/scrollMemory.d.ts +54 -28
- package/dist/Main/scrollMemory.d.ts.map +1 -1
- package/dist/Main/scrollMemory.js +81 -40
- package/dist/Main/scrollMemory.js.map +1 -1
- package/dist/Main/scrollMemoryContext.d.ts +18 -0
- package/dist/Main/scrollMemoryContext.d.ts.map +1 -0
- package/dist/Main/scrollMemoryContext.js +18 -0
- package/dist/Main/scrollMemoryContext.js.map +1 -0
- package/dist/Main/useScrollMemory.d.ts +20 -18
- package/dist/Main/useScrollMemory.d.ts.map +1 -1
- package/dist/Main/useScrollMemory.js +69 -42
- package/dist/Main/useScrollMemory.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/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/reactRouter/ReactRouterAdapter.d.ts +43 -0
- package/dist/reactRouter/ReactRouterAdapter.d.ts.map +1 -0
- package/dist/reactRouter/ReactRouterAdapter.js +50 -0
- package/dist/reactRouter/ReactRouterAdapter.js.map +1 -0
- package/dist/reactRouter/index.d.ts +14 -8
- package/dist/reactRouter/index.d.ts.map +1 -1
- package/dist/reactRouter/index.js +13 -8
- package/dist/reactRouter/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/Board/Board.test.tsx +8 -0
- package/src/Board/Board.tsx +7 -1
- package/src/Main/Main.mdx +35 -7
- package/src/Main/Main.stories.tsx +129 -64
- package/src/Main/Main.test.tsx +58 -0
- package/src/Main/Main.tsx +20 -20
- package/src/Main/ScrollMemoryProvider.tsx +48 -0
- package/src/Main/scrollMemory.test.ts +70 -16
- package/src/Main/scrollMemory.ts +98 -40
- package/src/Main/scrollMemoryContext.ts +21 -0
- package/src/Main/useScrollMemory.ts +83 -43
- 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/RouterLink/RouterLink.mdx +53 -16
- 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/index.ts +21 -0
- package/src/reactRouter/ReactRouterAdapter.stories.tsx +120 -0
- package/src/reactRouter/ReactRouterAdapter.test.tsx +92 -0
- package/src/reactRouter/ReactRouterAdapter.tsx +66 -0
- package/src/reactRouter/index.ts +14 -8
- package/src/sliderStyles.ts +69 -0
- package/src/sourceRules.test.ts +29 -3
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
2
|
+
import {
|
|
3
|
+
MemoryRouter,
|
|
4
|
+
Route,
|
|
5
|
+
Routes,
|
|
6
|
+
useNavigate,
|
|
7
|
+
} from "react-router"
|
|
8
|
+
|
|
9
|
+
import { Button } from "../Button/Button.tsx"
|
|
10
|
+
import { Card } from "../Card/Card.tsx"
|
|
11
|
+
import { Header } from "../Header/Header.tsx"
|
|
12
|
+
import { Main } from "../Main/Main.tsx"
|
|
13
|
+
import { Shell } from "../Shell/Shell.tsx"
|
|
14
|
+
import { TextLink } from "../TextLink/TextLink.tsx"
|
|
15
|
+
import { ReactRouterAdapter } from "./ReactRouterAdapter.tsx"
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The one component an app on react-router renders at its root.
|
|
19
|
+
*
|
|
20
|
+
* `Main.stories.tsx` proves the scroll memory against a hand-rolled
|
|
21
|
+
* history stack, which is the right subject for the mechanism. This
|
|
22
|
+
* one proves the **wiring**: a real router, a real `useLocation()`,
|
|
23
|
+
* and both seams arriving from one component — the piece that ships
|
|
24
|
+
* broken silently, because a page whose links still work and whose
|
|
25
|
+
* list still scrolls looks fine.
|
|
26
|
+
*
|
|
27
|
+
* `MemoryRouter` rather than `BrowserRouter`, so the story does not
|
|
28
|
+
* write to the Storybook page's own history.
|
|
29
|
+
*/
|
|
30
|
+
const meta = {
|
|
31
|
+
component: ReactRouterAdapter,
|
|
32
|
+
parameters: {
|
|
33
|
+
layout: "fullscreen",
|
|
34
|
+
},
|
|
35
|
+
title: "Components/Layout/ReactRouterAdapter",
|
|
36
|
+
} satisfies Meta<typeof ReactRouterAdapter>
|
|
37
|
+
|
|
38
|
+
export default meta
|
|
39
|
+
|
|
40
|
+
type Story = StoryObj<typeof meta>
|
|
41
|
+
|
|
42
|
+
const EPISODE_TITLES = Array.from(
|
|
43
|
+
{ length: 40 },
|
|
44
|
+
(_unused, index) => `Episode ${index + 1}`,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const BackControl = () => {
|
|
48
|
+
const navigate = useNavigate()
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Button
|
|
52
|
+
appearance="outline"
|
|
53
|
+
onClick={() => {
|
|
54
|
+
void navigate(-1)
|
|
55
|
+
}}
|
|
56
|
+
size="sm"
|
|
57
|
+
>
|
|
58
|
+
Back
|
|
59
|
+
</Button>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const EpisodeList = () => (
|
|
64
|
+
<Main>
|
|
65
|
+
<div className="flex flex-col gap-3">
|
|
66
|
+
{EPISODE_TITLES.map((title) => (
|
|
67
|
+
<Card heading={title} key={title} padding="sm">
|
|
68
|
+
<TextLink href={`/library/${title}`}>
|
|
69
|
+
Open {title}
|
|
70
|
+
</TextLink>
|
|
71
|
+
</Card>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
</Main>
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const EpisodePage = () => (
|
|
78
|
+
<Main>
|
|
79
|
+
<Card heading="Episode">
|
|
80
|
+
<p className="text-content-secondary text-sm">
|
|
81
|
+
Short enough that the scrollport collapses behind
|
|
82
|
+
it. Press Back.
|
|
83
|
+
</p>
|
|
84
|
+
</Card>
|
|
85
|
+
</Main>
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Scroll the list, press a link, then press Back.
|
|
90
|
+
*
|
|
91
|
+
* Nothing on this page is told about scrolling or about the router.
|
|
92
|
+
* `TextLink` becomes a soft navigation and `Main` keeps the
|
|
93
|
+
* reader's place because `ReactRouterAdapter` is above them both.
|
|
94
|
+
*/
|
|
95
|
+
export const Default: Story = {
|
|
96
|
+
render: () => (
|
|
97
|
+
<MemoryRouter initialEntries={["/library"]}>
|
|
98
|
+
<ReactRouterAdapter>
|
|
99
|
+
<Shell>
|
|
100
|
+
<Header
|
|
101
|
+
actions={<BackControl />}
|
|
102
|
+
heading="Library"
|
|
103
|
+
/>
|
|
104
|
+
|
|
105
|
+
<Routes>
|
|
106
|
+
<Route
|
|
107
|
+
element={<EpisodeList />}
|
|
108
|
+
path="/library"
|
|
109
|
+
/>
|
|
110
|
+
|
|
111
|
+
<Route
|
|
112
|
+
element={<EpisodePage />}
|
|
113
|
+
path="/library/:episode"
|
|
114
|
+
/>
|
|
115
|
+
</Routes>
|
|
116
|
+
</Shell>
|
|
117
|
+
</ReactRouterAdapter>
|
|
118
|
+
</MemoryRouter>
|
|
119
|
+
),
|
|
120
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { composeStories } from "@storybook/react"
|
|
2
|
+
import { expect, userEvent, waitFor } from "storybook/test"
|
|
3
|
+
import { afterEach, test } from "vitest"
|
|
4
|
+
|
|
5
|
+
import { forgetScrollOffsets } from "../Main/scrollMemory.ts"
|
|
6
|
+
import { mountStory } from "../mountStory.testHelpers.ts"
|
|
7
|
+
import { expectAgentDrivable } from "../testing/index.ts"
|
|
8
|
+
import {
|
|
9
|
+
DESKTOP,
|
|
10
|
+
setViewport,
|
|
11
|
+
} from "../viewport.testHelpers.ts"
|
|
12
|
+
import * as stories from "./ReactRouterAdapter.stories.tsx"
|
|
13
|
+
|
|
14
|
+
const { Default } = composeStories(stories)
|
|
15
|
+
|
|
16
|
+
// The offsets are module state, so they outlive a mount. A test
|
|
17
|
+
// must not.
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
forgetScrollOffsets()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The wiring, against a real router.
|
|
24
|
+
*
|
|
25
|
+
* `Main.test.tsx` proves the scroll mechanism against a hand-rolled
|
|
26
|
+
* history stack — the right subject for a `ResizeObserver` and a
|
|
27
|
+
* clamped offset. What it cannot prove is that the entry reaches
|
|
28
|
+
* `Main` at all when the value comes from `useLocation()` rather
|
|
29
|
+
* than from a story's own `useState`. That is the seam every app
|
|
30
|
+
* now depends on, and it is the kind that fails quietly: the links
|
|
31
|
+
* still navigate and the list still scrolls.
|
|
32
|
+
*/
|
|
33
|
+
test("an app renders one component and gets both seams", async () => {
|
|
34
|
+
await setViewport(DESKTOP)
|
|
35
|
+
|
|
36
|
+
const { canvas } = await mountStory(Default)
|
|
37
|
+
|
|
38
|
+
const main = canvas.getByRole("main")
|
|
39
|
+
|
|
40
|
+
// The link seam: react-router's `<Link>` renders an `<a href>`,
|
|
41
|
+
// so this is also what an unwired app looks like — the assertion
|
|
42
|
+
// that separates them is the absence of a page load below.
|
|
43
|
+
const link = expectAgentDrivable(canvas, {
|
|
44
|
+
name: "Open Episode 12",
|
|
45
|
+
role: "link",
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
await expect(link).toHaveAttribute(
|
|
49
|
+
"href",
|
|
50
|
+
"/library/Episode 12",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
main.scrollTop = 900
|
|
54
|
+
|
|
55
|
+
await waitFor(async () => {
|
|
56
|
+
await expect(main.scrollTop).toBe(900)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
await new Promise((resolve) => {
|
|
60
|
+
globalThis.requestAnimationFrame(() => {
|
|
61
|
+
globalThis.requestAnimationFrame(resolve)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
await userEvent.click(link)
|
|
66
|
+
|
|
67
|
+
// A soft navigation: the route changed inside the same document.
|
|
68
|
+
// A plain `<a>` would have left the story mounted and unchanged.
|
|
69
|
+
//
|
|
70
|
+
// `waitFor`, because the route commit is a state update and the
|
|
71
|
+
// click resolves before it. It passed locally and failed on CI
|
|
72
|
+
// for exactly that reason.
|
|
73
|
+
await waitFor(async () => {
|
|
74
|
+
await expect(
|
|
75
|
+
canvas.getByRole("heading", { name: "Episode" }),
|
|
76
|
+
).toBeVisible()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
await userEvent.click(
|
|
80
|
+
expectAgentDrivable(canvas, {
|
|
81
|
+
name: "Back",
|
|
82
|
+
role: "button",
|
|
83
|
+
}),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
// The scroll seam, from the same one component.
|
|
87
|
+
await waitFor(async () => {
|
|
88
|
+
await expect(canvas.getByRole("main").scrollTop).toBe(
|
|
89
|
+
900,
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
import { useLocation } from "react-router"
|
|
3
|
+
|
|
4
|
+
import { ScrollMemoryProvider } from "../Main/ScrollMemoryProvider.tsx"
|
|
5
|
+
import { RouterLinkProvider } from "../RouterLink/RouterLinkProvider.tsx"
|
|
6
|
+
import { ReactRouterLink } from "./ReactRouterLink.tsx"
|
|
7
|
+
|
|
8
|
+
export type ReactRouterAdapterProps = {
|
|
9
|
+
children?: ReactNode
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Every seam this library has, wired to react-router, in one
|
|
14
|
+
* component at the app root.
|
|
15
|
+
*
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <BrowserRouter>
|
|
18
|
+
* <ReactRouterAdapter>
|
|
19
|
+
* <Routes>…</Routes>
|
|
20
|
+
* </ReactRouterAdapter>
|
|
21
|
+
* </BrowserRouter>
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* It must sit **inside** the router, because it reads
|
|
25
|
+
* `useLocation()`, and **outside** the routes, because what it
|
|
26
|
+
* provides belongs to the whole app.
|
|
27
|
+
*
|
|
28
|
+
* ## Why one component and not two providers
|
|
29
|
+
*
|
|
30
|
+
* There are two seams today — the link seam and the scroll memory —
|
|
31
|
+
* and the fleet proved that a per-app opt-in does not reach the
|
|
32
|
+
* fleet. `Main`'s scroll memory shipped as a `scrollKey` prop, one
|
|
33
|
+
* app wired it, and the other three kept losing the reader's place
|
|
34
|
+
* with the library sitting right there supporting it. A seam an app
|
|
35
|
+
* has to know about is a seam most apps will not have.
|
|
36
|
+
*
|
|
37
|
+
* So the root wiring is one name to remember, and a seam added later
|
|
38
|
+
* arrives in every app that already renders this on its next
|
|
39
|
+
* release. `RouterLinkProvider`, `ReactRouterLink` and
|
|
40
|
+
* `ScrollMemoryProvider` all stay exported for an app that wants one
|
|
41
|
+
* half, or that renders its own root for another reason.
|
|
42
|
+
*
|
|
43
|
+
* ## What it does NOT do
|
|
44
|
+
*
|
|
45
|
+
* It does not render a router, and it takes no route table. This
|
|
46
|
+
* library has no opinion about either, and an app that wants
|
|
47
|
+
* `createBrowserRouter` should keep it.
|
|
48
|
+
*/
|
|
49
|
+
export const ReactRouterAdapter = ({
|
|
50
|
+
children,
|
|
51
|
+
}: ReactRouterAdapterProps): ReactNode => {
|
|
52
|
+
const location = useLocation()
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<RouterLinkProvider link={ReactRouterLink}>
|
|
56
|
+
<ScrollMemoryProvider
|
|
57
|
+
entry={{
|
|
58
|
+
key: location.key,
|
|
59
|
+
path: location.pathname,
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
{children}
|
|
63
|
+
</ScrollMemoryProvider>
|
|
64
|
+
</RouterLinkProvider>
|
|
65
|
+
)
|
|
66
|
+
}
|
package/src/reactRouter/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@charcuterie/ui/react-router` — the react-router adapter for
|
|
3
|
-
*
|
|
2
|
+
* `@charcuterie/ui/react-router` — the react-router adapter for this
|
|
3
|
+
* library's seams.
|
|
4
4
|
*
|
|
5
5
|
* **A subpath export with an optional peer dependency**, and both
|
|
6
6
|
* halves of that matter. The base package must stay router-free: the
|
|
@@ -13,17 +13,23 @@
|
|
|
13
13
|
* asserts exactly that — the main entry is measured for what it
|
|
14
14
|
* reaches, so this cannot leak into it by accident.
|
|
15
15
|
*
|
|
16
|
-
* The wiring is one
|
|
16
|
+
* The wiring is one component at the app root; `Guides/Routing` in
|
|
17
17
|
* Storybook is the copy-pasteable version.
|
|
18
18
|
*
|
|
19
19
|
* ```tsx
|
|
20
|
-
* import {
|
|
21
|
-
* import { ReactRouterLink } from "@charcuterie/ui/react-router"
|
|
20
|
+
* import { ReactRouterAdapter } from "@charcuterie/ui/react-router"
|
|
22
21
|
*
|
|
23
|
-
* <
|
|
24
|
-
* <
|
|
25
|
-
*
|
|
22
|
+
* <BrowserRouter>
|
|
23
|
+
* <ReactRouterAdapter>
|
|
24
|
+
* <Routes>…</Routes>
|
|
25
|
+
* </ReactRouterAdapter>
|
|
26
|
+
* </BrowserRouter>
|
|
26
27
|
* ```
|
|
28
|
+
*
|
|
29
|
+
* `ReactRouterLink` stays exported for an app that wires
|
|
30
|
+
* `RouterLinkProvider` itself.
|
|
27
31
|
*/
|
|
28
32
|
|
|
33
|
+
export type { ReactRouterAdapterProps } from "./ReactRouterAdapter.tsx"
|
|
34
|
+
export { ReactRouterAdapter } from "./ReactRouterAdapter.tsx"
|
|
29
35
|
export { ReactRouterLink } from "./ReactRouterLink.tsx"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The paint `Slider` and `RangeSlider` share.
|
|
3
|
+
*
|
|
4
|
+
* The two are separate components because their **focus models** are
|
|
5
|
+
* different — one `role="slider"` on the track against two of them in
|
|
6
|
+
* a `role="group"`, one tab stop against two
|
|
7
|
+
* ([decision](../../../docs/decisions/2026-09-01-a-two-thumb-range-is-its-own-component-and-shares-the-bar.md)).
|
|
8
|
+
* What is identical is the bar: a sunken rounded track, an intent
|
|
9
|
+
* fill inside it, and a round thumb pulled back by half its own
|
|
10
|
+
* width.
|
|
11
|
+
*
|
|
12
|
+
* Held here rather than copied for the reason `tileStyles.ts` exists:
|
|
13
|
+
* "the two look the same" spelled as two class strings is a promise
|
|
14
|
+
* that survives exactly one edit. `RangeSlider.test.tsx` compares the
|
|
15
|
+
* two thumbs as **computed** styles, so a class-name refactor cannot
|
|
16
|
+
* quietly break it either.
|
|
17
|
+
*
|
|
18
|
+
* No focus ring here. It is the one part of the bar the two do not
|
|
19
|
+
* share — `Slider` rings the track, because the track is the widget;
|
|
20
|
+
* `RangeSlider` rings each thumb, because each thumb is.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export type SliderSize = "lg" | "md" | "sm"
|
|
24
|
+
|
|
25
|
+
/** The bar itself. A `sm` bar is 2px; that is not a hit target. */
|
|
26
|
+
export const SLIDER_TRACK_SIZE_CLASS: Record<
|
|
27
|
+
SliderSize,
|
|
28
|
+
string
|
|
29
|
+
> = {
|
|
30
|
+
sm: "h-1",
|
|
31
|
+
md: "h-2",
|
|
32
|
+
lg: "h-3",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const SLIDER_THUMB_SIZE_CLASS: Record<
|
|
36
|
+
SliderSize,
|
|
37
|
+
string
|
|
38
|
+
> = {
|
|
39
|
+
sm: "size-3",
|
|
40
|
+
md: "size-4",
|
|
41
|
+
lg: "size-5",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Half of each `SLIDER_THUMB_SIZE_CLASS`, as the inline-start pull-back. */
|
|
45
|
+
export const SLIDER_THUMB_OFFSET: Record<
|
|
46
|
+
SliderSize,
|
|
47
|
+
string
|
|
48
|
+
> = {
|
|
49
|
+
sm: "-0.375rem",
|
|
50
|
+
md: "-0.5rem",
|
|
51
|
+
lg: "-0.625rem",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The row the bar sits in, which is taller than the bar: a 2px track
|
|
56
|
+
* is not a pointer target, and a finger is 44px. The height comes
|
|
57
|
+
* from the shared control-size system, so `[data-density]` decides
|
|
58
|
+
* it — never `MIN_TOUCH_TARGET_CLASS`, which would make this control
|
|
59
|
+
* taller than the ones beside it at desktop density
|
|
60
|
+
* ([decision](../../../docs/decisions/2026-08-05-controls-share-one-height-no-per-component-touch-floor.md)).
|
|
61
|
+
*/
|
|
62
|
+
export const SLIDER_ROW_CLASS =
|
|
63
|
+
"relative flex w-full touch-none cursor-pointer items-center rounded-full min-h-(--control-height-sm)"
|
|
64
|
+
|
|
65
|
+
export const SLIDER_TRACK_CLASS =
|
|
66
|
+
"w-full overflow-hidden rounded-full bg-surface-sunken"
|
|
67
|
+
|
|
68
|
+
export const SLIDER_THUMB_CLASS =
|
|
69
|
+
"absolute rounded-full border-2 border-surface-raised shadow-sm"
|
package/src/sourceRules.test.ts
CHANGED
|
@@ -375,8 +375,10 @@ test("the barrel is the only place components are re-exported", async () => {
|
|
|
375
375
|
// and `ButtonLink`, both a real `<a href>`, differing in paint
|
|
376
376
|
// rather than semantics: +2 -> 38, on top of `AdaptiveGrid`'s 36.
|
|
377
377
|
// `AnchorLink`,
|
|
378
|
-
// `RouterLinkProvider`,
|
|
379
|
-
//
|
|
378
|
+
// `RouterLinkProvider`, `ReactRouterLink`, `ScrollMemoryProvider`
|
|
379
|
+
// and `ReactRouterAdapter` are the router seam rather
|
|
380
|
+
// than components anybody stories, and live in `RouterLink/`,
|
|
381
|
+
// `Main/` and
|
|
380
382
|
// `reactRouter/` — outside this count by the `<Name>/<Name>.tsx`
|
|
381
383
|
// rule, exactly as `Overlay/`'s parts are.
|
|
382
384
|
// The unified app shell adds four: `Shell`, `Header`, `Rail`,
|
|
@@ -545,7 +547,31 @@ test("the barrel is the only place components are re-exported", async () => {
|
|
|
545
547
|
// what it deliberately does not cover — a tile that navigates is
|
|
546
548
|
// not a radio, and giving one `aria-checked` would be worse than
|
|
547
549
|
// the paint it replaced.
|
|
548
|
-
|
|
550
|
+
//
|
|
551
|
+
// `TimecodeInput` — a typed media position, with an `isRange` mode
|
|
552
|
+
// for a section of a file — is +1 -> 63. `timecode.ts` is its
|
|
553
|
+
// member and stays out of this count by the `<Name>/<Name>.tsx`
|
|
554
|
+
// rule, while still being exported from the barrel: the fleet
|
|
555
|
+
// spells the *printing* half five times over (queuepilot twice,
|
|
556
|
+
// castkit, mux-magic twice) and a read-only surface needs the
|
|
557
|
+
// formatter without the field.
|
|
558
|
+
//
|
|
559
|
+
// `RangeSlider` is +1 -> 64. `Slider`'s own record said a two-thumb
|
|
560
|
+
// range is *"a different widget with its own focus model"*, and the
|
|
561
|
+
// focus model is exactly what makes it a component rather than an
|
|
562
|
+
// `isRange` mode: one `role="slider"` on the track becomes two of
|
|
563
|
+
// them on the thumbs inside a `role="group"`, and one tab stop
|
|
564
|
+
// becomes two. So this shares the ARITHMETIC (`sliderValue.ts`,
|
|
565
|
+
// called rather than copied) and the BAR (`sliderStyles.ts`, the
|
|
566
|
+
// same extraction `tileStyles.ts` is), and nothing else.
|
|
567
|
+
// `rangeSliderValue.ts` is its member and stays out of this count
|
|
568
|
+
// by the `<Name>/<Name>.tsx` rule. It lands beside `TimecodeInput`
|
|
569
|
+
// rather than instead of it: the two halves of QueuePilot's clip
|
|
570
|
+
// editor are a typed field and a dragged bar over one value, and
|
|
571
|
+
// `isRange` is right there and wrong here for a reason that is
|
|
572
|
+
// written down — a second text field is still a text field, while a
|
|
573
|
+
// second thumb cannot share one `aria-valuenow`.
|
|
574
|
+
expect(componentNames.length).toBe(64)
|
|
549
575
|
|
|
550
576
|
for (const name of componentNames) {
|
|
551
577
|
expect(barrel).toContain(`export { ${name} }`)
|