@stonedogcode/style 0.22.0 → 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.22.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.",
@@ -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 }}>