@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,127 @@
1
+ "use client";
2
+
3
+ import React, { useId, useState } from "react";
4
+ import { styled } from "styled-system/jsx";
5
+
6
+ /**
7
+ * A disclosure: a trigger that shows and hides a region.
8
+ *
9
+ * Built here rather than in an app because three products want the same thing
10
+ * and one of them currently reaches for Chakra to get it. It knows nothing
11
+ * about what it contains.
12
+ *
13
+ * ## Why the content stays mounted
14
+ *
15
+ * Collapsed content is hidden with `hidden`, not unmounted. Unmounting looks
16
+ * tidier and is wrong for the audience this package serves: it discards focus,
17
+ * scroll position and any input a reader had part-typed, so a mis-click on the
18
+ * trigger destroys work rather than merely hiding it. `hidden` also keeps the
19
+ * region addressable by `aria-controls` at all times, which is what lets the
20
+ * trigger's `aria-expanded` mean anything.
21
+ *
22
+ * `hidden` rather than `display: none` in CSS: it is the accessibility default,
23
+ * it removes the content from the accessibility tree *and* from tab order in
24
+ * one attribute, and it cannot be defeated by a host stylesheet the way a
25
+ * utility class can.
26
+ */
27
+
28
+ export interface StyledCollapsibleProps {
29
+ /** The disclosure's own control. Rendered inside a `<button>`. */
30
+ trigger: React.ReactNode;
31
+ children: React.ReactNode;
32
+ /** Controlled. Omit to let the component own the state. */
33
+ open?: boolean | undefined;
34
+ /** Initial state when uncontrolled. Default `false`. */
35
+ defaultOpen?: boolean | undefined;
36
+ onOpenChange?: ((next: boolean) => void) | undefined;
37
+ /**
38
+ * Names the trigger.
39
+ *
40
+ * State belongs in `aria-expanded`, never in the name — a name that reads
41
+ * "Collapse tools" is announced as "Collapse tools, expanded", which says the
42
+ * same thing twice and in opposite tenses. Say what the control is *for*.
43
+ */
44
+ "aria-label"?: string | undefined;
45
+ /** Escape hatch for a host that must reach the trigger in a test. */
46
+ triggerTestId?: string | undefined;
47
+ contentTestId?: string | undefined;
48
+ }
49
+
50
+ /**
51
+ * 48px, matching every other interactive floor in this package: stated as a
52
+ * minimum rather than left to emerge from padding, so no density step or font
53
+ * profile can erode it below the WCAG target size.
54
+ */
55
+ const CONTROL_MIN_TARGET = "48px";
56
+
57
+ const CollapsibleTrigger = styled("button", {
58
+ base: {
59
+ display: "inline-flex",
60
+ alignItems: "center",
61
+ gap: "2",
62
+ minHeight: CONTROL_MIN_TARGET,
63
+ minWidth: CONTROL_MIN_TARGET,
64
+ px: "2",
65
+ borderRadius: "md",
66
+ borderWidth: "1px",
67
+ borderStyle: "solid",
68
+ borderColor: "borderBgPrimary",
69
+ color: "textPrimary",
70
+ background: "transparent",
71
+ cursor: "pointer",
72
+ },
73
+ });
74
+
75
+ const StyledCollapsible: React.FC<StyledCollapsibleProps> = ({
76
+ trigger,
77
+ children,
78
+ open,
79
+ defaultOpen = false,
80
+ onOpenChange,
81
+ "aria-label": ariaLabel,
82
+ triggerTestId,
83
+ contentTestId,
84
+ }) => {
85
+ const [uncontrolled, setUncontrolled] = useState(defaultOpen);
86
+
87
+ // Controlled the moment `open` is supplied, and uncontrolled otherwise —
88
+ // decided per render rather than latched at mount, because a host that
89
+ // switches between the two mid-life has a bug we should not paper over by
90
+ // silently ignoring the prop.
91
+ const isControlled = open !== undefined;
92
+ const isOpen = isControlled ? open : uncontrolled;
93
+
94
+ const contentId = useId();
95
+
96
+ const toggle = () => {
97
+ const next = !isOpen;
98
+ // The internal state moves even when controlled. If the host ignores the
99
+ // callback the component would otherwise appear dead to the pointer, and a
100
+ // control that does nothing when pressed is indistinguishable from a broken
101
+ // one — for this audience, the reader concludes the app is broken, not that
102
+ // they misread the affordance.
103
+ if (!isControlled) setUncontrolled(next);
104
+ onOpenChange?.(next);
105
+ };
106
+
107
+ return (
108
+ <>
109
+ <CollapsibleTrigger
110
+ type="button"
111
+ onClick={toggle}
112
+ aria-expanded={isOpen}
113
+ aria-controls={contentId}
114
+ aria-label={ariaLabel}
115
+ data-testid={triggerTestId}
116
+ >
117
+ {trigger}
118
+ </CollapsibleTrigger>
119
+ <div id={contentId} hidden={!isOpen} data-testid={contentTestId}>
120
+ {children}
121
+ </div>
122
+ </>
123
+ );
124
+ };
125
+
126
+ export default StyledCollapsible;
127
+ export { StyledCollapsible };
@@ -0,0 +1,134 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { styled } from "styled-system/jsx";
5
+ import { dlRecipe } from "styled-system/recipes";
6
+ import { useResolvedVariant } from "../config/style-config";
7
+ import { ALL_VARIANTS, type AllowedVariant } from "../config/types";
8
+
9
+ /**
10
+ * Term-and-definition pairs in two columns — the shape a details panel wants.
11
+ *
12
+ * `<dl>` rather than a table because these are *pairs*, not a grid: there is
13
+ * one value per label, no column headers, and nothing to sort. A screen reader
14
+ * announces "list, 6 items" and reads each term with its definition, which a
15
+ * two-column table of unheaded cells does not.
16
+ *
17
+ * ```tsx
18
+ * <StyledDefinitionList.Root>
19
+ * <StyledDefinitionList.Term>Admitted</StyledDefinitionList.Term>
20
+ * <StyledDefinitionList.Definition>4 March</StyledDefinitionList.Definition>
21
+ * </StyledDefinitionList.Root>
22
+ * ```
23
+ *
24
+ * ## Two defects fixed on the way across
25
+ *
26
+ * **It ignored the app-wide appearance.** The variant defaulted to a literal
27
+ * `"solid"` and never consulted the host's configuration, so a product set to
28
+ * `glass` or `matte` got one panel that stayed solid regardless. Every other
29
+ * component in this package resolves through `useResolvedVariant`; this one
30
+ * predated it. It does now, which means an unset variant follows the app
31
+ * rather than overriding it.
32
+ *
33
+ * **The row separators never rendered.** `dlRecipe` drew them with
34
+ * `& > li:not(:last-child)` — but a `<dl>` contains `<dt>` and `<dd>`, never
35
+ * `<li>`, so the selector matched nothing in the only element the recipe is
36
+ * ever applied to. The rule was present, valid, and dead. It is `& > dd` now,
37
+ * which puts a line after each pair rather than between a term and its own
38
+ * definition.
39
+ *
40
+ * That fix is in the recipe rather than here, so it reaches any consumer using
41
+ * `dlRecipe` directly. Nothing consumed it before this component, so no
42
+ * existing surface changes.
43
+ *
44
+ * ## The `link` / `selected` coercion is gone
45
+ *
46
+ * The originating version silently mapped `link`, `ghost` and `selected` onto
47
+ * `solid`. A coerced variant still renders, so a call site asking for `ghost`
48
+ * saw a solid panel and no warning — the same silent-coercion failure NEH-234
49
+ * documented. `useResolvedVariant`'s allow-list makes the supported set
50
+ * explicit instead: `DL_VARIANTS` is what the recipe actually defines.
51
+ */
52
+
53
+ const PandaDefinitionList = styled("dl", {
54
+ base: {
55
+ display: "grid",
56
+ gridTemplateColumns: "max-content 1fr",
57
+ gap: "0.5rem 1.5rem",
58
+ padding: "1rem",
59
+ },
60
+ });
61
+
62
+ const PandaTerm = styled("dt", {
63
+ base: {
64
+ fontWeight: "bold",
65
+ gridColumn: 1,
66
+ },
67
+ });
68
+
69
+ const PandaDescription = styled("dd", {
70
+ base: {
71
+ margin: 0,
72
+ gridColumn: 2,
73
+ },
74
+ });
75
+
76
+ /** The appearances `dlRecipe` actually defines. */
77
+ export const DL_VARIANTS = [
78
+ "solid",
79
+ "outline",
80
+ "aurora",
81
+ "glass",
82
+ "matte",
83
+ "none",
84
+ "unstyled",
85
+ ] as const;
86
+
87
+ export type DlVariant = (typeof DL_VARIANTS)[number];
88
+
89
+ export type StyledDefinitionListProps = React.PropsWithChildren<
90
+ React.HTMLAttributes<HTMLDListElement> & {
91
+ /**
92
+ * `undefined` is spelled out because the repo runs
93
+ * `exactOptionalPropertyTypes`: without it a caller holding a
94
+ * possibly-unset variant cannot forward it, even though "unset" is
95
+ * precisely the case this component handles by following the app.
96
+ */
97
+ variant?: AllowedVariant | DlVariant | undefined;
98
+ }
99
+ >;
100
+
101
+ const StyledDefinitionListRoot = React.forwardRef<
102
+ HTMLDListElement,
103
+ StyledDefinitionListProps
104
+ >(({ children, variant, className, ...rest }, ref) => {
105
+ const resolved = useResolvedVariant(variant, DL_VARIANTS);
106
+
107
+ return (
108
+ <PandaDefinitionList
109
+ ref={ref}
110
+ className={[dlRecipe({ variant: resolved }), className]
111
+ .filter(Boolean)
112
+ .join(" ")}
113
+ {...rest}
114
+ >
115
+ {children}
116
+ </PandaDefinitionList>
117
+ );
118
+ });
119
+
120
+ StyledDefinitionListRoot.displayName = "StyledDefinitionList.Root";
121
+
122
+ const StyledDefinitionList = {
123
+ Root: StyledDefinitionListRoot,
124
+ Term: PandaTerm,
125
+ Definition: PandaDescription,
126
+ };
127
+
128
+ /**
129
+ * Re-exported so a consumer can validate a variant before passing it — the
130
+ * same vocabulary `useResolvedVariant` gates on.
131
+ */
132
+ export { ALL_VARIANTS };
133
+
134
+ export default StyledDefinitionList;
@@ -0,0 +1,157 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { styled } from "styled-system/jsx";
5
+ import { formRecipe } from "styled-system/recipes";
6
+ import { useResolvedVariant } from "../config/style-config";
7
+ import { log } from "../config/logger";
8
+ import StyledFormLabel from "./StyledFormLabel";
9
+
10
+ /**
11
+ * A group of related form controls, with a visible name.
12
+ *
13
+ * The `<fieldset>`/`<legend>` pair is the only markup that gives a *group* an
14
+ * accessible name. Assistive tech announces the legend when focus enters the
15
+ * group and, for radios in particular, that legend is the question the options
16
+ * are answering — without it a screen reader meets "Yes / No" with no idea what
17
+ * is being asked. That is the reason to reach for this rather than a heading
18
+ * above a `<div>`, which looks identical and carries none of it.
19
+ *
20
+ * ```tsx
21
+ * <StyledFieldset>
22
+ * <StyledFieldset.Legend>Contact preferences</StyledFieldset.Legend>
23
+ * <StyledFieldset.Content>
24
+ * <StyledFieldset.Field>
25
+ * <StyledFieldset.Label htmlFor="email">Email</StyledFieldset.Label>
26
+ * <StyledFieldset.Value><input id="email" /></StyledFieldset.Value>
27
+ * </StyledFieldset.Field>
28
+ * </StyledFieldset.Content>
29
+ * </StyledFieldset>
30
+ * ```
31
+ *
32
+ * ## Two defects fixed on the way across
33
+ *
34
+ * **No variant meant no styling at all.** The root read
35
+ * `variant ? formRecipe({ variant }) : ""` — so a fieldset rendered without an
36
+ * explicit `variant` prop got an empty class string: no surface, no border, no
37
+ * padding, and no response to the app-wide appearance. Every other component in
38
+ * this package treats an absent variant as "follow the app", and 18 HopperGuard
39
+ * call sites were relying on a default that silently did nothing.
40
+ *
41
+ * It resolves through `useResolvedVariant` now, so an unset variant follows the
42
+ * host's configuration and an unrecognised one falls back to `solid` rather
43
+ * than to nothing. **This visibly changes existing fieldsets** — they gain the
44
+ * surface they were always supposed to have, which is why it is called out here
45
+ * rather than buried in the move.
46
+ *
47
+ * **`Label` was a second, divergent form label.** It was a bare `<label>` with
48
+ * `display: block` and a margin — no colour token, no font-size profile, and no
49
+ * `required`/`optional` markers. `StyledFormLabel` in this same package handles
50
+ * all four, and having two labels in one design system is precisely how a
51
+ * product ends up with form rows that do not match each other. `Label` now
52
+ * delegates to it, so a field inside a fieldset and a field outside one are the
53
+ * same control.
54
+ *
55
+ * The DOM shape is unchanged — both render a `<label>` — but the label now
56
+ * follows the user's text-size setting, which it did not before.
57
+ */
58
+
59
+ const PandaFieldsetLegend = styled("legend", {
60
+ base: { fontWeight: "bold", paddingX: "0.5rem" },
61
+ });
62
+ const PandaFieldsetContent = styled("div", { base: { marginTop: "1rem" } });
63
+ const PandaFieldsetField = styled("div", {
64
+ base: { width: "100%", marginBottom: "1rem" },
65
+ });
66
+ const PandaFieldsetValue = styled("div");
67
+
68
+ /** The appearances `formRecipe` defines. */
69
+ export const FIELDSET_VARIANTS = [
70
+ "solid",
71
+ "outline",
72
+ "lines",
73
+ "aurora",
74
+ "glass",
75
+ "matte",
76
+ "none",
77
+ "unstyled",
78
+ ] as const;
79
+
80
+ export type FieldsetVariant = (typeof FIELDSET_VARIANTS)[number];
81
+
82
+ export type StyledFieldsetProps = React.ComponentProps<"fieldset"> & {
83
+ /**
84
+ * `| undefined` is spelled out because the repo runs
85
+ * `exactOptionalPropertyTypes`: without it a caller holding a possibly-unset
86
+ * variant cannot forward it, even though "unset" is exactly the case this
87
+ * component handles by following the app.
88
+ */
89
+ variant?: FieldsetVariant | undefined;
90
+ };
91
+
92
+ const StyledFieldsetRoot = React.forwardRef<
93
+ HTMLFieldSetElement,
94
+ StyledFieldsetProps
95
+ >(({ children, variant, className, ...props }, ref) => {
96
+ log.trace("StyledFieldset.Root rendered");
97
+
98
+ const resolved = useResolvedVariant(variant, FIELDSET_VARIANTS);
99
+
100
+ return (
101
+ <fieldset
102
+ ref={ref}
103
+ className={[formRecipe({ variant: resolved }), className]
104
+ .filter(Boolean)
105
+ .join(" ")}
106
+ {...props}
107
+ >
108
+ {children}
109
+ </fieldset>
110
+ );
111
+ });
112
+ StyledFieldsetRoot.displayName = "StyledFieldset.Root";
113
+
114
+ const StyledFieldsetLegend = React.forwardRef<
115
+ HTMLLegendElement,
116
+ React.ComponentProps<typeof PandaFieldsetLegend>
117
+ >((props, ref) => <PandaFieldsetLegend ref={ref} {...props} />);
118
+ StyledFieldsetLegend.displayName = "StyledFieldset.Legend";
119
+
120
+ const StyledFieldsetContent = React.forwardRef<
121
+ HTMLDivElement,
122
+ React.ComponentProps<typeof PandaFieldsetContent>
123
+ >((props, ref) => <PandaFieldsetContent ref={ref} {...props} />);
124
+ StyledFieldsetContent.displayName = "StyledFieldset.Content";
125
+
126
+ const StyledFieldsetField = React.forwardRef<
127
+ HTMLDivElement,
128
+ React.ComponentProps<typeof PandaFieldsetField>
129
+ >((props, ref) => <PandaFieldsetField ref={ref} {...props} />);
130
+ StyledFieldsetField.displayName = "StyledFieldset.Field";
131
+
132
+ const StyledFieldsetValue = React.forwardRef<
133
+ HTMLDivElement,
134
+ React.ComponentProps<typeof PandaFieldsetValue>
135
+ >((props, ref) => <PandaFieldsetValue ref={ref} {...props} />);
136
+ StyledFieldsetValue.displayName = "StyledFieldset.Value";
137
+
138
+ interface StyledFieldsetComponentType
139
+ extends React.ForwardRefExoticComponent<
140
+ StyledFieldsetProps & React.RefAttributes<HTMLFieldSetElement>
141
+ > {
142
+ Legend: typeof StyledFieldsetLegend;
143
+ Content: typeof StyledFieldsetContent;
144
+ Field: typeof StyledFieldsetField;
145
+ /** `StyledFormLabel` — the same label used outside a fieldset. */
146
+ Label: typeof StyledFormLabel;
147
+ Value: typeof StyledFieldsetValue;
148
+ }
149
+
150
+ const StyledFieldset = StyledFieldsetRoot as StyledFieldsetComponentType;
151
+ StyledFieldset.Legend = StyledFieldsetLegend;
152
+ StyledFieldset.Content = StyledFieldsetContent;
153
+ StyledFieldset.Field = StyledFieldsetField;
154
+ StyledFieldset.Label = StyledFormLabel;
155
+ StyledFieldset.Value = StyledFieldsetValue;
156
+
157
+ export default StyledFieldset;
@@ -0,0 +1,13 @@
1
+ import { styled, type HTMLStyledProps } from "styled-system/jsx";
2
+
3
+ export const StyledFlex = styled("div", {
4
+ base: {
5
+ display: "flex",
6
+ },
7
+ });
8
+
9
+ export type StyledFlexProps = HTMLStyledProps<typeof StyledFlex>;
10
+
11
+ StyledFlex.displayName = "StyledFlex";
12
+
13
+ export default StyledFlex;