@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,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;