@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.
- package/LICENSE +201 -0
- package/NOTICE +18 -0
- package/README.md +699 -0
- package/package.json +95 -0
- package/src/components/DictationControls.tsx +141 -0
- package/src/components/DictationPrompt.tsx +78 -0
- package/src/components/StyledBox.tsx +174 -0
- package/src/components/StyledButton.tsx +144 -0
- package/src/components/StyledCollapsible.tsx +127 -0
- package/src/components/StyledDefinitionList.tsx +134 -0
- package/src/components/StyledFieldset.tsx +157 -0
- package/src/components/StyledFlex.tsx +13 -0
- package/src/components/StyledFooter.tsx +399 -0
- package/src/components/StyledFormLabel.tsx +141 -0
- package/src/components/StyledGrid.tsx +109 -0
- package/src/components/StyledGridItem.tsx +19 -0
- package/src/components/StyledHStack.tsx +145 -0
- package/src/components/StyledHeading.tsx +79 -0
- package/src/components/StyledHrRule.tsx +33 -0
- package/src/components/StyledIcon.tsx +172 -0
- package/src/components/StyledIconButton.tsx +135 -0
- package/src/components/StyledInputBool.tsx +81 -0
- package/src/components/StyledInputRadio.tsx +141 -0
- package/src/components/StyledInputSelect.tsx +115 -0
- package/src/components/StyledInputSlider.tsx +83 -0
- package/src/components/StyledInputText.tsx +146 -0
- package/src/components/StyledInputTextArea.tsx +119 -0
- package/src/components/StyledInputToggle.tsx +224 -0
- package/src/components/StyledList.tsx +188 -0
- package/src/components/StyledScrollbar.tsx +53 -0
- package/src/components/StyledSearch.tsx +78 -0
- package/src/components/StyledSeparator.tsx +38 -0
- package/src/components/StyledSidebar.tsx +555 -0
- package/src/components/StyledSimpleGrid.tsx +99 -0
- package/src/components/StyledSparkLine.tsx +119 -0
- package/src/components/StyledSpinner.tsx +91 -0
- package/src/components/StyledStack.tsx +62 -0
- package/src/components/StyledText.tsx +99 -0
- package/src/components/StyledTooltip.tsx +398 -0
- package/src/components/StyledVStack.tsx +143 -0
- package/src/components/TitleLogo.tsx +223 -0
- package/src/components/create-icon.tsx +66 -0
- package/src/components/create-intent-button.tsx +134 -0
- package/src/components/dictation.ts +71 -0
- package/src/components/intent-buttons.ts +154 -0
- package/src/config/can-hover.ts +75 -0
- package/src/config/density.ts +138 -0
- package/src/config/font-size.ts +113 -0
- package/src/config/intent-icons.tsx +116 -0
- package/src/config/logger.ts +60 -0
- package/src/config/style-config.tsx +263 -0
- package/src/config/types.ts +137 -0
- package/src/index.ts +259 -0
- package/src/preset/index.ts +243 -0
- package/src/preset/recipes/arrows.ts +29 -0
- package/src/preset/recipes/box.ts +122 -0
- package/src/preset/recipes/button.ts +161 -0
- package/src/preset/recipes/dl-list.ts +109 -0
- package/src/preset/recipes/drawer.ts +125 -0
- package/src/preset/recipes/form.ts +95 -0
- package/src/preset/recipes/icon-button.ts +161 -0
- package/src/preset/recipes/icon.ts +34 -0
- package/src/preset/recipes/input-bool.ts +184 -0
- package/src/preset/recipes/input-dropdown.ts +93 -0
- package/src/preset/recipes/input-radio.ts +158 -0
- package/src/preset/recipes/input-surface.ts +152 -0
- package/src/preset/recipes/input-text.ts +17 -0
- package/src/preset/recipes/list.ts +196 -0
- package/src/preset/recipes/menu.ts +28 -0
- package/src/preset/recipes/separator.ts +89 -0
- package/src/preset/recipes/stack.ts +89 -0
- package/src/preset/recipes/striped.ts +34 -0
- package/src/preset/recipes/text.ts +41 -0
- package/src/preset/recipes/tooltip.ts +77 -0
- 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;
|