@stonedogcode/style 0.20.2 → 0.22.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.22.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",
|
|
@@ -49,6 +49,8 @@
|
|
|
49
49
|
"//publish:stonedog-style": "Publish to npm, end to end. Run from a terminal, interactively — npm prompts for the 2FA one-time password and the login flow needs a browser. It refuses a checkout that is detached, dirty, or behind origin/main: a submodule sits detached at the consumer's gitlink by default, and publishing from one commit behind ships a tarball missing the very thing you are publishing for while looking like a success (it did, on 2026-08-04, without TitleLogo.tsx). Runs the gate, prints the tarball listing, then proves the result by installing from the registry into a temp directory.",
|
|
50
50
|
"publish:stonedog-style": "bash scripts/publish-package.sh",
|
|
51
51
|
"publishnpm": "npm run publish:stonedog-style",
|
|
52
|
+
"//release": "One memorable command per repo. It cannot be called `publish`: that is a reserved npm lifecycle name, so `npm publish` inside scripts/publish-package.sh would fire it and run the whole gate and publish a second time, hitting the already-published guard and reporting a successful release as a failure.",
|
|
53
|
+
"release": "npm run publish:stonedog-style",
|
|
52
54
|
"version:bump:patch": "npm version patch --no-git-tag-version",
|
|
53
55
|
"version:bump:minor": "npm version minor --no-git-tag-version",
|
|
54
56
|
"audit": "npm audit --audit-level=moderate"
|
|
@@ -62,6 +64,7 @@
|
|
|
62
64
|
"csstype": "^3.2.3"
|
|
63
65
|
},
|
|
64
66
|
"devDependencies": {
|
|
67
|
+
"@eslint/js": "^9.0.0",
|
|
65
68
|
"@pandacss/dev": "^1.11.1",
|
|
66
69
|
"@playwright/experimental-ct-react": "^1.62.1",
|
|
67
70
|
"@playwright/test": "^1.62.1",
|
|
@@ -73,9 +76,10 @@
|
|
|
73
76
|
"@types/react-dom": "^19.2.3",
|
|
74
77
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
75
78
|
"@typescript-eslint/parser": "^8.0.0",
|
|
76
|
-
"eslint": "^
|
|
79
|
+
"eslint": "^9.39.5",
|
|
77
80
|
"eslint-plugin-react": "^7.37.0",
|
|
78
81
|
"eslint-plugin-react-hooks": "^5.0.0",
|
|
82
|
+
"globals": "^17.12.0",
|
|
79
83
|
"jest": "^29.7.0",
|
|
80
84
|
"jest-environment-jsdom": "^29.7.0",
|
|
81
85
|
"react": "^19.0.4",
|
|
@@ -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
|
);
|
|
@@ -1,3 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `"use client"`, and it is load-bearing.
|
|
5
|
+
*
|
|
6
|
+
* This component calls `useFontSizeProfile()` (a client hook) during its
|
|
7
|
+
* render. Without the directive above, the module is a Server Component in a
|
|
8
|
+
* consumer's App Router tree, the hook is invoked on the server, and React
|
|
9
|
+
* throws:
|
|
10
|
+
*
|
|
11
|
+
* Attempted to call useFontSizeProfile() from the server but
|
|
12
|
+
* useFontSizeProfile is on the client.
|
|
13
|
+
*
|
|
14
|
+
* Next serves that as its blank "This page couldn't load" page with no detail
|
|
15
|
+
* anywhere in the browser, so a consumer sees a dead route and nothing naming
|
|
16
|
+
* this component. Every PRD and how-to page on stonedogcode.com was unreachable
|
|
17
|
+
* this way (NEH-1290).
|
|
18
|
+
*
|
|
19
|
+
* Every other component here that calls the hook already declared it — this was
|
|
20
|
+
* the only one that did not, which is why the failure looked like something
|
|
21
|
+
* specific to whichever page happened to render a heading on the server.
|
|
22
|
+
*
|
|
23
|
+
* `src/components/__tests__/client-directive.test.ts` keeps this honest. It has
|
|
24
|
+
* to be a source assertion: a jsdom render imports the module directly, so no
|
|
25
|
+
* RSC boundary exists and every test here passes with or without the directive.
|
|
26
|
+
*/
|
|
27
|
+
|
|
1
28
|
import React from "react";
|
|
2
29
|
import StyledSeparator from "./StyledSeparator";
|
|
3
30
|
import StyledText from "./StyledText";
|
|
@@ -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
|
};
|