@charcuterie/ui 3.34.0 → 4.1.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 (87) hide show
  1. package/dist/ActionTiles/ActionTiles.d.ts +66 -8
  2. package/dist/ActionTiles/ActionTiles.d.ts.map +1 -1
  3. package/dist/ActionTiles/ActionTiles.js +88 -11
  4. package/dist/ActionTiles/ActionTiles.js.map +1 -1
  5. package/dist/Main/Main.d.ts +18 -19
  6. package/dist/Main/Main.d.ts.map +1 -1
  7. package/dist/Main/Main.js +21 -10
  8. package/dist/Main/Main.js.map +1 -1
  9. package/dist/Main/ScrollMemoryProvider.d.ts +38 -0
  10. package/dist/Main/ScrollMemoryProvider.d.ts.map +1 -0
  11. package/dist/Main/ScrollMemoryProvider.js +30 -0
  12. package/dist/Main/ScrollMemoryProvider.js.map +1 -0
  13. package/dist/Main/scrollMemory.d.ts +54 -28
  14. package/dist/Main/scrollMemory.d.ts.map +1 -1
  15. package/dist/Main/scrollMemory.js +81 -40
  16. package/dist/Main/scrollMemory.js.map +1 -1
  17. package/dist/Main/scrollMemoryContext.d.ts +18 -0
  18. package/dist/Main/scrollMemoryContext.d.ts.map +1 -0
  19. package/dist/Main/scrollMemoryContext.js +18 -0
  20. package/dist/Main/scrollMemoryContext.js.map +1 -0
  21. package/dist/Main/useScrollMemory.d.ts +20 -18
  22. package/dist/Main/useScrollMemory.d.ts.map +1 -1
  23. package/dist/Main/useScrollMemory.js +69 -42
  24. package/dist/Main/useScrollMemory.js.map +1 -1
  25. package/dist/PortraitTiles/PortraitAvatar.d.ts +21 -0
  26. package/dist/PortraitTiles/PortraitAvatar.d.ts.map +1 -0
  27. package/dist/PortraitTiles/PortraitAvatar.js +29 -0
  28. package/dist/PortraitTiles/PortraitAvatar.js.map +1 -0
  29. package/dist/PortraitTiles/PortraitTiles.d.ts +162 -0
  30. package/dist/PortraitTiles/PortraitTiles.d.ts.map +1 -0
  31. package/dist/PortraitTiles/PortraitTiles.js +263 -0
  32. package/dist/PortraitTiles/PortraitTiles.js.map +1 -0
  33. package/dist/PortraitTiles/portraitColour.d.ts +49 -0
  34. package/dist/PortraitTiles/portraitColour.d.ts.map +1 -0
  35. package/dist/PortraitTiles/portraitColour.js +55 -0
  36. package/dist/PortraitTiles/portraitColour.js.map +1 -0
  37. package/dist/categoricalStyles.d.ts +27 -0
  38. package/dist/categoricalStyles.d.ts.map +1 -1
  39. package/dist/categoricalStyles.js +49 -0
  40. package/dist/categoricalStyles.js.map +1 -1
  41. package/dist/index.d.ts +7 -2
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +3 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/reactRouter/ReactRouterAdapter.d.ts +43 -0
  46. package/dist/reactRouter/ReactRouterAdapter.d.ts.map +1 -0
  47. package/dist/reactRouter/ReactRouterAdapter.js +50 -0
  48. package/dist/reactRouter/ReactRouterAdapter.js.map +1 -0
  49. package/dist/reactRouter/index.d.ts +14 -8
  50. package/dist/reactRouter/index.d.ts.map +1 -1
  51. package/dist/reactRouter/index.js +13 -8
  52. package/dist/reactRouter/index.js.map +1 -1
  53. package/dist/tileStyles.d.ts +15 -0
  54. package/dist/tileStyles.d.ts.map +1 -1
  55. package/dist/tileStyles.js +26 -1
  56. package/dist/tileStyles.js.map +1 -1
  57. package/package.json +2 -2
  58. package/src/ActionTiles/ActionTiles.mdx +64 -6
  59. package/src/ActionTiles/ActionTiles.stories.tsx +107 -2
  60. package/src/ActionTiles/ActionTiles.test.tsx +205 -5
  61. package/src/ActionTiles/ActionTiles.tsx +274 -110
  62. package/src/Main/Main.mdx +35 -7
  63. package/src/Main/Main.stories.tsx +129 -64
  64. package/src/Main/Main.test.tsx +58 -0
  65. package/src/Main/Main.tsx +20 -20
  66. package/src/Main/ScrollMemoryProvider.tsx +48 -0
  67. package/src/Main/scrollMemory.test.ts +70 -16
  68. package/src/Main/scrollMemory.ts +98 -40
  69. package/src/Main/scrollMemoryContext.ts +21 -0
  70. package/src/Main/useScrollMemory.ts +83 -43
  71. package/src/PortraitTiles/PortraitAvatar.tsx +49 -0
  72. package/src/PortraitTiles/PortraitTiles.mdx +153 -0
  73. package/src/PortraitTiles/PortraitTiles.stories.tsx +403 -0
  74. package/src/PortraitTiles/PortraitTiles.test.tsx +399 -0
  75. package/src/PortraitTiles/PortraitTiles.tsx +577 -0
  76. package/src/PortraitTiles/portraitColour.test.ts +39 -0
  77. package/src/PortraitTiles/portraitColour.ts +67 -0
  78. package/src/RadioGroup/RadioGroup.stories.tsx +39 -0
  79. package/src/RouterLink/RouterLink.mdx +53 -16
  80. package/src/categoricalStyles.ts +57 -0
  81. package/src/index.ts +12 -0
  82. package/src/reactRouter/ReactRouterAdapter.stories.tsx +120 -0
  83. package/src/reactRouter/ReactRouterAdapter.test.tsx +92 -0
  84. package/src/reactRouter/ReactRouterAdapter.tsx +66 -0
  85. package/src/reactRouter/index.ts +14 -8
  86. package/src/sourceRules.test.ts +19 -3
  87. package/src/tileStyles.ts +30 -1
