@charcuterie/ui 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +11 -0
  2. package/dist/AdaptiveGrid/AdaptiveGrid.d.ts +94 -0
  3. package/dist/AdaptiveGrid/AdaptiveGrid.d.ts.map +1 -0
  4. package/dist/AdaptiveGrid/AdaptiveGrid.js +73 -0
  5. package/dist/AdaptiveGrid/AdaptiveGrid.js.map +1 -0
  6. package/dist/AdaptiveGrid/chooseColumns.d.ts +206 -0
  7. package/dist/AdaptiveGrid/chooseColumns.d.ts.map +1 -0
  8. package/dist/AdaptiveGrid/chooseColumns.js +156 -0
  9. package/dist/AdaptiveGrid/chooseColumns.js.map +1 -0
  10. package/dist/AdaptiveGrid/useAdaptiveColumns.d.ts +155 -0
  11. package/dist/AdaptiveGrid/useAdaptiveColumns.d.ts.map +1 -0
  12. package/dist/AdaptiveGrid/useAdaptiveColumns.js +153 -0
  13. package/dist/AdaptiveGrid/useAdaptiveColumns.js.map +1 -0
  14. package/dist/Button/Button.d.ts.map +1 -1
  15. package/dist/Button/Button.js +11 -6
  16. package/dist/Button/Button.js.map +1 -1
  17. package/dist/ButtonLink/ButtonLink.d.ts +59 -0
  18. package/dist/ButtonLink/ButtonLink.d.ts.map +1 -0
  19. package/dist/ButtonLink/ButtonLink.js +69 -0
  20. package/dist/ButtonLink/ButtonLink.js.map +1 -0
  21. package/dist/Combobox/Combobox.d.ts.map +1 -1
  22. package/dist/Combobox/Combobox.js +1 -1
  23. package/dist/Combobox/Combobox.js.map +1 -1
  24. package/dist/Dialog/Dialog.js +1 -1
  25. package/dist/Dialog/Dialog.js.map +1 -1
  26. package/dist/LogViewer/LogViewer.d.ts.map +1 -1
  27. package/dist/LogViewer/LogViewer.js +33 -1
  28. package/dist/LogViewer/LogViewer.js.map +1 -1
  29. package/dist/RouterLink/AnchorLink.d.ts +17 -0
  30. package/dist/RouterLink/AnchorLink.d.ts.map +1 -0
  31. package/dist/RouterLink/AnchorLink.js +16 -0
  32. package/dist/RouterLink/AnchorLink.js.map +1 -0
  33. package/dist/RouterLink/RouterLinkProvider.d.ts +13 -0
  34. package/dist/RouterLink/RouterLinkProvider.d.ts.map +1 -0
  35. package/dist/RouterLink/RouterLinkProvider.js +47 -0
  36. package/dist/RouterLink/RouterLinkProvider.js.map +1 -0
  37. package/dist/RouterLink/routerLink.d.ts +45 -0
  38. package/dist/RouterLink/routerLink.d.ts.map +1 -0
  39. package/dist/RouterLink/routerLink.js +31 -0
  40. package/dist/RouterLink/routerLink.js.map +1 -0
  41. package/dist/TextLink/TextLink.d.ts +69 -0
  42. package/dist/TextLink/TextLink.d.ts.map +1 -0
  43. package/dist/TextLink/TextLink.js +79 -0
  44. package/dist/TextLink/TextLink.js.map +1 -0
  45. package/dist/controlStyles.d.ts +36 -1
  46. package/dist/controlStyles.d.ts.map +1 -1
  47. package/dist/controlStyles.js +30 -0
  48. package/dist/controlStyles.js.map +1 -1
  49. package/dist/index.d.ts +16 -2
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +10 -2
  52. package/dist/index.js.map +1 -1
  53. package/dist/intentStyles.d.ts +17 -0
  54. package/dist/intentStyles.d.ts.map +1 -1
  55. package/dist/intentStyles.js +17 -0
  56. package/dist/intentStyles.js.map +1 -1
  57. package/dist/reactRouter/ReactRouterLink.d.ts +23 -0
  58. package/dist/reactRouter/ReactRouterLink.d.ts.map +1 -0
  59. package/dist/reactRouter/ReactRouterLink.js +23 -0
  60. package/dist/reactRouter/ReactRouterLink.js.map +1 -0
  61. package/dist/reactRouter/index.d.ts +29 -0
  62. package/dist/reactRouter/index.d.ts.map +1 -0
  63. package/dist/reactRouter/index.js +29 -0
  64. package/dist/reactRouter/index.js.map +1 -0
  65. package/package.json +15 -3
  66. package/src/AdaptiveGrid/AdaptiveGrid.mdx +169 -0
  67. package/src/AdaptiveGrid/AdaptiveGrid.stories.tsx +240 -0
  68. package/src/AdaptiveGrid/AdaptiveGrid.test.tsx +243 -0
  69. package/src/AdaptiveGrid/AdaptiveGrid.tsx +150 -0
  70. package/src/AdaptiveGrid/chooseColumns.test.ts +253 -0
  71. package/src/AdaptiveGrid/chooseColumns.ts +270 -0
  72. package/src/AdaptiveGrid/useAdaptiveColumns.ts +355 -0
  73. package/src/Button/Button.tsx +10 -22
  74. package/src/ButtonLink/ButtonLink.mdx +75 -0
  75. package/src/ButtonLink/ButtonLink.stories.tsx +281 -0
  76. package/src/ButtonLink/ButtonLink.test.tsx +203 -0
  77. package/src/ButtonLink/ButtonLink.tsx +149 -0
  78. package/src/Combobox/Combobox.tsx +4 -1
  79. package/src/Dialog/Dialog.tsx +1 -1
  80. package/src/LogViewer/LogViewer.test.tsx +47 -0
  81. package/src/LogViewer/LogViewer.tsx +35 -1
  82. package/src/RouterLink/AnchorLink.tsx +23 -0
  83. package/src/RouterLink/RouterLink.mdx +178 -0
  84. package/src/RouterLink/RouterLinkProvider.tsx +68 -0
  85. package/src/RouterLink/routerLink.ts +56 -0
  86. package/src/TextLink/TextLink.mdx +93 -0
  87. package/src/TextLink/TextLink.stories.tsx +260 -0
  88. package/src/TextLink/TextLink.test.tsx +230 -0
  89. package/src/TextLink/TextLink.tsx +194 -0
  90. package/src/controlStyles.ts +69 -1
  91. package/src/icons.storyHelpers.tsx +32 -0
  92. package/src/index.ts +41 -0
  93. package/src/intentStyles.ts +19 -0
  94. package/src/reactRouter/ReactRouterLink.tsx +30 -0
  95. package/src/reactRouter/index.ts +29 -0
  96. package/src/sourceRules.test.ts +41 -6
