@charcuterie/ui 3.34.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.
Files changed (49) hide show
  1. package/dist/Main/Main.d.ts +18 -19
  2. package/dist/Main/Main.d.ts.map +1 -1
  3. package/dist/Main/Main.js +21 -10
  4. package/dist/Main/Main.js.map +1 -1
  5. package/dist/Main/ScrollMemoryProvider.d.ts +38 -0
  6. package/dist/Main/ScrollMemoryProvider.d.ts.map +1 -0
  7. package/dist/Main/ScrollMemoryProvider.js +30 -0
  8. package/dist/Main/ScrollMemoryProvider.js.map +1 -0
  9. package/dist/Main/scrollMemory.d.ts +54 -28
  10. package/dist/Main/scrollMemory.d.ts.map +1 -1
  11. package/dist/Main/scrollMemory.js +81 -40
  12. package/dist/Main/scrollMemory.js.map +1 -1
  13. package/dist/Main/scrollMemoryContext.d.ts +18 -0
  14. package/dist/Main/scrollMemoryContext.d.ts.map +1 -0
  15. package/dist/Main/scrollMemoryContext.js +18 -0
  16. package/dist/Main/scrollMemoryContext.js.map +1 -0
  17. package/dist/Main/useScrollMemory.d.ts +20 -18
  18. package/dist/Main/useScrollMemory.d.ts.map +1 -1
  19. package/dist/Main/useScrollMemory.js +69 -42
  20. package/dist/Main/useScrollMemory.js.map +1 -1
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/reactRouter/ReactRouterAdapter.d.ts +43 -0
  26. package/dist/reactRouter/ReactRouterAdapter.d.ts.map +1 -0
  27. package/dist/reactRouter/ReactRouterAdapter.js +50 -0
  28. package/dist/reactRouter/ReactRouterAdapter.js.map +1 -0
  29. package/dist/reactRouter/index.d.ts +14 -8
  30. package/dist/reactRouter/index.d.ts.map +1 -1
  31. package/dist/reactRouter/index.js +13 -8
  32. package/dist/reactRouter/index.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/Main/Main.mdx +35 -7
  35. package/src/Main/Main.stories.tsx +129 -64
  36. package/src/Main/Main.test.tsx +58 -0
  37. package/src/Main/Main.tsx +20 -20
  38. package/src/Main/ScrollMemoryProvider.tsx +48 -0
  39. package/src/Main/scrollMemory.test.ts +70 -16
  40. package/src/Main/scrollMemory.ts +98 -40
  41. package/src/Main/scrollMemoryContext.ts +21 -0
  42. package/src/Main/useScrollMemory.ts +83 -43
  43. package/src/RouterLink/RouterLink.mdx +53 -16
  44. package/src/index.ts +3 -0
  45. package/src/reactRouter/ReactRouterAdapter.stories.tsx +120 -0
  46. package/src/reactRouter/ReactRouterAdapter.test.tsx +92 -0
  47. package/src/reactRouter/ReactRouterAdapter.tsx +66 -0
  48. package/src/reactRouter/index.ts +14 -8
  49. package/src/sourceRules.test.ts +4 -2
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
- * `@charcuterie/ui/react-router` — the react-router adapter for the
3
- * link seam.
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 provider at the app root; `Guides/Routing` in
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 { RouterLinkProvider } from "@charcuterie/ui"
21
- * import { ReactRouterLink } from "@charcuterie/ui/react-router"
20
+ * import { ReactRouterAdapter } from "@charcuterie/ui/react-router"
22
21
  *
23
- * <RouterLinkProvider link={ReactRouterLink}>
24
- * <RouterProvider router={router} />
25
- * </RouterLinkProvider>
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"
@@ -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`, and `ReactRouterLink` are the seam rather
379
- // than components anybody stories, and live in `RouterLink/` and
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`,