@stonedogcode/style 0.20.3 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stonedogcode/style",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "A Panda CSS design system: a themeable Panda preset plus the React components built on it.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "StoneDogCode L.L.C.",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"prepare": "npm run panda:prepare",
|
|
38
38
|
"pretype-check": "npm run panda:prepare",
|
|
39
39
|
"type-check": "tsc --noEmit",
|
|
40
|
-
"lint": "eslint .
|
|
40
|
+
"lint": "eslint .",
|
|
41
41
|
"//pretest": "panda:build, not panda:prepare — the token-contract suite asserts against the generated stylesheet, which cssgen produces and codegen alone does not.",
|
|
42
42
|
"pretest": "npm run panda:build",
|
|
43
43
|
"test": "jest",
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"csstype": "^3.2.3"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
|
+
"@eslint/js": "^9.0.0",
|
|
67
68
|
"@pandacss/dev": "^1.11.1",
|
|
68
69
|
"@playwright/experimental-ct-react": "^1.62.1",
|
|
69
70
|
"@playwright/test": "^1.62.1",
|
|
@@ -75,9 +76,10 @@
|
|
|
75
76
|
"@types/react-dom": "^19.2.3",
|
|
76
77
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
77
78
|
"@typescript-eslint/parser": "^8.0.0",
|
|
78
|
-
"eslint": "^
|
|
79
|
+
"eslint": "^9.39.5",
|
|
79
80
|
"eslint-plugin-react": "^7.37.0",
|
|
80
81
|
"eslint-plugin-react-hooks": "^5.0.0",
|
|
82
|
+
"globals": "^17.12.0",
|
|
81
83
|
"jest": "^29.7.0",
|
|
82
84
|
"jest-environment-jsdom": "^29.7.0",
|
|
83
85
|
"react": "^19.0.4",
|
|
@@ -101,6 +101,79 @@ export interface StyledBoxProps
|
|
|
101
101
|
scrollbar?: "auto" | "on" | "off";
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* The layout props that describe how a caller wants THEIR CHILDREN arranged.
|
|
106
|
+
*
|
|
107
|
+
* Without `noWrap`, `StyledBox` puts children three levels down:
|
|
108
|
+
*
|
|
109
|
+
* ```
|
|
110
|
+
* StyledBoxRoot ← these props used to land here, and only here
|
|
111
|
+
* └ StyledVStack ← ...whose only child is this
|
|
112
|
+
* └ div
|
|
113
|
+
* └ StyledGridPanel
|
|
114
|
+
* └ div ← a plain block div, where the children actually live
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* So a root flex container laid out exactly ONE item and the caller's children
|
|
118
|
+
* stayed in ordinary block flow — `display="flex" flexDirection="column"` did
|
|
119
|
+
* nothing, `alignItems` centred a wrapper, and `gap` separated nothing.
|
|
120
|
+
*
|
|
121
|
+
* The visible cost, three times over: two adjacent `StyledText` siblings render
|
|
122
|
+
* as one welded run, because `StyledText` is a `<span>` and JSX strips the
|
|
123
|
+
* whitespace between elements on separate lines. Hopper Vitals shipped
|
|
124
|
+
* `264.2Weight` and `Sep 6Record another to see a trend.` (NEH-1473), which is
|
|
125
|
+
* the same symptom NEH-490 fixed twice at the call site. Fixing it at the call
|
|
126
|
+
* site is why it came back.
|
|
127
|
+
*
|
|
128
|
+
* **This cannot break anything that currently works.** These declarations were
|
|
129
|
+
* discarded before — a flex container with one child arranges nothing — so
|
|
130
|
+
* nothing can be depending on their effect. Same argument `StyledText`'s block
|
|
131
|
+
* promotion makes for vertical margins on an inline box.
|
|
132
|
+
*
|
|
133
|
+
* They are FORWARDED, not moved: the root keeps them too, so a caller relying
|
|
134
|
+
* on the root's own box (a `gap` between header, content and footer, say) is
|
|
135
|
+
* unaffected.
|
|
136
|
+
*
|
|
137
|
+
* Sizing props are deliberately absent — `width`, `height`, `padding`,
|
|
138
|
+
* `margin`, `overflow` and friends genuinely apply to the root's own box, and
|
|
139
|
+
* copying them inward would double padding and re-clip content.
|
|
140
|
+
*/
|
|
141
|
+
const LAYOUT_PROPS = [
|
|
142
|
+
"display",
|
|
143
|
+
"flexDirection",
|
|
144
|
+
"flexWrap",
|
|
145
|
+
"alignItems",
|
|
146
|
+
"alignContent",
|
|
147
|
+
"justifyContent",
|
|
148
|
+
"justifyItems",
|
|
149
|
+
"gap",
|
|
150
|
+
"rowGap",
|
|
151
|
+
"columnGap",
|
|
152
|
+
"gridTemplateColumns",
|
|
153
|
+
"gridTemplateRows",
|
|
154
|
+
"gridAutoFlow",
|
|
155
|
+
"placeItems",
|
|
156
|
+
"placeContent",
|
|
157
|
+
] as const;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Did the caller ask for a layout of their own?
|
|
161
|
+
*
|
|
162
|
+
* When they did, and nothing else needs the wrapper, `StyledBox` renders the
|
|
163
|
+
* children DIRECTLY inside the root so the root really is their parent — which
|
|
164
|
+
* is what makes every Panda prop work, `gap` included.
|
|
165
|
+
*
|
|
166
|
+
* Forwarding the props as inline CSS was tried first and is wrong: `gap="2"` is
|
|
167
|
+
* a Panda spacing TOKEN, not a length, so `style={{ gap: "2" }}` is invalid CSS
|
|
168
|
+
* and silently dropped. Panda resolves tokens at build time for props on a
|
|
169
|
+
* styled component; it cannot for a runtime value written into a style
|
|
170
|
+
* attribute. Making the root the parent sidesteps the problem entirely — the props
|
|
171
|
+
* stay exactly where Panda already handles them.
|
|
172
|
+
*/
|
|
173
|
+
function wantsOwnLayout(rest: Record<string, unknown>): boolean {
|
|
174
|
+
return LAYOUT_PROPS.some((prop) => rest[prop] !== undefined && rest[prop] !== null);
|
|
175
|
+
}
|
|
176
|
+
|
|
104
177
|
const StyledBoxRoot = styled("div", boxRecipe);
|
|
105
178
|
|
|
106
179
|
const StyledBox = React.forwardRef<HTMLDivElement, StyledBoxProps>(
|
|
@@ -137,6 +210,10 @@ const StyledBox = React.forwardRef<HTMLDivElement, StyledBoxProps>(
|
|
|
137
210
|
// scrollbar prop controls inner content overflow
|
|
138
211
|
const innerOverflow: Property.Overflow = scrollbar === "auto" ? "auto" : scrollbar === "on" ? "scroll" : "hidden";
|
|
139
212
|
|
|
213
|
+
// Does the caller lay out their own children? If so the wrapper is what
|
|
214
|
+
// stands between their props and the elements those props describe.
|
|
215
|
+
const callerLaysOutChildren = wantsOwnLayout(rest as Record<string, unknown>);
|
|
216
|
+
|
|
140
217
|
if (noWrap) {
|
|
141
218
|
if (resolvedHeader || resolvedFooter) {
|
|
142
219
|
return (
|
|
@@ -153,6 +230,30 @@ const StyledBox = React.forwardRef<HTMLDivElement, StyledBoxProps>(
|
|
|
153
230
|
</StyledBoxRoot>
|
|
154
231
|
);
|
|
155
232
|
}
|
|
233
|
+
/*
|
|
234
|
+
The caller lays out their own children and nothing else needs the
|
|
235
|
+
wrapper, so the root parents them directly.
|
|
236
|
+
|
|
237
|
+
Gated on there being no header, footer, panels or scrollbar: each of
|
|
238
|
+
those is a real reason the wrapper exists, and this must not take any of
|
|
239
|
+
them away. What is left is the case where the wrapper only ever stood
|
|
240
|
+
between a caller's layout props and the children they describe.
|
|
241
|
+
*/
|
|
242
|
+
if (
|
|
243
|
+
callerLaysOutChildren &&
|
|
244
|
+
!resolvedHeader &&
|
|
245
|
+
!resolvedFooter &&
|
|
246
|
+
!leftPanel &&
|
|
247
|
+
!rightPanel &&
|
|
248
|
+
scrollbar === undefined
|
|
249
|
+
) {
|
|
250
|
+
return (
|
|
251
|
+
<StyledBoxRoot ref={ref} {...(as ? { as } : {})} className={combinedClassName} {...rest}>
|
|
252
|
+
{children}
|
|
253
|
+
</StyledBoxRoot>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
156
257
|
return (
|
|
157
258
|
<StyledBoxRoot ref={ref} {...(as ? { as } : {})} className={combinedClassName} {...rest}>
|
|
158
259
|
<StyledVStack gap="0" width="100%" height="100%" style={{ overflow: innerOverflow }}>
|
|
@@ -3,7 +3,6 @@ import { cx, css } from "styled-system/css";
|
|
|
3
3
|
import { stripedRecipe } from "styled-system/recipes";
|
|
4
4
|
import React from "react";
|
|
5
5
|
import { log } from "../config/logger";
|
|
6
|
-
import type { ConditionalValue } from "styled-system/types";
|
|
7
6
|
|
|
8
7
|
const PandaGrid = styled("div", {
|
|
9
8
|
base: {
|
|
@@ -11,25 +10,300 @@ const PandaGrid = styled("div", {
|
|
|
11
10
|
},
|
|
12
11
|
});
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* The breakpoints this component's responsive objects understand.
|
|
15
|
+
*
|
|
16
|
+
* Exactly the six declared by the preset, plus `base`. Order matters: the
|
|
17
|
+
* runtime fills a value forward from each key to the next, so this array is the
|
|
18
|
+
* cascade.
|
|
19
|
+
*/
|
|
20
|
+
const GRID_BREAKPOINTS = ["base", "sm", "md", "lg", "xl", "2xl", "3xl"] as const;
|
|
21
|
+
|
|
22
|
+
export type GridBreakpoint = (typeof GRID_BREAKPOINTS)[number];
|
|
23
|
+
|
|
24
|
+
/** A plain value, or one value per breakpoint. */
|
|
25
|
+
export type GridResponsiveValue<T> = T | Partial<Record<GridBreakpoint, T>>;
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* ---------------------------------------------------------------------------
|
|
29
|
+
* Why the track definitions travel as CSS custom properties (NEH-1453)
|
|
30
|
+
* ---------------------------------------------------------------------------
|
|
31
|
+
*
|
|
32
|
+
* `StyledGrid` used to compute its `grid-template-*` values at runtime and hand
|
|
33
|
+
* them to Panda as style props:
|
|
34
|
+
*
|
|
35
|
+
* resolvedColumns = `repeat(${columns}, 1fr)`;
|
|
36
|
+
* <PandaGrid gridTemplateColumns={resolvedColumns} />
|
|
37
|
+
*
|
|
38
|
+
* Panda extracts styles by **statically parsing source at build time**. A value
|
|
39
|
+
* computed at runtime is never seen by `panda cssgen`, so Panda's runtime
|
|
40
|
+
* constructs a class *name* derived from the value and no rule is ever emitted
|
|
41
|
+
* for it. Measured in this package's own component tier, `<StyledGrid
|
|
42
|
+
* columns={2}>` inside a 320px container:
|
|
43
|
+
*
|
|
44
|
+
* class="d_grid grid-tc_repeat(2,_1fr)"
|
|
45
|
+
* inline style: null
|
|
46
|
+
* computed grid-template-columns: 445.188px <- ONE implicit, content-sized track
|
|
47
|
+
* rules in the sheet mentioning grid-template-columns: 1, and it is
|
|
48
|
+
* `.grid-tc_max-content_1fr` from StyledDefinitionList
|
|
49
|
+
*
|
|
50
|
+
* The same is true of `templateColumns` / `templateRows` / `templateAreas`,
|
|
51
|
+
* which are not Panda property names at all, so even a static literal at the
|
|
52
|
+
* call site is never extracted.
|
|
53
|
+
*
|
|
54
|
+
* Where it appeared to work in an app it was a **coincidence**: the class name
|
|
55
|
+
* is derived from the value, so a consumer whose own source happened to contain
|
|
56
|
+
* the same literal elsewhere got a rule by accident. That is why the fix cannot
|
|
57
|
+
* simply rewrite the emitted string — a new string breaks the coincidence and
|
|
58
|
+
* silently drops the columns.
|
|
59
|
+
*
|
|
60
|
+
* So the value has to reach the element by a route that does not depend on
|
|
61
|
+
* static extraction. Inline `style` is that route, and it is what
|
|
62
|
+
* `StyledSimpleGrid` already does. But a plain inline `grid-template-columns`
|
|
63
|
+
* cannot carry media queries, which is why `StyledSimpleGrid` pays for the
|
|
64
|
+
* responsive form with a JS resize listener (no server-rendered value, a
|
|
65
|
+
* listener per grid, a flash on first paint).
|
|
66
|
+
*
|
|
67
|
+
* This component avoids that trade by splitting the two halves:
|
|
68
|
+
*
|
|
69
|
+
* - the **rules** are literals in this file, so Panda really does extract
|
|
70
|
+
* them, one per breakpoint, with real `@media` conditions;
|
|
71
|
+
* - the **values** ride in on inline custom properties, which are never
|
|
72
|
+
* extracted, never parsed by Panda, and always applied.
|
|
73
|
+
*
|
|
74
|
+
* Result: real CSS breakpoints, correct on the server, no resize listener, and
|
|
75
|
+
* no upper bound on the column count.
|
|
76
|
+
*
|
|
77
|
+
* The runtime fills a value FORWARD across breakpoints rather than relying on
|
|
78
|
+
* nested `var()` fallbacks, so each rule is a flat `var(--x, none)`. `none` is
|
|
79
|
+
* the initial value of every `grid-template-*` property, so an axis nobody set
|
|
80
|
+
* resolves to exactly what it would have been.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
const TRACK_VAR = {
|
|
84
|
+
columns: "--sds-grid-tc",
|
|
85
|
+
rows: "--sds-grid-tr",
|
|
86
|
+
areas: "--sds-grid-ta",
|
|
87
|
+
} as const;
|
|
88
|
+
|
|
89
|
+
type GridAxis = keyof typeof TRACK_VAR;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* One class per axis, applied only when that axis has a value.
|
|
93
|
+
*
|
|
94
|
+
* They are separate constants on purpose. A single combined class would declare
|
|
95
|
+
* `grid-template-rows: none` on every grid, which is the initial value but is
|
|
96
|
+
* still a *declaration* — and it would then race, at equal specificity, with
|
|
97
|
+
* any `grid-template-rows` a consumer set through `className`. An axis nobody
|
|
98
|
+
* asked about is left untouched instead.
|
|
99
|
+
*
|
|
100
|
+
* Every value below is a string LITERAL. That is the entire point: Panda has to
|
|
101
|
+
* be able to read them without running anything.
|
|
102
|
+
*/
|
|
103
|
+
const AXIS_CLASS: Record<GridAxis, string> = {
|
|
104
|
+
columns: css({
|
|
105
|
+
gridTemplateColumns: "var(--sds-grid-tc-base, none)",
|
|
106
|
+
sm: { gridTemplateColumns: "var(--sds-grid-tc-sm, none)" },
|
|
107
|
+
md: { gridTemplateColumns: "var(--sds-grid-tc-md, none)" },
|
|
108
|
+
lg: { gridTemplateColumns: "var(--sds-grid-tc-lg, none)" },
|
|
109
|
+
xl: { gridTemplateColumns: "var(--sds-grid-tc-xl, none)" },
|
|
110
|
+
"2xl": { gridTemplateColumns: "var(--sds-grid-tc-2xl, none)" },
|
|
111
|
+
"3xl": { gridTemplateColumns: "var(--sds-grid-tc-3xl, none)" },
|
|
112
|
+
}),
|
|
113
|
+
rows: css({
|
|
114
|
+
gridTemplateRows: "var(--sds-grid-tr-base, none)",
|
|
115
|
+
sm: { gridTemplateRows: "var(--sds-grid-tr-sm, none)" },
|
|
116
|
+
md: { gridTemplateRows: "var(--sds-grid-tr-md, none)" },
|
|
117
|
+
lg: { gridTemplateRows: "var(--sds-grid-tr-lg, none)" },
|
|
118
|
+
xl: { gridTemplateRows: "var(--sds-grid-tr-xl, none)" },
|
|
119
|
+
"2xl": { gridTemplateRows: "var(--sds-grid-tr-2xl, none)" },
|
|
120
|
+
"3xl": { gridTemplateRows: "var(--sds-grid-tr-3xl, none)" },
|
|
121
|
+
}),
|
|
122
|
+
areas: css({
|
|
123
|
+
gridTemplateAreas: "var(--sds-grid-ta-base, none)",
|
|
124
|
+
sm: { gridTemplateAreas: "var(--sds-grid-ta-sm, none)" },
|
|
125
|
+
md: { gridTemplateAreas: "var(--sds-grid-ta-md, none)" },
|
|
126
|
+
lg: { gridTemplateAreas: "var(--sds-grid-ta-lg, none)" },
|
|
127
|
+
xl: { gridTemplateAreas: "var(--sds-grid-ta-xl, none)" },
|
|
128
|
+
"2xl": { gridTemplateAreas: "var(--sds-grid-ta-2xl, none)" },
|
|
129
|
+
"3xl": { gridTemplateAreas: "var(--sds-grid-ta-3xl, none)" },
|
|
130
|
+
}),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Spread a responsive value across every breakpoint, carrying each value
|
|
135
|
+
* forward until the next one overrides it.
|
|
136
|
+
*
|
|
137
|
+
* Filling forward is what lets each emitted rule be a flat `var(--x, none)`: at
|
|
138
|
+
* `lg` the rule reads `--sds-grid-tc-lg`, so that property has to hold the
|
|
139
|
+
* value in force at `lg` whether it was set there or inherited from `md`.
|
|
140
|
+
*
|
|
141
|
+
* Returns `undefined` when the caller supplied nothing, so the axis class is
|
|
142
|
+
* not applied at all.
|
|
143
|
+
*/
|
|
144
|
+
function resolveAxis(
|
|
145
|
+
value: GridResponsiveValue<string> | undefined,
|
|
146
|
+
componentName: string,
|
|
147
|
+
): Partial<Record<GridBreakpoint, string>> | undefined {
|
|
148
|
+
if (value === undefined || value === null) return undefined;
|
|
149
|
+
|
|
150
|
+
if (typeof value === "string") {
|
|
151
|
+
const filled: Partial<Record<GridBreakpoint, string>> = {};
|
|
152
|
+
for (const breakpoint of GRID_BREAKPOINTS) filled[breakpoint] = value;
|
|
153
|
+
return filled;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (typeof value !== "object") return undefined;
|
|
157
|
+
|
|
158
|
+
const unsupported = Object.keys(value).filter(
|
|
159
|
+
(key) => !(GRID_BREAKPOINTS as readonly string[]).includes(key),
|
|
160
|
+
);
|
|
161
|
+
if (unsupported.length > 0) {
|
|
162
|
+
// Loud rather than silent. Panda conditions other than a breakpoint
|
|
163
|
+
// (`_hover`, `_dark`, the array syntax) cannot be carried by a custom
|
|
164
|
+
// property, because there is no rule here that reads one under that
|
|
165
|
+
// condition. Before NEH-1453 they were dropped without a word; now they are
|
|
166
|
+
// dropped with one. Reach for `className={css({ ... })}` instead, which
|
|
167
|
+
// Panda extracts from the call site.
|
|
168
|
+
log.warn(`[${componentName}] ignoring unsupported responsive key(s)`, {
|
|
169
|
+
unsupported,
|
|
170
|
+
supported: GRID_BREAKPOINTS,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const filled: Partial<Record<GridBreakpoint, string>> = {};
|
|
175
|
+
let carried: string | undefined;
|
|
176
|
+
for (const breakpoint of GRID_BREAKPOINTS) {
|
|
177
|
+
const declared = (value as Partial<Record<GridBreakpoint, string>>)[breakpoint];
|
|
178
|
+
if (declared !== undefined) carried = declared;
|
|
179
|
+
if (carried !== undefined) filled[breakpoint] = carried;
|
|
180
|
+
}
|
|
181
|
+
return Object.keys(filled).length > 0 ? filled : undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* `repeat(n, minmax(<minTrackWidth>, 1fr))`, not `repeat(n, 1fr)`.
|
|
186
|
+
*
|
|
187
|
+
* In CSS `1fr` **is** shorthand for `minmax(auto, 1fr)`, and that `auto` floor
|
|
188
|
+
* is the grid item's automatic minimum size — its min-content. One child that
|
|
189
|
+
* cannot shrink therefore drags the whole track past the grid's container, and
|
|
190
|
+
* every sibling sized `width: 100%` inherits the overflow (NEH-1446/NEH-1447,
|
|
191
|
+
* measured at 425.875px inside a 375px container).
|
|
192
|
+
*
|
|
193
|
+
* `StyledSimpleGrid` made this change in 0.21.0 and `StyledGrid` deliberately
|
|
194
|
+
* did not, because at the time the emitted string still had to match a literal
|
|
195
|
+
* in the consumer's own source for any rule to exist at all — a new string
|
|
196
|
+
* would have broken that coincidence and dropped the columns entirely. Once the
|
|
197
|
+
* value stops travelling through Panda, that objection disappears, which is why
|
|
198
|
+
* the two changes belong in the same commit and not before it.
|
|
199
|
+
*/
|
|
200
|
+
function columnsToTemplate(count: number, minTrackWidth: string): string {
|
|
201
|
+
return `repeat(${count}, minmax(${minTrackWidth}, 1fr))`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function columnsToTemplateValue(
|
|
205
|
+
columns: GridResponsiveValue<number>,
|
|
206
|
+
minTrackWidth: string,
|
|
207
|
+
): GridResponsiveValue<string> | undefined {
|
|
208
|
+
if (typeof columns === "number") return columnsToTemplate(columns, minTrackWidth);
|
|
209
|
+
if (typeof columns !== "object" || columns === null) return undefined;
|
|
210
|
+
|
|
211
|
+
const mapped: Partial<Record<GridBreakpoint, string>> = {};
|
|
212
|
+
for (const [key, count] of Object.entries(columns)) {
|
|
213
|
+
if (typeof count === "number") {
|
|
214
|
+
mapped[key as GridBreakpoint] = columnsToTemplate(count, minTrackWidth);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return mapped;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Write one axis' resolved values out as inline custom properties. */
|
|
221
|
+
function writeAxisVars(
|
|
222
|
+
target: Record<string, string>,
|
|
223
|
+
axis: GridAxis,
|
|
224
|
+
resolved: Partial<Record<GridBreakpoint, string>>,
|
|
225
|
+
) {
|
|
226
|
+
for (const breakpoint of GRID_BREAKPOINTS) {
|
|
227
|
+
const value = resolved[breakpoint];
|
|
228
|
+
if (value !== undefined) target[`${TRACK_VAR[axis]}-${breakpoint}`] = value;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface StyledGridProps
|
|
233
|
+
extends Omit<
|
|
234
|
+
HTMLStyledProps<"div">,
|
|
235
|
+
"columns" | "gridTemplateColumns" | "gridTemplateRows" | "gridTemplateAreas"
|
|
236
|
+
> {
|
|
15
237
|
children?: React.ReactNode;
|
|
16
238
|
isStriped?: boolean;
|
|
17
239
|
showGridLines?: boolean;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
240
|
+
/**
|
|
241
|
+
* A `grid-template-columns` value, or one per breakpoint.
|
|
242
|
+
*
|
|
243
|
+
* Narrower than Panda's `ConditionalValue` on purpose: only `base` and the
|
|
244
|
+
* six preset breakpoints are carried. Any other condition is ignored with a
|
|
245
|
+
* warning — see `resolveAxis`.
|
|
246
|
+
*/
|
|
247
|
+
templateColumns?: GridResponsiveValue<string>;
|
|
248
|
+
templateRows?: GridResponsiveValue<string>;
|
|
249
|
+
templateAreas?: GridResponsiveValue<string>;
|
|
250
|
+
/** Alias for `templateColumns`; takes precedence when both are supplied. */
|
|
251
|
+
gridTemplateColumns?: GridResponsiveValue<string>;
|
|
252
|
+
gridTemplateRows?: GridResponsiveValue<string>;
|
|
253
|
+
gridTemplateAreas?: GridResponsiveValue<string>;
|
|
254
|
+
/** Column count. Emits `repeat(n, minmax(minTrackWidth, 1fr))`. */
|
|
255
|
+
columns?: GridResponsiveValue<number>;
|
|
256
|
+
/**
|
|
257
|
+
* The minimum size of each `columns`-generated track. Defaults to `"0"`.
|
|
258
|
+
*
|
|
259
|
+
* Pass `"auto"` to let a track refuse to shrink below its widest unbreakable
|
|
260
|
+
* child, which is occasionally what you want — a deliberately horizontally
|
|
261
|
+
* scrolled strip — but now has to be asked for by name. Mirrors
|
|
262
|
+
* `StyledSimpleGrid`.
|
|
263
|
+
*/
|
|
264
|
+
minTrackWidth?: string;
|
|
22
265
|
}
|
|
23
266
|
|
|
24
267
|
const StyledGrid = React.forwardRef<HTMLDivElement, StyledGridProps>(
|
|
25
|
-
(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
268
|
+
(
|
|
269
|
+
{
|
|
270
|
+
isStriped,
|
|
271
|
+
showGridLines,
|
|
272
|
+
className,
|
|
273
|
+
style,
|
|
274
|
+
templateColumns,
|
|
275
|
+
templateRows,
|
|
276
|
+
templateAreas,
|
|
277
|
+
gridTemplateColumns,
|
|
278
|
+
gridTemplateRows,
|
|
279
|
+
gridTemplateAreas,
|
|
280
|
+
columns,
|
|
281
|
+
minTrackWidth = "0",
|
|
282
|
+
...props
|
|
283
|
+
},
|
|
284
|
+
ref,
|
|
285
|
+
) => {
|
|
286
|
+
// Explicit template wins over the `columns` shorthand, as before.
|
|
287
|
+
const columnsValue =
|
|
288
|
+
gridTemplateColumns ??
|
|
289
|
+
templateColumns ??
|
|
290
|
+
(columns === undefined ? undefined : columnsToTemplateValue(columns, minTrackWidth));
|
|
291
|
+
|
|
292
|
+
const resolved: Record<GridAxis, Partial<Record<GridBreakpoint, string>> | undefined> = {
|
|
293
|
+
columns: resolveAxis(columnsValue, "StyledGrid"),
|
|
294
|
+
rows: resolveAxis(gridTemplateRows ?? templateRows, "StyledGrid"),
|
|
295
|
+
areas: resolveAxis(gridTemplateAreas ?? templateAreas, "StyledGrid"),
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const trackVars: Record<string, string> = {};
|
|
299
|
+
const axisClasses: string[] = [];
|
|
300
|
+
for (const axis of ["columns", "rows", "areas"] as const) {
|
|
301
|
+
const axisValues = resolved[axis];
|
|
302
|
+
if (axisValues === undefined) continue;
|
|
303
|
+
writeAxisVars(trackVars, axis, axisValues);
|
|
304
|
+
axisClasses.push(AXIS_CLASS[axis]);
|
|
305
|
+
}
|
|
306
|
+
|
|
33
307
|
const combinedClassName = cx(
|
|
34
308
|
isStriped ? stripedRecipe() : undefined,
|
|
35
309
|
showGridLines
|
|
@@ -42,35 +316,10 @@ const StyledGrid = React.forwardRef<HTMLDivElement, StyledGridProps>(
|
|
|
42
316
|
},
|
|
43
317
|
})
|
|
44
318
|
: undefined,
|
|
319
|
+
...axisClasses,
|
|
45
320
|
className,
|
|
46
321
|
);
|
|
47
322
|
|
|
48
|
-
// Map shorthand props to CSS grid properties
|
|
49
|
-
const gridTemplateColumns = props.gridTemplateColumns ?? templateColumns;
|
|
50
|
-
const gridTemplateRows = props.gridTemplateRows ?? templateRows;
|
|
51
|
-
const gridTemplateAreas = props.gridTemplateAreas ?? templateAreas;
|
|
52
|
-
delete props.gridTemplateColumns;
|
|
53
|
-
delete props.gridTemplateRows;
|
|
54
|
-
delete props.gridTemplateAreas;
|
|
55
|
-
|
|
56
|
-
// Handle columns prop (responsive column count)
|
|
57
|
-
let resolvedColumns = gridTemplateColumns;
|
|
58
|
-
if (columns !== undefined && !resolvedColumns) {
|
|
59
|
-
if (typeof columns === "number") {
|
|
60
|
-
resolvedColumns = `repeat(${columns}, 1fr)`;
|
|
61
|
-
} else if (typeof columns === "object" && columns !== null) {
|
|
62
|
-
const colObj = columns as { base?: number; sm?: number; md?: number; lg?: number; xl?: number };
|
|
63
|
-
// For responsive objects, we need to generate a responsive value
|
|
64
|
-
const responsive: Record<string, string> = {};
|
|
65
|
-
if (colObj.base !== undefined) responsive.base = `repeat(${colObj.base}, 1fr)`;
|
|
66
|
-
if (colObj.sm !== undefined) responsive.sm = `repeat(${colObj.sm}, 1fr)`;
|
|
67
|
-
if (colObj.md !== undefined) responsive.md = `repeat(${colObj.md}, 1fr)`;
|
|
68
|
-
if (colObj.lg !== undefined) responsive.lg = `repeat(${colObj.lg}, 1fr)`;
|
|
69
|
-
if (colObj.xl !== undefined) responsive.xl = `repeat(${colObj.xl}, 1fr)`;
|
|
70
|
-
resolvedColumns = responsive as ConditionalValue<string>;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
323
|
const childrenDetails = React.Children.map(props.children, (child) => {
|
|
75
324
|
if (React.isValidElement(child)) {
|
|
76
325
|
const element = child as React.ReactElement<{ id?: string }>;
|
|
@@ -91,13 +340,19 @@ const StyledGrid = React.forwardRef<HTMLDivElement, StyledGridProps>(
|
|
|
91
340
|
childrenDetails
|
|
92
341
|
});
|
|
93
342
|
|
|
343
|
+
// The caller's own `style` goes first: a `grid-template-columns` they set
|
|
344
|
+
// inline is their business and should still beat our class. Our custom
|
|
345
|
+
// properties are appended so nothing can accidentally shadow them.
|
|
346
|
+
const mergedStyle =
|
|
347
|
+
Object.keys(trackVars).length > 0
|
|
348
|
+
? ({ ...style, ...trackVars } as React.CSSProperties)
|
|
349
|
+
: style;
|
|
350
|
+
|
|
94
351
|
return (
|
|
95
352
|
<PandaGrid
|
|
96
353
|
ref={ref}
|
|
97
354
|
className={combinedClassName}
|
|
98
|
-
|
|
99
|
-
gridTemplateRows={gridTemplateRows}
|
|
100
|
-
gridTemplateAreas={gridTemplateAreas}
|
|
355
|
+
{...(mergedStyle === undefined ? {} : { style: mergedStyle })}
|
|
101
356
|
{...props}
|
|
102
357
|
/>
|
|
103
358
|
);
|
|
@@ -17,6 +17,19 @@ interface StyledSimpleGridProps extends Omit<HTMLStyledProps<"div">, "columns">
|
|
|
17
17
|
columns?: number | { base?: number; sm?: number; md?: number; lg?: number; xl?: number };
|
|
18
18
|
gridTemplateRows?: string;
|
|
19
19
|
gap?: string | number;
|
|
20
|
+
/**
|
|
21
|
+
* The minimum size of each `columns`-generated track. Defaults to `"0"`.
|
|
22
|
+
*
|
|
23
|
+
* `columns={n}` emits `repeat(n, minmax(<minTrackWidth>, 1fr))`. The default
|
|
24
|
+
* of `0` lets a track shrink below its content's min-content width, which is
|
|
25
|
+
* what keeps the grid inside its container.
|
|
26
|
+
*
|
|
27
|
+
* Pass `"auto"` to restore the pre-0.21.0 behaviour, in which a track refuses
|
|
28
|
+
* to shrink below its widest unbreakable child and the whole grid grows past
|
|
29
|
+
* its container. That is occasionally what you want — a deliberately
|
|
30
|
+
* horizontally scrolled strip — but it now has to be asked for by name.
|
|
31
|
+
*/
|
|
32
|
+
minTrackWidth?: string;
|
|
20
33
|
}
|
|
21
34
|
|
|
22
35
|
const PandaSimpleGrid = styled("div", {
|
|
@@ -51,6 +64,7 @@ const StyledSimpleGrid: React.FC<StyledSimpleGridProps> = ({
|
|
|
51
64
|
columns,
|
|
52
65
|
gridTemplateRows,
|
|
53
66
|
gap,
|
|
67
|
+
minTrackWidth = "0",
|
|
54
68
|
style,
|
|
55
69
|
children,
|
|
56
70
|
...rest
|
|
@@ -78,9 +92,22 @@ const StyledSimpleGrid: React.FC<StyledSimpleGridProps> = ({
|
|
|
78
92
|
}, [recalculate]);
|
|
79
93
|
|
|
80
94
|
// Runtime-computed grid values MUST use inline style — Panda CSS drops them at build time
|
|
95
|
+
//
|
|
96
|
+
// `minmax(minTrackWidth, 1fr)`, not a bare `1fr`. In CSS `1fr` IS shorthand
|
|
97
|
+
// for `minmax(auto, 1fr)`, and that `auto` floor is the grid item's automatic
|
|
98
|
+
// minimum size — its min-content. So one child that cannot shrink (a long
|
|
99
|
+
// unbroken string, a `white-space: nowrap` row, a fixed-width control) drags
|
|
100
|
+
// the track wider than the grid's own container, and every sibling sized
|
|
101
|
+
// `width: 100%` inherits the overflow. Measured on HopperGuard's dashboard at
|
|
102
|
+
// 375px: a 1-column grid resolved a 425.875px track inside a 375px container,
|
|
103
|
+
// and the shell clipped rather than scrolled (NEH-1446, NEH-1447).
|
|
104
|
+
//
|
|
105
|
+
// Guarded by grid-track-shrink.ct.tsx, which measures the resolved track in a
|
|
106
|
+
// real browser. jsdom cannot see this at all — it has no layout engine, so it
|
|
107
|
+
// reports every box as 0x0 and would agree that a 426px track fits 375px.
|
|
81
108
|
const gridStyles: React.CSSProperties = {
|
|
82
109
|
...style,
|
|
83
|
-
gridTemplateColumns: `repeat(${resolvedCols}, 1fr)`,
|
|
110
|
+
gridTemplateColumns: `repeat(${resolvedCols}, minmax(${minTrackWidth}, 1fr))`,
|
|
84
111
|
gridTemplateRows,
|
|
85
112
|
gap,
|
|
86
113
|
};
|