@charcuterie/ui 3.29.1 → 3.31.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 (62) hide show
  1. package/dist/Main/Main.d.ts +35 -1
  2. package/dist/Main/Main.d.ts.map +1 -1
  3. package/dist/Main/Main.js +38 -3
  4. package/dist/Main/Main.js.map +1 -1
  5. package/dist/Main/scrollMemory.d.ts +40 -0
  6. package/dist/Main/scrollMemory.d.ts.map +1 -0
  7. package/dist/Main/scrollMemory.js +63 -0
  8. package/dist/Main/scrollMemory.js.map +1 -0
  9. package/dist/Main/useScrollMemory.d.ts +29 -0
  10. package/dist/Main/useScrollMemory.d.ts.map +1 -0
  11. package/dist/Main/useScrollMemory.js +131 -0
  12. package/dist/Main/useScrollMemory.js.map +1 -0
  13. package/dist/Tabs/PanelTabs.d.ts +56 -0
  14. package/dist/Tabs/PanelTabs.d.ts.map +1 -0
  15. package/dist/Tabs/PanelTabs.js +163 -0
  16. package/dist/Tabs/PanelTabs.js.map +1 -0
  17. package/dist/Tabs/RoutedTabs.d.ts +19 -0
  18. package/dist/Tabs/RoutedTabs.d.ts.map +1 -0
  19. package/dist/Tabs/RoutedTabs.js +28 -0
  20. package/dist/Tabs/RoutedTabs.js.map +1 -0
  21. package/dist/Tabs/TabLink.d.ts +34 -0
  22. package/dist/Tabs/TabLink.d.ts.map +1 -0
  23. package/dist/Tabs/TabLink.js +66 -0
  24. package/dist/Tabs/TabLink.js.map +1 -0
  25. package/dist/Tabs/TabTrigger.d.ts +1 -1
  26. package/dist/Tabs/TabTrigger.d.ts.map +1 -1
  27. package/dist/Tabs/TabTrigger.js +6 -30
  28. package/dist/Tabs/TabTrigger.js.map +1 -1
  29. package/dist/Tabs/Tabs.d.ts +80 -64
  30. package/dist/Tabs/Tabs.d.ts.map +1 -1
  31. package/dist/Tabs/Tabs.js +53 -182
  32. package/dist/Tabs/Tabs.js.map +1 -1
  33. package/dist/Tabs/tabItems.d.ts +36 -0
  34. package/dist/Tabs/tabItems.d.ts.map +1 -0
  35. package/dist/Tabs/tabItems.js +2 -0
  36. package/dist/Tabs/tabItems.js.map +1 -0
  37. package/dist/Tabs/tabStyles.d.ts +41 -0
  38. package/dist/Tabs/tabStyles.d.ts.map +1 -0
  39. package/dist/Tabs/tabStyles.js +79 -0
  40. package/dist/Tabs/tabStyles.js.map +1 -0
  41. package/dist/index.d.ts +2 -1
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/Main/Main.mdx +53 -0
  46. package/src/Main/Main.stories.tsx +152 -0
  47. package/src/Main/Main.test.tsx +195 -4
  48. package/src/Main/Main.tsx +58 -1
  49. package/src/Main/scrollMemory.test.ts +59 -0
  50. package/src/Main/scrollMemory.ts +73 -0
  51. package/src/Main/useScrollMemory.ts +164 -0
  52. package/src/Tabs/PanelTabs.tsx +260 -0
  53. package/src/Tabs/RoutedTabs.tsx +58 -0
  54. package/src/Tabs/TabLink.tsx +132 -0
  55. package/src/Tabs/TabTrigger.tsx +10 -39
  56. package/src/Tabs/Tabs.mdx +70 -0
  57. package/src/Tabs/Tabs.stories.tsx +133 -1
  58. package/src/Tabs/Tabs.test.tsx +169 -0
  59. package/src/Tabs/Tabs.tsx +94 -296
  60. package/src/Tabs/tabItems.ts +40 -0
  61. package/src/Tabs/tabStyles.ts +107 -0
  62. package/src/index.ts +8 -3
@@ -2,6 +2,16 @@ import { type ComponentPropsWithRef, type ReactNode } from "react";
2
2
  import { type ContentWidth } from "../Shell/contentWidth.ts";
3
3
  export type MainProps = ComponentPropsWithRef<"main"> & {
4
4
  contentWidth?: ContentWidth;
5
+ /**
6
+ * The history entry this page belongs to — react-router's
7
+ * `useLocation().key`, and the same value under a different
8
+ * router. Given it, `<main>` remembers where each entry was
9
+ * scrolled to and puts the offset back on Back and Forward.
10
+ *
11
+ * Omit it and nothing is remembered, which is the right default
12
+ * for a page with no history to remember.
13
+ */
14
+ scrollKey?: string;
5
15
  };
