@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,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A tiny inline trend line — the shape of a series, at the size of a word.
|
|
7
|
+
*
|
|
8
|
+
* It is deliberately not a chart. No axes, no ticks, no tooltip: a sparkline
|
|
9
|
+
* earns its place by sitting *inside* a sentence or a table cell, and anything
|
|
10
|
+
* that makes it want its own space has turned it into a chart that belongs in
|
|
11
|
+
* a chart component.
|
|
12
|
+
*
|
|
13
|
+
* ## Two defects fixed on the way across
|
|
14
|
+
*
|
|
15
|
+
* **It hardcoded the host's custom-property namespace.** The default colour was
|
|
16
|
+
* `var(--hopper-text-primary-text, #666)` — a literal `--hopper-*` property, so
|
|
17
|
+
* the component only themed correctly inside an app that had chosen that
|
|
18
|
+
* prefix. `cssVarPrefix` is configurable and still defaults to `"hopper"`
|
|
19
|
+
* (NEH-256), so a consumer that renames it would silently have got `#666`
|
|
20
|
+
* everywhere. Same defect class as NEH-171.
|
|
21
|
+
*
|
|
22
|
+
* The replacement is `currentColor`, which is better than reaching for any
|
|
23
|
+
* token: a sparkline is punctuation inside running text, so inheriting the
|
|
24
|
+
* colour of the text it sits in is what a reader expects, and it costs the
|
|
25
|
+
* host nothing to define. A caller wanting a specific colour still passes one.
|
|
26
|
+
*
|
|
27
|
+
* **It was invisible to assistive technology.** A bare `<svg>` with no role and
|
|
28
|
+
* no name is skipped entirely by a screen reader, so the trend simply did not
|
|
29
|
+
* exist for anyone not looking at it. It now renders as `role="img"` with a
|
|
30
|
+
* name, and `label` is required for that reason — there is no sensible default,
|
|
31
|
+
* because only the caller knows what the series *is*.
|
|
32
|
+
*
|
|
33
|
+
* If the surrounding text already states the trend, pass `label=""`: that marks
|
|
34
|
+
* it explicitly decorative (`aria-hidden`) rather than leaving it unnamed by
|
|
35
|
+
* accident. The distinction is the point — an empty string is a decision, a
|
|
36
|
+
* missing prop is an oversight.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export interface StyledSparkLineProps {
|
|
40
|
+
/** The series, in order. Fewer than two points renders nothing. */
|
|
41
|
+
data: number[];
|
|
42
|
+
/**
|
|
43
|
+
* What the line describes, for assistive tech — e.g. "Blood pressure, last
|
|
44
|
+
* 7 days". Pass `""` to mark it decorative when the surrounding copy
|
|
45
|
+
* already says it.
|
|
46
|
+
*/
|
|
47
|
+
label: string;
|
|
48
|
+
width?: number;
|
|
49
|
+
height?: number;
|
|
50
|
+
/** Defaults to `currentColor` — it inherits from the text around it. */
|
|
51
|
+
color?: string;
|
|
52
|
+
fillOpacity?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const StyledSparkLine: React.FC<StyledSparkLineProps> = ({
|
|
56
|
+
data,
|
|
57
|
+
label,
|
|
58
|
+
width = 80,
|
|
59
|
+
height = 24,
|
|
60
|
+
color = "currentColor",
|
|
61
|
+
fillOpacity = 0.1,
|
|
62
|
+
}) => {
|
|
63
|
+
if (!data || data.length < 2) return null;
|
|
64
|
+
|
|
65
|
+
const max = Math.max(...data);
|
|
66
|
+
const min = Math.min(...data);
|
|
67
|
+
const range = max - min || 1;
|
|
68
|
+
const padding = 1;
|
|
69
|
+
|
|
70
|
+
const points = data.map((v, i) => {
|
|
71
|
+
const x = padding + (i / (data.length - 1)) * (width - padding * 2);
|
|
72
|
+
const y = padding + (1 - (v - min) / range) * (height - padding * 2);
|
|
73
|
+
return `${x},${y}`;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const linePoints = points.join(" ");
|
|
77
|
+
|
|
78
|
+
// Closed path for the fill area: the line, then down and back along the
|
|
79
|
+
// baseline.
|
|
80
|
+
const firstX = padding;
|
|
81
|
+
const lastX = padding + (width - padding * 2);
|
|
82
|
+
const fillPath = `M${points[0]} ${points
|
|
83
|
+
.slice(1)
|
|
84
|
+
.map((p) => `L${p}`)
|
|
85
|
+
.join(" ")} L${lastX},${height - padding} L${firstX},${height - padding} Z`;
|
|
86
|
+
|
|
87
|
+
const decorative = label === "";
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<svg
|
|
91
|
+
width={width}
|
|
92
|
+
height={height}
|
|
93
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
94
|
+
// Inline, not a Panda prop: these are layout facts the component owns,
|
|
95
|
+
// and `flexShrink` in particular is what stops a table cell squashing it
|
|
96
|
+
// into a vertical line.
|
|
97
|
+
style={{ display: "block", flexShrink: 0 }}
|
|
98
|
+
// An `<svg>` defaults to `role="graphics-document"` in some engines and
|
|
99
|
+
// to nothing in others. Naming the role removes the difference.
|
|
100
|
+
role={decorative ? undefined : "img"}
|
|
101
|
+
aria-hidden={decorative ? true : undefined}
|
|
102
|
+
aria-label={decorative ? undefined : label}
|
|
103
|
+
>
|
|
104
|
+
<path d={fillPath} fill={color} opacity={fillOpacity} />
|
|
105
|
+
<polyline
|
|
106
|
+
points={linePoints}
|
|
107
|
+
fill="none"
|
|
108
|
+
stroke={color}
|
|
109
|
+
strokeWidth="1.5"
|
|
110
|
+
strokeLinejoin="round"
|
|
111
|
+
strokeLinecap="round"
|
|
112
|
+
/>
|
|
113
|
+
</svg>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
StyledSparkLine.displayName = "StyledSparkLine";
|
|
118
|
+
|
|
119
|
+
export default StyledSparkLine;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import StyledHStack from "./StyledHStack";
|
|
5
|
+
import StyledText from "./StyledText";
|
|
6
|
+
import { log } from "../config/logger";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A "still working" indicator: a label followed by animating dots.
|
|
10
|
+
*
|
|
11
|
+
* Text rather than a rotating graphic, deliberately. This came out of a product
|
|
12
|
+
* for an often-elderly audience, where a spinner that says *"Loading…"* is
|
|
13
|
+
* understood immediately and an abstract rotating shape is not. It also degrades
|
|
14
|
+
* honestly under `prefers-reduced-motion`: the dots change once every 500ms
|
|
15
|
+
* rather than spinning continuously.
|
|
16
|
+
*
|
|
17
|
+
* ## What was deliberately left behind
|
|
18
|
+
*
|
|
19
|
+
* The originating component had a second mode, `spinLogo`, that rotated
|
|
20
|
+
* HopperGuard's brand mark. That is **branding, not a primitive** — the same
|
|
21
|
+
* reason this package ships no icons — so it stays in the app, which keeps its
|
|
22
|
+
* own `StyledSpinner` wrapping this one for the dots path.
|
|
23
|
+
*
|
|
24
|
+
* Five props went with it: `thickness`, `speed`, `color`, `emptyColor` and
|
|
25
|
+
* `logoSize` were all declared, accepted, and **silently discarded** — none was
|
|
26
|
+
* read, and a search of the originating app found zero call sites passing any of
|
|
27
|
+
* them. A prop that does nothing is worse than a missing one: it reads as a
|
|
28
|
+
* supported knob and fails silently when someone turns it.
|
|
29
|
+
*
|
|
30
|
+
* `size` is gone for the same reason. It sized the logo wrapper only, so on the
|
|
31
|
+
* dots path it never did anything.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** How long each dot stays before the next is added. */
|
|
35
|
+
const DOT_INTERVAL_MS = 500;
|
|
36
|
+
const DOT_STATES = [".", "..", "..."] as const;
|
|
37
|
+
|
|
38
|
+
export interface StyledSpinnerProps
|
|
39
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
40
|
+
/**
|
|
41
|
+
* What the user is waiting for. A string is wrapped in `StyledText` so it
|
|
42
|
+
* picks up the app-wide font-size profile; a node is rendered as given.
|
|
43
|
+
*
|
|
44
|
+
* Say what is loading where you can — "Loading" alone tells someone nothing
|
|
45
|
+
* about whether to wait or navigate away.
|
|
46
|
+
*/
|
|
47
|
+
loadText?: React.ReactNode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const StyledSpinner = ({
|
|
51
|
+
loadText = "Loading",
|
|
52
|
+
...rest
|
|
53
|
+
}: StyledSpinnerProps) => {
|
|
54
|
+
log.trace("StyledSpinner rendered");
|
|
55
|
+
|
|
56
|
+
const [dotIndex, setDotIndex] = React.useState(0);
|
|
57
|
+
|
|
58
|
+
React.useEffect(() => {
|
|
59
|
+
const interval = setInterval(
|
|
60
|
+
() => setDotIndex((i) => (i + 1) % DOT_STATES.length),
|
|
61
|
+
DOT_INTERVAL_MS,
|
|
62
|
+
);
|
|
63
|
+
return () => clearInterval(interval);
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
// `role="status"` — an implicit `aria-live="polite"` region, so a screen
|
|
68
|
+
// reader announces "Loading" once when this appears instead of leaving the
|
|
69
|
+
// user with silence. The original had no role at all: sighted users got an
|
|
70
|
+
// indicator and everyone else got nothing, which is a WCAG 4.1.3 gap rather
|
|
71
|
+
// than a missing nicety.
|
|
72
|
+
<StyledHStack gap={1} role="status" {...rest}>
|
|
73
|
+
{typeof loadText === "string" ? (
|
|
74
|
+
<StyledText>{loadText}</StyledText>
|
|
75
|
+
) : (
|
|
76
|
+
loadText
|
|
77
|
+
)}
|
|
78
|
+
{/*
|
|
79
|
+
The dots are decorative and deliberately hidden. The label beside them
|
|
80
|
+
already says what is happening, and inside a live region a screen reader
|
|
81
|
+
would otherwise re-announce the whole thing twice a second as they
|
|
82
|
+
change — turning a helpful status into unusable chatter.
|
|
83
|
+
*/}
|
|
84
|
+
<StyledText aria-hidden="true">{DOT_STATES[dotIndex]}</StyledText>
|
|
85
|
+
</StyledHStack>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
StyledSpinner.displayName = "StyledSpinner";
|
|
90
|
+
|
|
91
|
+
export default StyledSpinner;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import StyledHStack, { StyledHStackProps } from "./StyledHStack";
|
|
3
|
+
import StyledVStack, { StyledVStackProps } from "./StyledVStack";
|
|
4
|
+
import { ConditionalValue } from "styled-system/types";
|
|
5
|
+
import { Property } from "csstype";
|
|
6
|
+
|
|
7
|
+
export interface StyledStackProps
|
|
8
|
+
extends Omit<StyledHStackProps, "direction">,
|
|
9
|
+
Omit<StyledVStackProps, "direction"> {
|
|
10
|
+
direction?: ConditionalValue<Property.FlexDirection>;
|
|
11
|
+
divider?: React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const StyledStack: React.FC<StyledStackProps> = ({
|
|
15
|
+
direction = "column",
|
|
16
|
+
divider,
|
|
17
|
+
children,
|
|
18
|
+
...rest
|
|
19
|
+
}) => {
|
|
20
|
+
const isResponsive = typeof direction === "object";
|
|
21
|
+
|
|
22
|
+
const childrenWithDividers = divider
|
|
23
|
+
? React.Children.toArray(children).flatMap((child, index) => {
|
|
24
|
+
if (index === 0) {
|
|
25
|
+
return [<React.Fragment key={`child-${index}`}>{child}</React.Fragment>];
|
|
26
|
+
}
|
|
27
|
+
return [
|
|
28
|
+
<React.Fragment key={`divider-${index}`}>{divider}</React.Fragment>,
|
|
29
|
+
<React.Fragment key={`child-${index}`}>{child}</React.Fragment>,
|
|
30
|
+
];
|
|
31
|
+
})
|
|
32
|
+
: children;
|
|
33
|
+
|
|
34
|
+
if (isResponsive) {
|
|
35
|
+
const responsiveDirections = direction as Record<string, string>;
|
|
36
|
+
const baseDirection = responsiveDirections.base || "column";
|
|
37
|
+
|
|
38
|
+
if (baseDirection === "column") {
|
|
39
|
+
return (
|
|
40
|
+
<StyledVStack {...rest} flexDirection={direction}>
|
|
41
|
+
{childrenWithDividers}
|
|
42
|
+
</StyledVStack>
|
|
43
|
+
);
|
|
44
|
+
} else {
|
|
45
|
+
return (
|
|
46
|
+
<StyledHStack {...rest} flexDirection={direction}>
|
|
47
|
+
{childrenWithDividers}
|
|
48
|
+
</StyledHStack>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (direction === "row") {
|
|
54
|
+
return <StyledHStack {...rest}>{childrenWithDividers}</StyledHStack>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return <StyledVStack {...rest}>{childrenWithDividers}</StyledVStack>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
StyledStack.displayName = "StyledStack";
|
|
61
|
+
|
|
62
|
+
export default StyledStack;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { log } from "../config/logger";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { styled } from "styled-system/jsx";
|
|
6
|
+
import { css, cx } from "styled-system/css";
|
|
7
|
+
import StyledTooltip from "./StyledTooltip";
|
|
8
|
+
import { useFontSizeProfile } from "../config/style-config";
|
|
9
|
+
import { fontSizeMap } from "../config/font-size";
|
|
10
|
+
import type { AllowedTextVariant } from "../config/types";
|
|
11
|
+
import { textRecipe } from "styled-system/recipes";
|
|
12
|
+
|
|
13
|
+
const PandaText = styled("span", textRecipe);
|
|
14
|
+
|
|
15
|
+
export interface StyledTextProps
|
|
16
|
+
extends React.ComponentProps<typeof PandaText> {
|
|
17
|
+
children?: React.ReactNode | undefined;
|
|
18
|
+
|
|
19
|
+
tooltip?: React.ReactNode | undefined;
|
|
20
|
+
as?: React.ElementType | undefined; // Explicitly add the 'as' prop
|
|
21
|
+
size?: keyof typeof fontSizeMap | undefined;
|
|
22
|
+
fixedSize?: boolean | undefined;
|
|
23
|
+
color?: string | undefined;
|
|
24
|
+
ellipsis?: boolean | undefined;
|
|
25
|
+
wrap?: boolean | undefined;
|
|
26
|
+
|
|
27
|
+
variant?: AllowedTextVariant | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const StyledText = React.forwardRef<HTMLSpanElement, StyledTextProps>((props, ref) => {
|
|
31
|
+
log.trace("StyledText rendered");
|
|
32
|
+
const {
|
|
33
|
+
children,
|
|
34
|
+
tooltip,
|
|
35
|
+
size,
|
|
36
|
+
fixedSize,
|
|
37
|
+
color = "textPrimary",
|
|
38
|
+
variant,
|
|
39
|
+
style,
|
|
40
|
+
ellipsis,
|
|
41
|
+
wrap,
|
|
42
|
+
textAlign,
|
|
43
|
+
className,
|
|
44
|
+
...rest
|
|
45
|
+
} = props;
|
|
46
|
+
const fontSizeProfile = useFontSizeProfile();
|
|
47
|
+
|
|
48
|
+
let finalSize;
|
|
49
|
+
if (size) {
|
|
50
|
+
finalSize = size;
|
|
51
|
+
} else if (fixedSize) {
|
|
52
|
+
finalSize = "md";
|
|
53
|
+
} else {
|
|
54
|
+
finalSize = fontSizeProfile;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const fontSize = fontSizeMap[finalSize] || fontSizeMap.md;
|
|
58
|
+
|
|
59
|
+
const extraStyles: React.CSSProperties = {};
|
|
60
|
+
if (ellipsis) {
|
|
61
|
+
extraStyles.textOverflow = "ellipsis";
|
|
62
|
+
extraStyles.whiteSpace = "nowrap";
|
|
63
|
+
extraStyles.overflow = "hidden";
|
|
64
|
+
extraStyles.display = "block";
|
|
65
|
+
}
|
|
66
|
+
if (wrap === false) {
|
|
67
|
+
extraStyles.whiteSpace = "nowrap";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const textAlignClass = textAlign ? css({ textAlign }) : "";
|
|
71
|
+
const combinedClassName = cx(className, textAlignClass);
|
|
72
|
+
|
|
73
|
+
const textElement = (
|
|
74
|
+
<PandaText
|
|
75
|
+
ref={ref}
|
|
76
|
+
style={{ fontSize, ...extraStyles, ...style }}
|
|
77
|
+
color={color}
|
|
78
|
+
variant={variant}
|
|
79
|
+
className={combinedClassName}
|
|
80
|
+
{...rest}
|
|
81
|
+
>
|
|
82
|
+
{children}
|
|
83
|
+
</PandaText>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<>
|
|
88
|
+
{tooltip ? (
|
|
89
|
+
<StyledTooltip tooltip={tooltip}>{textElement}</StyledTooltip>
|
|
90
|
+
) : (
|
|
91
|
+
textElement
|
|
92
|
+
)}
|
|
93
|
+
</>
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
StyledText.displayName = "StyledText";
|
|
98
|
+
|
|
99
|
+
export default StyledText;
|