package/README.md CHANGED
@@ -27,6 +27,17 @@ an accordion panel is [a `group` rather than a landmark](../../docs/decisions/20
27
27
  and [not every boolean is a state kind](../../docs/decisions/2026-07-31-not-every-boolean-is-a-state-kind.md).
28
28
  Full write-up: [M6a handoff](../../docs/2026-07-31-m6a-the-p1-components.md).
29
29
 
30
+ **The link family** — `TextLink` and `ButtonLink` — closes the gap that had seven repos
31
+ hand-rolling a `←` back-link, plex-channels navigating with
32
+ `<button onClick={() => navigate(…)}>`, and mux-magic reloading the page through 14 raw
33
+ `<a href>`. Buttons are for on-page actions; links are for navigation, and both of these
34
+ render a real `<a href>` so middle-click, ctrl-click, open-in-new-tab and copy-link-address
35
+ work. They differ in **paint, not semantics**
36
+ ([decision](../../docs/decisions/2026-08-10-buttons-are-actions-links-are-navigation.md)).
37
+ The router is **injected** rather than depended on — `RouterLinkProvider` at the app root,
38
+ with `@charcuterie/ui/react-router` as an optional subpath adapter and a plain `<a>` when
39
+ nothing is provided. Recipe: **Guides/Routing** in Storybook.
40
+
30
41
  ## Install and wire
31
42
 
32
43
  ```ts
@@ -0,0 +1,94 @@
1
+ import type { ReactNode } from "react";
2
+ import type { BlockSizeResolver } from "./useAdaptiveColumns.ts";
3
+ export type AdaptiveGridProps = {
4
+ /**
5
+ * Where the available block size is read from. Defaults to the
6
+ * viewport, which is the only thing that can answer "will this
7
+ * scroll".
8
+ */
9
+ blockSizeResolver?: BlockSizeResolver;
10
+ children: ReactNode;
11
+ /**
12
+ * Everything down the page that is not this grid, in CSS px —
13
+ * header, rails, filter rows. Subtracted before asking how many
14
+ * items stack.
15
+ */
16
+ chromeBlockSize?: number;
17
+ className?: string;
18
+ /**
19
+ * Draw this many columns instead of the automatic answer — the
20
+ * manual override, for a picker or a page that hoisted
21
+ * `useAdaptiveColumns` itself.
22
+ */
23
+ columns?: number;
24
+ /**
25
+ * One item's block size in CSS px. **The one genuinely
26
+ * app-specific number.** Measure it off a running page and err
27
+ * high: a page that scrolls when it did not have to is the
28
+ * complaint this component exists to answer.
29
+ */
30
+ itemBlockSize: number;
31
+ /**
32
+ * How many items to lay out. Defaults to the number of children,
33
+ * and is worth passing explicitly when the rendered count churns
34
+ * — a grid that reflowed every time one item finished loading
35
+ * would be worse than one that is an item out.
36
+ */
37
+ itemCount?: number;
38
+ /** The most columns the automatic answer will take. */
39
+ maxColumns?: number;
40
+ /** The narrowest a column may be, in CSS px. */
41
+ minColumnInlineSize?: number;
42
+ /**
43
+ * The `localStorage` key a manual choice is remembered under.
44
+ * Namespace it to the app; omit it and nothing is written.
45
+ */
46
+ storageKey?: string;
47
+ };
48
+ /**
49
+ * A wrapping grid that spends height before it spends width.
50
+ *
51
+ * The fleet's grids are `auto-fill, minmax()`: they take every
52
+ * column the window allows and land on seven items strung across an
53
+ * ultrawide. This one adds a column only when the items will not
54
+ * stack inside the viewport, and caps the content at a reading
55
+ * measure the rest of the time —
56
+ *
57
+ * > "The idea is that, if you're going to vertically scroll, it's
58
+ * > best to widen the displayed items. If not, then keep them in a
59
+ * > smaller grid, so they all still fit on screen."
60
+ *
61
+ * The rule itself is `chooseColumns`; the measuring is
62
+ * `useAdaptiveColumns`. This is the markup, and it exists because
63
+ * three separate things about that markup are easy to get wrong and
64
+ * silent when you do.
65
+ *
66
+ * ### Two boxes, and why it is not one
67
+ *
68
+ * The outer box is measured and **never capped**; the inner box
69
+ * carries the cap and the tracks. Collapsing them is a real bug
70
+ * with no error message: the cap narrows the content when the item
71
+ * count is low, so an element that is both measured and capped
72
+ * feeds the fold its own output and can never widen again. One
73
+ * column, forever.
74
+ *
75
+ * ### `min-w-0` on every child
76
+ *
77
+ * `[&>*]:min-w-0`, because a grid item's automatic minimum size is
78
+ * `min-content` — so one long unbroken string (a file path, a URL)
79
+ * pushes its track wider than its share and shoves the whole grid
80
+ * out of the viewport. The rule that stops it has to be on the
81
+ * children, and they belong to the caller, so it is applied from
82
+ * here rather than asked for.
83
+ *
84
+ * ### The tracks are an inline style
85
+ *
86
+ * `repeat(N, minmax(0, 1fr))` is interpolated, which a className
87
+ * may not be: Tailwind v4 scans source *text* for complete class
88
+ * strings, so `grid-cols-${columns}` generates nothing at all — no
89
+ * error, no warning, an unstyled element. A class-per-count would
90
+ * need every count to survive the scan; an inline style needs
91
+ * none.
92
+ */
93
+ export declare const AdaptiveGrid: ({ blockSizeResolver, children, chromeBlockSize, className, columns, itemBlockSize, itemCount, maxColumns, minColumnInlineSize, storageKey, }: AdaptiveGridProps) => ReactNode;
94
+ //# sourceMappingURL=AdaptiveGrid.d.ts.map
@@ -0,0 +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;AAKtC,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,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,8IAW1B,iBAAiB,KAAG,SAuCtB,CAAA"}
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Children } from "react";
3
+ import { toClassName } from "../toClassName.js";
4
+ import { getContentMaxInlineSize } from "./chooseColumns.js";
5
+ import { useAdaptiveColumns } from "./useAdaptiveColumns.js";
6
+ /**
7
+ * A wrapping grid that spends height before it spends width.
8
+ *
9
+ * The fleet's grids are `auto-fill, minmax()`: they take every
10
+ * column the window allows and land on seven items strung across an
11
+ * ultrawide. This one adds a column only when the items will not
12
+ * stack inside the viewport, and caps the content at a reading
13
+ * measure the rest of the time —
14
+ *
15
+ * > "The idea is that, if you're going to vertically scroll, it's
16
+ * > best to widen the displayed items. If not, then keep them in a
17
+ * > smaller grid, so they all still fit on screen."
18
+ *
19
+ * The rule itself is `chooseColumns`; the measuring is
20
+ * `useAdaptiveColumns`. This is the markup, and it exists because
21
+ * three separate things about that markup are easy to get wrong and
22
+ * silent when you do.
23
+ *
24
+ * ### Two boxes, and why it is not one
25
+ *
26
+ * The outer box is measured and **never capped**; the inner box
27
+ * carries the cap and the tracks. Collapsing them is a real bug
28
+ * with no error message: the cap narrows the content when the item
29
+ * count is low, so an element that is both measured and capped
30
+ * feeds the fold its own output and can never widen again. One
31
+ * column, forever.
32
+ *
33
+ * ### `min-w-0` on every child
34
+ *
35
+ * `[&>*]:min-w-0`, because a grid item's automatic minimum size is
36
+ * `min-content` — so one long unbroken string (a file path, a URL)
37
+ * pushes its track wider than its share and shoves the whole grid
38
+ * out of the viewport. The rule that stops it has to be on the
39
+ * children, and they belong to the caller, so it is applied from
40
+ * here rather than asked for.
41
+ *
42
+ * ### The tracks are an inline style
43
+ *
44
+ * `repeat(N, minmax(0, 1fr))` is interpolated, which a className
45
+ * may not be: Tailwind v4 scans source *text* for complete class
46
+ * strings, so `grid-cols-${columns}` generates nothing at all — no
47
+ * error, no warning, an unstyled element. A class-per-count would
48
+ * need every count to survive the scan; an inline style needs
49
+ * none.
50
+ */
51
+ export const AdaptiveGrid = ({ blockSizeResolver, children, chromeBlockSize, className, columns, itemBlockSize, itemCount, maxColumns, minColumnInlineSize, storageKey, }) => {
52
+ const layout = useAdaptiveColumns({
53
+ blockSizeResolver,
54
+ chromeBlockSize,
55
+ itemBlockSize,
56
+ itemCount: itemCount ?? Children.count(children),
57
+ maxAutoColumns: maxColumns,
58
+ minColumnInlineSize,
59
+ storageKey,
60
+ });
61
+ const drawnColumns = columns ?? layout.columns;
62
+ // Folded from what is DRAWN rather than read off the hook, so an
63
+ // override widens the page cap with it. A four-column grid inside
64
+ // a one-column cap is the same bug as no cap at all, upside down.
65
+ const contentMaxInlineSize = getContentMaxInlineSize({
66
+ columns: drawnColumns,
67
+ });
68
+ 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
+ gridTemplateColumns: `repeat(${drawnColumns}, minmax(0, 1fr))`,
70
+ maxInlineSize: contentMaxInlineSize,
71
+ }, children: children }) }));
72
+ };
73
+ //# sourceMappingURL=AdaptiveGrid.js.map
@@ -0,0 +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;AAC/C,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAA;AAE5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAgD5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,EAC3B,iBAAiB,EACjB,QAAQ,EACR,eAAe,EACf,SAAS,EACT,OAAO,EACP,aAAa,EACb,SAAS,EACT,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,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;KACtB,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"}
@@ -0,0 +1,206 @@
1
+ /**
2
+ * How many columns a wrapping grid should draw, and how wide the
3
+ * page around it may get.
4
+ *
5
+ * The arithmetic, lifted out of rip-deck's `useLayoutColumns` and
6
+ * made generic. It stays a pure fold — no `window`, no ref, no
7
+ * React — because a heuristic nobody can unit-test at nine sizes is
8
+ * a heuristic hand-tuned against one screenshot, and the table in
9
+ * `chooseColumns.test.ts` is the only form in which the rule can be
10
+ * checked at all.
11
+ *
12
+ * ## Height is spent first
13
+ *
14
+ * The owner's ask, which is the whole design:
15
+ *
16
+ * > "All these apps seem to be really narrow with a very large
17
+ * > max-width — 1 column, but waaaaaaay too wide. What I really
18
+ * > want is something like Rip-Deck where the app has a narrower
19
+ * > main column but the wrapping grids are all full-width only when
20
+ * > you have too many items. […] The idea is that, if you're going
21
+ * > to vertically scroll, it's best to widen the displayed items.
22
+ * > If not, then keep them in a smaller grid, so they all still fit
23
+ * > on screen. Ideally, we wouldn't require users to scroll."
24
+ *
25
+ * And the sentence it replaced, from the app this came out of:
26
+ *
27
+ * > "Like if you have enough height, try to fill it. If you don't,
28
+ * > then abuse as much width side-by-side as possible. That way,
29
+ * > you don't get 9-wide just because you're on an ultrawide even
30
+ * > if you have more height available."
31
+ *
32
+ * Stated plainly: **a column is something the layout is FORCED
33
+ * into** when the items will not stack inside the space available —
34
+ * never something it takes merely because a monitor is wide. That
35
+ * inversion is why `availableInlineSize` only ever *caps* the
36
+ * answer here rather than producing it, and it is the opposite of
37
+ * every `auto-fill, minmax()` grid in the fleet, which takes every
38
+ * column the width allows and lands on seven items across an
39
+ * ultrawide.
40
+ *
41
+ * The visible consequence is deliberately non-monotonic: 1440x900
42
+ * takes three columns while the *larger* 1920x1080 takes two. That
43
+ * reads backwards and is the rule working — the taller window
44
+ * stacks the same items in fewer stacks, so it needs fewer.
45
+ */
46
+ /**
47
+ * The most columns the automatic answer will ever choose.
48
+ *
49
+ * > "It would look weird to do 9 side-by-side."
50
+ *
51
+ * Three is the number the owner said he could "easily fit". A
52
+ * caller with squarer content raises it; a caller with wide content
53
+ * lowers it. It is taste about one app's content, which is exactly
54
+ * why it is a default rather than a constant.
55
+ */
56
+ export declare const DEFAULT_MAX_AUTO_COLUMNS = 3;
57
+ /**
58
+ * The widest a manual picker offers, one above the automatic cap —
59
+ * because a cap is a default and not a ceiling on what a person is
60
+ * allowed to ask for.
61
+ */
62
+ export declare const DEFAULT_MAX_MANUAL_COLUMNS = 4;
63
+ /**
64
+ * The narrowest a column may be, in CSS px, before the grid stops
65
+ * adding them. `24rem` at a 16px root — `containerQuery.sm`.
66
+ *
67
+ * ## Which threshold system this is, and which it is not
68
+ *
69
+ * Two live in the fleet today and they answer different questions,
70
+ * so this reconciles them by *keeping them apart*:
71
+ *
72
+ * - A `cq-*` step is read **by the item, inside the track**: "how
73
+ * should this card lay itself out at the width it was handed."
74
+ * Charcuterie's are `--cq-xs` … `--cq-xl`, deliberately not
75
+ * Tailwind's `--container-*` (see
76
+ * `docs/decisions/2026-07-29-container-query-scale-is-cq-not-container.md`).
77
+ * - This number is read **by the grid, outside the item**: "is
78
+ * another track worth having at all."
79
+ *
80
+ * Nothing forces them to agree, and the source app's did not: its
81
+ * cards restyle at Tailwind's own `28rem` container variant while
82
+ * its grid floor sat at a hand-measured `380px`. It set the floor
83
+ * *below* the restyle threshold on purpose, because the denser card
84
+ * that appears under it is a real density rather than a failure.
85
+ *
86
+ * That relationship is what survives here, expressed in
87
+ * Charcuterie's own scale instead of a magic number: the floor
88
+ * defaults to `cq-sm` (24rem = 384px), which is within 4px of the
89
+ * measured 380 — every row of the ported spec table gives the same
90
+ * answer at either value — and stays a step below `cq-md` (32rem),
91
+ * where the library's own container-query components change shape.
92
+ *
93
+ * The link is asserted in `chooseColumns.test.ts` so it cannot
94
+ * drift back into a bare number.
95
+ */
96
+ export declare const DEFAULT_MIN_COLUMN_INLINE_SIZE_PX: number;
97
+ /**
98
+ * `"auto"`, or the number of columns a person explicitly asked for.
99
+ */
100
+ export type ColumnChoice = "auto" | number;
101
+ /**
102
+ * The column count the automatic rule wants, for one measured space
103
+ * and one set of items.
104
+ */
105
+ export declare const chooseColumns: ({ availableBlockSize, availableInlineSize, chromeBlockSize, itemBlockSize, itemCount, maxColumns, minColumnInlineSize, }: {
106
+ /**
107
+ * The block size the layout has to spend, in CSS px — the
108
+ * viewport's, not the grid's.
109
+ *
110
+ * It has to come from the viewport, because the question being
111
+ * asked is "will this scroll", and an element's own block size
112
+ * cannot answer it: a grid in normal flow is exactly as tall as
113
+ * its contents, so measuring it always says "it fits."
114
+ */
115
+ availableBlockSize: number;
116
+ /**
117
+ * The inline size the grid has to spend, in CSS px — the
118
+ * container's, not the viewport's.
119
+ *
120
+ * The container is the honest source here: a grid sitting beside
121
+ * a rail has less room than the window suggests, and this library
122
+ * is built around the case where "the component's container is
123
+ * what changed, not the window."
124
+ *
125
+ * It must be measured on an element that is **not itself capped
126
+ * by the answer**, or the fold feeds on its own output and can
127
+ * never widen. `AdaptiveGrid` guarantees that structurally.
128
+ */
129
+ availableInlineSize: number;
130
+ /**
131
+ * Everything down the page that is not this grid, in CSS px —
132
+ * headers, rails, filter rows, labels. Subtracted from
133
+ * `availableBlockSize` before asking how many items stack.
134
+ */
135
+ chromeBlockSize?: number;
136
+ /**
137
+ * One item's block size, in CSS px. **The one genuinely
138
+ * app-specific number**, and the only required one.
139
+ *
140
+ * Measure it off a running page rather than deriving it, and err
141
+ * high: over-estimating means the grid reaches for a column
142
+ * slightly early, and a page that scrolls when it did not have to
143
+ * is the complaint this exists to answer. It only has to be right
144
+ * to within about an item — being one out shifts a borderline
145
+ * size by one column, which is taste.
146
+ */
147
+ itemBlockSize: number;
148
+ /**
149
+ * How many items the grid is being asked to fit.
150
+ *
151
+ * The stable count rather than the rendered one where those
152
+ * differ: a column count that reflowed every time one item
153
+ * finished loading would be worse than one that is an item out.
154
+ */
155
+ itemCount: number;
156
+ /** @see DEFAULT_MAX_AUTO_COLUMNS */
157
+ maxColumns?: number;
158
+ /** @see DEFAULT_MIN_COLUMN_INLINE_SIZE_PX */
159
+ minColumnInlineSize?: number;
160
+ }) => number;
161
+ /**
162
+ * How wide the content around the grid may grow, as a CSS length.
163
+ *
164
+ * The other half of the ask — "the app has a narrower main column
165
+ * but the wrapping grids are all full-width only when you have too
166
+ * many items." One column stays at a reading measure, because a
167
+ * 2000px-wide card is not a better card, it is one line of text the
168
+ * eye has to track across a monitor. Past that the cap **grows with
169
+ * the columns**, so three columns get room to be three readable
170
+ * things rather than three slivers.
171
+ *
172
+ * ### Why the numbers live in `@charcuterie/tokens`
173
+ *
174
+ * They are `contentInlineSize`, not constants in this file. A
175
+ * readable measure is a structural fact about the fleet in exactly
176
+ * the sense `screen` and `containerQuery` are — a variant may
177
+ * change how a card *looks*, not how far the eye tracks — so it
178
+ * belongs in the one package a Satori renderer or a plain-CSS
179
+ * consumer can read without a React tree. The *arithmetic* is not a
180
+ * token, and stays here.
181
+ *
182
+ * The three parts are still overridable per call, because an app of
183
+ * dense tables and an app of prose disagree about a measure and
184
+ * neither is wrong.
185
+ */
186
+ export declare const getContentMaxInlineSize: ({ columnInlineSize, columns, gutterInlineSize, singleInlineSize, }: {
187
+ /** What one column is worth, once there are several. */
188
+ columnInlineSize?: string;
189
+ columns: number;
190
+ /** Slack added on top of a multi-column cap, for the gaps. */
191
+ gutterInlineSize?: string;
192
+ /** The cap when there is only one column. */
193
+ singleInlineSize?: string;
194
+ }) => string;
195
+ /**
196
+ * The choices a manual picker offers: `"auto"`, then every count up
197
+ * to `maxColumns`.
198
+ *
199
+ * A function rather than a constant so the picker and the cap
200
+ * cannot disagree — an app that lowers its cap gets a shorter list
201
+ * for free, instead of offering a count it will then refuse.
202
+ */
203
+ export declare const getColumnChoices: ({ maxColumns, }: {
204
+ maxColumns?: number;
205
+ }) => ColumnChoice[];
206
+ //# sourceMappingURL=chooseColumns.d.ts.map
@@ -0,0 +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,0HAQ3B;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,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,6CAA6C;IAC7C,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B,KAAG,MA4BH,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,uBAAuB,GAAI,oEAKrC;IACD,wDAAwD;IACxD,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,OAAO,EAAE,MAAM,CAAA;IACf,8DAA8D;IAC9D,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,6CAA6C;IAC7C,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B,KAAG,MAG6F,CAAA;AAEjG;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,GAAI,iBAE9B;IACD,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,KAAG,YAAY,EAMf,CAAA"}
@@ -0,0 +1,156 @@
1
+ import { containerQuery, contentInlineSize, } from "../tokens.js";
2
+ /**
3
+ * How many columns a wrapping grid should draw, and how wide the
4
+ * page around it may get.
5
+ *
6
+ * The arithmetic, lifted out of rip-deck's `useLayoutColumns` and
7
+ * made generic. It stays a pure fold — no `window`, no ref, no
8
+ * React — because a heuristic nobody can unit-test at nine sizes is
9
+ * a heuristic hand-tuned against one screenshot, and the table in
10
+ * `chooseColumns.test.ts` is the only form in which the rule can be
11
+ * checked at all.
12
+ *
13
+ * ## Height is spent first
14
+ *
15
+ * The owner's ask, which is the whole design:
16
+ *
17
+ * > "All these apps seem to be really narrow with a very large
18
+ * > max-width — 1 column, but waaaaaaay too wide. What I really
19
+ * > want is something like Rip-Deck where the app has a narrower
20
+ * > main column but the wrapping grids are all full-width only when
21
+ * > you have too many items. […] The idea is that, if you're going
22
+ * > to vertically scroll, it's best to widen the displayed items.
23
+ * > If not, then keep them in a smaller grid, so they all still fit
24
+ * > on screen. Ideally, we wouldn't require users to scroll."
25
+ *
26
+ * And the sentence it replaced, from the app this came out of:
27
+ *
28
+ * > "Like if you have enough height, try to fill it. If you don't,
29
+ * > then abuse as much width side-by-side as possible. That way,
30
+ * > you don't get 9-wide just because you're on an ultrawide even
31
+ * > if you have more height available."
32
+ *
33
+ * Stated plainly: **a column is something the layout is FORCED
34
+ * into** when the items will not stack inside the space available —
35
+ * never something it takes merely because a monitor is wide. That
36
+ * inversion is why `availableInlineSize` only ever *caps* the
37
+ * answer here rather than producing it, and it is the opposite of
38
+ * every `auto-fill, minmax()` grid in the fleet, which takes every
39
+ * column the width allows and lands on seven items across an
40
+ * ultrawide.
41
+ *
42
+ * The visible consequence is deliberately non-monotonic: 1440x900
43
+ * takes three columns while the *larger* 1920x1080 takes two. That
44
+ * reads backwards and is the rule working — the taller window
45
+ * stacks the same items in fewer stacks, so it needs fewer.
46
+ */
47
+ /**
48
+ * The most columns the automatic answer will ever choose.
49
+ *
50
+ * > "It would look weird to do 9 side-by-side."
51
+ *
52
+ * Three is the number the owner said he could "easily fit". A
53
+ * caller with squarer content raises it; a caller with wide content
54
+ * lowers it. It is taste about one app's content, which is exactly
55
+ * why it is a default rather than a constant.
56
+ */
57
+ export const DEFAULT_MAX_AUTO_COLUMNS = 3;
58
+ /**
59
+ * The widest a manual picker offers, one above the automatic cap —
60
+ * because a cap is a default and not a ceiling on what a person is
61
+ * allowed to ask for.
62
+ */
63
+ export const DEFAULT_MAX_MANUAL_COLUMNS = 4;
64
+ /**
65
+ * The narrowest a column may be, in CSS px, before the grid stops
66
+ * adding them. `24rem` at a 16px root — `containerQuery.sm`.
67
+ *
68
+ * ## Which threshold system this is, and which it is not
69
+ *
70
+ * Two live in the fleet today and they answer different questions,
71
+ * so this reconciles them by *keeping them apart*:
72
+ *
73
+ * - A `cq-*` step is read **by the item, inside the track**: "how
74
+ * should this card lay itself out at the width it was handed."
75
+ * Charcuterie's are `--cq-xs` … `--cq-xl`, deliberately not
76
+ * Tailwind's `--container-*` (see
77
+ * `docs/decisions/2026-07-29-container-query-scale-is-cq-not-container.md`).
78
+ * - This number is read **by the grid, outside the item**: "is
79
+ * another track worth having at all."
80
+ *
81
+ * Nothing forces them to agree, and the source app's did not: its
82
+ * cards restyle at Tailwind's own `28rem` container variant while
83
+ * its grid floor sat at a hand-measured `380px`. It set the floor
84
+ * *below* the restyle threshold on purpose, because the denser card
85
+ * that appears under it is a real density rather than a failure.
86
+ *
87
+ * That relationship is what survives here, expressed in
88
+ * Charcuterie's own scale instead of a magic number: the floor
89
+ * defaults to `cq-sm` (24rem = 384px), which is within 4px of the
90
+ * measured 380 — every row of the ported spec table gives the same
91
+ * answer at either value — and stays a step below `cq-md` (32rem),
92
+ * where the library's own container-query components change shape.
93
+ *
94
+ * The link is asserted in `chooseColumns.test.ts` so it cannot
95
+ * drift back into a bare number.
96
+ */
97
+ export const DEFAULT_MIN_COLUMN_INLINE_SIZE_PX = Number.parseFloat(containerQuery.sm) * 16;
98
+ /**
99
+ * The column count the automatic rule wants, for one measured space
100
+ * and one set of items.
101
+ */
102
+ export const chooseColumns = ({ availableBlockSize, availableInlineSize, chromeBlockSize = 0, itemBlockSize, itemCount, maxColumns = DEFAULT_MAX_AUTO_COLUMNS, minColumnInlineSize = DEFAULT_MIN_COLUMN_INLINE_SIZE_PX, }) => {
103
+ // Inline size is a CAP, never a reason. This is the "don't go
104
+ // 9-wide just because you're on an ultrawide" half of the rule.
105
+ const inlineSizeCap = Math.max(1, Math.min(maxColumns, Math.floor(availableInlineSize / minColumnInlineSize)));
106
+ if (itemCount <= 1) {
107
+ return 1;
108
+ }
109
+ // Block size is the reason. How many items stack in the space,
110
+ // then how many stacks that many items need.
111
+ const rowsThatFit = Math.max(1, Math.floor((availableBlockSize - chromeBlockSize) /
112
+ itemBlockSize));
113
+ const columnsNeeded = Math.ceil(itemCount / rowsThatFit);
114
+ return Math.max(1, Math.min(inlineSizeCap, columnsNeeded));
115
+ };
116
+ /**
117
+ * How wide the content around the grid may grow, as a CSS length.
118
+ *
119
+ * The other half of the ask — "the app has a narrower main column
120
+ * but the wrapping grids are all full-width only when you have too
121
+ * many items." One column stays at a reading measure, because a
122
+ * 2000px-wide card is not a better card, it is one line of text the
123
+ * eye has to track across a monitor. Past that the cap **grows with
124
+ * the columns**, so three columns get room to be three readable
125
+ * things rather than three slivers.
126
+ *
127
+ * ### Why the numbers live in `@charcuterie/tokens`
128
+ *
129
+ * They are `contentInlineSize`, not constants in this file. A
130
+ * readable measure is a structural fact about the fleet in exactly
131
+ * the sense `screen` and `containerQuery` are — a variant may
132
+ * change how a card *looks*, not how far the eye tracks — so it
133
+ * belongs in the one package a Satori renderer or a plain-CSS
134
+ * consumer can read without a React tree. The *arithmetic* is not a
135
+ * token, and stays here.
136
+ *
137
+ * The three parts are still overridable per call, because an app of
138
+ * dense tables and an app of prose disagree about a measure and
139
+ * neither is wrong.
140
+ */
141
+ export const getContentMaxInlineSize = ({ columnInlineSize = contentInlineSize.column, columns, gutterInlineSize = contentInlineSize.gutter, singleInlineSize = contentInlineSize.single, }) => columns <= 1
142
+ ? singleInlineSize
143
+ : `${Number.parseFloat(columnInlineSize) * columns + Number.parseFloat(gutterInlineSize)}rem`;
144
+ /**
145
+ * The choices a manual picker offers: `"auto"`, then every count up
146
+ * to `maxColumns`.
147
+ *
148
+ * A function rather than a constant so the picker and the cap
149
+ * cannot disagree — an app that lowers its cap gets a shorter list
150
+ * for free, instead of offering a count it will then refuse.
151
+ */
152
+ export const getColumnChoices = ({ maxColumns = DEFAULT_MAX_MANUAL_COLUMNS, }) => [
153
+ "auto",
154
+ ...Array.from({ length: Math.max(1, maxColumns) }, (_, index) => index + 1),
155
+ ];
156
+ //# sourceMappingURL=chooseColumns.js.map
@@ -0,0 +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;AAO3C;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,EAC5B,kBAAkB,EAClB,mBAAmB,EACnB,eAAe,GAAG,CAAC,EACnB,aAAa,EACb,SAAS,EACT,UAAU,GAAG,wBAAwB,EACrC,mBAAmB,GAAG,iCAAiC,GAwDxD,EAAU,EAAE;IACX,8DAA8D;IAC9D,gEAAgE;IAChE,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAC5B,CAAC,EACD,IAAI,CAAC,GAAG,CACN,UAAU,EACV,IAAI,CAAC,KAAK,CAAC,mBAAmB,GAAG,mBAAmB,CAAC,CACtD,CACF,CAAA;IAED,IAAI,SAAS,IAAI,CAAC,EAAE,CAAC;QACnB,OAAO,CAAC,CAAA;IACV,CAAC;IAED,+DAA+D;IAC/D,6CAA6C;IAC7C,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAC1B,CAAC,EACD,IAAI,CAAC,KAAK,CACR,CAAC,kBAAkB,GAAG,eAAe,CAAC;QACpC,aAAa,CAChB,CACF,CAAA;IAED,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,WAAW,CAAC,CAAA;IAExD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC,CAAA;AAC5D,CAAC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,EACtC,gBAAgB,GAAG,iBAAiB,CAAC,MAAM,EAC3C,OAAO,EACP,gBAAgB,GAAG,iBAAiB,CAAC,MAAM,EAC3C,gBAAgB,GAAG,iBAAiB,CAAC,MAAM,GAS5C,EAAU,EAAE,CACX,OAAO,IAAI,CAAC;IACV,CAAC,CAAC,gBAAgB;IAClB,CAAC,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,gBAAgB,CAAC,GAAG,OAAO,GAAG,MAAM,CAAC,UAAU,CAAC,gBAAgB,CAAC,KAAK,CAAA;AAEjG;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,EAC/B,UAAU,GAAG,0BAA0B,GAGxC,EAAkB,EAAE,CAAC;IACpB,MAAM;IACN,GAAG,KAAK,CAAC,IAAI,CACX,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,EACnC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,CACxB;CACF,CAAA"}