@stonedogcode/style 0.9.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 (75) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +18 -0
  3. package/README.md +699 -0
  4. package/package.json +95 -0
  5. package/src/components/DictationControls.tsx +141 -0
  6. package/src/components/DictationPrompt.tsx +78 -0
  7. package/src/components/StyledBox.tsx +174 -0
  8. package/src/components/StyledButton.tsx +144 -0
  9. package/src/components/StyledCollapsible.tsx +127 -0
  10. package/src/components/StyledDefinitionList.tsx +134 -0
  11. package/src/components/StyledFieldset.tsx +157 -0
  12. package/src/components/StyledFlex.tsx +13 -0
  13. package/src/components/StyledFooter.tsx +399 -0
  14. package/src/components/StyledFormLabel.tsx +141 -0
  15. package/src/components/StyledGrid.tsx +109 -0
  16. package/src/components/StyledGridItem.tsx +19 -0
  17. package/src/components/StyledHStack.tsx +145 -0
  18. package/src/components/StyledHeading.tsx +79 -0
  19. package/src/components/StyledHrRule.tsx +33 -0
  20. package/src/components/StyledIcon.tsx +172 -0
  21. package/src/components/StyledIconButton.tsx +135 -0
  22. package/src/components/StyledInputBool.tsx +81 -0
  23. package/src/components/StyledInputRadio.tsx +141 -0
  24. package/src/components/StyledInputSelect.tsx +115 -0
  25. package/src/components/StyledInputSlider.tsx +83 -0
  26. package/src/components/StyledInputText.tsx +146 -0
  27. package/src/components/StyledInputTextArea.tsx +119 -0
  28. package/src/components/StyledInputToggle.tsx +224 -0
  29. package/src/components/StyledList.tsx +188 -0
  30. package/src/components/StyledScrollbar.tsx +53 -0
  31. package/src/components/StyledSearch.tsx +78 -0
  32. package/src/components/StyledSeparator.tsx +38 -0
  33. package/src/components/StyledSidebar.tsx +555 -0
  34. package/src/components/StyledSimpleGrid.tsx +99 -0
  35. package/src/components/StyledSparkLine.tsx +119 -0
  36. package/src/components/StyledSpinner.tsx +91 -0
  37. package/src/components/StyledStack.tsx +62 -0
  38. package/src/components/StyledText.tsx +99 -0
  39. package/src/components/StyledTooltip.tsx +398 -0
  40. package/src/components/StyledVStack.tsx +143 -0
  41. package/src/components/TitleLogo.tsx +223 -0
  42. package/src/components/create-icon.tsx +66 -0
  43. package/src/components/create-intent-button.tsx +134 -0
  44. package/src/components/dictation.ts +71 -0
  45. package/src/components/intent-buttons.ts +154 -0
  46. package/src/config/can-hover.ts +75 -0
  47. package/src/config/density.ts +138 -0
  48. package/src/config/font-size.ts +113 -0
  49. package/src/config/intent-icons.tsx +116 -0
  50. package/src/config/logger.ts +60 -0
  51. package/src/config/style-config.tsx +263 -0
  52. package/src/config/types.ts +137 -0
  53. package/src/index.ts +259 -0
  54. package/src/preset/index.ts +243 -0
  55. package/src/preset/recipes/arrows.ts +29 -0
  56. package/src/preset/recipes/box.ts +122 -0
  57. package/src/preset/recipes/button.ts +161 -0
  58. package/src/preset/recipes/dl-list.ts +109 -0
  59. package/src/preset/recipes/drawer.ts +125 -0
  60. package/src/preset/recipes/form.ts +95 -0
  61. package/src/preset/recipes/icon-button.ts +161 -0
  62. package/src/preset/recipes/icon.ts +34 -0
  63. package/src/preset/recipes/input-bool.ts +184 -0
  64. package/src/preset/recipes/input-dropdown.ts +93 -0
  65. package/src/preset/recipes/input-radio.ts +158 -0
  66. package/src/preset/recipes/input-surface.ts +152 -0
  67. package/src/preset/recipes/input-text.ts +17 -0
  68. package/src/preset/recipes/list.ts +196 -0
  69. package/src/preset/recipes/menu.ts +28 -0
  70. package/src/preset/recipes/separator.ts +89 -0
  71. package/src/preset/recipes/stack.ts +89 -0
  72. package/src/preset/recipes/striped.ts +34 -0
  73. package/src/preset/recipes/text.ts +41 -0
  74. package/src/preset/recipes/tooltip.ts +77 -0
  75. package/src/preset/semantic-variables.ts +283 -0