@@ -1,6 +1,20 @@
1
- import type { ControlSize } from "@charcuterie/tokens";
1
+ import type { CategoricalIndex, ControlSize } from "@charcuterie/tokens";
2
2
  import type { ReactNode } from "react";
3
3
  export type ActionTileItem = {
4
+ /**
5
+ * Which of the ten hues this tile wears, overriding the one its
6
+ * position would have given it.
7
+ *
8
+ * Reach for it when a tile's colour means something outside the
9
+ * set — a destination that is red everywhere else in the app, a
10
+ * pair that must not drift apart when a third tile is inserted
11
+ * between them. An overridden tile keeps its hue and the rest go
12
+ * on walking the palette in order, so naming one does not force
13
+ * naming all of them.
14
+ *
15
+ * Ignored when the set is `accent="none"`.
16
+ */
17
+ categorical?: CategoricalIndex;
4
18
  /**
5
19
  * A line of help under the name. Drawn one step down the type ramp
6
20
  * in `content-muted`, **inside** the tile — so `getByRole` computes
@@ -20,10 +34,15 @@ export type ActionTileItem = {
20
34
  */
21
35
  href?: string;
22
36
  /**
23
- * Leading content, above the name. A `Badge`, an app's own icon, a
24
- * count.
37
+ * Leading content, drawn **beside** the name rather than above it.
38
+ * A glyph, a `Badge`, a count.
25
39
  *
26
- * `aria-hidden` by construction: the name it sits above is inside
40
+ * Takes the tile's hue, so an app passes a `stroke="currentColor"`
41
+ * path and states no colour of its own. Size is the app's: around
42
+ * 22px at `md` and 30px at `lg` reads the way the fleet's
43
+ * hand-rolled tool cards did.
44
+ *
45
+ * `aria-hidden` by construction: the name it sits beside is inside
27
46
  * the same tile, so an announced icon is the label read twice.
28
47
  */
29
48
  icon?: ReactNode;
@@ -40,7 +59,22 @@ export type ActionTileItem = {
40
59
  */
41
60
  value: string;
42
61
  };
62
+ /**
63
+ * Where a tile's colour comes from.
64
+ *
65
+ * - `auto` — the set walks the ten-hue categorical palette in
66
+ * order, so eight tiles get eight colours and no call site does
67
+ * any work. An item may still name its own with `categorical`.
68
+ * - `none` — no bar, no hue, and the neutral `border-strong`
69
+ * hover every other box uses.
70
+ */
71
+ export type ActionTilesAccent = "auto" | "none";
43
72
  export type ActionTilesProps = {
73
+ /**
74
+ * Whether the set colours itself. `auto` by default — see
75
+ * {@link ActionTilesAccent}.
76
+ */
77
+ accent?: ActionTilesAccent;
44
78
  className?: string;
45
79
  items: readonly ActionTileItem[];
46
80
  /** The set's accessible name. Required. */
@@ -59,9 +93,9 @@ export type ActionTilesProps = {
59
93
  size?: ControlSize;
60
94
  };
61
95
  /**
62
- * A set of ACTIONS drawn as tiles — a bordered card carrying a name
63
- * and a line of help, in a grid that gains columns with its
64
- * container.
96
+ * A set of ACTIONS drawn as tiles — a bordered card carrying a
97
+ * colour bar, an icon beside its name, and a line of help, in a
98
+ * grid that gains columns with its container.
65
99
  *
66
100
  * This is `RadioGroup`'s `itemShape="tile"` with the radio taken
67
101
  * away. The
@@ -90,6 +124,30 @@ export type ActionTilesProps = {
90
124
  * why QueuePilot's "Queue type" chooser is here and its Tonight
91
125
  * activity filter is not.
92
126
  *
127
+ * ### The colour is the point, and it is not decoration
128
+ *
129
+ * The first version of this component was neutral, and the owner's
130
+ * verdict on it was *"very boring, not colorful"*. Three apps had
131
+ * already answered the question themselves — mux-magic, Gallery
132
+ * Downloader and points-market all colour their tiles by hand — so
133
+ * the paint is now the library's and every one of them can stop.
134
+ *
135
+ * A tile wears a **bar down its leading edge**, drawn by `Card`'s
136
+ * accent-edge pseudo-element so a tile and a card on the same page
137
+ * are the same bar rather than two that nearly match. Its icon takes
138
+ * the same hue, and the box hovers in it too. The hue comes from the
139
+ * ten-wide categorical palette, taken **in order**, which is why a
140
+ * set of eight needs no colour props at all. See
141
+ * [the tile paint record](../../../../docs/decisions/2026-09-02-an-action-tile-is-coloured-and-the-icon-sits-beside-the-name.md)
142
+ * for the four other paints that were drawn and rejected.
143
+ *
144
+ * `categorical` and not `intent`, for the reason
145
+ * [`cardAccentEdge.ts`](../Card/cardAccentEdge.ts) gives: an intent
146
+ * is a *claim* — `danger` says what happens if you press the thing —
147
+ * and "Builder" beside "Jobs" is making no claim about either. Ten
148
+ * numbered hues say only "these are different from each other",
149
+ * which is exactly what a tile set means.
150
+ *
93
151
  * ### Why this is not a `Button` with a `className`
94
152
  *
95
153
  * A `Button` is sized by `h-(--control-height-md)` and carries **no
@@ -111,5 +169,5 @@ export type ActionTilesProps = {
111
169
  * true, so imposing that model would only cost a keyboard user the
112
170
  * ability to Tab to the second tile.
113
171
  */
114
- export declare const ActionTiles: ({ className, items, label, minTileInlineSize, newTabLabel, onChoose, size, }: ActionTilesProps) => ReactNode;
172
+ export declare const ActionTiles: ({ accent, className, items, label, minTileInlineSize, newTabLabel, onChoose, size, }: ActionTilesProps) => ReactNode;
115
173
  //# sourceMappingURL=ActionTiles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ActionTiles.d.ts","sourceRoot":"","sources":["../../src/ActionTiles/ActionTiles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAiBrD,MAAM,MAAM,cAAc,GAAG;IAC3B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,uEAAuE;IACvE,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,SAAS,CAAA;IAChB;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,SAAS,cAAc,EAAE,CAAA;IAChC,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,WAAW,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AACH,eAAO,MAAM,WAAW,GAAI,8EAQzB,gBAAgB,KAAG,SAwJrB,CAAA"}
1
+ {"version":3,"file":"ActionTiles.d.ts","sourceRoot":"","sources":["../../src/ActionTiles/ActionTiles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,WAAW,EACZ,MAAM,qBAAqB,CAAA;AAK5B,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAwBrD,MAAM,MAAM,cAAc,GAAG;IAC3B;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAA;IAC9B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;;;;;;;OAWG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,uEAAuE;IACvE,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,KAAK,EAAE,SAAS,CAAA;IAChB;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,MAAM,CAAA;AAE/C,MAAM,MAAM,gBAAgB,GAAG;IAC7B;;;OAGG;IACH,MAAM,CAAC,EAAE,iBAAiB,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,SAAS,cAAc,EAAE,CAAA;IAChC,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAA;IACb;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,WAAW,CAAA;CACnB,CAAA;AA4CD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4EG;AACH,eAAO,MAAM,WAAW,GAAI,sFASzB,gBAAgB,KAAG,SAwMrB,CAAA"}
@@ -1,14 +1,50 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { CATEGORICAL_INDEX_COUNT, CATEGORICAL_INDEXES, } from "@charcuterie/tokens";
3
+ import { getAccentEdgeClassName } from "../Card/cardAccentEdge.js";
4
+ import { CATEGORICAL_CONTENT_CLASS, CATEGORICAL_HOVER_BORDER_CLASS, CATEGORICAL_HOVER_CLASS, } from "../categoricalStyles.js";
2
5
  import { FOCUS_RING_CLASS } from "../intentStyles.js";
3
6
  import { useRouterLink } from "../RouterLink/RouterLinkProvider.js";
4
7
  import { getIsRoutedHref } from "../RouterLink/routerLink.js";
5
- import { TILE_BOX_CLASS, TILE_COLUMNS_CLASS, TILE_GAP_CLASS, TILE_HINT_TEXT_CLASS, TILE_MIN_INLINE_SIZE_PROPERTY, TILE_PADDING_CLASS, TILE_TEXT_SIZE_CLASS, } from "../tileStyles.js";
8
+ import { TILE_ACCENT_EDGE_PADDING_CLASS, TILE_BOX_CLASS, TILE_COLUMNS_CLASS, TILE_GAP_CLASS, TILE_HINT_TEXT_CLASS, TILE_MIN_INLINE_SIZE_PROPERTY, TILE_PADDING_CLASS, TILE_TEXT_SIZE_CLASS, } from "../tileStyles.js";
6
9
  import { toClassName } from "../toClassName.js";
7
10
  import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.js";
8
11
  /**
9
- * A set of ACTIONS drawn as tiles — a bordered card carrying a name
10
- * and a line of help, in a grid that gains columns with its
11
- * container.
12
+ * The gap between the icon and the name it sits beside.
13
+ *
14
+ * Local to this component rather than in `tileStyles.ts`, because a
15
+ * radio tile's inner layout is a control beside a label and has no
16
+ * head row to space. The shared file owns the BOX; this is what goes
17
+ * in it.
18
+ */
19
+ const HEAD_GAP_CLASS = {
20
+ sm: "gap-2",
21
+ md: "gap-2.5",
22
+ lg: "gap-3",
23
+ };
24
+ /** The gap between the head row and the hint under it. */
25
+ const STACK_GAP_CLASS = {
26
+ sm: "gap-1",
27
+ md: "gap-1",
28
+ lg: "gap-2",
29
+ };
30
+ /**
31
+ * The hue a tile wears when it did not name one: its position in
32
+ * the set, wrapping at ten.
33
+ *
34
+ * Position rather than `getCategoricalIndex(value)`. That helper
35
+ * hashes a key so a row keeps its colour forever across a list
36
+ * nobody controls the order of — right for Docket's labels, wrong
37
+ * here, because a hash of two tile values can collide and hand a
38
+ * two-tile set the same colour twice. A tile set is short, ordered,
39
+ * and written out in the source; the eye reads it as a row, so the
40
+ * palette is walked as one.
41
+ */
42
+ const getTileCategorical = (item, position) => item.categorical ??
43
+ CATEGORICAL_INDEXES[position % CATEGORICAL_INDEX_COUNT];
44
+ /**
45
+ * A set of ACTIONS drawn as tiles — a bordered card carrying a
46
+ * colour bar, an icon beside its name, and a line of help, in a
47
+ * grid that gains columns with its container.
12
48
  *
13
49
  * This is `RadioGroup`'s `itemShape="tile"` with the radio taken
14
50
  * away. The
@@ -37,6 +73,30 @@ import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.js";
37
73
  * why QueuePilot's "Queue type" chooser is here and its Tonight
38
74
  * activity filter is not.
39
75
  *
76
+ * ### The colour is the point, and it is not decoration
77
+ *
78
+ * The first version of this component was neutral, and the owner's
79
+ * verdict on it was *"very boring, not colorful"*. Three apps had
80
+ * already answered the question themselves — mux-magic, Gallery
81
+ * Downloader and points-market all colour their tiles by hand — so
82
+ * the paint is now the library's and every one of them can stop.
83
+ *
84
+ * A tile wears a **bar down its leading edge**, drawn by `Card`'s
85
+ * accent-edge pseudo-element so a tile and a card on the same page
86
+ * are the same bar rather than two that nearly match. Its icon takes
87
+ * the same hue, and the box hovers in it too. The hue comes from the
88
+ * ten-wide categorical palette, taken **in order**, which is why a
89
+ * set of eight needs no colour props at all. See
90
+ * [the tile paint record](../../../../docs/decisions/2026-09-02-an-action-tile-is-coloured-and-the-icon-sits-beside-the-name.md)
91
+ * for the four other paints that were drawn and rejected.
92
+ *
93
+ * `categorical` and not `intent`, for the reason
94
+ * [`cardAccentEdge.ts`](../Card/cardAccentEdge.ts) gives: an intent
95
+ * is a *claim* — `danger` says what happens if you press the thing —
96
+ * and "Builder" beside "Jobs" is making no claim about either. Ten
97
+ * numbered hues say only "these are different from each other",
98
+ * which is exactly what a tile set means.
99
+ *
40
100
  * ### Why this is not a `Button` with a `className`
41
101
  *
42
102
  * A `Button` is sized by `h-(--control-height-md)` and carries **no
@@ -58,11 +118,15 @@ import { VisuallyHidden } from "../VisuallyHidden/VisuallyHidden.js";
58
118
  * true, so imposing that model would only cost a keyboard user the
59
119
  * ability to Tab to the second tile.
60
120
  */
61
- export const ActionTiles = ({ className, items, label, minTileInlineSize = 200, newTabLabel = "(opens in a new tab)", onChoose, size = "md", }) => {
121
+ export const ActionTiles = ({ accent = "auto", className, items, label, minTileInlineSize = 200, newTabLabel = "(opens in a new tab)", onChoose, size = "md", }) => {
62
122
  const RouterLink = useRouterLink();
63
123
  return (_jsx("div", { "aria-label": label, className: toClassName("grid", TILE_COLUMNS_CLASS, TILE_GAP_CLASS[size], className), role: "group", style: {
64
124
  [TILE_MIN_INLINE_SIZE_PROPERTY]: `${minTileInlineSize}px`,
65
- }, children: items.map(({ hint, href, icon, isDisabled = false, isExternal = false, label: tileLabel, value, }) => {
125
+ }, children: items.map((item, position) => {
126
+ const { hint, href, icon, isDisabled = false, isExternal = false, label: tileLabel, value, } = item;
127
+ const categorical = accent === "none"
128
+ ? null
129
+ : getTileCategorical(item, position);
66
130
  const tileClassName = toClassName(
67
131
  // `flex`, not `inline-flex`: a grid item stretches to its
68
132
  // track, and an inline box would sit at max-content
@@ -71,15 +135,28 @@ export const ActionTiles = ({ className, items, label, minTileInlineSize = 200,
71
135
  // min-content width — one unbroken token would otherwise
72
136
  // set the track's floor and shove the grid out of its
73
137
  // container.
74
- "flex min-w-0 flex-col gap-0.5 text-start wrap-anywhere transition-colors duration-(--duration-fast) ease-standard", TILE_BOX_CLASS, TILE_PADDING_CLASS[size], TILE_TEXT_SIZE_CLASS[size],
138
+ "flex min-w-0 flex-col text-start wrap-anywhere transition-colors duration-(--duration-fast) ease-standard", STACK_GAP_CLASS[size], TILE_BOX_CLASS, TILE_PADDING_CLASS[size], TILE_TEXT_SIZE_CLASS[size],
139
+ // The bar is an overlay and takes no space in the box
140
+ // model, so the tile makes room for it or the text lands
141
+ // on top of it.
142
+ categorical === null
143
+ ? null
144
+ : toClassName(getAccentEdgeClassName({ categorical }), TILE_ACCENT_EDGE_PADDING_CLASS[size]),
75
145
  // Disabled turns the whole tile down with `opacity-60`,
76
146
  // the same family treatment as `Checkbox`, `Switch` and
77
147
  // the radio tile, rather than fading the border to a
78
- // token that disappeared on a pale theme.
148
+ // token that disappeared on a pale theme. It also drops
149
+ // every hover class: a tile that cannot be pressed must
150
+ // not light up under the pointer.
79
151
  isDisabled
80
152
  ? "cursor-not-allowed opacity-60"
81
- : "cursor-pointer hover:border-border-strong", FOCUS_RING_CLASS);
82
- const content = (_jsxs(_Fragment, { children: [icon === undefined ? null : (_jsx("span", { "aria-hidden": true, className: "flex", children: icon })), _jsx("span", { className: "font-semibold text-content-primary", children: tileLabel }), hint === undefined ? null : (_jsx("span", { className: toClassName("font-normal text-content-muted", TILE_HINT_TEXT_CLASS[size]), children: hint })), isExternal && href !== undefined ? (_jsx(VisuallyHidden, { children: newTabLabel })) : null] }));
153
+ : toClassName("cursor-pointer", categorical === null
154
+ ? "hover:border-border-strong"
155
+ : toClassName(CATEGORICAL_HOVER_BORDER_CLASS[categorical], CATEGORICAL_HOVER_CLASS[categorical]
156
+ .ghost)), FOCUS_RING_CLASS);
157
+ const content = (_jsxs(_Fragment, { children: [_jsxs("span", { className: toClassName("flex min-w-0 items-center", HEAD_GAP_CLASS[size]), children: [icon === undefined ? null : (_jsx("span", { "aria-hidden": true, className: toClassName("flex shrink-0", categorical === null
158
+ ? null
159
+ : CATEGORICAL_CONTENT_CLASS[categorical]), children: icon })), _jsx("span", { className: "min-w-0 font-semibold text-content-primary", children: tileLabel })] }), hint === undefined ? null : (_jsx("span", { className: toClassName("font-normal text-content-muted", TILE_HINT_TEXT_CLASS[size]), children: hint })), isExternal && href !== undefined ? (_jsx(VisuallyHidden, { children: newTabLabel })) : null] }));
83
160
  if (href !== undefined && !isDisabled) {
84
161
  // An external destination or a fragment is never the
85
162
  // router's — pushing `https://…` or `#credits` onto the
@@ -1 +1 @@
1
- {"version":3,"file":"ActionTiles.js","sourceRoot":"","sources":["../../src/ActionTiles/ActionTiles.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ActionTiles.js","sourceRoot":"","sources":["../../src/ActionTiles/ActionTiles.tsx"],"names":[],"mappings":"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"}
@@ -2,16 +2,6 @@ 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;
15
5
  };
16
6
  /**
17
7
  * The `<main>` landmark and the content column.
@@ -102,18 +92,27 @@ export type MainProps = ComponentPropsWithRef<"main"> & {
102
92
  * therefore lands at the top of a list the reader had scrolled
103
93
  * half-way down, and no browser setting changes that.
104
94
  *
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.
95
+ * So this element remembers it, and **there is no prop for it**.
96
+ * The history entry arrives on `ScrollMemoryContext`, which
97
+ * `ReactRouterAdapter` puts at the app root:
108
98
  *
109
99
  * ```tsx
110
- * const location = useLocation()
111
- *
112
- * <Main scrollKey={location.key}>
113
- * <Outlet />
114
- * </Main>
100
+ * <BrowserRouter>
101
+ * <ReactRouterAdapter>
102
+ * <Routes>…</Routes>
103
+ * </ReactRouterAdapter>
104
+ * </BrowserRouter>
115
105
  * ```
116
106
  *
107
+ * It was a prop for one release — `scrollKey` — and that release is
108
+ * the argument against a prop. One app wired it; the other three
109
+ * carried on losing the reader's place with the library sitting
110
+ * right there supporting it. Render no provider and nothing is
111
+ * remembered, which is what an app with no router should get.
112
+ *
113
+ * `useScrollMemory.ts` holds the mechanism and `scrollMemory.ts`
114
+ * the reasoning.
115
+ *
117
116
  * ## Two more things it does, both one line
118
117
  *
119
118
  * `tabIndex={-1}` so `Shell`'s skip link moves **focus** and not
@@ -143,5 +142,5 @@ export type MainProps = ComponentPropsWithRef<"main"> & {
143
142
  * wrap, so a wide one goes in an `overflow-x-auto` wrapper, which
144
143
  * `Main.mdx` shows.
145
144
  */
146
- export declare const Main: ({ children, className, contentWidth, id, ref, scrollKey, ...mainProps }: MainProps) => ReactNode;
145
+ export declare const Main: ({ children, className, contentWidth, id, ref, ...mainProps }: MainProps) => ReactNode;
147
146
  //# sourceMappingURL=Main.d.ts.map
@@ -1 +1 @@
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"}
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;AAMjC,MAAM,MAAM,SAAS,GAAG,qBAAqB,CAAC,MAAM,CAAC,GAAG;IACtD,YAAY,CAAC,EAAE,YAAY,CAAA;CAC5B,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0IG;AACH,eAAO,MAAM,IAAI,GAAI,8DAOlB,SAAS,KAAG,SA0Dd,CAAA"}
package/dist/Main/Main.js CHANGED
@@ -4,6 +4,7 @@ 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 { ScrollMemoryContext } from "./scrollMemoryContext.js";
7
8
  import { useScrollMemory } from "./useScrollMemory.js";
8
9
  /**
9
10
  * The `<main>` landmark and the content column.
@@ -94,18 +95,27 @@ import { useScrollMemory } from "./useScrollMemory.js";
94
95
  * therefore lands at the top of a list the reader had scrolled
95
96
  * half-way down, and no browser setting changes that.
96
97
  *
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.
98
+ * So this element remembers it, and **there is no prop for it**.
99
+ * The history entry arrives on `ScrollMemoryContext`, which
100
+ * `ReactRouterAdapter` puts at the app root:
100
101
  *
101
102
  * ```tsx
102
- * const location = useLocation()
103
- *
104
- * <Main scrollKey={location.key}>
105
- * <Outlet />
106
- * </Main>
103
+ * <BrowserRouter>
104
+ * <ReactRouterAdapter>
105
+ * <Routes>…</Routes>
106
+ * </ReactRouterAdapter>
107
+ * </BrowserRouter>
107
108
  * ```
108
109
  *
110
+ * It was a prop for one release — `scrollKey` — and that release is
111
+ * the argument against a prop. One app wired it; the other three
112
+ * carried on losing the reader's place with the library sitting
113
+ * right there supporting it. Render no provider and nothing is
114
+ * remembered, which is what an app with no router should get.
115
+ *
116
+ * `useScrollMemory.ts` holds the mechanism and `scrollMemory.ts`
117
+ * the reasoning.
118
+ *
109
119
  * ## Two more things it does, both one line
110
120
  *
111
121
  * `tabIndex={-1}` so `Shell`'s skip link moves **focus** and not
@@ -135,9 +145,10 @@ import { useScrollMemory } from "./useScrollMemory.js";
135
145
  * wrap, so a wide one goes in an `overflow-x-auto` wrapper, which
136
146
  * `Main.mdx` shows.
137
147
  */
138
- export const Main = ({ children, className, contentWidth, id, ref, scrollKey, ...mainProps }) => {
148
+ export const Main = ({ children, className, contentWidth, id, ref, ...mainProps }) => {
139
149
  const shell = useContext(ShellContext);
140
- const setScrollport = useScrollMemory(scrollKey);
150
+ const scrollEntry = useContext(ScrollMemoryContext);
151
+ const setScrollport = useScrollMemory(scrollEntry);
141
152
  // Always called, never conditionally: a `Main` outside a `Shell`
142
153
  // still needs an id an app can point its own skip link at.
143
154
  const fallbackId = useUniqueId();
@@ -1 +1 @@
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"}
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,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAMtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0IG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAAC,EACnB,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,EAAE,EACF,GAAG,EACH,GAAG,SAAS,EACF,EAAa,EAAE;IACzB,MAAM,KAAK,GAAG,UAAU,CAAC,YAAY,CAAC,CAAA;IACtC,MAAM,WAAW,GAAG,UAAU,CAAC,mBAAmB,CAAC,CAAA;IACnD,MAAM,aAAa,GAAG,eAAe,CAAC,WAAW,CAAC,CAAA;IAElD,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,38 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ScrollEntry } from "./scrollMemory.ts";
3
+ export type ScrollMemoryProviderProps = {
4
+ children?: ReactNode;
5
+ /**
6
+ * The history entry the page is showing, and the path it belongs
7
+ * to. Under react-router both come straight off `useLocation()`.
8
+ */
9
+ entry: ScrollEntry;
10
+ };
11
+ /**
12
+ * Tell every `Main` below this which history entry it is showing,
13
+ * so Back and Forward return the reader to where they were.
14
+ *
15
+ * An app that uses react-router should not render this directly —
16
+ * `ReactRouterAdapter` in the `./react-router` subpath wires it,
17
+ * along with the link seam, in one component at the root. This is
18
+ * the router-free half, for an app on a different router:
19
+ *
20
+ * ```tsx
21
+ * const [location] = useLocation() // wouter, say
22
+ *
23
+ * <ScrollMemoryProvider
24
+ * entry={{ key: historyKey, path: location }}
25
+ * >
26
+ * <App />
27
+ * </ScrollMemoryProvider>
28
+ * ```
29
+ *
30
+ * **`key` must identify the history entry, not the URL.** Two visits
31
+ * to the same list are two entries and the reader may leave them at
32
+ * different offsets; a key derived from the path would give the
33
+ * second visit the first one's position and could not tell Back from
34
+ * Forward. A router that exposes no such key can pass a counter it
35
+ * increments on every push and restores on every pop.
36
+ */
37
+ export declare const ScrollMemoryProvider: ({ children, entry, }: ScrollMemoryProviderProps) => ReactNode;
38
+ //# sourceMappingURL=ScrollMemoryProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScrollMemoryProvider.d.ts","sourceRoot":"","sources":["../../src/Main/ScrollMemoryProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAGpD,MAAM,MAAM,yBAAyB,GAAG;IACtC,QAAQ,CAAC,EAAE,SAAS,CAAA;IACpB;;;OAGG;IACH,KAAK,EAAE,WAAW,CAAA;CACnB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,oBAAoB,GAAI,sBAGlC,yBAAyB,KAAG,SAI9B,CAAA"}
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { ScrollMemoryContext } from "./scrollMemoryContext.js";
3
+ /**
4
+ * Tell every `Main` below this which history entry it is showing,
5
+ * so Back and Forward return the reader to where they were.
6
+ *
7
+ * An app that uses react-router should not render this directly —
8
+ * `ReactRouterAdapter` in the `./react-router` subpath wires it,
9
+ * along with the link seam, in one component at the root. This is
10
+ * the router-free half, for an app on a different router:
11
+ *
12
+ * ```tsx
13
+ * const [location] = useLocation() // wouter, say
14
+ *
15
+ * <ScrollMemoryProvider
16
+ * entry={{ key: historyKey, path: location }}
17
+ * >
18
+ * <App />
19
+ * </ScrollMemoryProvider>
20
+ * ```
21
+ *
22
+ * **`key` must identify the history entry, not the URL.** Two visits
23
+ * to the same list are two entries and the reader may leave them at
24
+ * different offsets; a key derived from the path would give the
25
+ * second visit the first one's position and could not tell Back from
26
+ * Forward. A router that exposes no such key can pass a counter it
27
+ * increments on every push and restores on every pop.
28
+ */
29
+ export const ScrollMemoryProvider = ({ children, entry, }) => (_jsx(ScrollMemoryContext.Provider, { value: entry, children: children }));
30
+ //# sourceMappingURL=ScrollMemoryProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ScrollMemoryProvider.js","sourceRoot":"","sources":["../../src/Main/ScrollMemoryProvider.tsx"],"names":[],"mappings":";AAGA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAW9D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,EACnC,QAAQ,EACR,KAAK,GACqB,EAAa,EAAE,CAAC,CAC1C,KAAC,mBAAmB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YACvC,QAAQ,GACoB,CAChC,CAAA"}
@@ -1,40 +1,66 @@
1
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:
2
+ * What the browser will not remember for you, and the one thing it
3
+ * would never have got wrong.
4
+ *
5
+ * `Shell` gives `<main>` the page's only vertical scrollport, so the
6
+ * header and the rails stay put while the content moves — that is
7
+ * what `Shell.test.tsx` asserts. It costs exactly one thing, and
8
+ * this module is it. A browser remembers **the document scroller's**
9
+ * offset per history entry and puts it back on Back. It does not do
10
+ * that for an arbitrary element with `overflow-y: auto`, and it does
11
+ * not do it at all for a same-document `pushState` navigation, which
12
+ * is every link press in a single-page app:
19
13
  *
20
14
  * > *"If I scroll, click on an item, then go back, it resets me to
21
15
  * > the top. […] It should remember where the scroll position was
22
16
  * > previously. Isn't this handled by Chrome already?"*
23
17
  *
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.
18
+ * ## An entry remembers its PATH as well as its offset
19
+ *
20
+ * Not decoration — the second field is what keeps this from being a
21
+ * worse bug than the one it fixes. A history entry is not a page. A
22
+ * filter chip, an expanded group, a selected tab: each writes a
23
+ * search param, and **`setSearchParams` navigates**, so each mints a
24
+ * history entry the memory has never seen. Treating an unseen entry
25
+ * as "start at the top" therefore threw the reader to the top of the
26
+ * list every time they opened a group — which is exactly what
27
+ * shipped, and what the path comparison exists to prevent.
28
+ *
29
+ * So the rule is about the PAGE, not the entry:
30
+ *
31
+ * - **An entry we have seen** → its remembered offset. This is the
32
+ * whole feature: Back and Forward land where you left them.
33
+ * - **A new entry on the SAME path** → leave the scrollport alone.
34
+ * A search param changed; the reader did not go anywhere. This is
35
+ * also precisely what the app did before this memory existed, so
36
+ * it can never be a regression.
37
+ * - **A new entry on a DIFFERENT path** → the top. A page you have
38
+ * not seen starts at its beginning, which is what a browser does
39
+ * for a document.
40
+ *
41
+ * ## The offsets are in memory, never `sessionStorage`
42
+ *
43
+ * A history key survives a reload; the DOM it described does not.
44
+ * The app re-fetches, re-measures and often re-orders, so a pixel
45
+ * offset from before the reload points at a different row. Restoring
46
+ * a stale number is worse than restoring nothing, because it looks
47
+ * deliberate.
30
48
  */
31
- export declare const rememberScrollOffset: (key: string, offset: number) => void;
49
+ export type ScrollEntry = {
50
+ /** The history entry — react-router's `useLocation().key`. */
51
+ key: string;
52
+ /** The page it belongs to — `useLocation().pathname`. */
53
+ path: string;
54
+ };
55
+ export declare const rememberScrollOffset: (entry: ScrollEntry, offset: number) => void;
32
56
  /**
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.
57
+ * Where the scrollport should go for this entry, or `null` for
58
+ * **leave it alone** — which is a third answer rather than a missing
59
+ * one, and the difference between a search param and a page.
36
60
  */
37
- export declare const recallScrollOffset: (key: string) => number;
61
+ export declare const resolveScrollOffset: (entry: ScrollEntry) => number | null;
62
+ /** Called by the hook once it owns the scrollport for this entry. */
63
+ export declare const enterScrollEntry: (entry: ScrollEntry) => void;
38
64
  /** Test-only. Module state outlives a mount; a test must not. */
39
65
  export declare const forgetScrollOffsets: () => void;
40
66
  //# sourceMappingURL=scrollMemory.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"scrollMemory.d.ts","sourceRoot":"","sources":["../../src/Main/scrollMemory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAUH,MAAM,MAAM,WAAW,GAAG;IACxB,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAA;IACX,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAkBD,eAAO,MAAM,oBAAoB,GAC/B,OAAO,WAAW,EAClB,QAAQ,MAAM,SAkBf,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,OAAO,WAAW,kBAYrD,CAAA;AAED,qEAAqE;AACrE,eAAO,MAAM,gBAAgB,GAAI,OAAO,WAAW,SAElD,CAAA;AAED,iEAAiE;AACjE,eAAO,MAAM,mBAAmB,YAG/B,CAAA"}