@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,224 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { styled } from "styled-system/jsx";
5
+ import StyledTooltip from "./StyledTooltip";
6
+
7
+ /**
8
+ * An on/off switch.
9
+ *
10
+ * ## No animation library
11
+ *
12
+ * The originating component used framer-motion for two things: sliding the
13
+ * handle, and growing a strike-through when disabled. Both are a CSS
14
+ * transition, and a whole animation library is a heavy thing to make three
15
+ * products carry for a 26px circle that moves 30px. This package has one
16
+ * runtime dependency and that is worth defending.
17
+ *
18
+ * The spring is gone with it. A spring on a binary control is decoration —
19
+ * there is no in-between state to communicate — and it delayed the settled
20
+ * position by longer than the ease does.
21
+ *
22
+ * **It honours `prefers-reduced-motion`.** Motion is a vestibular trigger, not
23
+ * only a preference; under that query the handle moves instantly.
24
+ *
25
+ * ## It is a real button
26
+ *
27
+ * The original was a `<div role="switch">` with hand-written `Space`/`Enter`
28
+ * handling and a manual `tabIndex`. A `<button>` gets all of that from the
29
+ * platform — focus, activation, the disabled semantics — and gets them right in
30
+ * the cases hand-rolled versions miss, like activation on key-up rather than
31
+ * key-down.
32
+ *
33
+ * ## Three defects fixed on the way in
34
+ *
35
+ * **It was under the tap-target floor.** The switch was 60×30 CSS px against a
36
+ * 44×44 minimum (WCAG 2.5.5). The track still *looks* 60×30 — the button pads
37
+ * out to 44 around it, so the appearance is unchanged and the target is legal.
38
+ * Shrinking the visible track would have been the wrong fix; a switch reads as
39
+ * a switch at that size.
40
+ *
41
+ * **It painted itself from the raw palette** — `gray.300`, `green.400`,
42
+ * `white` — so it ignored the theme and dark mode entirely. Now tokens, chosen
43
+ * from `TEXT_BACKGROUND_PAIRS` rather than by eye: the handle is the *text*
44
+ * token belonging to whichever track surface is under it, which is the
45
+ * package's own contract for "these two are readable together".
46
+ *
47
+ * **It could end up with no accessible name.** The old fallback used `tooltip`
48
+ * only when it happened to be a string, so a node tooltip left the switch
49
+ * announced as "switch" with no name at all. `label` is now explicit, with the
50
+ * string tooltip still serving as a fallback.
51
+ */
52
+
53
+ export interface StyledInputToggleProps {
54
+ id?: string;
55
+ value: boolean;
56
+ onChange: (value: boolean) => void;
57
+ /** Shown above the switch when on. */
58
+ iconOn?: React.ReactNode;
59
+ /** Shown above the switch when off. */
60
+ iconOff?: React.ReactNode;
61
+ disabled?: boolean;
62
+ tooltip?: React.ReactNode;
63
+ placement?: "top" | "bottom" | "left" | "right";
64
+ /** Accessible name. Pass it — see above. */
65
+ label?: string;
66
+ ["data-testid"]?: string;
67
+ }
68
+
69
+ const ToggleContainer = styled("div", {
70
+ base: {
71
+ display: "flex",
72
+ flexDirection: "column",
73
+ alignItems: "center",
74
+ gap: "2",
75
+ },
76
+ });
77
+
78
+ const IconContainer = styled("div", {
79
+ base: { position: "relative" },
80
+ });
81
+
82
+ const StrikeThrough = styled("div", {
83
+ base: {
84
+ position: "absolute",
85
+ top: "50%",
86
+ left: 0,
87
+ width: "100%",
88
+ height: "2px",
89
+ backgroundColor: "currentColor",
90
+ transformOrigin: "center",
91
+ transform: "scaleX(1)",
92
+ animation: "stonedogStrikeIn 200ms ease-out",
93
+ "@media (prefers-reduced-motion: reduce)": {
94
+ animation: "none",
95
+ },
96
+ },
97
+ });
98
+
99
+ /**
100
+ * The button. Its padding is what carries the 44px floor: the visible track
101
+ * inside stays 60×30, so nothing looks different and the target is legal.
102
+ */
103
+ const SwitchButton = styled("button", {
104
+ base: {
105
+ display: "inline-flex",
106
+ alignItems: "center",
107
+ justifyContent: "center",
108
+ minWidth: "48px",
109
+ minHeight: "48px",
110
+ padding: "0",
111
+ border: "none",
112
+ background: "transparent",
113
+ cursor: "pointer",
114
+ borderRadius: "md",
115
+ _disabled: {
116
+ cursor: "not-allowed",
117
+ opacity: 0.5,
118
+ },
119
+ },
120
+ });
121
+
122
+ const Track = styled("span", {
123
+ base: {
124
+ display: "flex",
125
+ alignItems: "center",
126
+ width: "60px",
127
+ height: "30px",
128
+ borderRadius: "15px",
129
+ padding: "2px",
130
+ transition: "background-color 200ms ease",
131
+ "@media (prefers-reduced-motion: reduce)": {
132
+ transition: "none",
133
+ },
134
+ },
135
+ variants: {
136
+ on: {
137
+ // Token pairs from TEXT_BACKGROUND_PAIRS — the handle below reads against
138
+ // whichever of these is under it.
139
+ true: { backgroundColor: "buttonBgAccent" },
140
+ false: { backgroundColor: "boxBgSecondary" },
141
+ },
142
+ },
143
+ });
144
+
145
+ const Handle = styled("span", {
146
+ base: {
147
+ width: "26px",
148
+ height: "26px",
149
+ borderRadius: "50%",
150
+ // translateX, not `margin-left: auto` — a margin change is not animatable,
151
+ // which is why the original needed a layout animation to move at all.
152
+ transform: "translateX(0)",
153
+ transition: "transform 200ms ease, background-color 200ms ease",
154
+ "@media (prefers-reduced-motion: reduce)": {
155
+ transition: "none",
156
+ },
157
+ },
158
+ variants: {
159
+ on: {
160
+ true: {
161
+ transform: "translateX(30px)",
162
+ backgroundColor: "buttonTextAccent",
163
+ },
164
+ false: { backgroundColor: "textSecondary" },
165
+ },
166
+ },
167
+ });
168
+
169
+ export default function StyledInputToggle({
170
+ id,
171
+ value,
172
+ onChange,
173
+ iconOn,
174
+ iconOff,
175
+ disabled,
176
+ tooltip,
177
+ placement,
178
+ label,
179
+ ...props
180
+ }: StyledInputToggleProps) {
181
+ const hasIcons = iconOn || iconOff;
182
+ const name = label ?? (typeof tooltip === "string" ? tooltip : undefined);
183
+
184
+ const content = (
185
+ <ToggleContainer id={id}>
186
+ {hasIcons && (
187
+ <IconContainer>
188
+ {value ? iconOn : iconOff}
189
+ {/* Decorative: the switch's own state is already announced by
190
+ aria-checked and aria-disabled. */}
191
+ {disabled && <StrikeThrough aria-hidden="true" data-testid="toggle-strike" />}
192
+ </IconContainer>
193
+ )}
194
+ <SwitchButton
195
+ type="button"
196
+ role="switch"
197
+ aria-checked={value}
198
+ aria-label={name}
199
+ disabled={disabled}
200
+ data-state={value ? "on" : "off"}
201
+ // The caller's id lands on the BUTTON, not the container. A test that
202
+ // does `click(getByTestId(...))` has to hit the interactive element —
203
+ // clicking a wrapper does nothing, and the failure looks like a broken
204
+ // component rather than a mis-aimed selector.
205
+ data-testid={props["data-testid"] ?? "toggle-switch"}
206
+ onClick={() => onChange(!value)}
207
+ >
208
+ <Track on={value}>
209
+ <Handle on={value} />
210
+ </Track>
211
+ </SwitchButton>
212
+ </ToggleContainer>
213
+ );
214
+
215
+ if (tooltip) {
216
+ return (
217
+ <StyledTooltip tooltip={tooltip} placement={placement}>
218
+ {content}
219
+ </StyledTooltip>
220
+ );
221
+ }
222
+
223
+ return content;
224
+ }
@@ -0,0 +1,188 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { styled } from "styled-system/jsx";
5
+ import { listRecipe } from "styled-system/recipes";
6
+ import { useResolvedVariant } from "../config/style-config";
7
+ import { log } from "../config/logger";
8
+
9
+ /**
10
+ * A vertical list of rows — the surface, borders and row separators that turn
11
+ * a `<ul>` into a panel.
12
+ *
13
+ * Compose it: `StyledList.Root` draws the container, `StyledList.Item` draws a
14
+ * row. Both are required; an `Item` outside a `Root` is a bare `<li>`, which is
15
+ * the correct behaviour and also the reason the previous version had a bug —
16
+ * see below.
17
+ *
18
+ * ## The variants a call site can ask for
19
+ *
20
+ * The five app-wide appearances plus `ghost`, `none` and `unstyled`. Anything
21
+ * else falls back to `solid` through `useResolvedVariant`, rather than being
22
+ * passed to the recipe and rendering as nothing.
23
+ *
24
+ * ## Three defects fixed on the way across
25
+ *
26
+ * **The default variant was a variant that does not exist.** The originating
27
+ * version read `variant ?? globalVariant ?? "list"` — and `"list"` is the
28
+ * recipe's *className*, not one of its variants. Any consumer rendering a list
29
+ * before a `StonedogStyleProvider` had supplied a global variant therefore
30
+ * asked the recipe for `variant: "list"`, matched nothing, and got an unstyled
31
+ * list: no surface, no border, no row separators. It looked like a list that
32
+ * had simply not been styled yet, which is exactly why it survived.
33
+ *
34
+ * Resolution goes through `useResolvedVariant` now, with the allow-list, which
35
+ * is the mechanism NEH-234 added for precisely this class of miss.
36
+ *
37
+ * **The row styling was injected by the parent, not owned by the row.** `Root`
38
+ * walked its children with `React.Children.map` and `cloneElement`d the item
39
+ * class onto each one. That breaks in the three ways cloning children always
40
+ * breaks: a `<>fragment</>` of rows received the class on the *fragment*, a
41
+ * row rendered by a child component never saw it, and any row that was not a
42
+ * direct child was skipped. It also meant `StyledList.Item` had no styling of
43
+ * its own — so the component only worked in the one arrangement its author
44
+ * happened to test.
45
+ *
46
+ * `Item` now carries its own slot class. Rows render correctly wherever they
47
+ * are, including inside a `.map()`, a fragment, or a wrapper component.
48
+ *
49
+ * **`gap` was hand-implemented as a margin on every child but the last.** The
50
+ * recipe root is already `display: flex; flex-direction: column`, so this was
51
+ * reimplementing the `gap` property — badly, because writing `marginBottom`
52
+ * into each child's inline `style` silently overwrote any margin the caller had
53
+ * set there. It is a real `gap` now.
54
+ *
55
+ * ## `role="list"` is not redundant here
56
+ *
57
+ * Safari removes list semantics from any `<ul>` whose `list-style` is `none` —
58
+ * a deliberate WebKit decision, and this component sets exactly that whenever
59
+ * bullets are off (the default). Without the explicit role, VoiceOver stops
60
+ * announcing "list, 6 items", which is the one piece of information a
61
+ * non-visual reader needs before deciding whether to read on.
62
+ */
63
+
64
+ const PandaUl = styled("ul");
65
+ const PandaOl = styled("ol");
66
+ const PandaListItem = styled("li");
67
+
68
+ /** What a call site may ask for, beyond the five app-wide appearances. */
69
+ export const LIST_VARIANTS = [
70
+ "solid",
71
+ "outline",
72
+ "lines",
73
+ "aurora",
74
+ "glass",
75
+ "matte",
76
+ "ghost",
77
+ "none",
78
+ "unstyled",
79
+ ] as const;
80
+
81
+ export type ListVariant = (typeof LIST_VARIANTS)[number];
82
+
83
+ /**
84
+ * Props derive from the Panda component, not from `React.HTMLAttributes`, so
85
+ * style props (`w`, `h`, `mt`, …) are typed rather than merely tolerated.
86
+ *
87
+ * The originating version reached this by declaring `[key: string]: unknown`
88
+ * — which accepted the style props *and* every typo alongside them, and
89
+ * defeated type checking on the whole component. Narrowing it to
90
+ * `HTMLAttributes` was the opposite mistake: 9 HopperGuard call sites pass
91
+ * `w`, `h` or `mt`, and they are legitimate. `ComponentProps<typeof PandaUl>`
92
+ * is the type that admits exactly the real ones.
93
+ */
94
+ export type StyledListRootProps = React.PropsWithChildren<
95
+ React.ComponentProps<typeof PandaUl> & {
96
+ /**
97
+ * `| undefined` is spelled out throughout this file because the repo runs
98
+ * `exactOptionalPropertyTypes`: without it a caller holding a
99
+ * possibly-unset variant cannot forward it, even though "unset" is exactly
100
+ * the case this component handles by following the app.
101
+ */
102
+ variant?: ListVariant | undefined;
103
+ /** Space between rows. A real CSS `gap` — see the note above. */
104
+ gap?: string | number | undefined;
105
+ /** Render as `<ol>` with numbers. */
106
+ showNumbers?: boolean;
107
+ /** Render bullets. Off by default; see the `role="list"` note. */
108
+ showBullets?: boolean;
109
+ }
110
+ >;
111
+
112
+ const StyledListRoot = React.forwardRef<
113
+ HTMLUListElement | HTMLOListElement,
114
+ StyledListRootProps
115
+ >(
116
+ (
117
+ { children, variant, gap, showNumbers, showBullets, className, style, ...rest },
118
+ ref,
119
+ ) => {
120
+ log.trace("StyledListRoot rendered");
121
+
122
+ const resolved = useResolvedVariant(variant, LIST_VARIANTS);
123
+ const recipeClasses = listRecipe({ variant: resolved });
124
+
125
+ const listStyle = showNumbers ? "decimal" : showBullets ? "disc" : "none";
126
+
127
+ const ListTag = showNumbers ? PandaOl : PandaUl;
128
+
129
+ return (
130
+ <ListTag
131
+ ref={ref as React.Ref<HTMLUListElement & HTMLOListElement>}
132
+ className={[recipeClasses.root, className].filter(Boolean).join(" ")}
133
+ // `role` only when the marker is gone — with real markers the native
134
+ // semantics are intact and an explicit role would be noise.
135
+ role={listStyle === "none" ? "list" : undefined}
136
+ style={{
137
+ listStyleType: listStyle,
138
+ paddingLeft: listStyle !== "none" ? "1.5em" : undefined,
139
+ // Inline rather than a Panda `gap` prop: the value arrives at
140
+ // runtime, and Panda extracts at build time, so a prop here would
141
+ // emit a class with no rule behind it. Same trap as NEH-233.
142
+ ...(gap !== undefined ? { gap } : {}),
143
+ ...style,
144
+ }}
145
+ {...rest}
146
+ >
147
+ {children}
148
+ </ListTag>
149
+ );
150
+ },
151
+ );
152
+
153
+ StyledListRoot.displayName = "StyledList.Root";
154
+
155
+ export type StyledListItemProps = React.ComponentProps<typeof PandaListItem> & {
156
+ /**
157
+ * Match the `Root`'s variant. Only needed when a row is rendered outside the
158
+ * `Root`'s own subtree — otherwise leave it; rows inherit their surface from
159
+ * the container's recipe.
160
+ */
161
+ variant?: ListVariant | undefined;
162
+ };
163
+
164
+ const StyledListItem = React.forwardRef<HTMLLIElement, StyledListItemProps>(
165
+ ({ children, className, variant, ...props }, ref) => {
166
+ const resolved = useResolvedVariant(variant, LIST_VARIANTS);
167
+ const recipeClasses = listRecipe({ variant: resolved });
168
+
169
+ return (
170
+ <PandaListItem
171
+ ref={ref}
172
+ className={[recipeClasses.item, className].filter(Boolean).join(" ")}
173
+ {...props}
174
+ >
175
+ {children}
176
+ </PandaListItem>
177
+ );
178
+ },
179
+ );
180
+
181
+ StyledListItem.displayName = "StyledList.Item";
182
+
183
+ const StyledList = {
184
+ Root: StyledListRoot,
185
+ Item: StyledListItem,
186
+ };
187
+
188
+ export default StyledList;
@@ -0,0 +1,53 @@
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
+ const PandaScrollBox = styled("div", {
9
+ base: {
10
+ overflow: "auto",
11
+ minHeight: 0,
12
+ flex: "1",
13
+ display: "flex",
14
+ paddingRight: "0.5rem",
15
+ flexDirection: "column",
16
+ scrollbarWidth: "thick",
17
+ // Via the token, not a literal `var(--hopper-…)`: the custom-property
18
+ // prefix is configurable per consumer (see the preset), so a hardcoded
19
+ // namespace paints nothing for any host that picked a different one.
20
+ scrollbarColor: "{colors.boxBgSecondary} transparent",
21
+ },
22
+ });
23
+
24
+ export interface StyledScrollbarProps extends HTMLStyledProps<"div"> {
25
+ border?: string | number;
26
+ }
27
+
28
+ const StyledScrollbar: React.FC<StyledScrollbarProps> = ({
29
+ children,
30
+ border,
31
+ ...rest
32
+ }) => {
33
+ log.trace("StyledScrollbar rendered");
34
+ const extraStyles: React.CSSProperties = {};
35
+ if (border !== undefined) {
36
+ if (border === "none" || border === "0") {
37
+ extraStyles.borderWidth = "5px";
38
+ extraStyles.borderStyle = "none";
39
+ extraStyles.borderRadius = "0";
40
+ }
41
+ }
42
+ return (
43
+ <PandaScrollBox
44
+ data-testid="styled-scrollbar"
45
+ style={extraStyles}
46
+ {...rest}
47
+ >
48
+ {children}
49
+ </PandaScrollBox>
50
+ );
51
+ };
52
+
53
+ export default StyledScrollbar;
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import StyledBox from "./StyledBox";
5
+ import StyledInputText from "./StyledInputText";
6
+ import type { Dictation } from "./dictation";
7
+
8
+ /**
9
+ * A search box.
10
+ *
11
+ * Thin on purpose: a `StyledInputText` of `type="search"` at a width that steps
12
+ * up with the viewport. The width is the only thing here worth sharing — a
13
+ * search field that spans a wide screen is harder to use, not easier, because
14
+ * the eye has to travel from the field to results that start back at the left.
15
+ *
16
+ * `type="search"` is not cosmetic. It gives the browser's own clear button, the
17
+ * right on-screen keyboard with a "search" action key, and the correct role for
18
+ * assistive tech.
19
+ *
20
+ * **It does not filter anything.** It owns a string and reports changes; what
21
+ * that string means belongs to the screen. The originating component took a
22
+ * `data` prop it never read, marked "for future filtering" — that is the shape
23
+ * this deliberately does not have.
24
+ */
25
+
26
+ export interface StyledSearchProps {
27
+ /** Current query. Controlled. */
28
+ search: string;
29
+ onSearchChange: (value: string) => void;
30
+ placeholder?: string;
31
+ onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
32
+ /**
33
+ * Accessible name. Defaults to the placeholder — but a field whose only name
34
+ * is placeholder text has no name once the user types, so pass one.
35
+ */
36
+ label?: string;
37
+ /** Host-supplied dictation. Omit for no microphone. */
38
+ dictation?: Dictation;
39
+ ["data-testid"]?: string;
40
+ }
41
+
42
+ const StyledSearch = React.forwardRef<HTMLInputElement, StyledSearchProps>(
43
+ function StyledSearch(
44
+ {
45
+ search,
46
+ onSearchChange,
47
+ placeholder = "Search...",
48
+ onKeyDown,
49
+ label,
50
+ dictation,
51
+ ...props
52
+ },
53
+ ref,
54
+ ) {
55
+ return (
56
+ <StyledBox
57
+ w={{ base: "300px", md: "400px", lg: "500px" }}
58
+ data-testid="search-container"
59
+ >
60
+ <StyledInputText
61
+ ref={ref}
62
+ type="search"
63
+ data-testid={props["data-testid"] ?? "search-input"}
64
+ aria-label={label ?? placeholder}
65
+ placeholder={placeholder}
66
+ value={search}
67
+ onChange={(e) => onSearchChange(e.target.value)}
68
+ onKeyDown={onKeyDown}
69
+ {...(dictation ? { dictation } : {})}
70
+ />
71
+ </StyledBox>
72
+ );
73
+ },
74
+ );
75
+
76
+ StyledSearch.displayName = "StyledSearch";
77
+
78
+ export default StyledSearch;
@@ -0,0 +1,38 @@
1
+ "use client";
2
+
3
+ import { log } from "../config/logger";
4
+ import { styled } from "styled-system/jsx";
5
+ import {
6
+ separatorHorizontalRecipe,
7
+ separatorVerticalRecipe,
8
+ } from "styled-system/recipes";
9
+ import type { HTMLStyledProps } from "styled-system/types";
10
+
11
+ const HorizontalSeparator = styled("div", separatorHorizontalRecipe);
12
+ const VerticalSeparator = styled("div", separatorVerticalRecipe);
13
+
14
+ type Orientation = "horizontal" | "vertical";
15
+ type HorizontalVariant = "solid";
16
+ type VerticalVariant = "solid";
17
+
18
+ export interface StyledSeparatorProps extends HTMLStyledProps<"div"> {
19
+ orientation?: Orientation;
20
+ variant?: HorizontalVariant | VerticalVariant;
21
+ }
22
+
23
+ export const StyledSeparator = (props: StyledSeparatorProps) => {
24
+ log.trace("StyledSeparator rendered");
25
+ const { orientation = "horizontal", variant = "solid", ...rest } = props;
26
+
27
+ if (orientation === "horizontal") {
28
+ return (
29
+ <HorizontalSeparator variant={variant as HorizontalVariant} {...rest} />
30
+ );
31
+ }
32
+
33
+ return <VerticalSeparator variant={variant as VerticalVariant} {...rest} />;
34
+ };
35
+
36
+ StyledSeparator.displayName = "StyledSeparator";
37
+
38
+ export default StyledSeparator;