@@ -0,0 +1,398 @@
1
+ "use client";
2
+
3
+ import { log } from "../config/logger";
4
+ import React, { useRef, useState, useLayoutEffect, useEffect } from "react";
5
+ import { createPortal } from "react-dom";
6
+ import { styled } from "styled-system/jsx";
7
+ import StyledText from "./StyledText";
8
+ import type { AllowedVariant } from "../config/types";
9
+ import { tooltipRecipe } from "styled-system/recipes";
10
+ import { useCanHover } from "../config/can-hover";
11
+ import { useResolvedVariant } from "../config/style-config";
12
+
13
+ const TooltipTrigger = styled("div", {
14
+ base: {
15
+ display: "inline-block",
16
+ position: "relative",
17
+ cursor: "pointer",
18
+ },
19
+ });
20
+
21
+ const TooltipContent = styled("div");
22
+
23
+ /**
24
+ * The click-mode affordance.
25
+ *
26
+ * In click mode the tooltip cannot be opened by clicking the wrapper, because
27
+ * the wrapper usually contains a button and that click belongs to the button.
28
+ * So click mode renders this next to the child: a separate, visible, focusable
29
+ * control whose only job is to reveal the explanation. The reader can see that
30
+ * there is help, and where to press for it, without discovering that pressing
31
+ * the thing itself does something else entirely.
32
+ *
33
+ * Deliberately not `StyledButton` — that imports this module, and a cycle
34
+ * between two components that render each other is a module-init hazard for
35
+ * every consumer. Layout only, no colours, per the package rule.
36
+ */
37
+ const HelpTrigger = styled("button", {
38
+ base: {
39
+ display: "inline-flex",
40
+ alignItems: "center",
41
+ justifyContent: "center",
42
+ // 48 rather than the WCAG 2.5.5 floor of 44: this component is used by
43
+ // applications whose readers are frequently older, and a help control that
44
+ // is hard to hit is a help control that does not get used.
45
+ minWidth: "48px",
46
+ minHeight: "48px",
47
+ marginLeft: "4px",
48
+ borderRadius: "9999px",
49
+ borderWidth: "1px",
50
+ borderStyle: "solid",
51
+ cursor: "pointer",
52
+ fontWeight: "bold",
53
+ lineHeight: "1",
54
+ verticalAlign: "middle",
55
+ },
56
+ });
57
+
58
+ /**
59
+ * Everything the browser already puts in the tab sequence, plus anything given
60
+ * an explicit non-negative tabindex. Used to decide whether the trigger needs a
61
+ * tab stop of its own, or whether the child already provides one.
62
+ */
63
+ const FOCUSABLE_SELECTOR = [
64
+ "a[href]",
65
+ "button:not([disabled])",
66
+ "input:not([disabled])",
67
+ "select:not([disabled])",
68
+ "textarea:not([disabled])",
69
+ '[tabindex]:not([tabindex="-1"])',
70
+ '[contenteditable="true"]',
71
+ ].join(", ");
72
+
73
+ export interface StyledTooltipProps {
74
+ tooltip: React.ReactNode;
75
+ children: React.ReactNode;
76
+ delay?: number | undefined;
77
+ placement?: "top" | "bottom" | "left" | "right" | undefined;
78
+ boxBgAccent?: string | undefined; // theme color or fallback
79
+ size?: string | undefined;
80
+ "aria-label"?: string;
81
+ variant?: AllowedVariant | undefined;
82
+ style?: React.CSSProperties | undefined;
83
+ /**
84
+ * How the tooltip opens.
85
+ *
86
+ * `"hover"` (the default, and what an unset value means) preserves the
87
+ * long-standing behaviour: reveal on hover or focus, hide on leave or blur.
88
+ *
89
+ * `"click"` renders a separate help control beside the child and opens only
90
+ * when that control is pressed. Hover does nothing at all. This exists for
91
+ * readers whose pointer drifts — a hover tooltip closes itself before they
92
+ * arrive, and a hover tooltip they *are* reading vanishes when their hand
93
+ * moves. Hosts typically wire this to a user preference rather than setting
94
+ * it per call site.
95
+ */
96
+ trigger?: "hover" | "click" | undefined;
97
+ /**
98
+ * Accessible name for the click-mode help control. Defaults to
99
+ * "More information". Give it something specific where the surrounding
100
+ * context does not already make the subject obvious.
101
+ */
102
+ helpLabel?: string | undefined;
103
+ }
104
+
105
+ const StyledTooltip: React.FC<StyledTooltipProps> = ({
106
+ tooltip,
107
+ children,
108
+ delay = 120,
109
+ placement = "top",
110
+ size = "md",
111
+ "aria-label": ariaLabel,
112
+ variant,
113
+ trigger = "hover",
114
+ helpLabel = "More information",
115
+ ...rest
116
+ }) => {
117
+ // Caller's variant, else the app-wide one, else `solid` — and anything the
118
+ // tooltip recipe has no case for (`ghost`, `selected`, `link`, …) coerces to
119
+ // `solid` rather than rendering an unstyled floating box. The original coerced
120
+ // only `ghost` and `selected` by name; useResolvedVariant generalises that to
121
+ // "any variant this recipe does not define", which is the rule that was meant.
122
+ const finalVariant = useResolvedVariant(variant);
123
+ log.trace("StyledTooltip rendered");
124
+ const tooltipId = React.useId(); // <-- Move useId to top-level, before any returns or conditionals
125
+ const [visible, setVisible] = useState(false);
126
+ const [styles, setStyles] = useState({});
127
+
128
+ // `ReturnType<typeof setTimeout>`, not `NodeJS.Timeout`: this is browser
129
+ // code, and naming the NodeJS namespace makes the whole package fail to
130
+ // typecheck for any consumer that has not installed @types/node. It also
131
+ // happens to be wrong — in a browser this is a number, not a Timeout object.
132
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
133
+ const triggerRef = useRef<HTMLDivElement>(null);
134
+ const tooltipRef = useRef<HTMLDivElement>(null);
135
+ const helpRef = useRef<HTMLButtonElement>(null);
136
+ /**
137
+ * A hover trigger on a device that cannot hover is not a worse experience —
138
+ * it is an unreachable one. There is no hover event, and tapping the control
139
+ * activates it rather than explaining it, so the help is rendered, correct
140
+ * and impossible to see.
141
+ *
142
+ * So `hover` becomes `click` there, which renders the explicit control the
143
+ * click path already has. This only ever changes cases that were broken:
144
+ * `click` is unaffected, and a device that can hover is unaffected.
145
+ */
146
+ const canHover = useCanHover();
147
+ const isClick = trigger === "click" || !canHover;
148
+
149
+ // The child may be any component (StyledIconButton, a link, a bare span), so
150
+ // whether it is focusable can only be known from the rendered DOM — React
151
+ // cannot see inside a child component's output. Starts as "yes" so the common
152
+ // case (an icon button) never renders a spurious tab stop, not even for the
153
+ // one frame before this layout effect runs.
154
+ const [focusableChild, setFocusableChild] = useState<HTMLElement | null>(null);
155
+ const [hasFocusableChild, setHasFocusableChild] = useState(true);
156
+
157
+ // When the trigger KEEPS its tab stop it must have a role and a name (WCAG
158
+ // 2.2 4.1.2) — but only if nothing else already provides one. Borrowing the
159
+ // tooltip text unconditionally is what broke SharedWithIndicator, which names
160
+ // an ANCESTOR: two elements then claimed the same label, with role="button"
161
+ // nested inside role="img" (NEH-151).
162
+ //
163
+ // Defaults to false so the component never invents a name before it has
164
+ // measured. Silence is the safe direction — a missing name is the status quo,
165
+ // a duplicated one is a new bug.
166
+ const [needsFallbackName, setNeedsFallbackName] = useState(false);
167
+
168
+ useLayoutEffect(() => {
169
+ const node = triggerRef.current;
170
+ const found = node?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR) ?? null;
171
+ // Same-value setState is a no-op in React, so this cannot loop.
172
+ setFocusableChild((prev) => (prev === found ? prev : found));
173
+ setHasFocusableChild(found !== null);
174
+
175
+ if (!node) return;
176
+ // parentElement, not the node itself: closest() would match our own
177
+ // aria-label once we set one, and the answer would flip every render.
178
+ const namedByAncestor = Boolean(
179
+ node.parentElement?.closest("[aria-label], [aria-labelledby]"),
180
+ );
181
+ // Text content names an element for free; so does a labelled descendant
182
+ // (an icon carrying its own aria-label, an <img alt>).
183
+ const namedByContent =
184
+ (node.textContent ?? "").trim().length > 0 ||
185
+ Boolean(
186
+ node.querySelector('[aria-label], [aria-labelledby], img[alt]:not([alt=""])'),
187
+ );
188
+ setNeedsFallbackName(!namedByAncestor && !namedByContent);
189
+ }, [children]);
190
+
191
+ // aria-describedby has to sit on whatever actually receives focus, or a screen
192
+ // reader announces the control with no description. Set imperatively rather
193
+ // than by cloning the child: cloneElement would depend on every child
194
+ // component forwarding the prop, and a child that quietly drops it would fail
195
+ // invisibly.
196
+ useLayoutEffect(() => {
197
+ const node = focusableChild;
198
+ if (!node || !visible) return;
199
+ const previous = node.getAttribute("aria-describedby");
200
+ node.setAttribute(
201
+ "aria-describedby",
202
+ previous ? `${previous} ${tooltipId}` : tooltipId,
203
+ );
204
+ return () => {
205
+ if (previous === null) node.removeAttribute("aria-describedby");
206
+ else node.setAttribute("aria-describedby", previous);
207
+ };
208
+ }, [focusableChild, visible, tooltipId]);
209
+
210
+ useLayoutEffect(() => {
211
+ if (visible && triggerRef.current && tooltipRef.current) {
212
+ const triggerRect = triggerRef.current.getBoundingClientRect();
213
+ const tooltipRect = tooltipRef.current.getBoundingClientRect();
214
+ const margin = 8;
215
+
216
+ // Try placements in order of preference
217
+ const placements = [placement, "top", "bottom", "left", "right"];
218
+
219
+ let top = 0,
220
+ left = 0;
221
+
222
+ for (const tryPlacement of placements) {
223
+ if (tryPlacement === "top") {
224
+ top = triggerRect.top - tooltipRect.height - margin;
225
+ left =
226
+ triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
227
+ if (top >= 0) {
228
+
229
+ break;
230
+ }
231
+ } else if (tryPlacement === "bottom") {
232
+ top = triggerRect.bottom + margin;
233
+ left =
234
+ triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2;
235
+ if (top + tooltipRect.height <= window.innerHeight) {
236
+
237
+ break;
238
+ }
239
+ } else if (tryPlacement === "left") {
240
+ top =
241
+ triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;
242
+ left = triggerRect.left - tooltipRect.width - margin;
243
+ if (left >= 0) {
244
+
245
+ break;
246
+ }
247
+ } else if (tryPlacement === "right") {
248
+ top =
249
+ triggerRect.top + triggerRect.height / 2 - tooltipRect.height / 2;
250
+ left = triggerRect.right + margin;
251
+ if (left + tooltipRect.width <= window.innerWidth) {
252
+
253
+ break;
254
+ }
255
+ }
256
+ }
257
+
258
+ // Clamp to viewport
259
+ top = Math.max(
260
+ margin,
261
+ Math.min(top, window.innerHeight - tooltipRect.height - margin),
262
+ );
263
+ left = Math.max(
264
+ margin,
265
+ Math.min(left, window.innerWidth - tooltipRect.width - margin),
266
+ );
267
+
268
+ setStyles({ top, left });
269
+ }
270
+ }, [visible, placement]);
271
+
272
+ // Click mode owns its own dismissal. Hover mode needs none of this: it
273
+ // closes when the pointer leaves. A panel opened by a deliberate press has
274
+ // to be closable by a deliberate action, and Escape has to work, or a
275
+ // keyboard user is stuck with it open.
276
+ useEffect(() => {
277
+ if (!isClick || !visible || typeof document === "undefined") return;
278
+
279
+ const onKeyDown = (event: KeyboardEvent) => {
280
+ if (event.key !== "Escape") return;
281
+ setVisible(false);
282
+ // Focus goes back to what opened it — never to the top of the document.
283
+ helpRef.current?.focus();
284
+ };
285
+ const onPointerDown = (event: MouseEvent) => {
286
+ const target = event.target as Node;
287
+ // A press inside the tooltip is not a dismissal: the explanation may
288
+ // contain a link, and text worth selecting.
289
+ if (helpRef.current?.contains(target) || tooltipRef.current?.contains(target)) return;
290
+ setVisible(false);
291
+ };
292
+
293
+ document.addEventListener("keydown", onKeyDown);
294
+ document.addEventListener("mousedown", onPointerDown);
295
+ return () => {
296
+ document.removeEventListener("keydown", onKeyDown);
297
+ document.removeEventListener("mousedown", onPointerDown);
298
+ };
299
+ }, [isClick, visible]);
300
+
301
+ if (!tooltip) {
302
+ return <>{children}</>;
303
+ }
304
+
305
+ // Only a string tooltip can serve as a name — stringifying a React element
306
+ // would produce "[object Object]" in the accessibility tree.
307
+ const tooltipLabel = typeof tooltip === "string" ? tooltip : undefined;
308
+
309
+ const show = () => {
310
+ timeoutRef.current = setTimeout(() => setVisible(true), delay);
311
+ };
312
+ const hide = () => {
313
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
314
+ setVisible(false);
315
+ };
316
+
317
+ return (
318
+ <>
319
+ <TooltipTrigger
320
+ ref={triggerRef}
321
+ // When the child is focusable it already owns the tab stop, and
322
+ // onFocus/onBlur use bubbling focusin/focusout semantics, so the
323
+ // tooltip still fires without the wrapper taking focus itself. Adding
324
+ // tabIndex here regardless is what gave every tooltipped control two
325
+ // tab stops, the second of them silent (NEH-127).
326
+ tabIndex={isClick || hasFocusableChild ? undefined : 0}
327
+ // A focusable element needs a role and a name (WCAG 2.2 4.1.2). Applied
328
+ // only when the trigger keeps the tab stop AND nothing else names it —
329
+ // see needsFallbackName above for why the condition matters (NEH-151).
330
+ //
331
+ // role="button" rather than no role: the trigger is focusable and
332
+ // reveals content on focus, which is the closest standard role and what
333
+ // the ARIA tooltip pattern assumes of a trigger. A focusable generic
334
+ // with only a name still fails 4.1.2, which asks for both.
335
+ role={!isClick && !hasFocusableChild && needsFallbackName ? "button" : undefined}
336
+ aria-label={
337
+ isClick || hasFocusableChild
338
+ ? undefined
339
+ : ariaLabel ?? (needsFallbackName ? tooltipLabel : undefined)
340
+ }
341
+ // Hover handlers exist only in hover mode. In click mode a drifting
342
+ // pointer must change nothing at all — that is the entire point.
343
+ onMouseEnter={isClick ? undefined : show}
344
+ onMouseLeave={isClick ? undefined : hide}
345
+ onFocus={isClick ? undefined : show}
346
+ onBlur={isClick ? undefined : hide}
347
+ aria-describedby={
348
+ !isClick && !hasFocusableChild && visible ? tooltipId : undefined
349
+ }
350
+ {...rest}
351
+ >
352
+ {children}
353
+ {isClick && (
354
+ <HelpTrigger
355
+ ref={helpRef}
356
+ type="button"
357
+ aria-label={helpLabel}
358
+ aria-expanded={visible}
359
+ aria-controls={visible ? tooltipId : undefined}
360
+ onClick={() => setVisible((open) => !open)}
361
+ >
362
+ ?
363
+ </HelpTrigger>
364
+ )}
365
+ </TooltipTrigger>
366
+ {visible && typeof document !== "undefined" &&
367
+ createPortal(
368
+ <TooltipContent
369
+ id={tooltipId}
370
+ role="tooltip"
371
+ ref={tooltipRef}
372
+ pt={3}
373
+ pl={8}
374
+ pb={3}
375
+ pr={8}
376
+ className={tooltipRecipe({
377
+ variant: finalVariant,
378
+ })}
379
+ style={{
380
+ ...styles,
381
+ position: "fixed",
382
+ opacity: 1,
383
+ pointerEvents: "auto",
384
+ zIndex: 200000,
385
+ }}
386
+ onMouseEnter={isClick ? undefined : show}
387
+ onMouseLeave={isClick ? undefined : hide}
388
+ >
389
+ <StyledText size={size}>{tooltip}</StyledText>
390
+ </TooltipContent>,
391
+ document.body,
392
+ )
393
+ }
394
+ </>
395
+ );
396
+ };
397
+
398
+ export default StyledTooltip;
@@ -0,0 +1,143 @@
1
+ import React from "react";
2
+ import { vstack } from "styled-system/patterns";
3
+ import type { ConditionalValue } from "styled-system/types";
4
+ import { cx } from "styled-system/css";
5
+ import { stripedRecipe } from "styled-system/recipes";
6
+ import { Property } from "csstype";
7
+
8
+ export interface StyledVStackProps
9
+ extends React.HTMLAttributes<HTMLDivElement> {
10
+ opacity?: ConditionalValue<number> | undefined;
11
+ gap?: ConditionalValue<string | number> | undefined;
12
+ align?: ConditionalValue<string> | undefined;
13
+ justify?: ConditionalValue<string> | undefined;
14
+ width?: ConditionalValue<string> | undefined;
15
+ w?: ConditionalValue<string> | undefined;
16
+ marginBottom?: ConditionalValue<string> | undefined;
17
+ mb?: ConditionalValue<string | number> | undefined;
18
+ marginTop?: ConditionalValue<string | number> | undefined;
19
+ mt?: ConditionalValue<string | number> | undefined;
20
+ flexWrap?: ConditionalValue<string> | undefined;
21
+ display?: ConditionalValue<string> | undefined;
22
+ alignItems?: ConditionalValue<string> | undefined;
23
+ justifyContent?: ConditionalValue<string> | undefined;
24
+ p?: ConditionalValue<string | number> | undefined;
25
+ py?: ConditionalValue<string | number> | undefined;
26
+ px?: ConditionalValue<string | number> | undefined;
27
+ pt?: ConditionalValue<string | number> | undefined;
28
+ pb?: ConditionalValue<string | number> | undefined;
29
+ pl?: ConditionalValue<string | number> | undefined;
30
+ pr?: ConditionalValue<string | number> | undefined;
31
+ minH?: ConditionalValue<string | number> | undefined;
32
+ minHeight?: ConditionalValue<string | number> | undefined;
33
+ minWidth?: ConditionalValue<string | number> | undefined;
34
+ maxW?: ConditionalValue<string | number> | undefined;
35
+ maxWidth?: ConditionalValue<string | number> | undefined;
36
+ flex?: ConditionalValue<string | number> | undefined;
37
+ position?: ConditionalValue<string> | undefined;
38
+ textAlign?: ConditionalValue<string> | undefined;
39
+ borderWidth?: ConditionalValue<string | number> | undefined;
40
+ borderRadius?: ConditionalValue<string | number> | undefined;
41
+ flexDirection?: ConditionalValue<Property.FlexDirection> | undefined;
42
+ height?: ConditionalValue<string | number> | undefined;
43
+ h?: ConditionalValue<string | number> | undefined;
44
+ bg?: ConditionalValue<string> | undefined;
45
+ backgroundColor?: ConditionalValue<string> | undefined;
46
+ isStriped?: boolean | undefined;
47
+
48
+ }
49
+
50
+ export const StyledVStack: React.FC<StyledVStackProps> = ({
51
+ gap = "2",
52
+ align,
53
+ justify,
54
+ width,
55
+ w,
56
+ marginBottom,
57
+ mb,
58
+ marginTop,
59
+ mt,
60
+ flexWrap,
61
+ display,
62
+ alignItems,
63
+ justifyContent,
64
+ p,
65
+ py,
66
+ px,
67
+ pt,
68
+ pb,
69
+ pl,
70
+ pr,
71
+ minH,
72
+ minHeight,
73
+ minWidth,
74
+ maxW,
75
+ maxWidth,
76
+ flex,
77
+ position,
78
+ textAlign,
79
+ borderWidth,
80
+ borderRadius,
81
+ flexDirection,
82
+ height,
83
+ h,
84
+ bg,
85
+ backgroundColor,
86
+ isStriped,
87
+ opacity,
88
+ className,
89
+ children,
90
+ ...rest
91
+ }) => { // Map React-style props to styled-system pattern props
92
+ const mappedProps = {
93
+ opacity,
94
+ gap: typeof gap === "number" ? String(gap) : gap,
95
+ alignItems: align || alignItems,
96
+ justify: justify || justifyContent,
97
+ width: width || w,
98
+ marginBottom: marginBottom || mb,
99
+ marginTop: marginTop || mt,
100
+ flexWrap,
101
+ display,
102
+ p,
103
+ py,
104
+ px,
105
+ pt,
106
+ pb,
107
+ pl,
108
+ pr,
109
+ minH,
110
+ minHeight,
111
+ minWidth,
112
+ maxWidth: maxWidth || maxW,
113
+ flex,
114
+ position,
115
+ textAlign,
116
+ borderWidth,
117
+ borderRadius,
118
+ flexDirection,
119
+ height: height || h,
120
+ background: bg || backgroundColor,
121
+ };
122
+
123
+ // Remove undefined values
124
+ Object.keys(mappedProps).forEach(
125
+ (key) => mappedProps[key as keyof typeof mappedProps] === undefined && delete mappedProps[key as keyof typeof mappedProps],
126
+ );
127
+
128
+ const combinedClassName = cx(
129
+ vstack(mappedProps as unknown as Parameters<typeof vstack>[0]),
130
+ isStriped ? stripedRecipe() : undefined,
131
+ className,
132
+ );
133
+ const { ...divProps } = rest;
134
+ return (
135
+ <div className={combinedClassName} {...divProps}>
136
+ {children}
137
+ </div>
138
+ );
139
+ };
140
+
141
+ StyledVStack.displayName = "StyledVStack";
142
+
143
+ export default StyledVStack;