6
16
  /**
7
17
  * The `<main>` landmark and the content column.
@@ -80,6 +90,30 @@ export type MainProps = ComponentPropsWithRef<"main"> & {
80
90
  * an app's own fixed chrome belongs in `Shell`, beside `Main`
81
91
  * rather than in it.
82
92
  *
93
+ * ## It remembers where each history entry was scrolled to
94
+ *
95
+ * `Shell` gives this element the page's only vertical scrollport,
96
+ * so the header and the rails stay put while the content moves.
97
+ * The browser's own scroll restoration does not follow it there: a
98
+ * browser remembers **the document scroller's** offset per history
99
+ * entry, and `<main>` is not the document scroller. It also does
100
+ * not restore anything at all across a same-document `pushState`
101
+ * navigation, which is every link press in a single-page app. Back
102
+ * therefore lands at the top of a list the reader had scrolled
103
+ * half-way down, and no browser setting changes that.
104
+ *
105
+ * Pass `scrollKey` — react-router's `useLocation().key` — and the
106
+ * offset comes back. `useScrollMemory.ts` holds the mechanism and
107
+ * `scrollMemory.ts` the reasoning.
108
+ *
109
+ * ```tsx
110
+ * const location = useLocation()
111
+ *
112
+ * <Main scrollKey={location.key}>
113
+ * <Outlet />
114
+ * </Main>
115
+ * ```
116
+ *
83
117
  * ## Two more things it does, both one line
84
118
  *
85
119
  * `tabIndex={-1}` so `Shell`'s skip link moves **focus** and not
@@ -109,5 +143,5 @@ export type MainProps = ComponentPropsWithRef<"main"> & {
109
143
  * wrap, so a wide one goes in an `overflow-x-auto` wrapper, which
110
144
  * `Main.mdx` shows.
111
145
  */
112
- export declare const Main: ({ children, className, contentWidth, id, ...mainProps }: MainProps) => ReactNode;
146
+ export declare const Main: ({ children, className, contentWidth, id, ref, scrollKey, ...mainProps }: MainProps) => ReactNode;
113
147
  //# sourceMappingURL=Main.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Main.d.ts","sourceRoot":"","sources":["../../src/Main/Main.tsx"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,SAAS,EAEf,MAAM,OAAO,CAAA;AAEd,OAAO,EACL,KAAK,YAAY,EAGlB,MAAM,0BAA0B,CAAA;AAIjC,MAAM,MAAM,SAAS,GAAG,qBAAqB,CAAC,MAAM,CAAC,GAAG;IACtD,YAAY,CAAC,EAAE,YAAY,CAAA;CAC5B,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyGG;AACH,eAAO,MAAM,IAAI,GAAI,yDAMlB,SAAS,KAAG,SA0Cd,CAAA"}
1
+ {"version":3,"file":"Main.d.ts","sourceRoot":"","sources":["../../src/Main/Main.tsx"],"names":[],"mappings":"AAKA,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,SAAS,EAGf,MAAM,OAAO,CAAA;AAEd,OAAO,EACL,KAAK,YAAY,EAGlB,MAAM,0BAA0B,CAAA;AAKjC,MAAM,MAAM,SAAS,GAAG,qBAAqB,CAAC,MAAM,CAAC,GAAG;IACtD,YAAY,CAAC,EAAE,YAAY,CAAA;IAC3B;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiIG;AACH,eAAO,MAAM,IAAI,GAAI,yEAQlB,SAAS,KAAG,SAyDd,CAAA"}
package/dist/Main/Main.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useUniqueId } from "@charcuterie/logic";
2
+ import { mergeRefs, useUniqueId, } from "@charcuterie/logic";
3
3
  import { useContext, } from "react";
4
4
  import { DEFAULT_CONTENT_WIDTH, toMaxInlineSize, } from "../Shell/contentWidth.js";
5
5
  import { ShellContext } from "../Shell/shellContext.js";
6
6
  import { toClassName } from "../toClassName.js";
7
+ import { useScrollMemory } from "./useScrollMemory.js";
7
8
  /**
8
9
  * The `<main>` landmark and the content column.
9
10
  *
@@ -81,6 +82,30 @@ import { toClassName } from "../toClassName.js";
81
82
  * an app's own fixed chrome belongs in `Shell`, beside `Main`
82
83
  * rather than in it.
83
84
  *
85
+ * ## It remembers where each history entry was scrolled to
86
+ *
87
+ * `Shell` gives this element the page's only vertical scrollport,
88
+ * so the header and the rails stay put while the content moves.
89
+ * The browser's own scroll restoration does not follow it there: a
90
+ * browser remembers **the document scroller's** offset per history
91
+ * entry, and `<main>` is not the document scroller. It also does
92
+ * not restore anything at all across a same-document `pushState`
93
+ * navigation, which is every link press in a single-page app. Back
94
+ * therefore lands at the top of a list the reader had scrolled
95
+ * half-way down, and no browser setting changes that.
96
+ *
97
+ * Pass `scrollKey` — react-router's `useLocation().key` — and the
98
+ * offset comes back. `useScrollMemory.ts` holds the mechanism and
99
+ * `scrollMemory.ts` the reasoning.
100
+ *
101
+ * ```tsx
102
+ * const location = useLocation()
103
+ *
104
+ * <Main scrollKey={location.key}>
105
+ * <Outlet />
106
+ * </Main>
107
+ * ```
108
+ *
84
109
  * ## Two more things it does, both one line
85
110
  *
86
111
  * `tabIndex={-1}` so `Shell`'s skip link moves **focus** and not
@@ -110,14 +135,24 @@ import { toClassName } from "../toClassName.js";
110
135
  * wrap, so a wide one goes in an `overflow-x-auto` wrapper, which
111
136
  * `Main.mdx` shows.
112
137
  */
