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