@charcuterie/ui 4.21.0 → 4.22.1
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/LICENSE +21 -0
- package/README.md +6 -6
- package/dist/AdaptiveGrid/AdaptiveGrid.d.ts +4 -1
- package/dist/AdaptiveGrid/AdaptiveGrid.d.ts.map +1 -1
- package/dist/AdaptiveGrid/AdaptiveGrid.js +3 -1
- package/dist/AdaptiveGrid/AdaptiveGrid.js.map +1 -1
- package/dist/AdaptiveGrid/chooseColumns.d.ts +10 -2
- package/dist/AdaptiveGrid/chooseColumns.d.ts.map +1 -1
- package/dist/AdaptiveGrid/chooseColumns.js +10 -3
- package/dist/AdaptiveGrid/chooseColumns.js.map +1 -1
- package/dist/AdaptiveGrid/useAdaptiveColumns.d.ts +6 -2
- package/dist/AdaptiveGrid/useAdaptiveColumns.d.ts.map +1 -1
- package/dist/AdaptiveGrid/useAdaptiveColumns.js +4 -1
- package/dist/AdaptiveGrid/useAdaptiveColumns.js.map +1 -1
- package/dist/Overlay/useAnchoredOverlay.d.ts +2 -2
- package/dist/Picker/Picker.d.ts +15 -0
- package/dist/Picker/Picker.d.ts.map +1 -1
- package/dist/Picker/Picker.js +26 -4
- package/dist/Picker/Picker.js.map +1 -1
- package/dist/VirtualizedGrid/VirtualizedGrid.d.ts +4 -1
- package/dist/VirtualizedGrid/VirtualizedGrid.d.ts.map +1 -1
- package/dist/VirtualizedGrid/VirtualizedGrid.js +3 -1
- package/dist/VirtualizedGrid/VirtualizedGrid.js.map +1 -1
- package/package.json +10 -8
- package/src/AdaptiveGrid/AdaptiveGrid.mdx +10 -0
- package/src/AdaptiveGrid/AdaptiveGrid.stories.tsx +17 -0
- package/src/AdaptiveGrid/AdaptiveGrid.test.tsx +36 -2
- package/src/AdaptiveGrid/AdaptiveGrid.tsx +6 -0
- package/src/AdaptiveGrid/chooseColumns.ts +29 -1
- package/src/AdaptiveGrid/useAdaptiveColumns.ts +13 -1
- package/src/CopyButton/CopyButton.test.tsx +25 -12
- package/src/Picker/Picker.mdx +14 -0
- package/src/Picker/Picker.stories.tsx +46 -0
- package/src/Picker/Picker.test.tsx +91 -2
- package/src/Picker/Picker.tsx +43 -4
- package/src/VirtualizedGrid/VirtualizedGrid.tsx +6 -0
- package/src/markdownEditorCodeMirror/MarkdownView.test.tsx +16 -3
- package/src/mdxReferences.test.ts +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kevin Ghadyani
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -210,7 +210,7 @@ M5 spent an hour on the second and third of these, so M5b and M6 do not have to:
|
|
|
210
210
|
// the PROJECT ROOT — `ui` declares its siblings as `workspace:*`, and that
|
|
211
211
|
// descriptor cannot resolve outside charcuterie's own workspace. Bare keys, not
|
|
212
212
|
// `@charcuterie/ui/@charcuterie/tokens`: the scoped form yields a different
|
|
213
|
-
// locator string from the app's own dependency on the same directory, and
|
|
213
|
+
// locator string from the app's own dependency on the same directory, and pnpm
|
|
214
214
|
// rejects the pair as conflicting.
|
|
215
215
|
"resolutions": {
|
|
216
216
|
"@charcuterie/logic": "portal:../charcuterie/packages/logic",
|
|
@@ -233,7 +233,7 @@ TypeError: Cannot read properties of null (reading 'useRef')
|
|
|
233
233
|
```
|
|
234
234
|
|
|
235
235
|
which mentions neither symlinks nor React identity. Keep the line after publish: it costs
|
|
236
|
-
nothing with one copy, and it is the difference between a working `
|
|
236
|
+
nothing with one copy, and it is the difference between a working `pnpm link` session and
|
|
237
237
|
an hour of confusion.
|
|
238
238
|
|
|
239
239
|
## How a component is put together
|
|
@@ -256,9 +256,9 @@ and fails on any candidate Tailwind cannot generate.
|
|
|
256
256
|
## Tests
|
|
257
257
|
|
|
258
258
|
```bash
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
259
|
+
pnpm vitest run --project ui # Node: class maps, status switches, clamping, boundaries
|
|
260
|
+
pnpm vitest run --project ui-dom # chromium: Component.test.tsx — behaviour, keyboard, ARIA
|
|
261
|
+
pnpm vitest run --project storybook # chromium: every story renders, axe at test: "error"
|
|
262
262
|
```
|
|
263
263
|
|
|
264
264
|
**Stories are demos and carry no assertions**
|
|
@@ -319,7 +319,7 @@ rename breaks only the rendered page).
|
|
|
319
319
|
from effects, so before those run a `RovingFocus` has no active value and a
|
|
320
320
|
`SinglePicker` / `VisibilityGroup` has no selected key — only a pending one. Two of M4's
|
|
321
321
|
four bugs were exactly that, and the isolated story runner saw neither —
|
|
322
|
-
`
|
|
322
|
+
`pnpm smoke:storybook` did.
|
|
323
323
|
7. **If it declares `@container`, every `StoryCell` holding it needs `align="stretch"`.**
|
|
324
324
|
`container-type: inline-size` forbids the element from being sized by its own
|
|
325
325
|
contents, so a default (shrink-to-fit) cell collapses it to min-content and every
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import type { GridLayout } from "./chooseColumns.ts";
|
|
2
3
|
import type { BlockSizeResolver } from "./useAdaptiveColumns.ts";
|
|
3
4
|
export type AdaptiveGridProps = {
|
|
4
5
|
/**
|
|
@@ -35,6 +36,8 @@ export type AdaptiveGridProps = {
|
|
|
35
36
|
* would be worse than one that is an item out.
|
|
36
37
|
*/
|
|
37
38
|
itemCount?: number;
|
|
39
|
+
/** Fill the container with readable columns instead of spending height first. */
|
|
40
|
+
layout?: GridLayout;
|
|
38
41
|
/** The most columns the automatic answer will take. */
|
|
39
42
|
maxColumns?: number;
|
|
40
43
|
/** The narrowest a column may be, in CSS px. */
|
|
@@ -90,5 +93,5 @@ export type AdaptiveGridProps = {
|
|
|
90
93
|
* need every count to survive the scan; an inline style needs
|
|
91
94
|
* none.
|
|
92
95
|
*/
|
|
93
|
-
export declare const AdaptiveGrid: ({ blockSizeResolver, children, chromeBlockSize, className, columns, itemBlockSize, itemCount, maxColumns, minColumnInlineSize, storageKey, }: AdaptiveGridProps) => ReactNode;
|
|
96
|
+
export declare const AdaptiveGrid: ({ blockSizeResolver, children, chromeBlockSize, className, columns, itemBlockSize, itemCount, layout: gridLayout, maxColumns, minColumnInlineSize, storageKey, }: AdaptiveGridProps) => ReactNode;
|
|
94
97
|
//# sourceMappingURL=AdaptiveGrid.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdaptiveGrid.d.ts","sourceRoot":"","sources":["../../src/AdaptiveGrid/AdaptiveGrid.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"AdaptiveGrid.d.ts","sourceRoot":"","sources":["../../src/AdaptiveGrid/AdaptiveGrid.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAEpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAGhE,MAAM,MAAM,iBAAiB,GAAG;IAC9B;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC,QAAQ,EAAE,SAAS,CAAA;IACnB;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;;OAKG;IACH,aAAa,EAAE,MAAM,CAAA;IACrB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iFAAiF;IACjF,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gDAAgD;IAChD,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,eAAO,MAAM,YAAY,GAAI,kKAY1B,iBAAiB,KAAG,SAyCtB,CAAA"}
|
|
@@ -48,12 +48,13 @@ import { useAdaptiveColumns } from "./useAdaptiveColumns.js";
|
|
|
48
48
|
* need every count to survive the scan; an inline style needs
|
|
49
49
|
* none.
|
|
50
50
|
*/
|
|
51
|
-
export const AdaptiveGrid = ({ blockSizeResolver, children, chromeBlockSize, className, columns, itemBlockSize, itemCount, maxColumns, minColumnInlineSize, storageKey, }) => {
|
|
51
|
+
export const AdaptiveGrid = ({ blockSizeResolver, children, chromeBlockSize, className, columns, itemBlockSize, itemCount, layout: gridLayout = "height-first", maxColumns, minColumnInlineSize, storageKey, }) => {
|
|
52
52
|
const layout = useAdaptiveColumns({
|
|
53
53
|
blockSizeResolver,
|
|
54
54
|
chromeBlockSize,
|
|
55
55
|
itemBlockSize,
|
|
56
56
|
itemCount: itemCount ?? Children.count(children),
|
|
57
|
+
layout: gridLayout,
|
|
57
58
|
maxAutoColumns: maxColumns,
|
|
58
59
|
minColumnInlineSize,
|
|
59
60
|
storageKey,
|
|
@@ -64,6 +65,7 @@ export const AdaptiveGrid = ({ blockSizeResolver, children, chromeBlockSize, cla
|
|
|
64
65
|
// a one-column cap is the same bug as no cap at all, upside down.
|
|
65
66
|
const contentMaxInlineSize = getContentMaxInlineSize({
|
|
66
67
|
columns: drawnColumns,
|
|
68
|
+
layout: gridLayout,
|
|
67
69
|
});
|
|
68
70
|
return (_jsx("div", { className: toClassName("flex justify-center", className), ref: layout.containerRef, children: _jsx("div", { className: "grid w-full gap-4 [&>*]:min-w-0", style: {
|
|
69
71
|
gridTemplateColumns: `repeat(${drawnColumns}, minmax(0, 1fr))`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdaptiveGrid.js","sourceRoot":"","sources":["../../src/AdaptiveGrid/AdaptiveGrid.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"AdaptiveGrid.js","sourceRoot":"","sources":["../../src/AdaptiveGrid/AdaptiveGrid.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAE5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAkD5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,EAC3B,iBAAiB,EACjB,QAAQ,EACR,eAAe,EACf,SAAS,EACT,OAAO,EACP,aAAa,EACb,SAAS,EACT,MAAM,EAAE,UAAU,GAAG,cAAc,EACnC,UAAU,EACV,mBAAmB,EACnB,UAAU,GACQ,EAAa,EAAE;IACjC,MAAM,MAAM,GAAG,kBAAkB,CAAC;QAChC,iBAAiB;QACjB,eAAe;QACf,aAAa;QACb,SAAS,EAAE,SAAS,IAAI,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC;QAChD,MAAM,EAAE,UAAU;QAClB,cAAc,EAAE,UAAU;QAC1B,mBAAmB;QACnB,UAAU;KACX,CAAC,CAAA;IAEF,MAAM,YAAY,GAAG,OAAO,IAAI,MAAM,CAAC,OAAO,CAAA;IAE9C,iEAAiE;IACjE,kEAAkE;IAClE,kEAAkE;IAClE,MAAM,oBAAoB,GAAG,uBAAuB,CAAC;QACnD,OAAO,EAAE,YAAY;QACrB,MAAM,EAAE,UAAU;KACnB,CAAC,CAAA;IAEF,OAAO,CACL,cACE,SAAS,EAAE,WAAW,CACpB,qBAAqB,EACrB,SAAS,CACV,EACD,GAAG,EAAE,MAAM,CAAC,YAAY,YAExB,cACE,SAAS,EAAC,iCAAiC,EAC3C,KAAK,EAAE;gBACL,mBAAmB,EAAE,UAAU,YAAY,mBAAmB;gBAC9D,aAAa,EAAE,oBAAoB;aACpC,YAEA,QAAQ,GACL,GACF,CACP,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -98,11 +98,13 @@ export declare const DEFAULT_MIN_COLUMN_INLINE_SIZE_PX: number;
|
|
|
98
98
|
* `"auto"`, or the number of columns a person explicitly asked for.
|
|
99
99
|
*/
|
|
100
100
|
export type ColumnChoice = "auto" | number;
|
|
101
|
+
/** Fill is an explicit width-first layout for media galleries and fleets. */
|
|
102
|
+
export type GridLayout = "height-first" | "fill";
|
|
101
103
|
/**
|
|
102
104
|
* The column count the automatic rule wants, for one measured space
|
|
103
105
|
* and one set of items.
|
|
104
106
|
*/
|
|
105
|
-
export declare const chooseColumns: ({ availableBlockSize, availableInlineSize, chromeBlockSize, itemBlockSize, itemCount, maxColumns, minColumnInlineSize, }: {
|
|
107
|
+
export declare const chooseColumns: ({ availableBlockSize, availableInlineSize, chromeBlockSize, itemBlockSize, itemCount, gap, layout, maxColumns, minColumnInlineSize, }: {
|
|
106
108
|
/**
|
|
107
109
|
* The block size the layout has to spend, in CSS px — the
|
|
108
110
|
* viewport's, not the grid's.
|
|
@@ -153,6 +155,10 @@ export declare const chooseColumns: ({ availableBlockSize, availableInlineSize,
|
|
|
153
155
|
* finished loading would be worse than one that is an item out.
|
|
154
156
|
*/
|
|
155
157
|
itemCount: number;
|
|
158
|
+
/** Height-first by default; fill spends the available container width. */
|
|
159
|
+
layout?: GridLayout;
|
|
160
|
+
/** Space between tracks, in CSS px, used by fill. */
|
|
161
|
+
gap?: number;
|
|
156
162
|
/** @see DEFAULT_MAX_AUTO_COLUMNS */
|
|
157
163
|
maxColumns?: number;
|
|
158
164
|
/** @see DEFAULT_MIN_COLUMN_INLINE_SIZE_PX */
|
|
@@ -183,12 +189,14 @@ export declare const chooseColumns: ({ availableBlockSize, availableInlineSize,
|
|
|
183
189
|
* dense tables and an app of prose disagree about a measure and
|
|
184
190
|
* neither is wrong.
|
|
185
191
|
*/
|
|
186
|
-
export declare const getContentMaxInlineSize: ({ columnInlineSize, columns, gutterInlineSize, singleInlineSize, }: {
|
|
192
|
+
export declare const getContentMaxInlineSize: ({ columnInlineSize, columns, gutterInlineSize, layout, singleInlineSize, }: {
|
|
187
193
|
/** What one column is worth, once there are several. */
|
|
188
194
|
columnInlineSize?: string;
|
|
189
195
|
columns: number;
|
|
190
196
|
/** Slack added on top of a multi-column cap, for the gaps. */
|
|
191
197
|
gutterInlineSize?: string;
|
|
198
|
+
/** Fill uses all available width once there are multiple columns. */
|
|
199
|
+
layout?: GridLayout;
|
|
192
200
|
/** The cap when there is only one column. */
|
|
193
201
|
singleInlineSize?: string;
|
|
194
202
|
}) => string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chooseColumns.d.ts","sourceRoot":"","sources":["../../src/AdaptiveGrid/chooseColumns.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,IAAI,CAAA;AAEzC;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,IAAI,CAAA;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,iCAAiC,QACH,CAAA;AAE3C;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,CAAA;AAE1C;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,
|
|
1
|
+
{"version":3,"file":"chooseColumns.d.ts","sourceRoot":"","sources":["../../src/AdaptiveGrid/chooseColumns.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,wBAAwB,IAAI,CAAA;AAEzC;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,IAAI,CAAA;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,iCAAiC,QACH,CAAA;AAE3C;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,CAAA;AAE1C,6EAA6E;AAC7E,MAAM,MAAM,UAAU,GAAG,cAAc,GAAG,MAAM,CAAA;AAEhD;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,uIAU3B;IACD;;;;;;;;OAQG;IACH,kBAAkB,EAAE,MAAM,CAAA;IAC1B;;;;;;;;;;;;OAYG;IACH,mBAAmB,EAAE,MAAM,CAAA;IAC3B;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB;;;;;;;;;;OAUG;IACH,aAAa,EAAE,MAAM,CAAA;IACrB;;;;;;OAMG;IACH,SAAS,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,qDAAqD;IACrD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,6CAA6C;IAC7C,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B,KAAG,MA0CH,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,uBAAuB,GAAI,4EAMrC;IACD,wDAAwD;IACxD,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,qEAAqE;IACrE,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,6CAA6C;IAC7C,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,KAAG,MAK+F,CAAA;AAEnG;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,iBAE9B;IACD,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,KAAG,YAAY,EAMf,CAAA"}
|
|
@@ -99,13 +99,18 @@ export const DEFAULT_MIN_COLUMN_INLINE_SIZE_PX = Number.parseFloat(containerQuer
|
|
|
99
99
|
* The column count the automatic rule wants, for one measured space
|
|
100
100
|
* and one set of items.
|
|
101
101
|
*/
|
|
102
|
-
export const chooseColumns = ({ availableBlockSize, availableInlineSize, chromeBlockSize = 0, itemBlockSize, itemCount, maxColumns = DEFAULT_MAX_AUTO_COLUMNS, minColumnInlineSize = DEFAULT_MIN_COLUMN_INLINE_SIZE_PX, }) => {
|
|
102
|
+
export const chooseColumns = ({ availableBlockSize, availableInlineSize, chromeBlockSize = 0, itemBlockSize, itemCount, gap = 16, layout = "height-first", maxColumns = DEFAULT_MAX_AUTO_COLUMNS, minColumnInlineSize = DEFAULT_MIN_COLUMN_INLINE_SIZE_PX, }) => {
|
|
103
103
|
// Inline size is a CAP, never a reason. This is the "don't go
|
|
104
104
|
// 9-wide just because you're on an ultrawide" half of the rule.
|
|
105
105
|
const inlineSizeCap = Math.max(1, Math.min(maxColumns, Math.floor(availableInlineSize / minColumnInlineSize)));
|
|
106
106
|
if (itemCount <= 1) {
|
|
107
107
|
return 1;
|
|
108
108
|
}
|
|
109
|
+
if (layout === "fill") {
|
|
110
|
+
const columnsThatFit = Math.max(1, Math.floor((availableInlineSize + gap) /
|
|
111
|
+
(minColumnInlineSize + gap)));
|
|
112
|
+
return Math.max(1, Math.min(maxColumns, itemCount, columnsThatFit));
|
|
113
|
+
}
|
|
109
114
|
// Block size is the reason. How many items stack in the space,
|
|
110
115
|
// then how many stacks that many items need.
|
|
111
116
|
const rowsThatFit = Math.max(1, Math.floor((availableBlockSize - chromeBlockSize) /
|
|
@@ -138,9 +143,11 @@ export const chooseColumns = ({ availableBlockSize, availableInlineSize, chromeB
|
|
|
138
143
|
* dense tables and an app of prose disagree about a measure and
|
|
139
144
|
* neither is wrong.
|
|
140
145
|
*/
|
|
141
|
-
export const getContentMaxInlineSize = ({ columnInlineSize = contentInlineSize.column, columns, gutterInlineSize = contentInlineSize.gutter, singleInlineSize = contentInlineSize.single, }) => columns <= 1
|
|
146
|
+
export const getContentMaxInlineSize = ({ columnInlineSize = contentInlineSize.column, columns, gutterInlineSize = contentInlineSize.gutter, layout = "height-first", singleInlineSize = contentInlineSize.single, }) => columns <= 1
|
|
142
147
|
? singleInlineSize
|
|
143
|
-
:
|
|
148
|
+
: layout === "fill"
|
|
149
|
+
? "none"
|
|
150
|
+
: `${Number.parseFloat(columnInlineSize) * columns + Number.parseFloat(gutterInlineSize)}rem`;
|
|
144
151
|
/**
|
|
145
152
|
* The choices a manual picker offers: `"auto"`, then every count up
|
|
146
153
|
* to `maxColumns`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chooseColumns.js","sourceRoot":"","sources":["../../src/AdaptiveGrid/chooseColumns.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,iBAAiB,GAClB,MAAM,cAAc,CAAA;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAA;AAEzC;;;;GAIG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAA;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,CAAC,MAAM,iCAAiC,GAC5C,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,CAAA;
|
|
1
|
+
{"version":3,"file":"chooseColumns.js","sourceRoot":"","sources":["../../src/AdaptiveGrid/chooseColumns.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RefObject } from "react";
|
|
2
|
-
import type { ColumnChoice } from "./chooseColumns.ts";
|
|
2
|
+
import type { ColumnChoice, GridLayout } from "./chooseColumns.ts";
|
|
3
3
|
/**
|
|
4
4
|
* The height-first column rule, wired to a real element.
|
|
5
5
|
*
|
|
@@ -96,7 +96,7 @@ export declare const readStoredChoice: ({ maxColumns, persistence, }: {
|
|
|
96
96
|
maxColumns?: number;
|
|
97
97
|
persistence: ColumnPersistence | undefined;
|
|
98
98
|
}) => ColumnChoice;
|
|
99
|
-
export declare const useAdaptiveColumns: <ContainerElement extends HTMLElement = HTMLDivElement>({ blockSizeResolver, chromeBlockSize, columnInlineSize, gutterInlineSize, itemBlockSize, itemCount, maxAutoColumns, maxManualColumns, minColumnInlineSize, persistence, singleInlineSize, storageKey, }: {
|
|
99
|
+
export declare const useAdaptiveColumns: <ContainerElement extends HTMLElement = HTMLDivElement>({ blockSizeResolver, chromeBlockSize, columnInlineSize, gutterInlineSize, gap, itemBlockSize, itemCount, layout, maxAutoColumns, maxManualColumns, minColumnInlineSize, persistence, singleInlineSize, storageKey, }: {
|
|
100
100
|
/** @see BlockSizeResolver. Defaults to the viewport. */
|
|
101
101
|
blockSizeResolver?: BlockSizeResolver;
|
|
102
102
|
/** Everything down the page that is not this grid, in CSS px. */
|
|
@@ -105,10 +105,14 @@ export declare const useAdaptiveColumns: <ContainerElement extends HTMLElement =
|
|
|
105
105
|
columnInlineSize?: string;
|
|
106
106
|
/** Override `contentInlineSize.gutter`. */
|
|
107
107
|
gutterInlineSize?: string;
|
|
108
|
+
/** Space between tracks for the fill layout. */
|
|
109
|
+
gap?: number;
|
|
108
110
|
/** One item's block size in CSS px. Measure it; err high. */
|
|
109
111
|
itemBlockSize: number;
|
|
110
112
|
/** How many items the grid is being asked to fit. */
|
|
111
113
|
itemCount: number;
|
|
114
|
+
/** Opt in to width-first media layout. */
|
|
115
|
+
layout?: GridLayout;
|
|
112
116
|
/** The most columns the automatic answer will take. */
|
|
113
117
|
maxAutoColumns?: number;
|
|
114
118
|
/** The most a person may ask for. One above the auto cap. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAdaptiveColumns.d.ts","sourceRoot":"","sources":["../../src/AdaptiveGrid/useAdaptiveColumns.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAStC,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"useAdaptiveColumns.d.ts","sourceRoot":"","sources":["../../src/AdaptiveGrid/useAdaptiveColumns.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAStC,OAAO,KAAK,EACV,YAAY,EACZ,UAAU,EACX,MAAM,oBAAoB,CAAA;AAS3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH;;;;;;;;;GASG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,GAAG,EAAE,MAAM,MAAM,CAAA;IACjB,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,IAAI,KAAK,MAAM,IAAI,CAAA;CAChD,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,MAAM,MAAM,GAAG,IAAI,CAAA;IACzB,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CAC/B,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,QAChC,iBAgBF,CAAA;AAEJ;;;;;;GAMG;AACH,eAAO,MAAM,6BAA6B,GACxC,KAAK,MAAM,KACV,iBAeD,CAAA;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,8BAG9B;IACD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,iBAAiB,GAAG,SAAS,CAAA;CAC3C,KAAG,YAkBH,CAAA;AAED,eAAO,MAAM,kBAAkB,GAC7B,gBAAgB,SAAS,WAAW,GAAG,cAAc,EACrD,sNAeC;IACD,wDAAwD;IACxD,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC,iEAAiE;IACjE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,2CAA2C;IAC3C,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,2CAA2C;IAC3C,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,gDAAgD;IAChD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,6DAA6D;IAC7D,aAAa,EAAE,MAAM,CAAA;IACrB,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAA;IACjB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,uDAAuD;IACvD,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,gDAAgD;IAChD,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;;;OAKG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAA;IAC/B,2CAA2C;IAC3C,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,KAAG;IACF,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAA;IACnB,oBAAoB;IACpB,OAAO,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,oBAAoB,EAAE,MAAM,CAAA;IAC5B;;;;OAIG;IACH,YAAY,EAAE,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;IAChD;;;;;;;;;;;;;;OAcG;IACH,SAAS,EAAE,OAAO,CAAA;IAClB,0CAA0C;IAC1C,MAAM,EAAE,YAAY,CAAA;IACpB,SAAS,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAA;CAkH1C,CAAA"}
|
|
@@ -70,7 +70,7 @@ export const readStoredChoice = ({ maxColumns = DEFAULT_MAX_MANUAL_COLUMNS, pers
|
|
|
70
70
|
}
|
|
71
71
|
return parsed;
|
|
72
72
|
};
|
|
73
|
-
export const useAdaptiveColumns = ({ blockSizeResolver, chromeBlockSize, columnInlineSize, gutterInlineSize, itemBlockSize, itemCount, maxAutoColumns = DEFAULT_MAX_AUTO_COLUMNS, maxManualColumns = DEFAULT_MAX_MANUAL_COLUMNS, minColumnInlineSize = DEFAULT_MIN_COLUMN_INLINE_SIZE_PX, persistence, singleInlineSize, storageKey, }) => {
|
|
73
|
+
export const useAdaptiveColumns = ({ blockSizeResolver, chromeBlockSize, columnInlineSize, gutterInlineSize, gap, itemBlockSize, itemCount, layout = "height-first", maxAutoColumns = DEFAULT_MAX_AUTO_COLUMNS, maxManualColumns = DEFAULT_MAX_MANUAL_COLUMNS, minColumnInlineSize = DEFAULT_MIN_COLUMN_INLINE_SIZE_PX, persistence, singleInlineSize, storageKey, }) => {
|
|
74
74
|
const containerRef = useRef(null);
|
|
75
75
|
// Built once, so the `resize` subscription and the storage
|
|
76
76
|
// handle survive re-renders and StrictMode's double mount — the
|
|
@@ -132,6 +132,8 @@ export const useAdaptiveColumns = ({ blockSizeResolver, chromeBlockSize, columnI
|
|
|
132
132
|
chromeBlockSize,
|
|
133
133
|
itemBlockSize,
|
|
134
134
|
itemCount,
|
|
135
|
+
gap,
|
|
136
|
+
layout,
|
|
135
137
|
maxColumns: maxAutoColumns,
|
|
136
138
|
minColumnInlineSize,
|
|
137
139
|
});
|
|
@@ -145,6 +147,7 @@ export const useAdaptiveColumns = ({ blockSizeResolver, chromeBlockSize, columnI
|
|
|
145
147
|
columnInlineSize,
|
|
146
148
|
columns,
|
|
147
149
|
gutterInlineSize,
|
|
150
|
+
layout,
|
|
148
151
|
singleInlineSize,
|
|
149
152
|
}),
|
|
150
153
|
isLaidOut: availableInlineSize > 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAdaptiveColumns.js","sourceRoot":"","sources":["../../src/AdaptiveGrid/useAdaptiveColumns.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EACX,SAAS,EACT,MAAM,EACN,QAAQ,EACR,oBAAoB,GACrB,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"useAdaptiveColumns.js","sourceRoot":"","sources":["../../src/AdaptiveGrid/useAdaptiveColumns.ts"],"names":[],"mappings":"AACA,OAAO,EACL,WAAW,EACX,SAAS,EACT,MAAM,EACN,QAAQ,EACR,oBAAoB,GACrB,MAAM,OAAO,CAAA;AAMd,OAAO,EACL,aAAa,EACb,wBAAwB,EACxB,0BAA0B,EAC1B,iCAAiC,EACjC,uBAAuB,GACxB,MAAM,oBAAoB,CAAA;AAuE3B;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,yBAAyB,GACpC,GAAsB,EAAE,CAAC,CAAC;IACxB,GAAG,EAAE,GAAG,EAAE,CACR,OAAO,MAAM,KAAK,WAAW;QAC3B,CAAC,CAAC,CAAC;QACH,CAAC,CAAC,MAAM,CAAC,WAAW;IACxB,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE;QACtB,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;YAClC,OAAO,GAAG,EAAE,GAAE,CAAC,CAAA;QACjB,CAAC;QAED,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;QAE3C,OAAO,GAAG,EAAE;YACV,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;QAChD,CAAC,CAAA;IACH,CAAC;CACF,CAAC,CAAA;AAEJ;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAC3C,GAAW,EACQ,EAAE,CAAC,CAAC;IACvB,IAAI,EAAE,GAAG,EAAE;QACT,IAAI,CAAC;YACH,OAAO,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QACzC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAA;QACb,CAAC;IACH,CAAC;IACD,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE;QACf,IAAI,CAAC;YACH,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QACzC,CAAC;QAAC,MAAM,CAAC;YACP,kDAAkD;QACpD,CAAC;IACH,CAAC;CACF,CAAC,CAAA;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,EAC/B,UAAU,GAAG,0BAA0B,EACvC,WAAW,GAIZ,EAAgB,EAAE;IACjB,MAAM,MAAM,GAAG,WAAW,EAAE,IAAI,EAAE,IAAI,IAAI,CAAA;IAE1C,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACzC,OAAO,MAAM,CAAA;IACf,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAA;IAE7B,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9B,OAAO,MAAM,CAAA;IACf,CAAC;IAED,IAAI,MAAM,GAAG,CAAC,IAAI,MAAM,GAAG,UAAU,EAAE,CAAC;QACtC,OAAO,MAAM,CAAA;IACf,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAEhC,EACA,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,GAAG,EACH,aAAa,EACb,SAAS,EACT,MAAM,GAAG,cAAc,EACvB,cAAc,GAAG,wBAAwB,EACzC,gBAAgB,GAAG,0BAA0B,EAC7C,mBAAmB,GAAG,iCAAiC,EACvD,WAAW,EACX,gBAAgB,EAChB,UAAU,GAyCX,EAmCC,EAAE;IACF,MAAM,YAAY,GAAG,MAAM,CAAmB,IAAI,CAAC,CAAA;IAEnD,2DAA2D;IAC3D,gEAAgE;IAChE,+DAA+D;IAC/D,QAAQ;IACR,MAAM,CAAC,KAAK,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC;QAC9B,SAAS,EACP,iBAAiB,IAAI,yBAAyB,EAAE;QAClD,WAAW,EACT,WAAW;YACX,CAAC,UAAU,KAAK,SAAS;gBACvB,CAAC,CAAC,SAAS;gBACX,CAAC,CAAC,6BAA6B,CAAC,UAAU,CAAC,CAAC;KACjD,CAAC,CAAC,CAAA;IAEH,MAAM,kBAAkB,GAAG,oBAAoB,CAC7C,KAAK,CAAC,SAAS,CAAC,SAAS,EACzB,KAAK,CAAC,SAAS,CAAC,GAAG;IACnB,8DAA8D;IAC9D,GAAG,EAAE,CAAC,CAAC,CACR,CAAA;IAED;;;;;;;;;OASG;IACH,MAAM,CAAC,mBAAmB,EAAE,sBAAsB,CAAC,GACjD,QAAQ,CAAC,CAAC,CAAC,CAAA;IAEb,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,CAAA;QAEtC,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAM;QACR,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE;YAC9C,MAAM,IAAI,GACR,KAAK,EAAE,WAAW,CAAC,KAAK;gBACxB,SAAS,CAAC,qBAAqB,EAAE,CAAC,KAAK,CAAA;YAEzC,yDAAyD;YACzD,yDAAyD;YACzD,0BAA0B;YAC1B,sBAAsB,CAAC,CAAC,OAAO,EAAE,EAAE,CACjC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAClC,CAAA;QACH,CAAC,CAAC,CAAA;QAEF,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;QAE3B,OAAO,GAAG,EAAE;YACV,QAAQ,CAAC,UAAU,EAAE,CAAA;QACvB,CAAC,CAAA;IACH,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,GAAG,QAAQ,CACxC,GAAG,EAAE,CACH,gBAAgB,CAAC;QACf,UAAU,EAAE,gBAAgB;QAC5B,WAAW,EAAE,KAAK,CAAC,WAAW;KAC/B,CAAC,CACL,CAAA;IAED,MAAM,SAAS,GAAG,WAAW,CAC3B,CAAC,IAAkB,EAAE,EAAE;QACrB,eAAe,CAAC,IAAI,CAAC,CAAA;QAErB,oDAAoD;QACpD,6DAA6D;QAC7D,OAAO;QACP,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAA;IACxC,CAAC,EACD,CAAC,KAAK,CAAC,CACR,CAAA;IAED,MAAM,WAAW,GAAG,aAAa,CAAC;QAChC,kBAAkB;QAClB,mBAAmB;QACnB,eAAe;QACf,aAAa;QACb,SAAS;QACT,GAAG;QACH,MAAM;QACN,UAAU,EAAE,cAAc;QAC1B,mBAAmB;KACpB,CAAC,CAAA;IAEF,MAAM,OAAO,GAAG,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAA;IAExD,OAAO;QACL,WAAW;QACX,MAAM;QACN,OAAO;QACP,YAAY;QACZ,oBAAoB,EAAE,uBAAuB,CAAC;YAC5C,gBAAgB;YAChB,OAAO;YACP,gBAAgB;YAChB,MAAM;YACN,gBAAgB;SACjB,CAAC;QACF,SAAS,EAAE,mBAAmB,GAAG,CAAC;QAClC,SAAS;KACV,CAAA;AACH,CAAC,CAAA"}
|
|
@@ -132,10 +132,10 @@ export declare const useAnchoredOverlay: ({ anchorRef, isEscapeDismissable, isHe
|
|
|
132
132
|
clonedTrigger: ReactElement<Record<string, unknown>, string | import("react").JSXElementConstructor<any>> | null;
|
|
133
133
|
context: {
|
|
134
134
|
placement: Placement;
|
|
135
|
-
strategy: import("@floating-ui/
|
|
135
|
+
strategy: import("@floating-ui/react").Strategy;
|
|
136
136
|
x: number;
|
|
137
137
|
y: number;
|
|
138
|
-
middlewareData: import("@floating-ui/
|
|
138
|
+
middlewareData: import("@floating-ui/react").MiddlewareData;
|
|
139
139
|
isPositioned: boolean;
|
|
140
140
|
update: () => void;
|
|
141
141
|
floatingStyles: React.CSSProperties;
|
package/dist/Picker/Picker.d.ts
CHANGED
|
@@ -84,6 +84,21 @@ export type PickerProps = Omit<ComponentPropsWithRef<"button">, "disabled" | "on
|
|
|
84
84
|
* mux-magic both moved to it while `id` was still being replaced — but
|
|
85
85
|
* `id` is no longer a trap. Everything else on the button (`data-*`,
|
|
86
86
|
* `onClick`, `form`, …) passes straight through.
|
|
87
|
+
*
|
|
88
|
+
* ### A stretched trigger reads like a select, not a button
|
|
89
|
+
*
|
|
90
|
+
* `Button` centers its content, which is right for an action and wrong
|
|
91
|
+
* for a value. Inside a `Field` (a `flex-col`, so the child stretches)
|
|
92
|
+
* or under `w-full`, the trigger used to become a bar with the value
|
|
93
|
+
* floating in the middle and the chevron beside it. The value now sits
|
|
94
|
+
* at the inline start, in a span that grows into the spare width and
|
|
95
|
+
* ends in an ellipsis when there is too little, and the chevron sits at
|
|
96
|
+
* the inline end. `justify-between` is passed through `className`, which
|
|
97
|
+
* `toClassName` merges over `Button`'s `justify-center` rather than
|
|
98
|
+
* leaving the two to stylesheet order — and the growing span makes the
|
|
99
|
+
* result correct even if a caller's `className` overrides it. A trigger
|
|
100
|
+
* at its content width has no spare width to distribute, so it renders
|
|
101
|
+
* exactly as it did before.
|
|
87
102
|
*/
|
|
88
103
|
export declare const Picker: ({ appearance, className, iconEnd, intent, isDisabled, label, onChange, options, placement, placeholder, size, value, ...buttonProps }: PickerProps) => ReactNode;
|
|
89
104
|
//# sourceMappingURL=Picker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Picker.d.ts","sourceRoot":"","sources":["../../src/Picker/Picker.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAGd,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"Picker.d.ts","sourceRoot":"","sources":["../../src/Picker/Picker.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACX,MAAM,qBAAqB,CAAA;AAC5B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,KAAK,EACV,qBAAqB,EACrB,SAAS,EACV,MAAM,OAAO,CAAA;AAGd,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAIzD,MAAM,MAAM,YAAY,GAAG,WAAW,CAAA;AAwBtC,MAAM,MAAM,WAAW,GAAG,IAAI,CAC5B,qBAAqB,CAAC,QAAQ,CAAC,EAC/B,UAAU,GAAG,UAAU,GAAG,OAAO,CAClC,GAAG;IACF,UAAU,CAAC,EAAE,gBAAgB,CAAA;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAA;IACb;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;IAChC,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,IAAI,CAAC,EAAE,WAAW,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqEG;AACH,eAAO,MAAM,MAAM,GAAI,uIAcpB,WAAW,KAAG,SA2EhB,CAAA"}
|
package/dist/Picker/Picker.js
CHANGED
|
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useVisibility } from "@charcuterie/logic";
|
|
3
3
|
import { Button } from "../Button/Button.js";
|
|
4
4
|
import { Listbox } from "../Listbox/Listbox.js";
|
|
5
|
+
import { toClassName } from "../toClassName.js";
|
|
5
6
|
/**
|
|
6
7
|
* A constant element rather than a component, for the same reason
|
|
7
8
|
* `Select` inlines its own: a second component in this file trips the
|
|
@@ -62,6 +63,21 @@ const CHEVRON_DOWN = (_jsx("svg", { "aria-hidden": "true", className: "size-4",
|
|
|
62
63
|
* mux-magic both moved to it while `id` was still being replaced — but
|
|
63
64
|
* `id` is no longer a trap. Everything else on the button (`data-*`,
|
|
64
65
|
* `onClick`, `form`, …) passes straight through.
|
|
66
|
+
*
|
|
67
|
+
* ### A stretched trigger reads like a select, not a button
|
|
68
|
+
*
|
|
69
|
+
* `Button` centers its content, which is right for an action and wrong
|
|
70
|
+
* for a value. Inside a `Field` (a `flex-col`, so the child stretches)
|
|
71
|
+
* or under `w-full`, the trigger used to become a bar with the value
|
|
72
|
+
* floating in the middle and the chevron beside it. The value now sits
|
|
73
|
+
* at the inline start, in a span that grows into the spare width and
|
|
74
|
+
* ends in an ellipsis when there is too little, and the chevron sits at
|
|
75
|
+
* the inline end. `justify-between` is passed through `className`, which
|
|
76
|
+
* `toClassName` merges over `Button`'s `justify-center` rather than
|
|
77
|
+
* leaving the two to stylesheet order — and the growing span makes the
|
|
78
|
+
* result correct even if a caller's `className` overrides it. A trigger
|
|
79
|
+
* at its content width has no spare width to distribute, so it renders
|
|
80
|
+
* exactly as it did before.
|
|
65
81
|
*/
|
|
66
82
|
export const Picker = ({ appearance = "outline", className, iconEnd = CHEVRON_DOWN, intent = "neutral", isDisabled = false, label, onChange, options, placement, placeholder, size, value, ...buttonProps }) => {
|
|
67
83
|
const { hide, isVisible, toggle } = useVisibility();
|
|
@@ -73,6 +89,14 @@ export const Picker = ({ appearance = "outline", className, iconEnd = CHEVRON_DO
|
|
|
73
89
|
? currentOption.label
|
|
74
90
|
: undefined));
|
|
75
91
|
const triggerText = currentText ?? placeholder ?? "";
|
|
92
|
+
const triggerContent = currentOption === undefined
|
|
93
|
+
? placeholder
|
|
94
|
+
: currentOption.label;
|
|
95
|
+
// No content, no span: an empty flex item would still earn a `gap`
|
|
96
|
+
// before the chevron and widen a content-width trigger.
|
|
97
|
+
const hasTriggerContent = triggerContent !== undefined &&
|
|
98
|
+
triggerContent !== null &&
|
|
99
|
+
triggerContent !== "";
|
|
76
100
|
return (_jsx(Listbox
|
|
77
101
|
// A disabled control must not open, and hiding the panel is a
|
|
78
102
|
// truer way to say that than trusting the trigger's click alone —
|
|
@@ -81,11 +105,9 @@ export const Picker = ({ appearance = "outline", className, iconEnd = CHEVRON_DO
|
|
|
81
105
|
// A disabled control must not open, and hiding the panel is a
|
|
82
106
|
// truer way to say that than trusting the trigger's click alone —
|
|
83
107
|
// Escape and outside-press both route through `onDismiss`.
|
|
84
|
-
isVisible: isVisible && !isDisabled, onDismiss: hide, onSelect: onChange, options: options, placement: placement, selectedValue: value, trigger: _jsx(Button, { ...buttonProps, appearance: appearance, "aria-label": `${label}: ${triggerText}`, className: className, iconEnd: iconEnd, intent: intent, isDisabled: isDisabled, onClick: (clickEvent) => {
|
|
108
|
+
isVisible: isVisible && !isDisabled, onDismiss: hide, onSelect: onChange, options: options, placement: placement, selectedValue: value, trigger: _jsx(Button, { ...buttonProps, appearance: appearance, "aria-label": `${label}: ${triggerText}`, className: toClassName("justify-between", className), iconEnd: iconEnd, intent: intent, isDisabled: isDisabled, onClick: (clickEvent) => {
|
|
85
109
|
buttonProps.onClick?.(clickEvent);
|
|
86
110
|
toggle();
|
|
87
|
-
}, size: size, type: "button", children:
|
|
88
|
-
? placeholder
|
|
89
|
-
: currentOption.label }) }));
|
|
111
|
+
}, size: size, type: "button", children: hasTriggerContent ? (_jsx("span", { className: "-mx-1 min-w-0 grow overflow-x-clip px-1 text-start text-ellipsis whitespace-nowrap", children: triggerContent })) : null }) }));
|
|
90
112
|
};
|
|
91
113
|
//# sourceMappingURL=Picker.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Picker.js","sourceRoot":"","sources":["../../src/Picker/Picker.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAWlD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAsB,CAAA;AAG7C,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"Picker.js","sourceRoot":"","sources":["../../src/Picker/Picker.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import type { GridLayout } from "../AdaptiveGrid/chooseColumns.ts";
|
|
2
3
|
import type { BlockSizeResolver } from "../AdaptiveGrid/useAdaptiveColumns.ts";
|
|
3
4
|
export type VirtualizedGridProps<Item> = {
|
|
4
5
|
/**
|
|
@@ -46,6 +47,8 @@ export type VirtualizedGridProps<Item> = {
|
|
|
46
47
|
* is one whose length a reader cannot count for itself.
|
|
47
48
|
*/
|
|
48
49
|
label?: string;
|
|
50
|
+
/** Opt in to width-first media layout. */
|
|
51
|
+
layout?: GridLayout;
|
|
49
52
|
/** The most columns the automatic answer will take. */
|
|
50
53
|
maxColumns?: number;
|
|
51
54
|
/** The narrowest a column may be, in CSS px. */
|
|
@@ -176,5 +179,5 @@ export type VirtualizedGridProps<Item> = {
|
|
|
176
179
|
* a cache — the same thing a routed tab does, and the reason a
|
|
177
180
|
* routed tab never had this bug.
|
|
178
181
|
*/
|
|
179
|
-
export declare const VirtualizedGrid: <Item>({ blockSizeResolver, chromeBlockSize, className, columns, gap, getItemKey, itemBlockSize, items, label, maxColumns, minColumnInlineSize, overscanRows, renderItem, storageKey, }: VirtualizedGridProps<Item>) => ReactNode;
|
|
182
|
+
export declare const VirtualizedGrid: <Item>({ blockSizeResolver, chromeBlockSize, className, columns, gap, getItemKey, itemBlockSize, items, label, layout: gridLayout, maxColumns, minColumnInlineSize, overscanRows, renderItem, storageKey, }: VirtualizedGridProps<Item>) => ReactNode;
|
|
180
183
|
//# sourceMappingURL=VirtualizedGrid.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VirtualizedGrid.d.ts","sourceRoot":"","sources":["../../src/VirtualizedGrid/VirtualizedGrid.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAA;AAY9E,MAAM,MAAM,oBAAoB,CAAC,IAAI,IAAI;IACvC;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,kDAAkD;IAClD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CACX,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,MAAM,KACV,MAAM,GAAG,MAAM,CAAA;IACpB;;;;;;;;OAQG;IACH,aAAa,EAAE,MAAM,CAAA;IACrB,kCAAkC;IAClC,KAAK,EAAE,SAAS,IAAI,EAAE,CAAA;IACtB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gDAAgD;IAChD,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,qBAAqB;IACrB,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,SAAS,CAAA;IACpD;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0GG;AACH,eAAO,MAAM,eAAe,GAAI,IAAI,EAAG,
|
|
1
|
+
{"version":3,"file":"VirtualizedGrid.d.ts","sourceRoot":"","sources":["../../src/VirtualizedGrid/VirtualizedGrid.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAA;AAClE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAA;AAY9E,MAAM,MAAM,oBAAoB,CAAC,IAAI,IAAI;IACvC;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,kDAAkD;IAClD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CACX,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,MAAM,KACV,MAAM,GAAG,MAAM,CAAA;IACpB;;;;;;;;OAQG;IACH,aAAa,EAAE,MAAM,CAAA;IACrB,kCAAkC;IAClC,KAAK,EAAE,SAAS,IAAI,EAAE,CAAA;IACtB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,0CAA0C;IAC1C,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gDAAgD;IAChD,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,qBAAqB;IACrB,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,KAAK,SAAS,CAAA;IACpD;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0GG;AACH,eAAO,MAAM,eAAe,GAAI,IAAI,EAAG,sMAgBpC,oBAAoB,CAAC,IAAI,CAAC,KAAG,SAmE/B,CAAA"}
|
|
@@ -113,12 +113,14 @@ import { WindowGrid } from "./WindowGrid.js";
|
|
|
113
113
|
* a cache — the same thing a routed tab does, and the reason a
|
|
114
114
|
* routed tab never had this bug.
|
|
115
115
|
*/
|
|
116
|
-
export const VirtualizedGrid = ({ blockSizeResolver, chromeBlockSize, className, columns, gap = DEFAULT_GRID_GAP_PX, getItemKey, itemBlockSize, items, label, maxColumns, minColumnInlineSize, overscanRows = 4, renderItem, storageKey, }) => {
|
|
116
|
+
export const VirtualizedGrid = ({ blockSizeResolver, chromeBlockSize, className, columns, gap = DEFAULT_GRID_GAP_PX, getItemKey, itemBlockSize, items, label, layout: gridLayout, maxColumns, minColumnInlineSize, overscanRows = 4, renderItem, storageKey, }) => {
|
|
117
117
|
const layout = useAdaptiveColumns({
|
|
118
118
|
blockSizeResolver,
|
|
119
119
|
chromeBlockSize,
|
|
120
120
|
itemBlockSize,
|
|
121
121
|
itemCount: items.length,
|
|
122
|
+
gap,
|
|
123
|
+
layout: gridLayout,
|
|
122
124
|
maxAutoColumns: maxColumns,
|
|
123
125
|
minColumnInlineSize,
|
|
124
126
|
storageKey,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VirtualizedGrid.js","sourceRoot":"","sources":["../../src/VirtualizedGrid/VirtualizedGrid.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"VirtualizedGrid.js","sourceRoot":"","sources":["../../src/VirtualizedGrid/VirtualizedGrid.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAIjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAA;AAC1E,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAmB,CAAA;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAA;AAK5D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAkB,CAAA;AA6E7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0GG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAQ,EACrC,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,OAAO,EACP,GAAG,GAAG,mBAAmB,EACzB,UAAU,EACV,aAAa,EACb,KAAK,EACL,KAAK,EACL,MAAM,EAAE,UAAU,EAClB,UAAU,EACV,mBAAmB,EACnB,YAAY,GAAG,CAAC,EAChB,UAAU,EACV,UAAU,GACiB,EAAa,EAAE;IAC1C,MAAM,MAAM,GAAG,kBAAkB,CAAC;QAChC,iBAAiB;QACjB,eAAe;QACf,aAAa;QACb,SAAS,EAAE,KAAK,CAAC,MAAM;QACvB,GAAG;QACH,MAAM,EAAE,UAAU;QAClB,cAAc,EAAE,UAAU;QAC1B,mBAAmB;QACnB,UAAU;KACX,CAAC,CAAA;IAEF,MAAM,YAAY,GAAG,OAAO,IAAI,MAAM,CAAC,OAAO,CAAA;IAE9C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,YAAY,CAAC,CAAA;IAEvD;;;;;OAKG;IACH,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GACjC,QAAQ,CAAqB,IAAI,CAAC,CAAA;IAEpC,eAAe,CAAC,GAAG,EAAE;QACnB,MAAM,OAAO,GAAG,MAAM,CAAC,YAAY,CAAC,OAAO,CAAA;QAE3C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAM;QACR,CAAC;QAED,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAA;IAC1C,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAA;IAEzB,MAAM,SAAS,GAA0B;QACvC,YAAY;QACZ,GAAG;QACH,UAAU;QACV,aAAa;QACb,KAAK;QACL,KAAK;QACL,YAAY;QACZ,UAAU;QACV,QAAQ;KACT,CAAA;IAED,OAAO,CACL,cACE,SAAS,EAAE,WAAW,CACpB,qBAAqB,EACrB,SAAS,CACV,EACD,GAAG,EAAE,MAAM,CAAC,YAAY,YAEvB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI;YAC3C,SAAS,CAAC,CAAC,CAAC,CACZ,KAAC,WAAW,OACN,SAAS,EACb,WAAW,EAAE,WAAW,GACxB,CACH,CAAC,CAAC,CAAC,WAAW,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,CACnC,KAAC,UAAU,OAAK,SAAS,GAAI,CAC9B,CAAC,CAAC,CAAC,IAAI,GACJ,CACP,CAAA;AACH,CAAC,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@charcuterie/ui",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.22.1",
|
|
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",
|
|
@@ -56,12 +56,6 @@
|
|
|
56
56
|
"dist",
|
|
57
57
|
"src"
|
|
58
58
|
],
|
|
59
|
-
"scripts": {
|
|
60
|
-
"build": "tsc -p tsconfig.build.json && node scripts/copyStyles.ts",
|
|
61
|
-
"prepack": "yarn build",
|
|
62
|
-
"test": "vitest",
|
|
63
|
-
"typecheck": "tsc --noEmit"
|
|
64
|
-
},
|
|
65
59
|
"dependencies": {
|
|
66
60
|
"@charcuterie/logic": "2.10.0",
|
|
67
61
|
"@charcuterie/tokens": "1.10.2",
|
|
@@ -161,11 +155,19 @@
|
|
|
161
155
|
"storybook": "^10.3.6",
|
|
162
156
|
"tailwindcss": "^4.3.0",
|
|
163
157
|
"typescript": "^6.0.3",
|
|
164
|
-
"vitest": "^4.1.8"
|
|
158
|
+
"vitest": "^4.1.8",
|
|
159
|
+
"axe-core": "4.12.1",
|
|
160
|
+
"@vitest/browser": "^4.1.8",
|
|
161
|
+
"@storybook/addon-docs": "^10.3.6"
|
|
165
162
|
},
|
|
166
163
|
"repository": {
|
|
167
164
|
"type": "git",
|
|
168
165
|
"url": "git+https://github.com/Sawtaytoes/charcuterie.git",
|
|
169
166
|
"directory": "packages/ui"
|
|
167
|
+
},
|
|
168
|
+
"scripts": {
|
|
169
|
+
"build": "tsc -p tsconfig.build.json && node scripts/copyStyles.ts",
|
|
170
|
+
"test": "vitest",
|
|
171
|
+
"typecheck": "tsc --noEmit"
|
|
170
172
|
}
|
|
171
173
|
}
|
|
@@ -16,6 +16,16 @@ across an ultrawide and nothing below the fold.
|
|
|
16
16
|
|
|
17
17
|
<Canvas of={AdaptiveGridStories.Playground} />
|
|
18
18
|
|
|
19
|
+
## Fill a media workspace
|
|
20
|
+
|
|
21
|
+
Opt in with `layout="fill"` when a gallery or printer fleet should spend the
|
|
22
|
+
available width even if its cards would fit in a taller stack. Set `maxColumns`
|
|
23
|
+
and `minColumnInlineSize` for the content. The shared 16px gap counts toward the
|
|
24
|
+
column floor. Multiple columns use the whole container; a lone column retains
|
|
25
|
+
its reading measure. The default remains height-first.
|
|
26
|
+
|
|
27
|
+
<Canvas of={AdaptiveGridStories.Fill} />
|
|
28
|
+
|
|
19
29
|
## Props
|
|
20
30
|
|
|
21
31
|
<Controls />
|
|
@@ -240,3 +240,20 @@ export const Interactive: Story = {
|
|
|
240
240
|
)
|
|
241
241
|
},
|
|
242
242
|
}
|
|
243
|
+
|
|
244
|
+
/** A small fleet and a gallery opt in to spending width first. */
|
|
245
|
+
export const Fill: Story = {
|
|
246
|
+
args: {
|
|
247
|
+
layout: "fill",
|
|
248
|
+
minColumnInlineSize: 300,
|
|
249
|
+
maxColumns: 6,
|
|
250
|
+
blockSizeResolver: fixedBlockSize(1920),
|
|
251
|
+
},
|
|
252
|
+
render: (gridProps) => (
|
|
253
|
+
<AdaptiveGrid {...gridProps}>
|
|
254
|
+
{NINE_ITEMS.slice(0, 6).map((label) => (
|
|
255
|
+
<Item key={label} label={label} />
|
|
256
|
+
))}
|
|
257
|
+
</AdaptiveGrid>
|
|
258
|
+
),
|
|
259
|
+
}
|
|
@@ -36,8 +36,13 @@ import * as stories from "./AdaptiveGrid.stories.tsx"
|
|
|
36
36
|
* all.
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
-
const {
|
|
40
|
-
|
|
39
|
+
const {
|
|
40
|
+
AllStates,
|
|
41
|
+
AllVariants,
|
|
42
|
+
Fill,
|
|
43
|
+
Playground,
|
|
44
|
+
Responsive,
|
|
45
|
+
} = composeStories(stories)
|
|
41
46
|
|
|
42
47
|
/**
|
|
43
48
|
* The grids this component drew.
|
|
@@ -241,3 +246,32 @@ test("the inline size is read from the container, not the window", async () => {
|
|
|
241
246
|
).toEqual([1, 2, 3])
|
|
242
247
|
})
|
|
243
248
|
})
|
|
249
|
+
|
|
250
|
+
test("fill uses the available container width and keeps its column floor", async () => {
|
|
251
|
+
const { canvasElement } = await mountAtInlineSize(
|
|
252
|
+
Fill,
|
|
253
|
+
1900,
|
|
254
|
+
)
|
|
255
|
+
await waitFor(async () => {
|
|
256
|
+
const grid = getGrid(canvasElement)
|
|
257
|
+
await expect(getTrackCount(grid)).toBe(6)
|
|
258
|
+
await expect(grid.style.maxInlineSize).toBe("none")
|
|
259
|
+
await expect(grid.getBoundingClientRect().width).toBe(
|
|
260
|
+
1900,
|
|
261
|
+
)
|
|
262
|
+
})
|
|
263
|
+
canvasElement.style.inlineSize = "700px"
|
|
264
|
+
await waitFor(
|
|
265
|
+
async () =>
|
|
266
|
+
await expect(
|
|
267
|
+
getTrackCount(getGrid(canvasElement)),
|
|
268
|
+
).toBe(2),
|
|
269
|
+
)
|
|
270
|
+
canvasElement.style.inlineSize = "384px"
|
|
271
|
+
await waitFor(
|
|
272
|
+
async () =>
|
|
273
|
+
await expect(
|
|
274
|
+
getTrackCount(getGrid(canvasElement)),
|
|
275
|
+
).toBe(1),
|
|
276
|
+
)
|
|
277
|
+
})
|
|
@@ -2,6 +2,7 @@ import type { ReactNode } from "react"
|
|
|
2
2
|
import { Children } from "react"
|
|
3
3
|
|
|
4
4
|
import { toClassName } from "../toClassName.ts"
|
|
5
|
+
import type { GridLayout } from "./chooseColumns.ts"
|
|
5
6
|
import { getContentMaxInlineSize } from "./chooseColumns.ts"
|
|
6
7
|
import type { BlockSizeResolver } from "./useAdaptiveColumns.ts"
|
|
7
8
|
import { useAdaptiveColumns } from "./useAdaptiveColumns.ts"
|
|
@@ -41,6 +42,8 @@ export type AdaptiveGridProps = {
|
|
|
41
42
|
* would be worse than one that is an item out.
|
|
42
43
|
*/
|
|
43
44
|
itemCount?: number
|
|
45
|
+
/** Fill the container with readable columns instead of spending height first. */
|
|
46
|
+
layout?: GridLayout
|
|
44
47
|
/** The most columns the automatic answer will take. */
|
|
45
48
|
maxColumns?: number
|
|
46
49
|
/** The narrowest a column may be, in CSS px. */
|
|
@@ -105,6 +108,7 @@ export const AdaptiveGrid = ({
|
|
|
105
108
|
columns,
|
|
106
109
|
itemBlockSize,
|
|
107
110
|
itemCount,
|
|
111
|
+
layout: gridLayout = "height-first",
|
|
108
112
|
maxColumns,
|
|
109
113
|
minColumnInlineSize,
|
|
110
114
|
storageKey,
|
|
@@ -114,6 +118,7 @@ export const AdaptiveGrid = ({
|
|
|
114
118
|
chromeBlockSize,
|
|
115
119
|
itemBlockSize,
|
|
116
120
|
itemCount: itemCount ?? Children.count(children),
|
|
121
|
+
layout: gridLayout,
|
|
117
122
|
maxAutoColumns: maxColumns,
|
|
118
123
|
minColumnInlineSize,
|
|
119
124
|
storageKey,
|
|
@@ -126,6 +131,7 @@ export const AdaptiveGrid = ({
|
|
|
126
131
|
// a one-column cap is the same bug as no cap at all, upside down.
|
|
127
132
|
const contentMaxInlineSize = getContentMaxInlineSize({
|
|
128
133
|
columns: drawnColumns,
|
|
134
|
+
layout: gridLayout,
|
|
129
135
|
})
|
|
130
136
|
|
|
131
137
|
return (
|
|
@@ -109,6 +109,9 @@ export const DEFAULT_MIN_COLUMN_INLINE_SIZE_PX =
|
|
|
109
109
|
*/
|
|
110
110
|
export type ColumnChoice = "auto" | number
|
|
111
111
|
|
|
112
|
+
/** Fill is an explicit width-first layout for media galleries and fleets. */
|
|
113
|
+
export type GridLayout = "height-first" | "fill"
|
|
114
|
+
|
|
112
115
|
/**
|
|
113
116
|
* The column count the automatic rule wants, for one measured space
|
|
114
117
|
* and one set of items.
|
|
@@ -119,6 +122,8 @@ export const chooseColumns = ({
|
|
|
119
122
|
chromeBlockSize = 0,
|
|
120
123
|
itemBlockSize,
|
|
121
124
|
itemCount,
|
|
125
|
+
gap = 16,
|
|
126
|
+
layout = "height-first",
|
|
122
127
|
maxColumns = DEFAULT_MAX_AUTO_COLUMNS,
|
|
123
128
|
minColumnInlineSize = DEFAULT_MIN_COLUMN_INLINE_SIZE_PX,
|
|
124
129
|
}: {
|
|
@@ -172,6 +177,10 @@ export const chooseColumns = ({
|
|
|
172
177
|
* finished loading would be worse than one that is an item out.
|
|
173
178
|
*/
|
|
174
179
|
itemCount: number
|
|
180
|
+
/** Height-first by default; fill spends the available container width. */
|
|
181
|
+
layout?: GridLayout
|
|
182
|
+
/** Space between tracks, in CSS px, used by fill. */
|
|
183
|
+
gap?: number
|
|
175
184
|
/** @see DEFAULT_MAX_AUTO_COLUMNS */
|
|
176
185
|
maxColumns?: number
|
|
177
186
|
/** @see DEFAULT_MIN_COLUMN_INLINE_SIZE_PX */
|
|
@@ -191,6 +200,20 @@ export const chooseColumns = ({
|
|
|
191
200
|
return 1
|
|
192
201
|
}
|
|
193
202
|
|
|
203
|
+
if (layout === "fill") {
|
|
204
|
+
const columnsThatFit = Math.max(
|
|
205
|
+
1,
|
|
206
|
+
Math.floor(
|
|
207
|
+
(availableInlineSize + gap) /
|
|
208
|
+
(minColumnInlineSize + gap),
|
|
209
|
+
),
|
|
210
|
+
)
|
|
211
|
+
return Math.max(
|
|
212
|
+
1,
|
|
213
|
+
Math.min(maxColumns, itemCount, columnsThatFit),
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
194
217
|
// Block size is the reason. How many items stack in the space,
|
|
195
218
|
// then how many stacks that many items need.
|
|
196
219
|
const rowsThatFit = Math.max(
|
|
@@ -235,6 +258,7 @@ export const getContentMaxInlineSize = ({
|
|
|
235
258
|
columnInlineSize = contentInlineSize.column,
|
|
236
259
|
columns,
|
|
237
260
|
gutterInlineSize = contentInlineSize.gutter,
|
|
261
|
+
layout = "height-first",
|
|
238
262
|
singleInlineSize = contentInlineSize.single,
|
|
239
263
|
}: {
|
|
240
264
|
/** What one column is worth, once there are several. */
|
|
@@ -242,12 +266,16 @@ export const getContentMaxInlineSize = ({
|
|
|
242
266
|
columns: number
|
|
243
267
|
/** Slack added on top of a multi-column cap, for the gaps. */
|
|
244
268
|
gutterInlineSize?: string
|
|
269
|
+
/** Fill uses all available width once there are multiple columns. */
|
|
270
|
+
layout?: GridLayout
|
|
245
271
|
/** The cap when there is only one column. */
|
|
246
272
|
singleInlineSize?: string
|
|
247
273
|
}): string =>
|
|
248
274
|
columns <= 1
|
|
249
275
|
? singleInlineSize
|
|
250
|
-
:
|
|
276
|
+
: layout === "fill"
|
|
277
|
+
? "none"
|
|
278
|
+
: `${Number.parseFloat(columnInlineSize) * columns + Number.parseFloat(gutterInlineSize)}rem`
|
|
251
279
|
|
|
252
280
|
/**
|
|
253
281
|
* The choices a manual picker offers: `"auto"`, then every count up
|
|
@@ -7,7 +7,10 @@ import {
|
|
|
7
7
|
useSyncExternalStore,
|
|
8
8
|
} from "react"
|
|
9
9
|
|
|
10
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
ColumnChoice,
|
|
12
|
+
GridLayout,
|
|
13
|
+
} from "./chooseColumns.ts"
|
|
11
14
|
import {
|
|
12
15
|
chooseColumns,
|
|
13
16
|
DEFAULT_MAX_AUTO_COLUMNS,
|
|
@@ -180,8 +183,10 @@ export const useAdaptiveColumns = <
|
|
|
180
183
|
chromeBlockSize,
|
|
181
184
|
columnInlineSize,
|
|
182
185
|
gutterInlineSize,
|
|
186
|
+
gap,
|
|
183
187
|
itemBlockSize,
|
|
184
188
|
itemCount,
|
|
189
|
+
layout = "height-first",
|
|
185
190
|
maxAutoColumns = DEFAULT_MAX_AUTO_COLUMNS,
|
|
186
191
|
maxManualColumns = DEFAULT_MAX_MANUAL_COLUMNS,
|
|
187
192
|
minColumnInlineSize = DEFAULT_MIN_COLUMN_INLINE_SIZE_PX,
|
|
@@ -197,10 +202,14 @@ export const useAdaptiveColumns = <
|
|
|
197
202
|
columnInlineSize?: string
|
|
198
203
|
/** Override `contentInlineSize.gutter`. */
|
|
199
204
|
gutterInlineSize?: string
|
|
205
|
+
/** Space between tracks for the fill layout. */
|
|
206
|
+
gap?: number
|
|
200
207
|
/** One item's block size in CSS px. Measure it; err high. */
|
|
201
208
|
itemBlockSize: number
|
|
202
209
|
/** How many items the grid is being asked to fit. */
|
|
203
210
|
itemCount: number
|
|
211
|
+
/** Opt in to width-first media layout. */
|
|
212
|
+
layout?: GridLayout
|
|
204
213
|
/** The most columns the automatic answer will take. */
|
|
205
214
|
maxAutoColumns?: number
|
|
206
215
|
/** The most a person may ask for. One above the auto cap. */
|
|
@@ -349,6 +358,8 @@ export const useAdaptiveColumns = <
|
|
|
349
358
|
chromeBlockSize,
|
|
350
359
|
itemBlockSize,
|
|
351
360
|
itemCount,
|
|
361
|
+
gap,
|
|
362
|
+
layout,
|
|
352
363
|
maxColumns: maxAutoColumns,
|
|
353
364
|
minColumnInlineSize,
|
|
354
365
|
})
|
|
@@ -364,6 +375,7 @@ export const useAdaptiveColumns = <
|
|
|
364
375
|
columnInlineSize,
|
|
365
376
|
columns,
|
|
366
377
|
gutterInlineSize,
|
|
378
|
+
layout,
|
|
367
379
|
singleInlineSize,
|
|
368
380
|
}),
|
|
369
381
|
isLaidOut: availableInlineSize > 0,
|
|
@@ -46,8 +46,12 @@ test("copies the value and confirms in place", async () => {
|
|
|
46
46
|
await expect(copy).toHaveBeenCalledWith("335590")
|
|
47
47
|
// The press and its proof are the same object — no toast, no
|
|
48
48
|
// second place to look.
|
|
49
|
+
//
|
|
50
|
+
// `findBy`: `copy` is async, so the label changes a render after
|
|
51
|
+
// the click resolves. A synchronous read lost that race in CI's
|
|
52
|
+
// ultrawide window (master run 37298376242).
|
|
49
53
|
await expect(
|
|
50
|
-
canvas.
|
|
54
|
+
await canvas.findByRole("button", { name: "Copied" }),
|
|
51
55
|
).toBeVisible()
|
|
52
56
|
})
|
|
53
57
|
|
|
@@ -73,7 +77,9 @@ test("says so when the clipboard refuses", async () => {
|
|
|
73
77
|
)
|
|
74
78
|
|
|
75
79
|
await expect(
|
|
76
|
-
canvas.
|
|
80
|
+
await canvas.findByRole("button", {
|
|
81
|
+
name: "Copy failed",
|
|
82
|
+
}),
|
|
77
83
|
).toBeVisible()
|
|
78
84
|
await expect(onCopy).toHaveBeenCalledWith(false, "335590")
|
|
79
85
|
})
|
|
@@ -103,26 +109,30 @@ test("tells the caller what happened, both ways", async () => {
|
|
|
103
109
|
test("announces the outcome in a status region", async () => {
|
|
104
110
|
const { canvas } = await mountStory(Confirmed)
|
|
105
111
|
|
|
106
|
-
await
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
await waitFor(async () => {
|
|
113
|
+
await expect(
|
|
114
|
+
canvas.getByRole("status"),
|
|
115
|
+
).toHaveTextContent("Copied to the clipboard.")
|
|
116
|
+
})
|
|
109
117
|
})
|
|
110
118
|
|
|
111
119
|
test("the failure says what to do instead", async () => {
|
|
112
120
|
const { canvas } = await mountStory(Refused)
|
|
113
121
|
|
|
114
|
-
await
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
await waitFor(async () => {
|
|
123
|
+
await expect(
|
|
124
|
+
canvas.getByRole("status"),
|
|
125
|
+
).toHaveTextContent(
|
|
126
|
+
"The clipboard refused. Select the value and press Control C.",
|
|
127
|
+
)
|
|
128
|
+
})
|
|
119
129
|
})
|
|
120
130
|
|
|
121
131
|
/** The confirmation goes back on its own. */
|
|
122
132
|
test("returns to the resting label", async () => {
|
|
123
133
|
const { canvas } = await mountStory(
|
|
124
134
|
composeStory(
|
|
125
|
-
{ args: { confirmDuration:
|
|
135
|
+
{ args: { confirmDuration: 500, copy: () => true } },
|
|
126
136
|
meta,
|
|
127
137
|
),
|
|
128
138
|
)
|
|
@@ -130,8 +140,11 @@ test("returns to the resting label", async () => {
|
|
|
130
140
|
await userEvent.click(
|
|
131
141
|
canvas.getByRole("button", { name: "Copy" }),
|
|
132
142
|
)
|
|
143
|
+
// 500ms, not 20: on a loaded runner a 20ms confirmation can come
|
|
144
|
+
// and go before the test looks, which fails the read below for the
|
|
145
|
+
// opposite reason to the race above.
|
|
133
146
|
await expect(
|
|
134
|
-
canvas.
|
|
147
|
+
await canvas.findByRole("button", { name: "Copied" }),
|
|
135
148
|
).toBeVisible()
|
|
136
149
|
|
|
137
150
|
await waitFor(async () => {
|
package/src/Picker/Picker.mdx
CHANGED
|
@@ -77,6 +77,20 @@ all flush; they are all the same trap.
|
|
|
77
77
|
|
|
78
78
|
<Canvas of={PickerStories.NextToText} />
|
|
79
79
|
|
|
80
|
+
## A stretched trigger reads like a select
|
|
81
|
+
|
|
82
|
+
`Picker` is a `Button`, and a `Button` centers its content. Inside a `Field` — a `flex-col`,
|
|
83
|
+
so the trigger stretches to the column — or under `w-full`, that used to give a full-width bar
|
|
84
|
+
with the value floating in the middle. The trigger now lays its value out at the **inline
|
|
85
|
+
start** and the chevron at the **inline end**, and a value wider than the trigger truncates
|
|
86
|
+
with an ellipsis rather than pushing the chevron out. The full value stays in the accessible
|
|
87
|
+
name and in the open list.
|
|
88
|
+
|
|
89
|
+
The width still comes from the container: a trigger the parent does not stretch keeps its
|
|
90
|
+
content width and renders exactly as before.
|
|
91
|
+
|
|
92
|
+
<Canvas of={PickerStories.Responsive} />
|
|
93
|
+
|
|
80
94
|
## `id` does not survive; `data-testid` does
|
|
81
95
|
|
|
82
96
|
`useAnchoredOverlay` overwrites the trigger's `id` with a generated one so the portalled
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "../argTypes.storyHelpers.ts"
|
|
11
11
|
import { Badge } from "../Badge/Badge.tsx"
|
|
12
12
|
import {
|
|
13
|
+
ContainerBoard,
|
|
13
14
|
StoryRow,
|
|
14
15
|
StorySection,
|
|
15
16
|
} from "../board.storyHelpers.tsx"
|
|
@@ -258,3 +259,48 @@ export const NextToText: Story = {
|
|
|
258
259
|
</div>
|
|
259
260
|
),
|
|
260
261
|
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* One value long enough to outgrow the narrowest container, so the
|
|
265
|
+
* board shows the truncation as well as the stretch.
|
|
266
|
+
*/
|
|
267
|
+
const SUBTITLES: PickerOption[] = [
|
|
268
|
+
{ label: "Off", value: "off" },
|
|
269
|
+
{
|
|
270
|
+
label: "English (SDH, signs and songs, full dialogue)",
|
|
271
|
+
value: "eng-sdh",
|
|
272
|
+
},
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Inside a `Field` the trigger stretches to the container, the way a
|
|
277
|
+
* select does: the value sits at the inline start, the chevron at the
|
|
278
|
+
* inline end, and a value wider than the container truncates rather
|
|
279
|
+
* than pushing the chevron out.
|
|
280
|
+
*/
|
|
281
|
+
export const Responsive: Story = {
|
|
282
|
+
render: () => (
|
|
283
|
+
<ContainerBoard>
|
|
284
|
+
{(width) => (
|
|
285
|
+
<div className="flex flex-col gap-4">
|
|
286
|
+
<Field label={`Language at ${width}`}>
|
|
287
|
+
<Picker
|
|
288
|
+
label={`Language at ${width}`}
|
|
289
|
+
onChange={() => undefined}
|
|
290
|
+
options={LANGUAGES}
|
|
291
|
+
value="eng"
|
|
292
|
+
/>
|
|
293
|
+
</Field>
|
|
294
|
+
<Field label={`Subtitles at ${width}`}>
|
|
295
|
+
<Picker
|
|
296
|
+
label={`Subtitles at ${width}`}
|
|
297
|
+
onChange={() => undefined}
|
|
298
|
+
options={SUBTITLES}
|
|
299
|
+
value="eng-sdh"
|
|
300
|
+
/>
|
|
301
|
+
</Field>
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
304
|
+
</ContainerBoard>
|
|
305
|
+
),
|
|
306
|
+
}
|
|
@@ -7,8 +7,13 @@ import { mountStory } from "../mountStory.testHelpers.ts"
|
|
|
7
7
|
import { expectAgentDrivable } from "../testing/index.ts"
|
|
8
8
|
import * as stories from "./Picker.stories.tsx"
|
|
9
9
|
|
|
10
|
-
const {
|
|
11
|
-
|
|
10
|
+
const {
|
|
11
|
+
AllStates,
|
|
12
|
+
Playground,
|
|
13
|
+
Interactive,
|
|
14
|
+
NextToText,
|
|
15
|
+
Responsive,
|
|
16
|
+
} = composeStories(stories)
|
|
12
17
|
|
|
13
18
|
test("the trigger's accessible name carries the current value", async () => {
|
|
14
19
|
const { canvas, canvasElement } =
|
|
@@ -108,3 +113,87 @@ test("a rich option label keeps a plain-string trigger via textValue", async ()
|
|
|
108
113
|
|
|
109
114
|
await expectNoAxeViolations(canvasElement)
|
|
110
115
|
})
|
|
116
|
+
|
|
117
|
+
test("a stretched trigger puts the value at the start and the chevron at the end", async () => {
|
|
118
|
+
const { canvas, canvasElement } =
|
|
119
|
+
await mountStory(Responsive)
|
|
120
|
+
|
|
121
|
+
const trigger = expectAgentDrivable(canvas, {
|
|
122
|
+
name: "Language at 34rem: English",
|
|
123
|
+
role: "button",
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const valueElement = canvas
|
|
127
|
+
.getAllByText("English")
|
|
128
|
+
.find((element: HTMLElement) =>
|
|
129
|
+
trigger.contains(element),
|
|
130
|
+
) as HTMLElement
|
|
131
|
+
|
|
132
|
+
// The text's own box, not the span's: the span grows to fill the
|
|
133
|
+
// trigger, so its box would sit at the start even with centered text.
|
|
134
|
+
const valueRange = document.createRange()
|
|
135
|
+
|
|
136
|
+
valueRange.selectNodeContents(valueElement)
|
|
137
|
+
|
|
138
|
+
const valueBox = valueRange.getBoundingClientRect()
|
|
139
|
+
const triggerBox = trigger.getBoundingClientRect()
|
|
140
|
+
const chevronBox = (
|
|
141
|
+
trigger.querySelector("svg") as SVGElement
|
|
142
|
+
).getBoundingClientRect()
|
|
143
|
+
|
|
144
|
+
const triggerStyle = getComputedStyle(trigger)
|
|
145
|
+
const contentStart =
|
|
146
|
+
triggerBox.left +
|
|
147
|
+
Number.parseFloat(triggerStyle.borderInlineStartWidth) +
|
|
148
|
+
Number.parseFloat(triggerStyle.paddingInlineStart)
|
|
149
|
+
const contentEnd =
|
|
150
|
+
triggerBox.right -
|
|
151
|
+
Number.parseFloat(triggerStyle.borderInlineEndWidth) -
|
|
152
|
+
Number.parseFloat(triggerStyle.paddingInlineEnd)
|
|
153
|
+
|
|
154
|
+
// The `Field` stretched the trigger well past its content, so a
|
|
155
|
+
// centered value would start far from the inline start.
|
|
156
|
+
await expect(
|
|
157
|
+
triggerBox.width - valueBox.width - chevronBox.width,
|
|
158
|
+
).toBeGreaterThan(200)
|
|
159
|
+
await expect(
|
|
160
|
+
Math.abs(valueBox.left - contentStart),
|
|
161
|
+
).toBeLessThan(1)
|
|
162
|
+
await expect(
|
|
163
|
+
Math.abs(chevronBox.right - contentEnd),
|
|
164
|
+
).toBeLessThan(1)
|
|
165
|
+
|
|
166
|
+
await expectNoAxeViolations(canvasElement)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
test("a value wider than the trigger truncates and keeps the chevron inside", async () => {
|
|
170
|
+
const { canvas } = await mountStory(Responsive)
|
|
171
|
+
|
|
172
|
+
const trigger = expectAgentDrivable(canvas, {
|
|
173
|
+
name: "Subtitles at 15rem: English (SDH, signs and songs, full dialogue)",
|
|
174
|
+
role: "button",
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const valueElement = canvas
|
|
178
|
+
.getAllByText(
|
|
179
|
+
"English (SDH, signs and songs, full dialogue)",
|
|
180
|
+
)
|
|
181
|
+
.find((element: HTMLElement) =>
|
|
182
|
+
trigger.contains(element),
|
|
183
|
+
) as HTMLElement
|
|
184
|
+
|
|
185
|
+
const triggerBox = trigger.getBoundingClientRect()
|
|
186
|
+
const chevronBox = (
|
|
187
|
+
trigger.querySelector("svg") as SVGElement
|
|
188
|
+
).getBoundingClientRect()
|
|
189
|
+
|
|
190
|
+
await expect(valueElement.scrollWidth).toBeGreaterThan(
|
|
191
|
+
valueElement.clientWidth,
|
|
192
|
+
)
|
|
193
|
+
await expect(trigger.scrollWidth).toBe(
|
|
194
|
+
trigger.clientWidth,
|
|
195
|
+
)
|
|
196
|
+
await expect(chevronBox.right).toBeLessThanOrEqual(
|
|
197
|
+
triggerBox.right,
|
|
198
|
+
)
|
|
199
|
+
})
|
package/src/Picker/Picker.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import { Button } from "../Button/Button.tsx"
|
|
|
13
13
|
import type { IntentAppearance } from "../intentStyles.ts"
|
|
14
14
|
import type { ListboxItem } from "../Listbox/Listbox.tsx"
|
|
15
15
|
import { Listbox } from "../Listbox/Listbox.tsx"
|
|
16
|
+
import { toClassName } from "../toClassName.ts"
|
|
16
17
|
|
|
17
18
|
export type PickerOption = ListboxItem
|
|
18
19
|
|
|
@@ -122,6 +123,21 @@ export type PickerProps = Omit<
|
|
|
122
123
|
* mux-magic both moved to it while `id` was still being replaced — but
|
|
123
124
|
* `id` is no longer a trap. Everything else on the button (`data-*`,
|
|
124
125
|
* `onClick`, `form`, …) passes straight through.
|
|
126
|
+
*
|
|
127
|
+
* ### A stretched trigger reads like a select, not a button
|
|
128
|
+
*
|
|
129
|
+
* `Button` centers its content, which is right for an action and wrong
|
|
130
|
+
* for a value. Inside a `Field` (a `flex-col`, so the child stretches)
|
|
131
|
+
* or under `w-full`, the trigger used to become a bar with the value
|
|
132
|
+
* floating in the middle and the chevron beside it. The value now sits
|
|
133
|
+
* at the inline start, in a span that grows into the spare width and
|
|
134
|
+
* ends in an ellipsis when there is too little, and the chevron sits at
|
|
135
|
+
* the inline end. `justify-between` is passed through `className`, which
|
|
136
|
+
* `toClassName` merges over `Button`'s `justify-center` rather than
|
|
137
|
+
* leaving the two to stylesheet order — and the growing span makes the
|
|
138
|
+
* result correct even if a caller's `className` overrides it. A trigger
|
|
139
|
+
* at its content width has no spare width to distribute, so it renders
|
|
140
|
+
* exactly as it did before.
|
|
125
141
|
*/
|
|
126
142
|
export const Picker = ({
|
|
127
143
|
appearance = "outline",
|
|
@@ -154,6 +170,18 @@ export const Picker = ({
|
|
|
154
170
|
|
|
155
171
|
const triggerText = currentText ?? placeholder ?? ""
|
|
156
172
|
|
|
173
|
+
const triggerContent =
|
|
174
|
+
currentOption === undefined
|
|
175
|
+
? placeholder
|
|
176
|
+
: currentOption.label
|
|
177
|
+
|
|
178
|
+
// No content, no span: an empty flex item would still earn a `gap`
|
|
179
|
+
// before the chevron and widen a content-width trigger.
|
|
180
|
+
const hasTriggerContent =
|
|
181
|
+
triggerContent !== undefined &&
|
|
182
|
+
triggerContent !== null &&
|
|
183
|
+
triggerContent !== ""
|
|
184
|
+
|
|
157
185
|
return (
|
|
158
186
|
<Listbox
|
|
159
187
|
// A disabled control must not open, and hiding the panel is a
|
|
@@ -170,7 +198,10 @@ export const Picker = ({
|
|
|
170
198
|
{...buttonProps}
|
|
171
199
|
appearance={appearance}
|
|
172
200
|
aria-label={`${label}: ${triggerText}`}
|
|
173
|
-
className={
|
|
201
|
+
className={toClassName(
|
|
202
|
+
"justify-between",
|
|
203
|
+
className,
|
|
204
|
+
)}
|
|
174
205
|
iconEnd={iconEnd}
|
|
175
206
|
intent={intent}
|
|
176
207
|
isDisabled={isDisabled}
|
|
@@ -182,9 +213,17 @@ export const Picker = ({
|
|
|
182
213
|
size={size}
|
|
183
214
|
type="button"
|
|
184
215
|
>
|
|
185
|
-
{
|
|
186
|
-
|
|
187
|
-
|
|
216
|
+
{hasTriggerContent ? (
|
|
217
|
+
// Not `truncate`. Its `overflow: hidden` clips the
|
|
218
|
+
// anti-aliased edge pixels of glyphs and a rich label's
|
|
219
|
+
// badge, so a content-width trigger stopped rendering
|
|
220
|
+
// pixel-identically to the unwrapped one. `overflow-x-clip`
|
|
221
|
+
// leaves the block axis alone, and `-mx-1 px-1` moves the
|
|
222
|
+
// inline clip edge 4px outward without moving the text.
|
|
223
|
+
<span className="-mx-1 min-w-0 grow overflow-x-clip px-1 text-start text-ellipsis whitespace-nowrap">
|
|
224
|
+
{triggerContent}
|
|
225
|
+
</span>
|
|
226
|
+
) : null}
|
|
188
227
|
</Button>
|
|
189
228
|
}
|
|
190
229
|
/>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from "react"
|
|
2
2
|
import { useLayoutEffect, useState } from "react"
|
|
3
3
|
|
|
4
|
+
import type { GridLayout } from "../AdaptiveGrid/chooseColumns.ts"
|
|
4
5
|
import type { BlockSizeResolver } from "../AdaptiveGrid/useAdaptiveColumns.ts"
|
|
5
6
|
import { useAdaptiveColumns } from "../AdaptiveGrid/useAdaptiveColumns.ts"
|
|
6
7
|
import { toClassName } from "../toClassName.ts"
|
|
@@ -62,6 +63,8 @@ export type VirtualizedGridProps<Item> = {
|
|
|
62
63
|
* is one whose length a reader cannot count for itself.
|
|
63
64
|
*/
|
|
64
65
|
label?: string
|
|
66
|
+
/** Opt in to width-first media layout. */
|
|
67
|
+
layout?: GridLayout
|
|
65
68
|
/** The most columns the automatic answer will take. */
|
|
66
69
|
maxColumns?: number
|
|
67
70
|
/** The narrowest a column may be, in CSS px. */
|
|
@@ -203,6 +206,7 @@ export const VirtualizedGrid = <Item,>({
|
|
|
203
206
|
itemBlockSize,
|
|
204
207
|
items,
|
|
205
208
|
label,
|
|
209
|
+
layout: gridLayout,
|
|
206
210
|
maxColumns,
|
|
207
211
|
minColumnInlineSize,
|
|
208
212
|
overscanRows = 4,
|
|
@@ -214,6 +218,8 @@ export const VirtualizedGrid = <Item,>({
|
|
|
214
218
|
chromeBlockSize,
|
|
215
219
|
itemBlockSize,
|
|
216
220
|
itemCount: items.length,
|
|
221
|
+
gap,
|
|
222
|
+
layout: gridLayout,
|
|
217
223
|
maxAutoColumns: maxColumns,
|
|
218
224
|
minColumnInlineSize,
|
|
219
225
|
storageKey,
|
|
@@ -349,11 +349,16 @@ test("inline markup in link text does not drop the document to raw source", asyn
|
|
|
349
349
|
// No raw source anywhere in the document.
|
|
350
350
|
const article = canvas.getByRole("article")
|
|
351
351
|
|
|
352
|
-
|
|
352
|
+
// The heading can paint before lower link decorations finish parsing.
|
|
353
|
+
// Retry the complete contract rather than treating that partial paint
|
|
354
|
+
// as the final document; all three source-marker assertions still apply.
|
|
355
|
+
await waitFor(async () => {
|
|
356
|
+
await expect(article.textContent).not.toContain("](")
|
|
353
357
|
|
|
354
|
-
|
|
358
|
+
await expect(article.textContent).not.toContain("**")
|
|
355
359
|
|
|
356
|
-
|
|
360
|
+
await expect(article.textContent).not.toContain("~~")
|
|
361
|
+
})
|
|
357
362
|
})
|
|
358
363
|
|
|
359
364
|
/**
|
|
@@ -420,6 +425,14 @@ test("a code span is smaller than the prose it sits in", async () => {
|
|
|
420
425
|
|
|
421
426
|
const article = canvas.getByRole("article")
|
|
422
427
|
|
|
428
|
+
// CodeMirror can finish its syntax decorations after the story mounts.
|
|
429
|
+
// Wait for the measured span itself rather than capturing a stale null.
|
|
430
|
+
await waitFor(() => {
|
|
431
|
+
expect(
|
|
432
|
+
article.querySelector(".cm-md-code"),
|
|
433
|
+
).not.toBeNull()
|
|
434
|
+
})
|
|
435
|
+
|
|
423
436
|
const code = article.querySelector(".cm-md-code")
|
|
424
437
|
|
|
425
438
|
await expect(code).not.toBeNull()
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* and does not fail `vitest` — MDX resolves `of` at *runtime*, so a
|
|
7
7
|
* renamed story turns into `of={undefined}` and the docs page
|
|
8
8
|
* renders Storybook's "component failed to render" panel instead of
|
|
9
|
-
* the page. `
|
|
9
|
+
* the page. `pnpm smoke:storybook` catches it, which is good, but
|
|
10
10
|
* only after a full Storybook build; this catches it in under a
|
|
11
11
|
* second, which is the difference between noticing during the edit
|
|
12
12
|
* and noticing in CI.
|