@charcuterie/ui 4.2.0 → 4.3.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/dist/ActionTiles/ActionTiles.d.ts +6 -4
- package/dist/ActionTiles/ActionTiles.d.ts.map +1 -1
- package/dist/ActionTiles/ActionTiles.js +13 -4
- package/dist/ActionTiles/ActionTiles.js.map +1 -1
- package/dist/Main/Main.d.ts +33 -0
- package/dist/Main/Main.d.ts.map +1 -1
- package/dist/Main/Main.js +34 -1
- package/dist/Main/Main.js.map +1 -1
- package/dist/PortraitTiles/PortraitTiles.d.ts +4 -2
- package/dist/PortraitTiles/PortraitTiles.d.ts.map +1 -1
- package/dist/PortraitTiles/PortraitTiles.js +2 -2
- package/dist/PortraitTiles/PortraitTiles.js.map +1 -1
- package/package.json +2 -2
- package/src/ActionTiles/ActionTiles.mdx +8 -2
- package/src/ActionTiles/ActionTiles.stories.tsx +3 -2
- package/src/ActionTiles/ActionTiles.test.tsx +6 -5
- package/src/ActionTiles/ActionTiles.tsx +15 -8
- package/src/Main/Main.mdx +30 -1
- package/src/Main/Main.stories.tsx +39 -7
- package/src/Main/Main.test.tsx +84 -1
- package/src/Main/Main.tsx +34 -1
- package/src/PortraitTiles/PortraitTiles.tsx +7 -5
- package/src/Shell/Shell.stories.tsx +11 -0
- package/src/shell.storyHelpers.tsx +67 -19
|
@@ -9,8 +9,8 @@ export type ActionTileItem = {
|
|
|
9
9
|
* set — a destination that is red everywhere else in the app, a
|
|
10
10
|
* pair that must not drift apart when a third tile is inserted
|
|
11
11
|
* between them. An overridden tile keeps its hue and the rest go
|
|
12
|
-
* on walking
|
|
13
|
-
* naming all of them.
|
|
12
|
+
* on walking `CATEGORICAL_SEQUENCE` from their own positions, so
|
|
13
|
+
* naming one does not force naming all of them.
|
|
14
14
|
*
|
|
15
15
|
* Ignored when the set is `accent="none"`.
|
|
16
16
|
*/
|
|
@@ -136,8 +136,10 @@ export type ActionTilesProps = {
|
|
|
136
136
|
* accent-edge pseudo-element so a tile and a card on the same page
|
|
137
137
|
* are the same bar rather than two that nearly match. Its icon takes
|
|
138
138
|
* the same hue, and the box hovers in it too. The hue comes from the
|
|
139
|
-
* ten-wide categorical palette,
|
|
140
|
-
*
|
|
139
|
+
* ten-wide categorical palette, walked in `CATEGORICAL_SEQUENCE`
|
|
140
|
+
* rather than 1..10 — the ring is hue-ordered for the swatch
|
|
141
|
+
* picker's sake, so taking it in order would hand a two-tile set
|
|
142
|
+
* red and orange. A set of eight needs no colour props at all. See
|
|
141
143
|
* [the tile paint record](../../../../docs/decisions/2026-09-02-an-action-tile-is-coloured-and-the-icon-sits-beside-the-name.md)
|
|
142
144
|
* for the four other paints that were drawn and rejected.
|
|
143
145
|
*
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;AAiDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8EG;AACH,eAAO,MAAM,WAAW,GAAI,sFASzB,gBAAgB,KAAG,SAwMrB,CAAA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { CATEGORICAL_INDEX_COUNT,
|
|
2
|
+
import { CATEGORICAL_INDEX_COUNT, CATEGORICAL_SEQUENCE, } from "@charcuterie/tokens";
|
|
3
3
|
import { getAccentEdgeClassName } from "../Card/cardAccentEdge.js";
|
|
4
4
|
import { CATEGORICAL_CONTENT_CLASS, CATEGORICAL_HOVER_BORDER_CLASS, CATEGORICAL_HOVER_CLASS, } from "../categoricalStyles.js";
|
|
5
5
|
import { FOCUS_RING_CLASS } from "../intentStyles.js";
|
|
@@ -38,9 +38,16 @@ const STACK_GAP_CLASS = {
|
|
|
38
38
|
* two-tile set the same colour twice. A tile set is short, ordered,
|
|
39
39
|
* and written out in the source; the eye reads it as a row, so the
|
|
40
40
|
* palette is walked as one.
|
|
41
|
+
*
|
|
42
|
+
* `CATEGORICAL_SEQUENCE`, not `CATEGORICAL_INDEXES`. The ring is
|
|
43
|
+
* hue-ordered for the swatch picker's sake, so walking it in order
|
|
44
|
+
* hands a two-tile set the two hardest colours in the family to
|
|
45
|
+
* tell apart — 34 degrees, the tightest pair the palette has. The
|
|
46
|
+
* sequence visits the same ten in an order that keeps every
|
|
47
|
+
* neighbour 105 degrees or more away.
|
|
41
48
|
*/
|
|
42
49
|
const getTileCategorical = (item, position) => item.categorical ??
|
|
43
|
-
|
|
50
|
+
CATEGORICAL_SEQUENCE[position % CATEGORICAL_INDEX_COUNT];
|
|
44
51
|
/**
|
|
45
52
|
* A set of ACTIONS drawn as tiles — a bordered card carrying a
|
|
46
53
|
* colour bar, an icon beside its name, and a line of help, in a
|
|
@@ -85,8 +92,10 @@ const getTileCategorical = (item, position) => item.categorical ??
|
|
|
85
92
|
* accent-edge pseudo-element so a tile and a card on the same page
|
|
86
93
|
* are the same bar rather than two that nearly match. Its icon takes
|
|
87
94
|
* the same hue, and the box hovers in it too. The hue comes from the
|
|
88
|
-
* ten-wide categorical palette,
|
|
89
|
-
*
|
|
95
|
+
* ten-wide categorical palette, walked in `CATEGORICAL_SEQUENCE`
|
|
96
|
+
* rather than 1..10 — the ring is hue-ordered for the swatch
|
|
97
|
+
* picker's sake, so taking it in order would hand a two-tile set
|
|
98
|
+
* red and orange. A set of eight needs no colour props at all. See
|
|
90
99
|
* [the tile paint record](../../../../docs/decisions/2026-09-02-an-action-tile-is-coloured-and-the-icon-sits-beside-the-name.md)
|
|
91
100
|
* for the four other paints that were drawn and rejected.
|
|
92
101
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActionTiles.js","sourceRoot":"","sources":["../../src/ActionTiles/ActionTiles.tsx"],"names":[],"mappings":";AAIA,OAAO,EACL,uBAAuB,EACvB,
|
|
1
|
+
{"version":3,"file":"ActionTiles.js","sourceRoot":"","sources":["../../src/ActionTiles/ActionTiles.tsx"],"names":[],"mappings":"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"}
|
package/dist/Main/Main.d.ts
CHANGED
|
@@ -80,6 +80,39 @@ export type MainProps = ComponentPropsWithRef<"main"> & {
|
|
|
80
80
|
* an app's own fixed chrome belongs in `Shell`, beside `Main`
|
|
81
81
|
* rather than in it.
|
|
82
82
|
*
|
|
83
|
+
* ## `relative` — the scrollport has to be the containing block
|
|
84
|
+
*
|
|
85
|
+
* `<main>` is the page's only vertical scrollport, so everything
|
|
86
|
+
* in it is supposed to scroll *inside* it. An absolutely
|
|
87
|
+
* positioned descendant does not, unless this element is its
|
|
88
|
+
* containing block: with `position: static` here, the nearest
|
|
89
|
+
* positioned ancestor is `Shell`, which is **outside** the
|
|
90
|
+
* scrollport. The box then resolves against `Shell`, `<main>`'s
|
|
91
|
+
* `overflow-y` has no authority over it, and its static position —
|
|
92
|
+
* a couple of thousand pixels down a long document — becomes
|
|
93
|
+
* scrollable overflow on `documentElement`. The page grows to
|
|
94
|
+
* reach it and the reader gets a **second scrollbar** beside the
|
|
95
|
+
* one `<main>` already has.
|
|
96
|
+
*
|
|
97
|
+
* The box does not have to be exotic. Tailwind's `sr-only` is
|
|
98
|
+
* `position: absolute`, so the visually hidden
|
|
99
|
+
* `<span>(opens in a new tab)</span>` on one link in a document
|
|
100
|
+
* footer is enough. Measured on Folio at 1440x900: `<main>` was
|
|
101
|
+
* 835px tall and `document.documentElement.scrollHeight` read
|
|
102
|
+
* 2085, which is exactly where that span sat.
|
|
103
|
+
*
|
|
104
|
+
* `overflow-x: hidden` comes with it and is not decoration.
|
|
105
|
+
* `relative` moves every absolutely positioned descendant's
|
|
106
|
+
* containing block from `Shell` to here, which also moves what
|
|
107
|
+
* clips them **sideways**: `Shell`'s `overflow-x: clip` no longer
|
|
108
|
+
* reaches them, so a drawer parked at `translateX(110%)` inside
|
|
109
|
+
* `Main` would draw a horizontal scrollbar on the scrollport
|
|
110
|
+
* instead of being clipped. `hidden` rather than `clip` because
|
|
111
|
+
* this element already scrolls on the other axis — a `clip` beside
|
|
112
|
+
* an `auto` computes to `hidden` anyway, and writing what the
|
|
113
|
+
* browser will use is the honest spelling. `Shell`'s note on the
|
|
114
|
+
* clip explains the other half.
|
|
115
|
+
*
|
|
83
116
|
* ## It remembers where each history entry was scrolled to
|
|
84
117
|
*
|
|
85
118
|
* `Shell` gives this element the page's only vertical scrollport,
|
package/dist/Main/Main.d.ts.map
CHANGED
|
@@ -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;AAMjC,MAAM,MAAM,SAAS,GAAG,qBAAqB,CAAC,MAAM,CAAC,GAAG;IACtD,YAAY,CAAC,EAAE,YAAY,CAAA;CAC5B,CAAA;AAED
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2KG;AACH,eAAO,MAAM,IAAI,GAAI,8DAOlB,SAAS,KAAG,SA0Dd,CAAA"}
|
package/dist/Main/Main.js
CHANGED
|
@@ -83,6 +83,39 @@ import { useScrollMemory } from "./useScrollMemory.js";
|
|
|
83
83
|
* an app's own fixed chrome belongs in `Shell`, beside `Main`
|
|
84
84
|
* rather than in it.
|
|
85
85
|
*
|
|
86
|
+
* ## `relative` — the scrollport has to be the containing block
|
|
87
|
+
*
|
|
88
|
+
* `<main>` is the page's only vertical scrollport, so everything
|
|
89
|
+
* in it is supposed to scroll *inside* it. An absolutely
|
|
90
|
+
* positioned descendant does not, unless this element is its
|
|
91
|
+
* containing block: with `position: static` here, the nearest
|
|
92
|
+
* positioned ancestor is `Shell`, which is **outside** the
|
|
93
|
+
* scrollport. The box then resolves against `Shell`, `<main>`'s
|
|
94
|
+
* `overflow-y` has no authority over it, and its static position —
|
|
95
|
+
* a couple of thousand pixels down a long document — becomes
|
|
96
|
+
* scrollable overflow on `documentElement`. The page grows to
|
|
97
|
+
* reach it and the reader gets a **second scrollbar** beside the
|
|
98
|
+
* one `<main>` already has.
|
|
99
|
+
*
|
|
100
|
+
* The box does not have to be exotic. Tailwind's `sr-only` is
|
|
101
|
+
* `position: absolute`, so the visually hidden
|
|
102
|
+
* `<span>(opens in a new tab)</span>` on one link in a document
|
|
103
|
+
* footer is enough. Measured on Folio at 1440x900: `<main>` was
|
|
104
|
+
* 835px tall and `document.documentElement.scrollHeight` read
|
|
105
|
+
* 2085, which is exactly where that span sat.
|
|
106
|
+
*
|
|
107
|
+
* `overflow-x: hidden` comes with it and is not decoration.
|
|
108
|
+
* `relative` moves every absolutely positioned descendant's
|
|
109
|
+
* containing block from `Shell` to here, which also moves what
|
|
110
|
+
* clips them **sideways**: `Shell`'s `overflow-x: clip` no longer
|
|
111
|
+
* reaches them, so a drawer parked at `translateX(110%)` inside
|
|
112
|
+
* `Main` would draw a horizontal scrollbar on the scrollport
|
|
113
|
+
* instead of being clipped. `hidden` rather than `clip` because
|
|
114
|
+
* this element already scrolls on the other axis — a `clip` beside
|
|
115
|
+
* an `auto` computes to `hidden` anyway, and writing what the
|
|
116
|
+
* browser will use is the honest spelling. `Shell`'s note on the
|
|
117
|
+
* clip explains the other half.
|
|
118
|
+
*
|
|
86
119
|
* ## It remembers where each history entry was scrolled to
|
|
87
120
|
*
|
|
88
121
|
* `Shell` gives this element the page's only vertical scrollport,
|
|
@@ -155,7 +188,7 @@ export const Main = ({ children, className, contentWidth, id, ref, ...mainProps
|
|
|
155
188
|
const maxInlineSize = toMaxInlineSize(contentWidth ??
|
|
156
189
|
shell?.contentWidth ??
|
|
157
190
|
DEFAULT_CONTENT_WIDTH);
|
|
158
|
-
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,
|
|
191
|
+
return (_jsx("main", { ...mainProps, className: toClassName("charcuterie-scrollbar relative col-start-1 row-start-3 min-h-0 min-w-0 overflow-x-hidden overflow-y-auto md:col-start-2 md:row-start-2", className), id: id ?? shell?.mainId ?? fallbackId,
|
|
159
192
|
// A caller's `ref` is a subscription, not a value, so the
|
|
160
193
|
// scroll memory composes with it rather than replacing it.
|
|
161
194
|
//
|
package/dist/Main/Main.js.map
CHANGED
|
@@ -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,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AAMtD
|
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2KG;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,wIAAwI,EACxI,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"}
|
|
@@ -18,8 +18,10 @@ import type { ReactNode } from "react";
|
|
|
18
18
|
* palette hues cannot represent that, and swapping them in would
|
|
19
19
|
* have made the app disagree with the cards on the fridge.
|
|
20
20
|
*
|
|
21
|
-
* Neither is a default. A portrait that names no colour takes
|
|
22
|
-
*
|
|
21
|
+
* Neither is a default. A portrait that names no colour takes its
|
|
22
|
+
* position's entry in `CATEGORICAL_SEQUENCE` — the palette walked so
|
|
23
|
+
* neighbours contrast, rather than the hue-ordered ring, which would
|
|
24
|
+
* put red beside orange.
|
|
23
25
|
*/
|
|
24
26
|
type PortraitTileColour = {
|
|
25
27
|
categorical?: CategoricalIndex;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PortraitTiles.d.ts","sourceRoot":"","sources":["../../src/PortraitTiles/PortraitTiles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,WAAW,EACZ,MAAM,qBAAqB,CAAA;AAK5B,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAqBrD
|
|
1
|
+
{"version":3,"file":"PortraitTiles.d.ts","sourceRoot":"","sources":["../../src/PortraitTiles/PortraitTiles.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,WAAW,EACZ,MAAM,qBAAqB,CAAA;AAK5B,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAqBrD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,KAAK,kBAAkB,GACnB;IAAE,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,GACjD;IAAE,WAAW,CAAC,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAE1C,MAAM,MAAM,gBAAgB,GAAG,kBAAkB,GAAG;IAClD;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,yEAAyE;IACzE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,uEAAuE;IACvE,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,gBAAgB;IAChB,KAAK,EAAE,SAAS,CAAA;IAChB;;;;OAIG;IACH,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAA;AAE3D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,SAAS,gBAAgB,EAAE,CAAA;IAClC,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,mBAAmB,CAAA;IAC5B,wDAAwD;IACxD,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,IAAI,CAAC,EAAE,WAAW,CAAA;CACnB,CAAA;AAiLD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgEG;AACH,eAAO,MAAM,aAAa,GAAI,sFAS3B,kBAAkB,KAAG,SA8MvB,CAAA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { CATEGORICAL_INDEX_COUNT,
|
|
2
|
+
import { CATEGORICAL_INDEX_COUNT, CATEGORICAL_SEQUENCE, } from "@charcuterie/tokens";
|
|
3
3
|
import { CATEGORICAL_APPEARANCE_CLASS, CATEGORICAL_CONTENT_CLASS, CATEGORICAL_HOVER_BORDER_CLASS, CATEGORICAL_RING_CLASS, } from "../categoricalStyles.js";
|
|
4
4
|
import { FOCUS_RING_CLASS } from "../intentStyles.js";
|
|
5
5
|
import { useRouterLink } from "../RouterLink/RouterLinkProvider.js";
|
|
@@ -133,7 +133,7 @@ const GAP_CLASS = {
|
|
|
133
133
|
*/
|
|
134
134
|
const getPortraitCategorical = (item, position) => item.color === undefined
|
|
135
135
|
? (item.categorical ??
|
|
136
|
-
|
|
136
|
+
CATEGORICAL_SEQUENCE[position % CATEGORICAL_INDEX_COUNT])
|
|
137
137
|
: null;
|
|
138
138
|
/**
|
|
139
139
|
* The classes that read the four custom properties, spelled whole.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PortraitTiles.js","sourceRoot":"","sources":["../../src/PortraitTiles/PortraitTiles.tsx"],"names":[],"mappings":";AAIA,OAAO,EACL,uBAAuB,EACvB,
|
|
1
|
+
{"version":3,"file":"PortraitTiles.js","sourceRoot":"","sources":["../../src/PortraitTiles/PortraitTiles.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@charcuterie/ui",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "The Charcuterie components. Tokens for look, @charcuterie/logic for state, ARIA that agents can actually drive.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@charcuterie/logic": "2.3.0",
|
|
57
|
-
"@charcuterie/tokens": "1.
|
|
57
|
+
"@charcuterie/tokens": "1.8.0",
|
|
58
58
|
"@floating-ui/react": "^0.27.20",
|
|
59
59
|
"@tanstack/react-virtual": "^3.13.12",
|
|
60
60
|
"tailwind-merge": "^3.6.0"
|
|
@@ -30,8 +30,14 @@ the library's and every one of them can stop.
|
|
|
30
30
|
|
|
31
31
|
A tile wears a **bar down its leading edge**, its icon takes the same hue, and the box hovers
|
|
32
32
|
in it. The hue comes from the ten-wide
|
|
33
|
-
[categorical palette](/docs/foundations-colour--docs)
|
|
34
|
-
|
|
33
|
+
[categorical palette](/docs/foundations-colour--docs), which is why a set of five needs no
|
|
34
|
+
colour props at all.
|
|
35
|
+
|
|
36
|
+
It is walked in **`CATEGORICAL_SEQUENCE`**, not `1..10`. The ring itself is hue-ordered
|
|
37
|
+
because a swatch picker wants a spectrum to scan, and that is exactly what makes walking it in
|
|
38
|
+
order the worst assignment for a set: ring neighbours are 34–41° apart, the tightest pair the
|
|
39
|
+
palette has, so a two-tile set got red and orange every time. The sequence visits the same ten
|
|
40
|
+
and holds every neighbour 105° or more away.
|
|
35
41
|
|
|
36
42
|
<Canvas of={ActionTilesStories.AutoHues} />
|
|
37
43
|
|
|
@@ -59,8 +59,9 @@ const START_ITEMS: ActionTileItem[] = [
|
|
|
59
59
|
]
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* Enough tiles to see the palette walk. Ten hues
|
|
63
|
-
*
|
|
62
|
+
* Enough tiles to see the palette walk. Ten hues in
|
|
63
|
+
* `CATEGORICAL_SEQUENCE` order — every neighbouring pair a third of
|
|
64
|
+
* the wheel apart — and not one colour prop between them.
|
|
64
65
|
*/
|
|
65
66
|
const LIBRARY_ITEMS: ActionTileItem[] = [
|
|
66
67
|
{
|
|
@@ -358,8 +358,8 @@ test("a coloured tile makes room for its bar, and only on the leading side", asy
|
|
|
358
358
|
})
|
|
359
359
|
|
|
360
360
|
/**
|
|
361
|
-
* Ten hues
|
|
362
|
-
* no colour props at all.
|
|
361
|
+
* Ten hues walked in `CATEGORICAL_SEQUENCE`, which is what makes a
|
|
362
|
+
* five-tile set need no colour props at all.
|
|
363
363
|
*
|
|
364
364
|
* Asserted through the pseudo-element's own `box-shadow`, because
|
|
365
365
|
* that is where the bar actually lives — a class-name assertion
|
|
@@ -416,9 +416,10 @@ test("a named hue survives a tile being inserted above it", async () => {
|
|
|
416
416
|
throw new Error("the auto-hue set drew no second tile")
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
-
// In the auto set this tile is position 1
|
|
420
|
-
// `categorical: 7` has to
|
|
421
|
-
// every test above still
|
|
419
|
+
// In the auto set this tile is position 1, so it wears the second
|
|
420
|
+
// entry of `CATEGORICAL_SEQUENCE`. Naming `categorical: 7` has to
|
|
421
|
+
// change it, or the prop does nothing and every test above still
|
|
422
|
+
// passes.
|
|
422
423
|
await expect(
|
|
423
424
|
getAccentEdgeLayer(
|
|
424
425
|
getComputedStyle(pinned, "::before").boxShadow,
|
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
} from "@charcuterie/tokens"
|
|
5
5
|
import {
|
|
6
6
|
CATEGORICAL_INDEX_COUNT,
|
|
7
|
-
|
|
7
|
+
CATEGORICAL_SEQUENCE,
|
|
8
8
|
} from "@charcuterie/tokens"
|
|
9
9
|
import type { CSSProperties, ReactNode } from "react"
|
|
10
10
|
|
|
@@ -39,8 +39,8 @@ export type ActionTileItem = {
|
|
|
39
39
|
* set — a destination that is red everywhere else in the app, a
|
|
40
40
|
* pair that must not drift apart when a third tile is inserted
|
|
41
41
|
* between them. An overridden tile keeps its hue and the rest go
|
|
42
|
-
* on walking
|
|
43
|
-
* naming all of them.
|
|
42
|
+
* on walking `CATEGORICAL_SEQUENCE` from their own positions, so
|
|
43
|
+
* naming one does not force naming all of them.
|
|
44
44
|
*
|
|
45
45
|
* Ignored when the set is `accent="none"`.
|
|
46
46
|
*/
|
|
@@ -157,15 +157,20 @@ const STACK_GAP_CLASS: Record<ControlSize, string> = {
|
|
|
157
157
|
* two-tile set the same colour twice. A tile set is short, ordered,
|
|
158
158
|
* and written out in the source; the eye reads it as a row, so the
|
|
159
159
|
* palette is walked as one.
|
|
160
|
+
*
|
|
161
|
+
* `CATEGORICAL_SEQUENCE`, not `CATEGORICAL_INDEXES`. The ring is
|
|
162
|
+
* hue-ordered for the swatch picker's sake, so walking it in order
|
|
163
|
+
* hands a two-tile set the two hardest colours in the family to
|
|
164
|
+
* tell apart — 34 degrees, the tightest pair the palette has. The
|
|
165
|
+
* sequence visits the same ten in an order that keeps every
|
|
166
|
+
* neighbour 105 degrees or more away.
|
|
160
167
|
*/
|
|
161
168
|
const getTileCategorical = (
|
|
162
169
|
item: ActionTileItem,
|
|
163
170
|
position: number,
|
|
164
171
|
): CategoricalIndex =>
|
|
165
172
|
item.categorical ??
|
|
166
|
-
|
|
167
|
-
position % CATEGORICAL_INDEX_COUNT
|
|
168
|
-
] as CategoricalIndex)
|
|
173
|
+
CATEGORICAL_SEQUENCE[position % CATEGORICAL_INDEX_COUNT]
|
|
169
174
|
|
|
170
175
|
/**
|
|
171
176
|
* A set of ACTIONS drawn as tiles — a bordered card carrying a
|
|
@@ -211,8 +216,10 @@ const getTileCategorical = (
|
|
|
211
216
|
* accent-edge pseudo-element so a tile and a card on the same page
|
|
212
217
|
* are the same bar rather than two that nearly match. Its icon takes
|
|
213
218
|
* the same hue, and the box hovers in it too. The hue comes from the
|
|
214
|
-
* ten-wide categorical palette,
|
|
215
|
-
*
|
|
219
|
+
* ten-wide categorical palette, walked in `CATEGORICAL_SEQUENCE`
|
|
220
|
+
* rather than 1..10 — the ring is hue-ordered for the swatch
|
|
221
|
+
* picker's sake, so taking it in order would hand a two-tile set
|
|
222
|
+
* red and orange. A set of eight needs no colour props at all. See
|
|
216
223
|
* [the tile paint record](../../../../docs/decisions/2026-09-02-an-action-tile-is-coloured-and-the-icon-sits-beside-the-name.md)
|
|
217
224
|
* for the four other paints that were drawn and rejected.
|
|
218
225
|
*
|
package/src/Main/Main.mdx
CHANGED
|
@@ -109,7 +109,9 @@ for a `getBoundingClientRect()` assertion to catch. That is why the gate asserts
|
|
|
109
109
|
`document.documentElement.scrollWidth`, and why `Main.test.tsx` pins the computed value.
|
|
110
110
|
|
|
111
111
|
That is the cheap half of the horizontal-scroll fix; the other halves are `minmax(0, 1fr)`
|
|
112
|
-
and `overflow-x: clip` on `Shell
|
|
112
|
+
and `overflow-x: clip` on `Shell`, plus `overflow-x: hidden` here for anything absolutely
|
|
113
|
+
positioned **inside** the scrollport — the next section says why that one cannot be left to
|
|
114
|
+
the frame.
|
|
113
115
|
|
|
114
116
|
A `<table>` is the one thing neither can save. Table columns have min-content widths that no
|
|
115
117
|
`overflow-wrap` reaches, so a wide table goes in a scroll container:
|
|
@@ -140,6 +142,33 @@ satisfy it and the choice is not a matter of taste:
|
|
|
140
142
|
`aria-label` on the container is worth it either way: it turns the scroller into a named
|
|
141
143
|
region, so the table is announced as a thing rather than as loose content.
|
|
142
144
|
|
|
145
|
+
## `relative`, and the second scrollbar
|
|
146
|
+
|
|
147
|
+
`<main>` is the page's only vertical scrollport, so the document itself must never scroll.
|
|
148
|
+
An absolutely positioned descendant breaks that unless `<main>` is its **containing block**.
|
|
149
|
+
With `position: static` the nearest positioned ancestor is `Shell`, which sits *outside* the
|
|
150
|
+
scrollport: the box resolves against the frame, `<main>`'s `overflow-y` has no authority over
|
|
151
|
+
it, and its static position — a couple of thousand pixels down a long document — lands as
|
|
152
|
+
scrollable overflow on `documentElement`. The page grows to reach it, and the reader gets a
|
|
153
|
+
**second scrollbar** beside the one `<main>` already has.
|
|
154
|
+
|
|
155
|
+
The box does not have to be exotic, which is what makes this hard to spot. Tailwind's
|
|
156
|
+
`sr-only` is `position: absolute`, so one visually hidden `(opens in a new tab)` span in a
|
|
157
|
+
document footer is enough. Measured on Folio at 1440x900 before this: `<main>` 835px tall,
|
|
158
|
+
`document.documentElement.scrollHeight` 2085 — the span's offset, exactly. Nothing on the
|
|
159
|
+
page looked wrong; there were simply two scrollbars.
|
|
160
|
+
|
|
161
|
+
`overflow-x: hidden` comes with the `relative` and is not decoration. Moving every
|
|
162
|
+
absolutely positioned descendant's containing block from `Shell` to `<main>` also moves what
|
|
163
|
+
clips them sideways, so `Shell`'s `overflow-x: clip` no longer reaches them — a drawer parked
|
|
164
|
+
at `translateX(110%)` **inside** `Main` would draw a horizontal scrollbar on the scrollport
|
|
165
|
+
instead of being clipped. `hidden` rather than `clip` because this element already scrolls on
|
|
166
|
+
the other axis: `clip` beside an `auto` computes to `hidden` anyway, so writing `hidden` is
|
|
167
|
+
writing what the browser will use.
|
|
168
|
+
|
|
169
|
+
An app's own parked chrome still belongs in `Shell`, beside `Main` rather than in it. That is
|
|
170
|
+
what `Shell`'s clip is for, and it is the arrangement `Shell.stories.tsx` demonstrates.
|
|
171
|
+
|
|
143
172
|
## `tabIndex={-1}`
|
|
144
173
|
|
|
145
174
|
<Canvas of={MainStories.Interactive} />
|
|
@@ -10,10 +10,13 @@ import { toMaxInlineSize } from "../Shell/contentWidth.ts"
|
|
|
10
10
|
import { Shell } from "../Shell/Shell.tsx"
|
|
11
11
|
import { Spinner } from "../Spinner/Spinner.tsx"
|
|
12
12
|
import {
|
|
13
|
+
ExternalLinkFooter,
|
|
13
14
|
HeaderSchemeToggle,
|
|
14
15
|
OverflowingContent,
|
|
15
16
|
PageContent,
|
|
17
|
+
ParkedDrawer,
|
|
16
18
|
RailNavigation,
|
|
19
|
+
ScrollFiller,
|
|
17
20
|
} from "../shell.storyHelpers.tsx"
|
|
18
21
|
import { Main } from "./Main.tsx"
|
|
19
22
|
import { ScrollMemoryProvider } from "./ScrollMemoryProvider.tsx"
|
|
@@ -124,14 +127,29 @@ export const AllVariants: Story = {
|
|
|
124
127
|
}
|
|
125
128
|
|
|
126
129
|
/**
|
|
127
|
-
* The 390px fixture
|
|
128
|
-
*
|
|
130
|
+
* The 390px fixture: everything that tries to make the **page**
|
|
131
|
+
* scroll when only `<main>` should, in one column.
|
|
129
132
|
*
|
|
130
|
-
* The path wraps because `Main` sets
|
|
131
|
-
* the table cannot wrap at all and
|
|
132
|
-
* keyboard-reachable scroll container
|
|
133
|
-
*
|
|
134
|
-
*
|
|
133
|
+
* Sideways, three ways. The path wraps because `Main` sets
|
|
134
|
+
* `overflow-wrap: anywhere`; the table cannot wrap at all and
|
|
135
|
+
* lives in a labelled, keyboard-reachable scroll container; and
|
|
136
|
+
* the drawer parked at `translateX(110%)` is clipped. The drawer
|
|
137
|
+
* is **inside** `Main` here, unlike `Shell.stories.tsx`, so the
|
|
138
|
+
* element clipping it is `Main` — `position: relative` moved that
|
|
139
|
+
* job in from the frame, and `overflow-x: hidden` is `Main`
|
|
140
|
+
* taking it.
|
|
141
|
+
*
|
|
142
|
+
* Downwards, once, and it is the shape nobody looks for: the
|
|
143
|
+
* footer link's `sr-only` "(opens in a new tab)" span is
|
|
144
|
+
* `position: absolute`, so it resolves against the nearest
|
|
145
|
+
* positioned ancestor. Before `Main` was one that was `Shell`,
|
|
146
|
+
* outside the scrollport, and the span's static position past the
|
|
147
|
+
* fold became scrollable overflow on the page. `ScrollFiller` is
|
|
148
|
+
* what puts it past the fold; without the height there is nothing
|
|
149
|
+
* to escape.
|
|
150
|
+
*
|
|
151
|
+
* The assertions are in `Main.test.tsx` at a real 390px viewport
|
|
152
|
+
* — a story is a demo.
|
|
135
153
|
*/
|
|
136
154
|
export const Responsive: Story = {
|
|
137
155
|
render: (mainProps) => (
|
|
@@ -139,7 +157,21 @@ export const Responsive: Story = {
|
|
|
139
157
|
<Header heading="Transfers" />
|
|
140
158
|
|
|
141
159
|
<Main {...mainProps}>
|
|
160
|
+
<ParkedDrawer />
|
|
161
|
+
|
|
142
162
|
<OverflowingContent />
|
|
163
|
+
|
|
164
|
+
{/*
|
|
165
|
+
Twice. One is a few dozen pixels short of filling an
|
|
166
|
+
844px phone, and the footer below has to sit past the
|
|
167
|
+
fold or there is nothing for its hidden span to escape
|
|
168
|
+
to.
|
|
169
|
+
*/}
|
|
170
|
+
<ScrollFiller />
|
|
171
|
+
|
|
172
|
+
<ScrollFiller />
|
|
173
|
+
|
|
174
|
+
<ExternalLinkFooter />
|
|
143
175
|
</Main>
|
|
144
176
|
</Shell>
|
|
145
177
|
),
|
package/src/Main/Main.test.tsx
CHANGED
|
@@ -89,7 +89,7 @@ test("main is the query container its content responds to", async () => {
|
|
|
89
89
|
).toHaveLength(2)
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
-
test("
|
|
92
|
+
test("nothing in the fixture widens the page at 390px", async () => {
|
|
93
93
|
await setViewport(PHONE)
|
|
94
94
|
|
|
95
95
|
const { canvas, canvasElement } =
|
|
@@ -131,6 +131,30 @@ test("neither an unbroken path nor a wide table widens the page at 390px", async
|
|
|
131
131
|
scroller.clientWidth,
|
|
132
132
|
)
|
|
133
133
|
|
|
134
|
+
// The drawer parked at `translateX(110%)`, which is inside
|
|
135
|
+
// `Main` in this story rather than beside it. `Shell`'s
|
|
136
|
+
// `overflow-x: clip` cannot reach it any more — `Main` is
|
|
137
|
+
// `position: relative`, so `Main` is its containing block and
|
|
138
|
+
// `Main`'s own `overflow-x` is the only thing between it and a
|
|
139
|
+
// horizontal scrollbar on the scrollport.
|
|
140
|
+
const parkedDrawer = canvasElement.querySelector(
|
|
141
|
+
"#parked-drawer",
|
|
142
|
+
) as HTMLElement
|
|
143
|
+
|
|
144
|
+
const main = canvas.getByRole("main")
|
|
145
|
+
|
|
146
|
+
await expect(parkedDrawer.offsetParent).toBe(main)
|
|
147
|
+
|
|
148
|
+
// `hidden` rather than `clip`, and the browser decides that:
|
|
149
|
+
// `overflow-x: clip` beside an `overflow-y: auto` computes to
|
|
150
|
+
// `hidden`, so `Main` writes what will actually be used. It is
|
|
151
|
+
// also why the assertion is on the *page* below rather than on
|
|
152
|
+
// `main.scrollWidth` — a `hidden` box still reports the
|
|
153
|
+
// overflow it is refusing to scroll to.
|
|
154
|
+
await expect(
|
|
155
|
+
globalThis.getComputedStyle(main).overflowX,
|
|
156
|
+
).toBe("hidden")
|
|
157
|
+
|
|
134
158
|
const { documentElement } = document
|
|
135
159
|
|
|
136
160
|
await expect(
|
|
@@ -140,6 +164,65 @@ test("neither an unbroken path nor a wide table widens the page at 390px", async
|
|
|
140
164
|
await expectNoAxeViolations(canvasElement)
|
|
141
165
|
})
|
|
142
166
|
|
|
167
|
+
/**
|
|
168
|
+
* The second scrollbar, and how cheaply it is bought.
|
|
169
|
+
*
|
|
170
|
+
* `<main>` is the page's only vertical scrollport, so the document
|
|
171
|
+
* itself must not scroll. An absolutely positioned descendant
|
|
172
|
+
* breaks that unless `<main>` is its containing block: resolve it
|
|
173
|
+
* against `Shell` instead — which is what `position: static` here
|
|
174
|
+
* used to mean — and the box keeps its static position, deep down
|
|
175
|
+
* a long document, while sitting outside the scrollport. The page
|
|
176
|
+
* grows to reach it.
|
|
177
|
+
*
|
|
178
|
+
* The box in this fixture is Tailwind's `sr-only`, one pixel of
|
|
179
|
+
* nothing on an "opens in a new tab" hint. Measured on Folio
|
|
180
|
+
* before this: `<main>` 835px tall,
|
|
181
|
+
* `document.documentElement.scrollHeight` 2085 — the span's
|
|
182
|
+
* offset, exactly.
|
|
183
|
+
*/
|
|
184
|
+
test("a visually hidden span past the fold does not give the page a second scrollbar", async () => {
|
|
185
|
+
await setViewport(PHONE)
|
|
186
|
+
|
|
187
|
+
const { canvas, canvasElement } =
|
|
188
|
+
await mountStory(Responsive)
|
|
189
|
+
|
|
190
|
+
const main = canvas.getByRole("main")
|
|
191
|
+
|
|
192
|
+
// The fixture is only a fixture if `<main>` genuinely scrolls
|
|
193
|
+
// and the span genuinely sits past the fold. Without the
|
|
194
|
+
// height there is nothing for it to escape to, and every
|
|
195
|
+
// assertion below would pass against a short page.
|
|
196
|
+
await expect(main.scrollHeight).toBeGreaterThan(
|
|
197
|
+
main.clientHeight,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
// By id rather than by text: the hint's words are part of the
|
|
201
|
+
// link's accessible name too, so a text query matches both the
|
|
202
|
+
// span and the anchor around it.
|
|
203
|
+
const hint = canvasElement.querySelector(
|
|
204
|
+
"#new-tab-hint",
|
|
205
|
+
) as HTMLElement
|
|
206
|
+
|
|
207
|
+
await expect(
|
|
208
|
+
globalThis.getComputedStyle(hint).position,
|
|
209
|
+
).toBe("absolute")
|
|
210
|
+
|
|
211
|
+
await expect(
|
|
212
|
+
hint.getBoundingClientRect().top,
|
|
213
|
+
).toBeGreaterThan(main.getBoundingClientRect().bottom)
|
|
214
|
+
|
|
215
|
+
await expect(hint.offsetParent).toBe(main)
|
|
216
|
+
|
|
217
|
+
const { documentElement } = document
|
|
218
|
+
|
|
219
|
+
await expect(
|
|
220
|
+
documentElement.scrollHeight,
|
|
221
|
+
).toBeLessThanOrEqual(documentElement.clientHeight)
|
|
222
|
+
|
|
223
|
+
await expectNoAxeViolations(canvasElement)
|
|
224
|
+
})
|
|
225
|
+
|
|
143
226
|
/**
|
|
144
227
|
* `tabIndex={-1}` is the difference between a skip link that
|
|
145
228
|
* works and one that only scrolls. It must not put `<main>` in
|
package/src/Main/Main.tsx
CHANGED
|
@@ -101,6 +101,39 @@ export type MainProps = ComponentPropsWithRef<"main"> & {
|
|
|
101
101
|
* an app's own fixed chrome belongs in `Shell`, beside `Main`
|
|
102
102
|
* rather than in it.
|
|
103
103
|
*
|
|
104
|
+
* ## `relative` — the scrollport has to be the containing block
|
|
105
|
+
*
|
|
106
|
+
* `<main>` is the page's only vertical scrollport, so everything
|
|
107
|
+
* in it is supposed to scroll *inside* it. An absolutely
|
|
108
|
+
* positioned descendant does not, unless this element is its
|
|
109
|
+
* containing block: with `position: static` here, the nearest
|
|
110
|
+
* positioned ancestor is `Shell`, which is **outside** the
|
|
111
|
+
* scrollport. The box then resolves against `Shell`, `<main>`'s
|
|
112
|
+
* `overflow-y` has no authority over it, and its static position —
|
|
113
|
+
* a couple of thousand pixels down a long document — becomes
|
|
114
|
+
* scrollable overflow on `documentElement`. The page grows to
|
|
115
|
+
* reach it and the reader gets a **second scrollbar** beside the
|
|
116
|
+
* one `<main>` already has.
|
|
117
|
+
*
|
|
118
|
+
* The box does not have to be exotic. Tailwind's `sr-only` is
|
|
119
|
+
* `position: absolute`, so the visually hidden
|
|
120
|
+
* `<span>(opens in a new tab)</span>` on one link in a document
|
|
121
|
+
* footer is enough. Measured on Folio at 1440x900: `<main>` was
|
|
122
|
+
* 835px tall and `document.documentElement.scrollHeight` read
|
|
123
|
+
* 2085, which is exactly where that span sat.
|
|
124
|
+
*
|
|
125
|
+
* `overflow-x: hidden` comes with it and is not decoration.
|
|
126
|
+
* `relative` moves every absolutely positioned descendant's
|
|
127
|
+
* containing block from `Shell` to here, which also moves what
|
|
128
|
+
* clips them **sideways**: `Shell`'s `overflow-x: clip` no longer
|
|
129
|
+
* reaches them, so a drawer parked at `translateX(110%)` inside
|
|
130
|
+
* `Main` would draw a horizontal scrollbar on the scrollport
|
|
131
|
+
* instead of being clipped. `hidden` rather than `clip` because
|
|
132
|
+
* this element already scrolls on the other axis — a `clip` beside
|
|
133
|
+
* an `auto` computes to `hidden` anyway, and writing what the
|
|
134
|
+
* browser will use is the honest spelling. `Shell`'s note on the
|
|
135
|
+
* clip explains the other half.
|
|
136
|
+
*
|
|
104
137
|
* ## It remembers where each history entry was scrolled to
|
|
105
138
|
*
|
|
106
139
|
* `Shell` gives this element the page's only vertical scrollport,
|
|
@@ -189,7 +222,7 @@ export const Main = ({
|
|
|
189
222
|
<main
|
|
190
223
|
{...mainProps}
|
|
191
224
|
className={toClassName(
|
|
192
|
-
"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",
|
|
225
|
+
"charcuterie-scrollbar relative col-start-1 row-start-3 min-h-0 min-w-0 overflow-x-hidden overflow-y-auto md:col-start-2 md:row-start-2",
|
|
193
226
|
className,
|
|
194
227
|
)}
|
|
195
228
|
id={id ?? shell?.mainId ?? fallbackId}
|
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
} from "@charcuterie/tokens"
|
|
5
5
|
import {
|
|
6
6
|
CATEGORICAL_INDEX_COUNT,
|
|
7
|
-
|
|
7
|
+
CATEGORICAL_SEQUENCE,
|
|
8
8
|
} from "@charcuterie/tokens"
|
|
9
9
|
import type { CSSProperties, ReactNode } from "react"
|
|
10
10
|
|
|
@@ -45,8 +45,10 @@ import { getPortraitColourProperties } from "./portraitColour.ts"
|
|
|
45
45
|
* palette hues cannot represent that, and swapping them in would
|
|
46
46
|
* have made the app disagree with the cards on the fridge.
|
|
47
47
|
*
|
|
48
|
-
* Neither is a default. A portrait that names no colour takes
|
|
49
|
-
*
|
|
48
|
+
* Neither is a default. A portrait that names no colour takes its
|
|
49
|
+
* position's entry in `CATEGORICAL_SEQUENCE` — the palette walked so
|
|
50
|
+
* neighbours contrast, rather than the hue-ordered ring, which would
|
|
51
|
+
* put red beside orange.
|
|
50
52
|
*/
|
|
51
53
|
type PortraitTileColour =
|
|
52
54
|
| { categorical?: CategoricalIndex; color?: never }
|
|
@@ -273,9 +275,9 @@ const getPortraitCategorical = (
|
|
|
273
275
|
): CategoricalIndex | null =>
|
|
274
276
|
item.color === undefined
|
|
275
277
|
? (item.categorical ??
|
|
276
|
-
|
|
278
|
+
CATEGORICAL_SEQUENCE[
|
|
277
279
|
position % CATEGORICAL_INDEX_COUNT
|
|
278
|
-
]
|
|
280
|
+
])
|
|
279
281
|
: null
|
|
280
282
|
|
|
281
283
|
/**
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
HeaderSchemeToggle,
|
|
9
9
|
OverflowingContent,
|
|
10
10
|
PageContent,
|
|
11
|
+
ParkedDrawer,
|
|
11
12
|
RailDetail,
|
|
12
13
|
RailNavigation,
|
|
13
14
|
ScrollFiller,
|
|
@@ -130,6 +131,14 @@ export const WithBothRails: Story = {
|
|
|
130
131
|
* scrolls left and right today for the first reason; the second
|
|
131
132
|
* is what an `overflow-wrap` alone cannot save.
|
|
132
133
|
*
|
|
134
|
+
* The third shape is `ParkedDrawer`, and it sits **beside**
|
|
135
|
+
* `Main` rather than in it — an app's parked chrome is parked
|
|
136
|
+
* against the frame, which is the arrangement `Shell`'s
|
|
137
|
+
* `overflow-x: clip` exists for. Absolutely positioned children
|
|
138
|
+
* of a grid container take no track, so it costs the layout
|
|
139
|
+
* nothing. `Main.stories.tsx` renders the same drawer *inside*
|
|
140
|
+
* `Main`, where the clip that answers it is `Main`'s own.
|
|
141
|
+
*
|
|
133
142
|
* `document.documentElement.scrollWidth <= clientWidth` at 390px
|
|
134
143
|
* is the assertion, and it is in `Shell.test.tsx` rather than
|
|
135
144
|
* here — a story is a demo.
|
|
@@ -150,6 +159,8 @@ export const Responsive: Story = {
|
|
|
150
159
|
<OverflowingContent />
|
|
151
160
|
</Main>
|
|
152
161
|
|
|
162
|
+
<ParkedDrawer />
|
|
163
|
+
|
|
153
164
|
<Rail label="Job detail" side="end">
|
|
154
165
|
<RailDetail />
|
|
155
166
|
</Rail>
|
|
@@ -140,8 +140,73 @@ export const ScrollFiller = (): ReactNode => (
|
|
|
140
140
|
)
|
|
141
141
|
|
|
142
142
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
143
|
+
* A closed drawer parked off the inline edge by a transform — the
|
|
144
|
+
* third shape that makes a page scroll sideways, and the one that
|
|
145
|
+
* broke plex-channels.
|
|
146
|
+
*
|
|
147
|
+
* It is still laid out, still painted, and still part of the
|
|
148
|
+
* scrollable overflow region of whatever it resolves against: a
|
|
149
|
+
* transform does not remove a box from that region, and neither
|
|
150
|
+
* does `visibility: hidden`. So the page scrolls sideways to reach
|
|
151
|
+
* a panel nobody can see, and every `min-width: 0` in the tree is
|
|
152
|
+
* irrelevant. Only a clip on the box's containing block answers
|
|
153
|
+
* it.
|
|
154
|
+
*
|
|
155
|
+
* **Which element that is depends on where the drawer is
|
|
156
|
+
* rendered, and the two stories place it differently on purpose.**
|
|
157
|
+
* Beside `Main` it is `Shell`'s to clip, which is where an app's
|
|
158
|
+
* parked chrome belongs. Inside `Main` it is `Main`'s, because
|
|
159
|
+
* `Main` is `position: relative` — and that is why `Main` carries
|
|
160
|
+
* an `overflow-x: hidden` of its own rather than leaning on the
|
|
161
|
+
* frame's.
|
|
162
|
+
*/
|
|
163
|
+
export const ParkedDrawer = (): ReactNode => (
|
|
164
|
+
<div
|
|
165
|
+
aria-hidden="true"
|
|
166
|
+
className="pointer-events-none absolute top-0 end-0 h-40 w-80 translate-x-[110%] rounded-lg border border-border-subtle bg-surface-raised"
|
|
167
|
+
id="parked-drawer"
|
|
168
|
+
/>
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The shape that makes a page scroll **down** when only `<main>`
|
|
173
|
+
* was supposed to: an ordinary external link whose "opens in a new
|
|
174
|
+
* tab" hint is visually hidden.
|
|
175
|
+
*
|
|
176
|
+
* `sr-only` is `position: absolute`. Nothing here looks like
|
|
177
|
+
* layout at all — the span is one pixel and paints nothing — and
|
|
178
|
+
* that is the point: whatever it resolves against inherits its
|
|
179
|
+
* static position, which is wherever the link happens to sit in a
|
|
180
|
+
* long document. Resolve it outside the scrollport and the page
|
|
181
|
+
* grows to reach it, which is a second scrollbar beside `Main`'s.
|
|
182
|
+
* Folio drew both.
|
|
183
|
+
*
|
|
184
|
+
* It goes at the END of a fixture, past the fold, or the escape it
|
|
185
|
+
* demonstrates has nowhere to escape to.
|
|
186
|
+
*/
|
|
187
|
+
export const ExternalLinkFooter = (): ReactNode => (
|
|
188
|
+
<p className="text-content-secondary text-sm">
|
|
189
|
+
<a
|
|
190
|
+
className="text-intent-accent-content underline"
|
|
191
|
+
href="#runbook"
|
|
192
|
+
id="runbook-link"
|
|
193
|
+
rel="noreferrer"
|
|
194
|
+
target="_blank"
|
|
195
|
+
>
|
|
196
|
+
The transfer runbook
|
|
197
|
+
<span className="sr-only" id="new-tab-hint">
|
|
198
|
+
{" "}
|
|
199
|
+
(opens in a new tab)
|
|
200
|
+
</span>
|
|
201
|
+
</a>
|
|
202
|
+
</p>
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The two shapes of *content* that make a page scroll sideways,
|
|
207
|
+
* and each needs a different answer. The third shape is a box
|
|
208
|
+
* rather than content and lives in `ParkedDrawer`, because where
|
|
209
|
+
* it is rendered is half of what it demonstrates.
|
|
145
210
|
*
|
|
146
211
|
* The **path** is content that *can* wrap and does not, because
|
|
147
212
|
* nothing in it is a break opportunity. `Main`'s `wrap-anywhere`
|
|
@@ -166,23 +231,6 @@ export const ScrollFiller = (): ReactNode => (
|
|
|
166
231
|
*/
|
|
167
232
|
export const OverflowingContent = (): ReactNode => (
|
|
168
233
|
<>
|
|
169
|
-
{/*
|
|
170
|
-
The third shape, and the one that broke plex-channels: a
|
|
171
|
-
closed drawer parked off the inline edge by a transform.
|
|
172
|
-
|
|
173
|
-
It is still laid out, still painted, and still part of the
|
|
174
|
-
document's scrollable overflow region — a transform does not
|
|
175
|
-
remove a box from it, and neither does `visibility: hidden`.
|
|
176
|
-
So the page scrolls sideways to reach a panel nobody can see,
|
|
177
|
-
and every `min-width: 0` in the tree is irrelevant. `Shell`'s
|
|
178
|
-
`overflow-x: clip` is what actually answers it.
|
|
179
|
-
*/}
|
|
180
|
-
<div
|
|
181
|
-
aria-hidden="true"
|
|
182
|
-
className="pointer-events-none absolute top-0 end-0 h-40 w-80 translate-x-[110%] rounded-lg border border-border-subtle bg-surface-raised"
|
|
183
|
-
id="parked-drawer"
|
|
184
|
-
/>
|
|
185
|
-
|
|
186
234
|
<p className="text-content-secondary text-sm">
|
|
187
235
|
/mnt/Bunnies/Family/Media/Television/Some-Very-Long-Show-Name/Season-01/Some-Very-Long-Show-Name-S01E01-Pilot-2160p-HDR-DTS-HD-MA.mkv
|
|
188
236
|
</p>
|