@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,145 @@
1
+ import React from "react";
2
+ import { hstack } from "styled-system/patterns";
3
+ import type { ConditionalValue } from "styled-system/types";
4
+ import { Property } from "csstype";
5
+
6
+ export interface StyledHStackProps
7
+ extends Omit<React.HTMLAttributes<HTMLDivElement>, "color"> {
8
+ as?: React.ElementType | undefined;
9
+ gap?: ConditionalValue<string | number> | undefined;
10
+ align?: ConditionalValue<string> | undefined;
11
+ justify?: ConditionalValue<string> | undefined;
12
+ width?: ConditionalValue<string> | undefined;
13
+ w?: ConditionalValue<string> | undefined;
14
+ marginBottom?: ConditionalValue<string> | undefined;
15
+ mb?: ConditionalValue<string | number> | undefined;
16
+ marginTop?: ConditionalValue<string | number> | undefined;
17
+ mt?: ConditionalValue<string | number> | undefined;
18
+ flexWrap?: ConditionalValue<string> | undefined;
19
+ display?: ConditionalValue<string> | undefined;
20
+ alignItems?: ConditionalValue<string> | undefined;
21
+ justifyContent?: ConditionalValue<string> | undefined;
22
+ p?: ConditionalValue<string | number> | undefined;
23
+ py?: ConditionalValue<string | number> | undefined;
24
+ px?: ConditionalValue<string | number> | undefined;
25
+ pt?: ConditionalValue<string | number> | undefined;
26
+ minH?: ConditionalValue<string | number> | undefined;
27
+ flexShrink?: ConditionalValue<string | number> | undefined;
28
+ flexDirection?: ConditionalValue<Property.FlexDirection> | undefined;
29
+ bg?: ConditionalValue<string> | undefined;
30
+ backgroundColor?: ConditionalValue<string> | undefined;
31
+ height?: ConditionalValue<string | number> | undefined;
32
+ h?: ConditionalValue<string | number> | undefined;
33
+ borderRadius?: ConditionalValue<string | number> | undefined;
34
+ borderWidth?: ConditionalValue<string | number> | undefined;
35
+ borderColor?: ConditionalValue<string> | undefined;
36
+ border?: ConditionalValue<string> | undefined;
37
+ opacity?: ConditionalValue<number> | undefined;
38
+ color?: ConditionalValue<string> | undefined;
39
+ _dark?: Record<string, unknown> | undefined;
40
+ // Allow additional Panda CSS style props
41
+ [key: string]: unknown;
42
+ }
43
+
44
+ export const StyledHStack: React.FC<StyledHStackProps> = ({
45
+ as: Component = "div",
46
+ gap = "2",
47
+ align,
48
+ justify,
49
+ width,
50
+ w,
51
+ marginBottom,
52
+ mb,
53
+ flexWrap,
54
+ display,
55
+ alignItems,
56
+ justifyContent,
57
+ p,
58
+ py,
59
+ px,
60
+ minH,
61
+ pt,
62
+ bg,
63
+ backgroundColor,
64
+ children,
65
+ flexDirection,
66
+ flexShrink,
67
+ marginTop,
68
+ mt,
69
+ height,
70
+ h,
71
+ borderRadius,
72
+ borderWidth,
73
+ borderColor,
74
+ border,
75
+ opacity,
76
+ color,
77
+ _dark,
78
+ className: _className,
79
+ style: _style,
80
+ ...rest
81
+ }) => {
82
+ // Separate HTML attributes from style props
83
+ const htmlAttrs: Record<string, unknown> = {};
84
+ const extraStyleProps: Record<string, unknown> = {};
85
+ for (const [key, value] of Object.entries(rest)) {
86
+ if (key.startsWith("on") || key.startsWith("data-") || key.startsWith("aria-") || ["id", "ref", "role", "tabIndex", "draggable", "title"].includes(key)) {
87
+ htmlAttrs[key] = value;
88
+ } else {
89
+ extraStyleProps[key] = value;
90
+ }
91
+ }
92
+
93
+ // Map React-style props to styled-system pattern props
94
+ const mappedProps: Record<string, unknown> = {
95
+ gap: typeof gap === "number" ? String(gap) : gap,
96
+ // `alignItems`, not `align`. The generated `hstack` pattern destructures
97
+ // only `justify` and `gap`, hard-codes `alignItems: "center"`, then spreads
98
+ // the rest — so only a key literally named `alignItems` overrides that
99
+ // centre. `align` is not a Panda utility either, so it used to survive into
100
+ // `css()` and emit a class name with no rule behind it (NEH-288).
101
+ alignItems: align || alignItems,
102
+ justify: justify || justifyContent,
103
+ width: width || w,
104
+ marginBottom: marginBottom || mb,
105
+ marginTop: marginTop || mt,
106
+ flexWrap,
107
+ display,
108
+ p,
109
+ py,
110
+ px,
111
+ minH,
112
+ pt,
113
+ background: bg || backgroundColor,
114
+ flexDirection,
115
+ flexShrink,
116
+ height: height || h,
117
+ borderRadius,
118
+ borderWidth,
119
+ borderColor,
120
+ border,
121
+ opacity,
122
+ color,
123
+ _dark,
124
+ ...extraStyleProps,
125
+ };
126
+
127
+ // Remove undefined values
128
+ Object.keys(mappedProps).forEach(
129
+ (key) => mappedProps[key] === undefined && delete mappedProps[key],
130
+ );
131
+
132
+ return (
133
+ <Component
134
+ className={hstack(mappedProps as Parameters<typeof hstack>[0])}
135
+ style={_style}
136
+ {...htmlAttrs}
137
+ >
138
+ {children}
139
+ </Component>
140
+ );
141
+ };
142
+
143
+ StyledHStack.displayName = "StyledHStack";
144
+
145
+ export default StyledHStack;
@@ -0,0 +1,79 @@
1
+ import React from "react";
2
+ import StyledSeparator from "./StyledSeparator";
3
+ import StyledText from "./StyledText";
4
+ import { useFontSizeProfile } from "../config/style-config";
5
+ import { stepUpFontSize } from "../config/font-size";
6
+ import type { FontSizeKey } from "../config/types";
7
+ import type { HTMLStyledProps } from "styled-system/jsx";
8
+
9
+ type SizeKey = FontSizeKey;
10
+
11
+ type StyledHeadingProps = HTMLStyledProps<"h1"> & {
12
+ addSeparator?: boolean;
13
+ size?: SizeKey;
14
+ as?: React.ElementType;
15
+ fixedSize?: boolean;
16
+ color?: string;
17
+ ellipsis?: boolean;
18
+ wrap?: boolean;
19
+ };
20
+
21
+ const StyledHeading = React.forwardRef<HTMLElement, StyledHeadingProps>(
22
+ (
23
+ {
24
+ children,
25
+ addSeparator,
26
+ size,
27
+ as = "h1",
28
+ fixedSize,
29
+ color,
30
+ ellipsis = false,
31
+ wrap = true,
32
+ ...rest
33
+ },
34
+ ref,
35
+ ) => {
36
+ const fontSizeProfile = useFontSizeProfile();
37
+ let baseSize: SizeKey;
38
+ if (size) {
39
+ baseSize = size;
40
+ } else if (fixedSize) {
41
+ baseSize = "md";
42
+ } else {
43
+ baseSize = fontSizeProfile;
44
+ }
45
+
46
+ // A heading reads one tier above whatever body text is currently set to,
47
+ // so the hierarchy survives every font-size profile rather than only the
48
+ // default one. Clamped at the top of the scale by stepUpFontSize.
49
+ const headingSize = stepUpFontSize(baseSize);
50
+
51
+ return (
52
+ <>
53
+ <StyledText
54
+ as={as}
55
+ ref={ref}
56
+ size={headingSize}
57
+ // The theme's heading face, so a theme can pair a display face with
58
+ // its body face (NEH-289). Asked for here rather than in textRecipe
59
+ // because StyledHeading shares that recipe with body copy. Written as
60
+ // a literal so Panda's extractor, which only reads source text, sees
61
+ // it.
62
+ fontFamily="heading"
63
+ fontWeight="bold"
64
+ fixedSize={fixedSize}
65
+ color={color}
66
+ ellipsis={ellipsis}
67
+ wrap={wrap}
68
+ {...rest}
69
+ >
70
+ {children}
71
+ </StyledText>
72
+ {addSeparator && <StyledSeparator />}
73
+ </>
74
+ );
75
+ },
76
+ );
77
+
78
+ StyledHeading.displayName = "StyledHeading";
79
+ export default StyledHeading;
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import { log } from "../config/logger";
4
+ import React from "react";
5
+ import { styled } from "styled-system/jsx";
6
+ import type { HTMLStyledProps } from "styled-system/types";
7
+
8
+ /**
9
+ * Modern styled `<hr>` — a thin horizontal rule with a subtle gradient
10
+ * fade to transparent at each end. Inherits Panda style props so callers
11
+ * can override `borderColor`, `my`, `opacity`, etc. as needed.
12
+ */
13
+ const PandaHr = styled("hr", {
14
+ base: {
15
+ border: "0",
16
+ height: "1px",
17
+ width: "100%",
18
+ margin: "0",
19
+
20
+ opacity: 0.85,
21
+ },
22
+ });
23
+
24
+ export type StyledHrRuleProps = HTMLStyledProps<"hr">;
25
+
26
+ export const StyledHrRule = React.forwardRef<HTMLHRElement, StyledHrRuleProps>(
27
+ function StyledHrRule(props, ref) {
28
+ log.trace("StyledHrRule rendered");
29
+ return <PandaHr bg="boxBgSecondary" ref={ref} role="separator" {...props} />;
30
+ },
31
+ );
32
+
33
+ export default StyledHrRule;
@@ -0,0 +1,172 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { iconRecipe } from "styled-system/recipes";
5
+ import { log } from "../config/logger";
6
+ import { useIconSize } from "../config/style-config";
7
+ import type { IconSize } from "../config/types";
8
+
9
+ /**
10
+ * The icon seam.
11
+ *
12
+ * This package ships **no icons**, on purpose: the components it was extracted
13
+ * from used a per-seat commercial icon set whose artwork cannot be
14
+ * redistributed under this licence. Rather than pick a replacement and impose
15
+ * it on every consumer, `StyledIcon` is a sizing-and-colouring wrapper that
16
+ * renders whatever node you hand it. Bring Lucide, Heroicons, Font Awesome,
17
+ * Material Symbols, or your own SVGs — the system only cares that the result
18
+ * lands in a consistently sized, theme-coloured box.
19
+ *
20
+ * Two mechanisms do the colouring, because icon libraries disagree about how
21
+ * they take a colour:
22
+ *
23
+ * 1. **`color` is set on the wrapper**, so any icon drawn with
24
+ * `fill="currentColor"` or `stroke="currentColor"` inherits it. That covers
25
+ * Lucide, Heroicons, Feather, Material Symbols, and most hand-rolled SVGs —
26
+ * for those, nothing further is needed.
27
+ * 2. **Custom properties are published** — `--icon-primary-color`,
28
+ * `--icon-secondary-color`, `--icon-secondary-opacity` — for sets that paint
29
+ * from variables instead, such as Font Awesome's duotone family. An adapter
30
+ * maps them in one line of CSS; see the README.
31
+ *
32
+ * Publishing neutral property names rather than any library's own is what keeps
33
+ * this package free of a dependency it must not have.
34
+ */
35
+
36
+ /**
37
+ * Size vocabulary. Defined in `config/types` because `StyleConfig` names it
38
+ * too — re-exported here so the long-standing import path keeps working.
39
+ */
40
+ export type { IconSize };
41
+
42
+ /** Rendered box size, in CSS px, for each size name. */
43
+ const SIZE_TO_PX: Record<string, number> = {
44
+ "2xs": 10,
45
+ xs: 12,
46
+ sm: 16,
47
+ "1x": 20,
48
+ md: 20,
49
+ lg: 24,
50
+ "2x": 32,
51
+ xl: 32,
52
+ "2xl": 40,
53
+ "3x": 48,
54
+ "4x": 64,
55
+ "5x": 80,
56
+ "6x": 96,
57
+ "7x": 112,
58
+ "8x": 128,
59
+ "9x": 144,
60
+ "10x": 160,
61
+ };
62
+
63
+ /** The recipe defines four size variants; the rest fall back to the base. */
64
+ const RECIPE_SIZES = new Set(["sm", "md", "lg", "xl"]);
65
+
66
+ export interface StyledIconProps
67
+ extends Omit<React.HTMLAttributes<HTMLSpanElement>, "color"> {
68
+ /**
69
+ * The icon to render — any React node. Passing `children` instead is
70
+ * equivalent; `icon` reads better at a call site that renders nothing else.
71
+ */
72
+ icon?: React.ReactNode;
73
+ children?: React.ReactNode;
74
+ /**
75
+ * Box size. Omit it and the app-wide default from `StonedogStyleProvider`
76
+ * applies (`"2x"` if the host sets nothing).
77
+ *
78
+ * **Prefer omitting it.** Setting the app-wide default once is what keeps an
79
+ * application to a single icon scale; a size named at the call site opts that
80
+ * icon out of ever being retuned. Pass it only where this specific icon must
81
+ * differ from everything around it.
82
+ */
83
+ size?: IconSize;
84
+ /** Primary colour. Defaults to the theme's main text colour. */
85
+ color?: string;
86
+ /** Secondary colour, for two-tone icon sets. Ignored by single-tone ones. */
87
+ secondaryColor?: string;
88
+ /** Opacity of the secondary tone, for sets that support it. */
89
+ secondaryOpacity?: number;
90
+ /**
91
+ * Accessible label. **Omit it for a decorative icon** — one that sits beside
92
+ * text already saying the same thing. An icon-only control needs one; an icon
93
+ * next to its own label must not have one, or screen readers announce the
94
+ * name twice.
95
+ */
96
+ title?: string;
97
+ }
98
+
99
+ /**
100
+ * Defaults reference the token layer rather than literal colours, so an icon
101
+ * with no explicit colour still follows the host's theme and colour mode.
102
+ */
103
+ const DEFAULT_PRIMARY = "var(--colors-text-main)";
104
+ const DEFAULT_SECONDARY = "var(--colors-icon-bg-primary)";
105
+
106
+ const StyledIcon: React.FC<StyledIconProps> = ({
107
+ icon,
108
+ children,
109
+ size: sizeProp,
110
+ color,
111
+ secondaryColor,
112
+ secondaryOpacity,
113
+ title,
114
+ className,
115
+ style,
116
+ ...rest
117
+ }) => {
118
+ log.trace("StyledIcon rendered");
119
+
120
+ // Caller's size, else the app-wide default, else "2x". Same precedence rule
121
+ // as `useResolvedVariant`, and shared for the same reason: when each control
122
+ // picks its own default, the app quietly grows several scales at once.
123
+ const appIconSize = useIconSize();
124
+ const size = sizeProp ?? appIconSize;
125
+
126
+ const recipeClass = iconRecipe(
127
+ RECIPE_SIZES.has(size) ? { size: size as "sm" | "md" | "lg" | "xl" } : {},
128
+ );
129
+ const mergedClassName = [recipeClass, className].filter(Boolean).join(" ");
130
+
131
+ const boxSize = SIZE_TO_PX[size] ?? 20;
132
+ const primary = color ?? DEFAULT_PRIMARY;
133
+
134
+ const mergedStyle = {
135
+ // Published for icon sets that paint from variables. Neutral names, so no
136
+ // particular library is baked in — adapters map them (see the README).
137
+ "--icon-primary-color": primary,
138
+ "--icon-secondary-color": secondaryColor ?? DEFAULT_SECONDARY,
139
+ ...(secondaryOpacity != null
140
+ ? { "--icon-secondary-opacity": secondaryOpacity }
141
+ : {}),
142
+ // For everything drawn with currentColor, which is most of them.
143
+ color: primary,
144
+ ...(typeof style === "object" && style !== null ? style : {}),
145
+ // After the caller's style on purpose: `size` is the prop that exists to
146
+ // control the box, so it must not be silently defeated by a stray height in
147
+ // a spread style object.
148
+ height: `${boxSize}px`,
149
+ width: `${boxSize}px`,
150
+ } as React.CSSProperties;
151
+
152
+ return (
153
+ <span
154
+ {...rest}
155
+ className={mergedClassName}
156
+ style={mergedStyle}
157
+ // A titled icon is being used AS the label for something, so it needs a
158
+ // role to go with the name. An untitled one is decorative and is hidden,
159
+ // rather than read out as an anonymous graphic.
160
+ role={title ? "img" : undefined}
161
+ aria-label={title}
162
+ aria-hidden={title ? undefined : true}
163
+ >
164
+ {icon ?? children}
165
+ </span>
166
+ );
167
+ };
168
+
169
+ StyledIcon.displayName = "StyledIcon";
170
+
171
+ export default StyledIcon;
172
+ export { StyledIcon };
@@ -0,0 +1,135 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import type { HTMLStyledProps } from "styled-system/types";
5
+ import { buttonIconRecipe } from "styled-system/recipes";
6
+ import type { AllowedVariant } from "../config/types";
7
+ import { useStyleConfig } from "../config/style-config";
8
+ import StyledTooltip from "./StyledTooltip";
9
+
10
+ /**
11
+ * A button whose whole label is an icon.
12
+ *
13
+ * ## The variant coercion is necessary, not arbitrary
14
+ *
15
+ * `buttonIconRecipe` defines **seven** variants — `solid`, `outline`, `aurora`,
16
+ * `glass`, `matte`, `ghost`, `none` — and the vocabulary has ten. The missing
17
+ * three have to go somewhere, or a caller passing one gets a button with a class
18
+ * name and no rules behind it:
19
+ *
20
+ * | Asked for | Rendered as | Why |
21
+ * |---|---|---|
22
+ * | `unstyled` | `ghost` | closest thing the recipe has to "no chrome" |
23
+ * | `link` | `outline` | a link-styled icon button has no affordance at all |
24
+ * | `selected` | `outline` | same — the recipe has no selected treatment |
25
+ *
26
+ * This is why the component does not use `useResolvedVariant`, which would send
27
+ * all three to `solid` and give a "no chrome" request a filled background.
28
+ * `StyledButton` does something different again, because *its* recipe defines
29
+ * all ten. Three components, three variant sets — check the recipe before you
30
+ * assume.
31
+ *
32
+ * ## An icon button MUST be named
33
+ *
34
+ * There is no visible text, so `aria-label` (or `tooltip`, which supplies one)
35
+ * is the only thing standing between a screen-reader user and a control
36
+ * announced as "button". Nothing here can enforce that, so it is asserted in the
37
+ * tests and stated here.
38
+ */
39
+
40
+ export type IconButtonSize = "1x" | "sm" | "md" | "lg";
41
+
42
+ /** Variants the icon recipe actually defines. */
43
+ type IconButtonVariant = "solid" | "outline" | "aurora" | "glass" | "matte" | "ghost" | "none";
44
+
45
+ export interface StyledIconButtonProps extends HTMLStyledProps<"button"> {
46
+ variant?: AllowedVariant;
47
+ size?: IconButtonSize;
48
+ children?: React.ReactNode;
49
+ disabled?: boolean;
50
+ /** Also supplies the accessible name when no `aria-label` is given. */
51
+ tooltip?: string;
52
+ placement?: "top" | "bottom" | "left" | "right";
53
+ /** Render as something else — an anchor, for instance. */
54
+ as?: React.ElementType;
55
+ href?: string;
56
+ target?: string;
57
+ rel?: string;
58
+ }
59
+
60
+ /** Map the full vocabulary onto what the recipe can actually paint. */
61
+ function toIconVariant(variant: AllowedVariant): IconButtonVariant {
62
+ if (variant === "unstyled") return "ghost";
63
+ if (variant === "link" || variant === "selected") return "outline";
64
+ return variant as IconButtonVariant;
65
+ }
66
+
67
+ const StyledIconButton = React.forwardRef<HTMLButtonElement, StyledIconButtonProps>(
68
+ function StyledIconButton(
69
+ {
70
+ children,
71
+ variant,
72
+ size = "md",
73
+ disabled,
74
+ tooltip,
75
+ placement,
76
+ as = "button",
77
+ onClick,
78
+ "aria-label": ariaLabel,
79
+ ...rest
80
+ },
81
+ ref,
82
+ ) {
83
+ const { variant: appVariant } = useStyleConfig();
84
+ const requested = variant ?? appVariant;
85
+ const painted = toIconVariant(requested);
86
+
87
+ const Element = as as React.ElementType;
88
+
89
+ // zIndex is pulled out of the Panda prop bag and applied inline. Panda would
90
+ // emit it as an atomic class, which loses to the recipe's own class in the
91
+ // cascade — so a caller stacking a button above a sibling would find it
92
+ // ignored.
93
+ const { zIndex, style, className: incoming, ...restWithoutZIndex } = rest;
94
+
95
+ return (
96
+ <StyledTooltip tooltip={tooltip} placement={placement}>
97
+ <Element
98
+ ref={ref}
99
+ className={[buttonIconRecipe({ variant: painted, size }), incoming]
100
+ .filter(Boolean)
101
+ .join(" ")}
102
+ // `data-panda-variant` reports what was ASKED for, not what was
103
+ // painted, so a caller inspecting the DOM can see their `link` was
104
+ // honoured as a request even though the recipe drew an outline.
105
+ data-panda-variant={requested}
106
+ // Fall back to the tooltip for the accessible name. An icon button has
107
+ // no visible text, so without one it announces as just "button" —
108
+ // WCAG 4.1.2. The tooltip is already the human-readable description of
109
+ // what the control does, so it is the right string, and an explicit
110
+ // aria-label still wins.
111
+ //
112
+ // This is a fix, not a port: upstream, a tooltip named the trigger
113
+ // ONLY when the child was not focusable. A button always is, so it got
114
+ // aria-describedby and no name. In the originating app that left 9 of
115
+ // 43 icon buttons unnamed (a further 10 have neither, which only the
116
+ // call sites can fix).
117
+ aria-label={ariaLabel ?? tooltip}
118
+ style={zIndex !== undefined ? { ...(style || {}), zIndex } : style}
119
+ onClick={onClick}
120
+ // Only a real <button> understands `disabled`; on an <a> it is
121
+ // meaningless and React would emit an invalid attribute.
122
+ disabled={as === "button" ? disabled : undefined}
123
+ {...restWithoutZIndex}
124
+ >
125
+ {children}
126
+ </Element>
127
+ </StyledTooltip>
128
+ );
129
+ },
130
+ );
131
+
132
+ StyledIconButton.displayName = "StyledIconButton";
133
+
134
+ export default StyledIconButton;
135
+ export { StyledIconButton };
@@ -0,0 +1,81 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import StyledText from "./StyledText";
5
+ import StyledHStack from "./StyledHStack";
6
+ import { styled } from "styled-system/jsx";
7
+ import { inputBoolRecipe } from "styled-system/recipes";
8
+ import { useResolvedVariant } from "../config/style-config";
9
+
10
+
11
+ /**
12
+ * A checkbox with its label.
13
+ *
14
+ * The whole thing is a `<label>`, so the text is part of the target rather than
15
+ * something next to it. A bare checkbox is roughly 14×14 CSS px — far under the
16
+ * 44×44 floor — and wrapping is what makes it hittable without demanding fine
17
+ * motor control. It is also why the label is not optional in practice: pass
18
+ * one, or the control has no accessible name and no usable target.
19
+ *
20
+ * Styling comes from `inputBoolRecipe`, which paints the box, the tick and the
21
+ * label as three slots. The variant resolves through `useResolvedVariant`, so a
22
+ * call site that says nothing inherits the app-wide choice.
23
+ */
24
+
25
+ /**
26
+ * What `inputBoolRecipe` actually defines — wider than the five a user can pick
27
+ * app-wide, because `ghost` and `none` are reachable per call site.
28
+ *
29
+ * Passed to `useResolvedVariant` explicitly. Without it the default gate is the
30
+ * theme five and `variant="ghost"` silently becomes `solid`.
31
+ *
32
+ * `button` is omitted deliberately: the recipe defines it, but it restyles the
33
+ * checkbox as a push button, which is a different control rather than a
34
+ * different appearance of this one.
35
+ */
36
+ export const INPUT_BOOL_VARIANTS = [
37
+ "solid",
38
+ "outline",
39
+ "aurora",
40
+ "glass",
41
+ "matte",
42
+ "ghost",
43
+ "none",
44
+ ] as const;
45
+
46
+ export type InputBoolVariant = (typeof INPUT_BOOL_VARIANTS)[number];
47
+
48
+ export interface StyledInputBoolProps
49
+ extends React.InputHTMLAttributes<HTMLInputElement> {
50
+ label?: string | React.ReactNode;
51
+ ["data-testid"]?: string;
52
+ isReadOnly?: boolean;
53
+ variant?: InputBoolVariant;
54
+ }
55
+
56
+ const StyledInputBool = React.forwardRef<
57
+ HTMLInputElement,
58
+ StyledInputBoolProps
59
+ >(function StyledInputBool({ label, isReadOnly, variant, ...props }, ref) {
60
+ const slots = inputBoolRecipe({
61
+ variant: useResolvedVariant(variant, INPUT_BOOL_VARIANTS),
62
+ });
63
+
64
+ return (
65
+ <StyledHStack as="label" className={slots.root}>
66
+ <styled.input
67
+ type="checkbox"
68
+ ref={ref}
69
+ data-testid={props["data-testid"]}
70
+ readOnly={isReadOnly}
71
+ {...props}
72
+ className={slots.control}
73
+ />
74
+ {label && <StyledText className={slots.label}>{label}</StyledText>}
75
+ </StyledHStack>
76
+ );
77
+ });
78
+
79
+ StyledInputBool.displayName = "StyledInputBool";
80
+
81
+ export default StyledInputBool;