113
- export const Main = ({ children, className, contentWidth, id, ...mainProps }) => {
138
+ export const Main = ({ children, className, contentWidth, id, ref, scrollKey, ...mainProps }) => {
114
139
  const shell = useContext(ShellContext);
140
+ const setScrollport = useScrollMemory(scrollKey);
115
141
  // Always called, never conditionally: a `Main` outside a `Shell`
116
142
  // still needs an id an app can point its own skip link at.
117
143
  const fallbackId = useUniqueId();
118
144
  const maxInlineSize = toMaxInlineSize(contentWidth ??
119
145
  shell?.contentWidth ??
120
146
  DEFAULT_CONTENT_WIDTH);
121
- return (_jsx("main", { ...mainProps, className: toClassName("charcuterie-scrollbar col-start-1 row-start-3 min-h-0 min-w-0 overflow-y-auto md:col-start-2 md:row-start-2", className), id: id ?? shell?.mainId ?? fallbackId, tabIndex: -1, children: _jsx("div", { className: "@container mx-auto flex w-full min-w-0 flex-col gap-6 wrap-anywhere px-4 py-6 sm:px-6", style: { maxInlineSize }, children: children }) }));
147
+ return (_jsx("main", { ...mainProps, className: toClassName("charcuterie-scrollbar col-start-1 row-start-3 min-h-0 min-w-0 overflow-y-auto md:col-start-2 md:row-start-2", className), id: id ?? shell?.mainId ?? fallbackId,
148
+ // A caller's `ref` is a subscription, not a value, so the
149
+ // scroll memory composes with it rather than replacing it.
150
+ //
151
+ // The cast is `mergeRefs`' prop-merging signature, where
152
+ // every value is `unknown`; a caller's `ref` is narrower than
153
+ // that, never wider.
154
+ ref: ref
155
+ ? mergeRefs(ref, setScrollport)
156
+ : setScrollport, tabIndex: -1, children: _jsx("div", { className: "@container mx-auto flex w-full min-w-0 flex-col gap-6 wrap-anywhere px-4 py-6 sm:px-6", style: { maxInlineSize }, children: children }) }));
122
157
  };
123
158
  //# sourceMappingURL=Main.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Main.js","sourceRoot":"","sources":["../../src/Main/Main.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAChD,OAAO,EAGL,UAAU,GACX,MAAM,OAAO,CAAA;AAEd,OAAO,EAEL,qBAAqB,EACrB,eAAe,GAChB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAM/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyGG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,EACnB,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,EAAE,EACF,GAAG,SAAS,EACF,EAAa,EAAE;IACzB,MAAM,KAAK,GAAG,UAAU,CAAC,YAAY,CAAC,CAAA;IAEtC,iEAAiE;IACjE,2DAA2D;IAC3D,MAAM,UAAU,GAAG,WAAW,EAAE,CAAA;IAEhC,MAAM,aAAa,GAAG,eAAe,CACnC,YAAY;QACV,KAAK,EAAE,YAAY;QACnB,qBAAqB,CACxB,CAAA;IAED,OAAO,CACL,kBACM,SAAS,EACb,SAAS,EAAE,WAAW,CACpB,6GAA6G,EAC7G,SAAS,CACV,EACD,EAAE,EAAE,EAAE,IAAI,KAAK,EAAE,MAAM,IAAI,UAAU,EACrC,QAAQ,EAAE,CAAC,CAAC,YAaZ,cACE,SAAS,EAAC,uFAAuF,EACjG,KAAK,EAAE,EAAE,aAAa,EAAE,YAEvB,QAAQ,GACL,GACD,CACR,CAAA;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"Main.js","sourceRoot":"","sources":["../../src/Main/Main.tsx"],"names":[],"mappings":";AAAA,OAAO,EAEL,SAAS,EACT,WAAW,GACZ,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAIL,UAAU,GACX,MAAM,OAAO,CAAA;AAEd,OAAO,EAEL,qBAAqB,EACrB,eAAe,GAChB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAgBtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiIG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,EACnB,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,EAAE,EACF,GAAG,EACH,SAAS,EACT,GAAG,SAAS,EACF,EAAa,EAAE;IACzB,MAAM,KAAK,GAAG,UAAU,CAAC,YAAY,CAAC,CAAA;IACtC,MAAM,aAAa,GAAG,eAAe,CAAC,SAAS,CAAC,CAAA;IAEhD,iEAAiE;IACjE,2DAA2D;IAC3D,MAAM,UAAU,GAAG,WAAW,EAAE,CAAA;IAEhC,MAAM,aAAa,GAAG,eAAe,CACnC,YAAY;QACV,KAAK,EAAE,YAAY;QACnB,qBAAqB,CACxB,CAAA;IAED,OAAO,CACL,kBACM,SAAS,EACb,SAAS,EAAE,WAAW,CACpB,6GAA6G,EAC7G,SAAS,CACV,EACD,EAAE,EAAE,EAAE,IAAI,KAAK,EAAE,MAAM,IAAI,UAAU;QACrC,0DAA0D;QAC1D,2DAA2D;QAC3D,EAAE;QACF,yDAAyD;QACzD,8DAA8D;QAC9D,qBAAqB;QACrB,GAAG,EACD,GAAG;YACD,CAAC,CAAE,SAAS,CACR,GAAmB,EACnB,aAAa,CACe;YAChC,CAAC,CAAC,aAAa,EAEnB,QAAQ,EAAE,CAAC,CAAC,YAaZ,cACE,SAAS,EAAC,uFAAuF,EACjG,KAAK,EAAE,EAAE,aAAa,EAAE,YAEvB,QAAQ,GACL,GACD,CACR,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * What the browser will not remember for you.
3
+ *
4
+ * `Shell` gives `<main>` the page's only vertical scrollport, so
5
+ * the header and the rails stay put while the content moves —
6
+ * that is the whole reason `Shell.test.tsx` asserts "only main
7
+ * scrolls vertically, keeping the rail available".
8
+ *
9
+ * It costs exactly one thing, and this module is it. A browser
10
+ * remembers **the document scroller's** offset per history entry
11
+ * and puts it back on Back. It does that for a cross-document
12
+ * load, and `history.scrollRestoration` is the switch for it. It
13
+ * does **not** do it for an arbitrary element with
14
+ * `overflow-y: auto`, and it does not do it at all for a
15
+ * same-document `pushState` navigation, which is every link press
16
+ * in a single-page app. So a page that scrolls its own `<main>`
17
+ * loses the reader's place on Back, and no browser setting brings
18
+ * it back:
19
+ *
20
+ * > *"If I scroll, click on an item, then go back, it resets me to
21
+ * > the top. […] It should remember where the scroll position was
22
+ * > previously. Isn't this handled by Chrome already?"*
23
+ *
24
+ * The offsets are held **in memory**, not in `sessionStorage`. A
25
+ * history key survives a reload, but the DOM it described does
26
+ * not: the app re-fetches, re-measures and often re-orders, so a
27
+ * pixel offset from before the reload points at a different row.
28
+ * Restoring a stale number is worse than restoring nothing,
29
+ * because it looks deliberate.
30
+ */
31
+ export declare const rememberScrollOffset: (key: string, offset: number) => void;
32
+ /**
33
+ * `0` for an entry nobody has scrolled, which is also the right
34
+ * answer for an entry nobody has *seen* — a link press lands at
35
+ * the top of the new page.
36
+ */
37
+ export declare const recallScrollOffset: (key: string) => number;
38
+ /** Test-only. Module state outlives a mount; a test must not. */
39
+ export declare const forgetScrollOffsets: () => void;
40
+ //# sourceMappingURL=scrollMemory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrollMemory.d.ts","sourceRoot":"","sources":["../../src/Main/scrollMemory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAaH,eAAO,MAAM,oBAAoB,GAC/B,KAAK,MAAM,EACX,QAAQ,MAAM,SAef,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,KAAK,MAAM,WACb,CAAA;AAEjC,iEAAiE;AACjE,eAAO,MAAM,mBAAmB,YAE/B,CAAA"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * What the browser will not remember for you.
3
+ *
4
+ * `Shell` gives `<main>` the page's only vertical scrollport, so
5
+ * the header and the rails stay put while the content moves —
6
+ * that is the whole reason `Shell.test.tsx` asserts "only main
7
+ * scrolls vertically, keeping the rail available".
8
+ *
9
+ * It costs exactly one thing, and this module is it. A browser
10
+ * remembers **the document scroller's** offset per history entry
11
+ * and puts it back on Back. It does that for a cross-document
12
+ * load, and `history.scrollRestoration` is the switch for it. It
13
+ * does **not** do it for an arbitrary element with
14
+ * `overflow-y: auto`, and it does not do it at all for a
15
+ * same-document `pushState` navigation, which is every link press
16
+ * in a single-page app. So a page that scrolls its own `<main>`
17
+ * loses the reader's place on Back, and no browser setting brings
18
+ * it back:
19
+ *
20
+ * > *"If I scroll, click on an item, then go back, it resets me to
21
+ * > the top. […] It should remember where the scroll position was
22
+ * > previously. Isn't this handled by Chrome already?"*
23
+ *
24
+ * The offsets are held **in memory**, not in `sessionStorage`. A
25
+ * history key survives a reload, but the DOM it described does
26
+ * not: the app re-fetches, re-measures and often re-orders, so a
27
+ * pixel offset from before the reload points at a different row.
28
+ * Restoring a stale number is worse than restoring nothing,
29
+ * because it looks deliberate.
30
+ */
31
+ /**
32
+ * How many history entries keep an offset. A reader goes back
33
+ * through a handful of screens, never fifty, so this is a ceiling
34
+ * against a long session rather than a working set — the map holds
35
+ * one small number per entry and evicts the least recently
36
+ * written.
37
+ */
38
+ const REMEMBERED_ENTRY_LIMIT = 50;
39
+ const rememberedOffsets = new Map();
40
+ export const rememberScrollOffset = (key, offset) => {
41
+ // Delete before set, so insertion order is least-recent-first
42
+ // and the eviction below takes the right one. A plain `set` on
43
+ // an existing key keeps its original position.
44
+ rememberedOffsets.delete(key);
45
+ rememberedOffsets.set(key, offset);
46
+ for (const oldestKey of rememberedOffsets.keys()) {
47
+ if (rememberedOffsets.size <= REMEMBERED_ENTRY_LIMIT) {
48
+ break;
49
+ }
50
+ rememberedOffsets.delete(oldestKey);
51
+ }
52
+ };
53
+ /**
54
+ * `0` for an entry nobody has scrolled, which is also the right
55
+ * answer for an entry nobody has *seen* — a link press lands at
56
+ * the top of the new page.
57
+ */
58
+ export const recallScrollOffset = (key) => rememberedOffsets.get(key) ?? 0;
59
+ /** Test-only. Module state outlives a mount; a test must not. */
60
+ export const forgetScrollOffsets = () => {
61
+ rememberedOffsets.clear();
62
+ };
63
+ //# sourceMappingURL=scrollMemory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrollMemory.js","sourceRoot":"","sources":["../../src/Main/scrollMemory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH;;;;;;GAMG;AACH,MAAM,sBAAsB,GAAG,EAAE,CAAA;AAEjC,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAAkB,CAAA;AAEnD,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAClC,GAAW,EACX,MAAc,EACd,EAAE;IACF,8DAA8D;IAC9D,+DAA+D;IAC/D,+CAA+C;IAC/C,iBAAiB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IAC7B,iBAAiB,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;IAElC,KAAK,MAAM,SAAS,IAAI,iBAAiB,CAAC,IAAI,EAAE,EAAE,CAAC;QACjD,IAAI,iBAAiB,CAAC,IAAI,IAAI,sBAAsB,EAAE,CAAC;YACrD,MAAK;QACP,CAAC;QAED,iBAAiB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IACrC,CAAC;AACH,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,GAAW,EAAE,EAAE,CAChD,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AAEjC,iEAAiE;AACjE,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,EAAE;IACtC,iBAAiB,CAAC,KAAK,EAAE,CAAA;AAC3B,CAAC,CAAA"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Remember where this history entry was scrolled to, and put it
3
+ * back when the reader returns to it.
4
+ *
5
+ * `scrollMemory.ts` says why the browser will not. This is the
6
+ * part that has to survive the two things a real page does:
7
+ *
8
+ * 1. **The content arrives late.** An offset applied against a
9
+ * 600px skeleton clamps to `0` and looks like it did nothing.
10
+ * A `ResizeObserver` on the scrollport and on its content
11
+ * column re-applies the offset every time either grows, until
12
+ * the offset lands or the window closes.
13
+ * 2. **The reader scrolls during all that.** Any wheel, touch,
14
+ * key or pointer press ends the restore immediately. The
15
+ * alternative is a page that jumps out from under somebody who
16
+ * has already started reading.
17
+ *
18
+ * The save is a passive `scroll` listener, suppressed while a
19
+ * restore is in flight. Without that suppression the feature eats
20
+ * itself: writing `scrollTop` on a still-short page clamps to `0`
21
+ * and fires a `scroll` event reading `0`, which would be saved
22
+ * over the very offset being restored.
23
+ *
24
+ * @param scrollKey Identifies the history entry — react-router's
25
+ * `useLocation().key`. `undefined` turns the memory off, which is
26
+ * what an app with no router gets.
27
+ */
28
+ export declare const useScrollMemory: (scrollKey: string | undefined) => (node: unknown) => void;
29
+ //# sourceMappingURL=useScrollMemory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useScrollMemory.d.ts","sourceRoot":"","sources":["../../src/Main/useScrollMemory.ts"],"names":[],"mappings":"AA6BA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,eAAe,GAC1B,WAAW,MAAM,GAAG,SAAS,YAaS,OAAO,SA6F9C,CAAA"}
@@ -0,0 +1,131 @@
1
+ import { useCallback, useLayoutEffect, useRef } from "react";
2
+ import { recallScrollOffset, rememberScrollOffset, } from "./scrollMemory.js";
3
+ /**
4
+ * How long a restore keeps trying while the content is still
5
+ * arriving.
6
+ *
7
+ * The offset cannot be applied until the scrollport is tall enough
8
+ * to hold it, and on a client-rendered page it is not: the commit
9
+ * that changes the route draws a skeleton, and the rows land a
10
+ * fetch later. So the restore re-applies as the content grows, and
11
+ * gives up after this. A page that takes longer than three seconds
12
+ * to fill has already been read from the top, and moving it then
13
+ * is a worse answer than leaving it where the reader put it.
14
+ */
15
+ const RESTORE_WINDOW_MS = 3000;
16
+ /** A scroll the reader asked for. It ends the restore at once. */
17
+ const READER_INTENT_EVENTS = [
18
+ "keydown",
19
+ "pointerdown",
20
+ "touchstart",
21
+ "wheel",
22
+ ];
23
+ /**
24
+ * Remember where this history entry was scrolled to, and put it
25
+ * back when the reader returns to it.
26
+ *
27
+ * `scrollMemory.ts` says why the browser will not. This is the
28
+ * part that has to survive the two things a real page does:
29
+ *
30
+ * 1. **The content arrives late.** An offset applied against a
31
+ * 600px skeleton clamps to `0` and looks like it did nothing.
32
+ * A `ResizeObserver` on the scrollport and on its content
33
+ * column re-applies the offset every time either grows, until
34
+ * the offset lands or the window closes.
35
+ * 2. **The reader scrolls during all that.** Any wheel, touch,
36
+ * key or pointer press ends the restore immediately. The
37
+ * alternative is a page that jumps out from under somebody who
38
+ * has already started reading.
39
+ *
40
+ * The save is a passive `scroll` listener, suppressed while a
41
+ * restore is in flight. Without that suppression the feature eats
42
+ * itself: writing `scrollTop` on a still-short page clamps to `0`
43
+ * and fires a `scroll` event reading `0`, which would be saved
44
+ * over the very offset being restored.
45
+ *
46
+ * @param scrollKey Identifies the history entry — react-router's
47
+ * `useLocation().key`. `undefined` turns the memory off, which is
48
+ * what an app with no router gets.
49
+ */
50
+ export const useScrollMemory = (scrollKey) => {
51
+ const elementRef = useRef(null);
52
+ const isRestoringRef = useRef(false);
53
+ // A ref rather than a query for the `<main>` id: the effect
54
+ // below runs in the same commit that mounts the node, and only a
55
+ // ref can say which node that was.
56
+ //
57
+ // `unknown` in, narrowed here, so the returned function is both a
58
+ // `RefCallback<HTMLElement>` and something `mergeRefs` accepts —
59
+ // that library's merge is typed for the prop-merging path, where
60
+ // every value is `unknown`.
61
+ const setElement = useCallback((node) => {
62
+ elementRef.current =
63
+ node instanceof HTMLElement ? node : null;
64
+ }, []);
65
+ useLayoutEffect(() => {
66
+ const element = elementRef.current;
67
+ if (!element || scrollKey === undefined) {
68
+ return undefined;
69
+ }
70
+ const offset = recallScrollOffset(scrollKey);
71
+ // The arrow defers the read of `applyOffset`, so the three
72
+ // pieces can be declared in the order they depend on each
73
+ // other rather than in the order they are used.
74
+ const observer = new ResizeObserver(() => {
75
+ applyOffset();
76
+ });
77
+ const deadline = globalThis.setTimeout(() => {
78
+ stopRestoring();
79
+ }, RESTORE_WINDOW_MS);
80
+ const stopRestoring = () => {
81
+ if (!isRestoringRef.current) {
82
+ return;
83
+ }
84
+ isRestoringRef.current = false;
85
+ observer.disconnect();
86
+ globalThis.clearTimeout(deadline);
87
+ for (const eventName of READER_INTENT_EVENTS) {
88
+ element.removeEventListener(eventName, stopRestoring);
89
+ }
90
+ };
91
+ const applyOffset = () => {
92
+ if (!isRestoringRef.current) {
93
+ return;
94
+ }
95
+ element.scrollTop = offset;
96
+ // Landed. Anything further would only fight the reader.
97
+ if (element.scrollTop === offset) {
98
+ stopRestoring();
99
+ }
100
+ };
101
+ const onScroll = () => {
102
+ if (isRestoringRef.current) {
103
+ return;
104
+ }
105
+ rememberScrollOffset(scrollKey, element.scrollTop);
106
+ };
107
+ isRestoringRef.current = true;
108
+ for (const eventName of READER_INTENT_EVENTS) {
109
+ element.addEventListener(eventName, stopRestoring, {
110
+ passive: true,
111
+ });
112
+ }
113
+ // The scrollport's own height changes with the window; its
114
+ // content column's changes when the rows land. Both move the
115
+ // ceiling the offset is clamped against.
116
+ observer.observe(element);
117
+ for (const child of Array.from(element.children)) {
118
+ observer.observe(child);
119
+ }
120
+ element.addEventListener("scroll", onScroll, {
121
+ passive: true,
122
+ });
123
+ applyOffset();
124
+ return () => {
125
+ stopRestoring();
126
+ element.removeEventListener("scroll", onScroll);
127
+ };
128
+ }, [scrollKey]);
129
+ return setElement;
130
+ };
131
+ //# sourceMappingURL=useScrollMemory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useScrollMemory.js","sourceRoot":"","sources":["../../src/Main/useScrollMemory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ import type { ReactNode } from "react";
2
+ import type { TabsPanelProps } from "./Tabs.tsx";
3
+ /**
4
+ * The panel bar — every tab a `<button>`, every section a
5
+ * `role="tabpanel"` this component renders and hides. See `Tabs`
6
+ * for which of the two bars a screen wants.
7
+ *
8
+ * The composition is two hooks and one decision:
9
+ *
10
+ * ```ts
11
+ * const selection = useSinglePicker({ selectedValue: initialKey })
12
+ * const focus = useRovingFocus({ activeValue: initialKey })
13
+ *
14
+ * // `automatic` is this line, and `manual` is its absence.
15
+ * if (activation === "automatic") selection.select(focus.activeValue)
16
+ * ```
17
+ *
18
+ * ### Selection, not visibility — and the difference is not cosmetic
19
+ *
20
+ * M4 built this on `VisibilityGroup`, and the panels do end up
21
+ * one-at-a-time either way, because the two cores are the same
22
+ * shape. But the thing that **registers** here is the tab, not the
23
+ * panel, and what a tab bar is asking is *which one did you pick*.
24
+ * `SinglePicker` is the kind that answers that; the panel's
25
+ * `hidden` is derived from the answer.
26
+ *
27
+ * Reading it the other way round cost something concrete: it made
28
+ * `aria-selected` a report about a *panel's* visibility, so a
29
+ * consumer wanting a tab bar with no panels at all — a segmented
30
+ * filter, a view switcher that swaps a route — had to model its
31
+ * choice as a group of visibilities it did not have. `Alert`'s
32
+ * sibling `SegmentedControl` is exactly that consumer, and it now
33
+ * shares this component's core rather than hand-rolling a fourth
34
+ * one.
35
+ * [Decision](../../../../docs/decisions/2026-07-30-tab-selection-is-a-single-picker.md).
36
+ *
37
+ * ### What it does not use, and why that is the right answer
38
+ *
39
+ * The plan expected `createLinkedIds` here as well. It does not
40
+ * fit, and forcing it would have been the wrong call: that kind
41
+ * exists for a **dynamic** trigger↔target pair — a `Popover` whose
42
+ * panel mounts and unmounts, where the multiset is what stops
43
+ * `aria-controls` pointing at a node React has already removed. A
44
+ * tab bar's pairing is static and known at render, so the ids come
45
+ * from one `useUniqueId` and cannot get out of step at all.
46
+ *
47
+ * ### Focus is moved deliberately, and only then
48
+ *
49
+ * `RovingFocus` is state; something still has to call `.focus()`.
50
+ * That happens only while the tablist already contains the focused
51
+ * element — otherwise mounting a tab bar, or any parent re-render
52
+ * that moved `activeValue`, yanks the caret out of whatever the
53
+ * user was actually doing.
54
+ */
55
+ export declare const PanelTabs: ({ activation, activeKey, className, label, onChange, orientation, tabs, }: TabsPanelProps) => ReactNode;
56
+ //# sourceMappingURL=PanelTabs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PanelTabs.d.ts","sourceRoot":"","sources":["../../src/Tabs/PanelTabs.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAgBhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,eAAO,MAAM,SAAS,GAAI,2EAQvB,cAAc,KAAG,SA6KnB,CAAA"}
@@ -0,0 +1,163 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { selectTabIndex, useRovingFocus, useSinglePicker, useUniqueId, } from "@charcuterie/logic";
3
+ import { useCallback, useEffect, useRef } from "react";
4
+ import { toClassName } from "../toClassName.js";
5
+ import { TabTrigger } from "./TabTrigger.js";
6
+ import { toTabListClass } from "./tabStyles.js";
7
+ const ORIENTATION_KEYS = {
8
+ horizontal: {
9
+ next: "ArrowRight",
10
+ previous: "ArrowLeft",
11
+ },
12
+ vertical: { next: "ArrowDown", previous: "ArrowUp" },
13
+ };
14
+ /**
15
+ * The panel bar — every tab a `<button>`, every section a
16
+ * `role="tabpanel"` this component renders and hides. See `Tabs`
17
+ * for which of the two bars a screen wants.
18
+ *
19
+ * The composition is two hooks and one decision:
20
+ *
21
+ * ```ts
22
+ * const selection = useSinglePicker({ selectedValue: initialKey })
23
+ * const focus = useRovingFocus({ activeValue: initialKey })
24
+ *
25
+ * // `automatic` is this line, and `manual` is its absence.
26
+ * if (activation === "automatic") selection.select(focus.activeValue)
27
+ * ```
28
+ *
29
+ * ### Selection, not visibility — and the difference is not cosmetic
30
+ *
31
+ * M4 built this on `VisibilityGroup`, and the panels do end up
32
+ * one-at-a-time either way, because the two cores are the same
33
+ * shape. But the thing that **registers** here is the tab, not the
34
+ * panel, and what a tab bar is asking is *which one did you pick*.
35
+ * `SinglePicker` is the kind that answers that; the panel's
36
+ * `hidden` is derived from the answer.
37
+ *
38
+ * Reading it the other way round cost something concrete: it made
39
+ * `aria-selected` a report about a *panel's* visibility, so a
40
+ * consumer wanting a tab bar with no panels at all — a segmented
41
+ * filter, a view switcher that swaps a route — had to model its
42
+ * choice as a group of visibilities it did not have. `Alert`'s
43
+ * sibling `SegmentedControl` is exactly that consumer, and it now
44
+ * shares this component's core rather than hand-rolling a fourth
45
+ * one.
46
+ * [Decision](../../../../docs/decisions/2026-07-30-tab-selection-is-a-single-picker.md).
47
+ *
48
+ * ### What it does not use, and why that is the right answer
49
+ *
50
+ * The plan expected `createLinkedIds` here as well. It does not
51
+ * fit, and forcing it would have been the wrong call: that kind
52
+ * exists for a **dynamic** trigger↔target pair — a `Popover` whose
53
+ * panel mounts and unmounts, where the multiset is what stops
54
+ * `aria-controls` pointing at a node React has already removed. A
55
+ * tab bar's pairing is static and known at render, so the ids come
56
+ * from one `useUniqueId` and cannot get out of step at all.
57
+ *
58
+ * ### Focus is moved deliberately, and only then
59
+ *
60
+ * `RovingFocus` is state; something still has to call `.focus()`.
61
+ * That happens only while the tablist already contains the focused
62
+ * element — otherwise mounting a tab bar, or any parent re-render
63
+ * that moved `activeValue`, yanks the caret out of whatever the
64
+ * user was actually doing.
65
+ */
66
+ export const PanelTabs = ({ activation = "automatic", activeKey, className, label, onChange, orientation = "horizontal", tabs, }) => {
67
+ const baseId = useUniqueId();
68
+ const tablistRef = useRef(null);
69
+ const tabElements = useRef(new Map());
70
+ const initialKey = activeKey ??
71
+ tabs.find((one) => !one.isDisabled)?.key ??
72
+ null;
73
+ const selection = useSinglePicker({
74
+ onChange,
75
+ selectedValue: initialKey,
76
+ });
77
+ const focus = useRovingFocus({
78
+ activeValue: initialKey,
79
+ });
80
+ const { select } = selection;
81
+ const { activeValue, setActiveValue } = focus;
82
+ /**
83
+ * Selected **or** pending, and the fallback is load-bearing.
84
+ *
85
+ * Members register from an effect, so on the first paint
86
+ * `selectedValue` is still null and the intent lives in
87
+ * `pendingValue` — a tab bar reading only the former renders with
88
+ * no tab selected and no panel shown, then corrects itself a
89
+ * frame later. It is a flash on a fast machine and a visible
90
+ * blank on a kiosk Pi.
91
+ *
92
+ * The core distinguishes the two on purpose, which is why this
93
+ * belongs here rather than in the selector — unlike
94
+ * `RovingFocus`, where the equivalent first-paint hole was in
95
+ * `selectTabIndex` itself and got fixed there.
96
+ */
97
+ const shownKey = selection.selectedValue ?? selection.pendingValue;
98
+ const selectTab = useCallback((key) => {
99
+ // Both, because a pointer press moves focus *and* chooses.
100
+ // Only selecting would leave `automatic`'s effect below to
101
+ // put the old panel straight back.
102
+ setActiveValue(key);
103
+ select(key);
104
+ }, [select, setActiveValue]);
105
+ useEffect(() => {
106
+ // Automatic activation, and the only behaviour that differs
107
+ // between the two modes. `select` is idempotent, so this is a
108
+ // no-op on every render where focus did not actually move.
109
+ if (activation === "automatic" &&
110
+ activeValue !== null) {
111
+ select(activeValue);
112
+ }
113
+ }, [activation, activeValue, select]);
114
+ useEffect(() => {
115
+ const tablist = tablistRef.current;
116
+ if (!tablist ||
117
+ activeValue === null ||
118
+ !tablist.contains(document.activeElement)) {
119
+ return;
120
+ }
121
+ tabElements.current.get(activeValue)?.focus();
122
+ }, [activeValue]);
123
+ const keys = ORIENTATION_KEYS[orientation];
124
+ return (_jsxs("div", { className: toClassName("flex flex-col gap-3", className), children: [_jsx("div", { "aria-label": label, "aria-orientation": orientation, className: toTabListClass(orientation), onKeyDown: (keyEvent) => {
125
+ const commands = {
126
+ [keys.next]: focus.next,
127
+ [keys.previous]: focus.previous,
128
+ End: focus.last,
129
+ Home: focus.first,
130
+ };
131
+ const command = commands[keyEvent.key];
132
+ if (command) {
133
+ // Arrow keys scroll the page by default, and a tab bar
134
+ // that scrolls the window while it moves focus is the
135
+ // hand-rolled version of this component.
136
+ keyEvent.preventDefault();
137
+ command();
138
+ return;
139
+ }
140
+ // `manual`'s commit. In `automatic` the panel is showing
141
+ // already, so this is the no-op that makes Enter behave
142
+ // identically in both modes.
143
+ if ((keyEvent.key === "Enter" ||
144
+ keyEvent.key === " ") &&
145
+ activeValue !== null) {
146
+ keyEvent.preventDefault();
147
+ select(activeValue);
148
+ }
149
+ }, ref: tablistRef, role: "tablist", children: tabs.map((tab, index) => (_jsx(TabTrigger, { id: `${baseId}-tab-${index}`, isSelected: shownKey === tab.key, onSelect: selectTab, panelId: `${baseId}-panel-${index}`, registerFocus: focus.register, orientation: orientation, registerSelection: selection.register, tab: tab,
150
+ // The roving-tabindex rule read from the core rather
151
+ // than restated here: exactly one member is in the tab
152
+ // order, so Tab enters and leaves the bar once while
153
+ // the arrow keys move inside it.
154
+ tabIndex: selectTabIndex(focus, tab.key), trackElement: (key, element) => {
155
+ if (element) {
156
+ tabElements.current.set(key, element);
157
+ }
158
+ else {
159
+ tabElements.current.delete(key);
160
+ }
161
+ } }, tab.key))) }), tabs.map((tab, index) => (_jsx("div", { "aria-labelledby": `${baseId}-tab-${index}`, hidden: shownKey !== tab.key, id: `${baseId}-panel-${index}`, role: "tabpanel", children: tab.content }, tab.key)))] }));
162
+ };
163
+ //# sourceMappingURL=PanelTabs.js.map