@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
package/README.md
ADDED
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
# @stonedogcode/style
|
|
2
|
+
|
|
3
|
+
A themeable [Panda CSS](https://panda-css.com) design system: a preset of design
|
|
4
|
+
tokens and recipes, plus the React components built on them.
|
|
5
|
+
|
|
6
|
+
Every colour in the system is a token that resolves to a bare CSS custom
|
|
7
|
+
property — `boxBgPrimary` is `var(--hopper-box-primary-bg)` and nothing more.
|
|
8
|
+
Your application defines those properties, from wherever you keep themes, and
|
|
9
|
+
the whole component set re-skins at runtime. No component here knows a colour.
|
|
10
|
+
|
|
11
|
+
[](./LICENSE)
|
|
12
|
+
|
|
13
|
+
## Status
|
|
14
|
+
|
|
15
|
+
Early. The preset is complete (22 recipes, 43 colour tokens); the component set
|
|
16
|
+
is being extracted incrementally and currently covers the layout and typography
|
|
17
|
+
primitives. See [CLAUDE.md](./CLAUDE.md) for the architecture and the
|
|
18
|
+
contribution rules.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
**Not published to npm.** Consume it from git — either a plain dependency, or a
|
|
23
|
+
submodule if you want to develop against it:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Option A — git dependency, pinned to a commit
|
|
27
|
+
npm install "git+https://github.com/stonedog-code/stonedog-style.git#<sha>"
|
|
28
|
+
|
|
29
|
+
# Option B — submodule + file: dependency (use this in a monorepo)
|
|
30
|
+
git submodule add git@github.com:stonedog-code/stonedog-style.git packages/stonedog-style
|
|
31
|
+
# then in the consuming app's package.json:
|
|
32
|
+
# "@stonedogcode/style": "file:../../packages/stonedog-style"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Pin to a commit rather than tracking a branch: this package ships source that
|
|
36
|
+
your build parses, so an unpinned bump changes your CSS without changing your
|
|
37
|
+
lockfile in any way you'd notice.
|
|
38
|
+
|
|
39
|
+
Peer dependencies: `react` ≥18, `react-dom` ≥18, `@pandacss/dev` ≥1.9.
|
|
40
|
+
|
|
41
|
+
## Setup
|
|
42
|
+
|
|
43
|
+
Four steps. **All four are required** — miss step 3 or 4 and the app renders,
|
|
44
|
+
but invisibly or unstyled, with no error anywhere to tell you why.
|
|
45
|
+
|
|
46
|
+
**1 — add the preset to your `panda.config.ts`:**
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { defineConfig } from "@pandacss/dev";
|
|
50
|
+
import { stonedogStylePreset } from "@stonedogcode/style/preset";
|
|
51
|
+
|
|
52
|
+
export default defineConfig({
|
|
53
|
+
// Listing `presets` REPLACES Panda's defaults rather than adding to them,
|
|
54
|
+
// so the two base presets must be named explicitly. Without them the recipes
|
|
55
|
+
// lose the tokens they build on, and Panda drops those styles silently.
|
|
56
|
+
presets: [
|
|
57
|
+
"@pandacss/preset-base",
|
|
58
|
+
"@pandacss/preset-panda",
|
|
59
|
+
stonedogStylePreset(),
|
|
60
|
+
],
|
|
61
|
+
include: [
|
|
62
|
+
"./src/**/*.{ts,tsx}",
|
|
63
|
+
// Panda finds styles by parsing source. A package it never parses
|
|
64
|
+
// contributes no CSS, and its components render unstyled.
|
|
65
|
+
"./node_modules/@stonedogcode/style/src/**/*.tsx",
|
|
66
|
+
],
|
|
67
|
+
outdir: "styled-system",
|
|
68
|
+
jsxFramework: "react",
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**2 — transpile the package.** It ships TypeScript source, not a bundle,
|
|
73
|
+
because Panda extracts styles statically at *your* build. In Next.js:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
// next.config.js
|
|
77
|
+
module.exports = { transpilePackages: ["@stonedogcode/style"] };
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**3 — define the custom properties. This is the step that bites.** Every colour
|
|
81
|
+
token reads one, and **a token whose property is undefined renders as nothing** —
|
|
82
|
+
no fallback, no warning, no error. An app that skips this compiles, builds,
|
|
83
|
+
serves, and shows you a blank page.
|
|
84
|
+
|
|
85
|
+
There are **44** of them. Get the list at runtime rather than copying one:
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { requiredCssCustomProperties } from "@stonedogcode/style/preset";
|
|
89
|
+
|
|
90
|
+
requiredCssCustomProperties(); // --hopper-* (default)
|
|
91
|
+
requiredCssCustomProperties("optima"); // --optima-*, if you set cssVarPrefix
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
A complete starter theme — all 44, nothing elided. Dark, and every text/surface
|
|
95
|
+
pair clears WCAG AA (measured: worst 5.17:1, ten of thirteen pairs at AAA), so
|
|
96
|
+
it is a legitimate starting point rather than a placeholder. Replace the values;
|
|
97
|
+
keep every key.
|
|
98
|
+
|
|
99
|
+
```css
|
|
100
|
+
:root {
|
|
101
|
+
/* Surfaces */
|
|
102
|
+
--hopper-box-main-bg: #0f172a;
|
|
103
|
+
--hopper-box-primary-bg: #1e293b;
|
|
104
|
+
--hopper-box-secondary-bg: #334155;
|
|
105
|
+
--hopper-box-accent-bg: #0b1220;
|
|
106
|
+
--hopper-box-info-bg: #1e3a5f;
|
|
107
|
+
|
|
108
|
+
/* Text on those surfaces */
|
|
109
|
+
--hopper-box-main-text: #f8fafc;
|
|
110
|
+
--hopper-box-primary-text: #f8fafc;
|
|
111
|
+
--hopper-box-secondary-text: #f1f5f9;
|
|
112
|
+
--hopper-box-accent-text: #e2e8f0;
|
|
113
|
+
|
|
114
|
+
/* Text that carries meaning on its own */
|
|
115
|
+
--hopper-text-pop-text: #38bdf8;
|
|
116
|
+
--hopper-text-error-text: #f87171;
|
|
117
|
+
--hopper-text-warning-text: #fbbf24;
|
|
118
|
+
|
|
119
|
+
/* Borders */
|
|
120
|
+
--hopper-box-primary-border: #475569;
|
|
121
|
+
--hopper-box-secondary-border: #64748b;
|
|
122
|
+
--hopper-box-accent-border: #334155;
|
|
123
|
+
|
|
124
|
+
/* Shadows */
|
|
125
|
+
--hopper-shadow-primary-bg: rgb(0 0 0 / 0.4);
|
|
126
|
+
--hopper-shadow-secondary-bg: rgb(0 0 0 / 0.3);
|
|
127
|
+
--hopper-shadow-accent-bg: rgb(0 0 0 / 0.5);
|
|
128
|
+
|
|
129
|
+
/* Buttons */
|
|
130
|
+
--hopper-button-primary-bg: #2563eb;
|
|
131
|
+
--hopper-button-secondary-bg: #475569;
|
|
132
|
+
--hopper-button-accent-bg: #1e293b;
|
|
133
|
+
--hopper-button-primary-hover-bg: #1d4ed8;
|
|
134
|
+
--hopper-button-secondary-hover-bg: #334155;
|
|
135
|
+
--hopper-button-accent-hover-bg: #334155;
|
|
136
|
+
--hopper-button-primary-text: #ffffff;
|
|
137
|
+
--hopper-button-secondary-text: #f8fafc;
|
|
138
|
+
--hopper-button-accent-text: #f8fafc;
|
|
139
|
+
--hopper-button-primary-hover-text: #ffffff;
|
|
140
|
+
--hopper-button-secondary-hover-text: #ffffff;
|
|
141
|
+
--hopper-button-accent-hover-text: #ffffff;
|
|
142
|
+
--hopper-button-plain-bg: transparent;
|
|
143
|
+
--hopper-button-plain-text: #f8fafc;
|
|
144
|
+
|
|
145
|
+
/* Icons */
|
|
146
|
+
--hopper-icon-primary-bg: #94a3b8;
|
|
147
|
+
--hopper-icon-secondary-bg: #64748b;
|
|
148
|
+
--hopper-icon-accent-bg: #cbd5e1;
|
|
149
|
+
--hopper-icon-primary-hover-bg: #cbd5e1;
|
|
150
|
+
--hopper-icon-secondary-hover-bg: #94a3b8;
|
|
151
|
+
--hopper-icon-accent-hover-bg: #e2e8f0;
|
|
152
|
+
|
|
153
|
+
/* Arrows / carets */
|
|
154
|
+
--hopper-arrow-primary-bg: #94a3b8;
|
|
155
|
+
--hopper-arrow-secondary-bg: #64748b;
|
|
156
|
+
--hopper-arrow-accent-bg: #cbd5e1;
|
|
157
|
+
--hopper-arrow-primary-border: #475569;
|
|
158
|
+
--hopper-arrow-secondary-border: #64748b;
|
|
159
|
+
--hopper-arrow-accent-border: #334155;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Guard it with a test rather than trusting a checklist — the failure is invisible,
|
|
164
|
+
so nothing else will tell you:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
it("defines every property the design system reads", () => {
|
|
168
|
+
const css = readFileSync("src/theme.css", "utf8");
|
|
169
|
+
for (const prop of requiredCssCustomProperties()) {
|
|
170
|
+
expect(css).toContain(`${prop}:`);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
One optional extra, not in that list because it has a working fallback:
|
|
176
|
+
`--hopper-widget-base-height` (default `240px`) caps dropdown menus.
|
|
177
|
+
|
|
178
|
+
**4 — mount the provider** (optional; omitting it gives readable defaults):
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
import { StonedogStyleProvider } from "@stonedogcode/style";
|
|
182
|
+
|
|
183
|
+
<StonedogStyleProvider fontSizeProfile="md" variant="solid">
|
|
184
|
+
<App />
|
|
185
|
+
</StonedogStyleProvider>;
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Check it actually worked
|
|
189
|
+
|
|
190
|
+
Three greps against your generated stylesheet, in order. Each isolates one of
|
|
191
|
+
the three ways this goes wrong silently:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npx panda cssgen --outfile styled-system/styles.css
|
|
195
|
+
|
|
196
|
+
# 1. Did the preset load? Expect ~44 matches, not 0.
|
|
197
|
+
grep -c 'var(--hopper-' styled-system/styles.css
|
|
198
|
+
|
|
199
|
+
# 2. Did Panda parse the package's source? Expect ~240 classes, not ~0.
|
|
200
|
+
# A low number means your `include` glob is wrong (step 1).
|
|
201
|
+
grep -oE '\.[a-zA-Z][a-zA-Z0-9_-]+' styled-system/styles.css | sort -u | wc -l
|
|
202
|
+
|
|
203
|
+
# 3. Did you keep the base presets? Expect all six breakpoints.
|
|
204
|
+
grep 'BreakpointToken =' styled-system/tokens/tokens.d.ts
|
|
205
|
+
# -> "sm" | "md" | "lg" | "xl" | "2xl" | "3xl"
|
|
206
|
+
# Only "3xl" means you dropped @pandacss/preset-base and preset-panda.
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
If all three pass and the UI is still blank, you are missing step 3.
|
|
210
|
+
|
|
211
|
+
## Use
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
import { StyledBox, StyledHeading, StyledText, StyledVStack } from "@stonedogcode/style";
|
|
215
|
+
|
|
216
|
+
export function Panel() {
|
|
217
|
+
return (
|
|
218
|
+
<StyledBox p="4" header={<StyledHeading>Overview</StyledHeading>}>
|
|
219
|
+
<StyledVStack gap="3">
|
|
220
|
+
<StyledText>Colours come from the host's theme.</StyledText>
|
|
221
|
+
<StyledText tooltip="Shown on hover and on keyboard focus">
|
|
222
|
+
Hover me
|
|
223
|
+
</StyledText>
|
|
224
|
+
</StyledVStack>
|
|
225
|
+
</StyledBox>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Theming
|
|
231
|
+
|
|
232
|
+
**Three settings** drive the system app-wide, all supplied by your app through
|
|
233
|
+
the provider:
|
|
234
|
+
|
|
235
|
+
- `fontSizeProfile` — `xs | sm | md | lg | xl`. The scale is rem-based, so it
|
|
236
|
+
compounds with the browser's own font-size setting rather than overriding it.
|
|
237
|
+
`StyledHeading` renders one tier above whatever body text is set to, so the
|
|
238
|
+
hierarchy survives every profile.
|
|
239
|
+
- `variant` — `solid | outline | aurora | glass | matte`. Any call site may
|
|
240
|
+
override it; `useResolvedVariant` applies the precedence (caller → app-wide →
|
|
241
|
+
`solid`) and coerces anything the recipes have no case for.
|
|
242
|
+
- `iconSize` — the default box for every `StyledIcon` that is not given an
|
|
243
|
+
explicit `size`. Defaults to `2x` (32px), which is large: this library came
|
|
244
|
+
out of an application built for an often-elderly audience. A conventional web
|
|
245
|
+
app wants `md` (20px).
|
|
246
|
+
|
|
247
|
+
### Retuning the scale for your audience
|
|
248
|
+
|
|
249
|
+
The defaults lean large on purpose, and both halves are host-tunable without
|
|
250
|
+
forking anything:
|
|
251
|
+
|
|
252
|
+
```tsx
|
|
253
|
+
<StonedogStyleProvider fontSizeProfile="md" iconSize="md" variant="solid">
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
```css
|
|
257
|
+
/* Every fontSizeMap entry is var(--font-sizes-KEY, <large fallback>),
|
|
258
|
+
so defining the properties replaces the scale wholesale. */
|
|
259
|
+
:root {
|
|
260
|
+
--font-sizes-sm: 0.875rem;
|
|
261
|
+
--font-sizes-md: 1rem;
|
|
262
|
+
--font-sizes-lg: 1.125rem;
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Set the icon size once, at the provider.** Naming a `size` at each call site
|
|
267
|
+
works, but it opts that icon out of ever being retuned — which is how an
|
|
268
|
+
application ends up with three icon scales and no single place to fix them.
|
|
269
|
+
Because an icon set built with `createIcon` names no size of its own, setting
|
|
270
|
+
`iconSize` retunes the entire set at once.
|
|
271
|
+
|
|
272
|
+
**Your own namespace.** If `--hopper-*` does not suit, rename the whole
|
|
273
|
+
namespace at build time:
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
stonedogStylePreset({ cssVarPrefix: "acme" }); // → var(--acme-box-primary-bg)
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The rename is total — every token re-points, and no `--hopper-*` reference
|
|
280
|
+
survives anywhere in the generated CSS. Choose it **before** you write a theme,
|
|
281
|
+
because it changes all 44 property names you have to define.
|
|
282
|
+
|
|
283
|
+
## Adopting it in a new app — a worked example
|
|
284
|
+
|
|
285
|
+
Verified end to end against a clean project. Substitute your own prefix and
|
|
286
|
+
paths; nothing else here is optional.
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
# 1. Take the dependency (see Install — it is not on npm)
|
|
290
|
+
npm install "git+https://github.com/stonedog-code/stonedog-style.git#<sha>"
|
|
291
|
+
npm install -D @pandacss/dev @types/react @types/react-dom
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`@types/react-dom` is not optional: the tooltip portals through `react-dom`,
|
|
295
|
+
and without the types your build fails on our source, not yours.
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
// 2. panda.config.ts — all four points below matter
|
|
299
|
+
import { defineConfig } from "@pandacss/dev";
|
|
300
|
+
import { stonedogStylePreset } from "@stonedogcode/style/preset";
|
|
301
|
+
|
|
302
|
+
export default defineConfig({
|
|
303
|
+
preflight: false,
|
|
304
|
+
presets: [
|
|
305
|
+
"@pandacss/preset-base", // (a) REQUIRED — presets replaces, not merges
|
|
306
|
+
"@pandacss/preset-panda", // (b) REQUIRED — gray.*, radii, spacing
|
|
307
|
+
stonedogStylePreset({ cssVarPrefix: "acme" }),
|
|
308
|
+
],
|
|
309
|
+
include: [
|
|
310
|
+
"./src/**/*.{ts,tsx}",
|
|
311
|
+
"./node_modules/@stonedogcode/style/src/**/*.tsx", // (c) REQUIRED
|
|
312
|
+
],
|
|
313
|
+
exclude: ["./node_modules/@stonedogcode/style/src/**/__tests__/**/*"], // (d)
|
|
314
|
+
outdir: "styled-system",
|
|
315
|
+
jsxFramework: "react",
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
> **Upgrading from `stonedog-style`?** The package moved to the
|
|
320
|
+
> `@stonedogcode` scope at `0.8.1`, and npm installs a scoped package one
|
|
321
|
+
> directory deeper — `node_modules/@stonedogcode/style/`, not
|
|
322
|
+
> `node_modules/stonedog-style/`. **So the `include` glob above changes, and
|
|
323
|
+
> getting it wrong is silent.** A glob that matches nothing produces no build
|
|
324
|
+
> error: components still render, with class names that have no CSS behind
|
|
325
|
+
> them. Only a component using an inline `styled(…, { base: … })` shows it,
|
|
326
|
+
> because everything else takes its CSS from the preset recipes, which Panda
|
|
327
|
+
> emits from config *without reading source*.
|
|
328
|
+
>
|
|
329
|
+
> Two things do **not** move. A `packages/stonedog-style/**` glob names a
|
|
330
|
+
> *submodule checkout directory*, which is unaffected by the package's name —
|
|
331
|
+
> changing it is its own silent breakage. And the `transpilePackages` entry in
|
|
332
|
+
> `next.config` **does** move, because that one names the package.
|
|
333
|
+
>
|
|
334
|
+
> Assert it rather than eyeballing it — this is the only failure here with no
|
|
335
|
+
> other symptom:
|
|
336
|
+
>
|
|
337
|
+
> ```ts
|
|
338
|
+
> // panda.test.ts
|
|
339
|
+
> import { globSync } from "tinyglobby";
|
|
340
|
+
> it("every stonedog glob resolves to real files", () => {
|
|
341
|
+
> for (const g of config.include.filter((p) => p.includes("stonedogcode"))) {
|
|
342
|
+
> expect(globSync(g).length).toBeGreaterThan(0);
|
|
343
|
+
> }
|
|
344
|
+
> });
|
|
345
|
+
> ```
|
|
346
|
+
|
|
347
|
+
```jsonc
|
|
348
|
+
// 3. tsconfig.json — so the generated `styled-system/*` imports resolve
|
|
349
|
+
{
|
|
350
|
+
"compilerOptions": {
|
|
351
|
+
// NOT `baseUrl`. TypeScript 6 removed it, and a project on a current
|
|
352
|
+
// toolchain fails immediately with TS5102. This form does the same job
|
|
353
|
+
// and works on both.
|
|
354
|
+
"paths": { "*": ["./*"] },
|
|
355
|
+
"jsx": "react-jsx",
|
|
356
|
+
"moduleResolution": "bundler"
|
|
357
|
+
},
|
|
358
|
+
"include": ["src/**/*", "styled-system/**/*.ts"]
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
```tsx
|
|
363
|
+
// 4. Your root — theme first, then the provider
|
|
364
|
+
import "./theme.css"; // the 44 properties, from step 3 above
|
|
365
|
+
import { StonedogStyleProvider } from "@stonedogcode/style";
|
|
366
|
+
|
|
367
|
+
export function Root({ children }) {
|
|
368
|
+
return (
|
|
369
|
+
<StonedogStyleProvider fontSizeProfile="md" variant="solid">
|
|
370
|
+
{children}
|
|
371
|
+
</StonedogStyleProvider>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
# 5. Generate, then run the three checks under "Check it actually worked"
|
|
378
|
+
npx panda codegen && npx panda cssgen --outfile styled-system/styles.css
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**What each mistake looks like**, since none of them raise an error:
|
|
382
|
+
|
|
383
|
+
| Symptom | Cause |
|
|
384
|
+
|---|---|
|
|
385
|
+
| Page renders, everything invisible or unstyled colours | Step 3 — properties undefined |
|
|
386
|
+
| Components render but have no styling at all | `include` missing the package (c) |
|
|
387
|
+
| Some styles apply, spacing and radii look wrong | Dropped a base preset (a/b) |
|
|
388
|
+
| `md`/`lg` responsive props rejected by the type-checker | Dropped a base preset (a/b) |
|
|
389
|
+
| `Cannot find module 'styled-system/jsx'` | No `paths` mapping, or codegen not run |
|
|
390
|
+
| `TS5102: Option 'baseUrl' has been removed` | TypeScript 6+; use `paths` (step 3) |
|
|
391
|
+
| `Could not find a declaration file for 'react-dom'` | Missing `@types/react-dom` (step 1) |
|
|
392
|
+
| Works in dev, breaks in a Next.js build | Missing `transpilePackages` |
|
|
393
|
+
|
|
394
|
+
Every row is a failure this walkthrough actually hit on a clean project, not a
|
|
395
|
+
list of things that might go wrong.
|
|
396
|
+
|
|
397
|
+
## Logging
|
|
398
|
+
|
|
399
|
+
Silent by default — a component that renders a few hundred times a second must
|
|
400
|
+
not decide your console should fill up. Opt in at startup:
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
import { setStyleLogger } from "@stonedogcode/style";
|
|
404
|
+
setStyleLogger(myLogger); // trace / debug / info / warn / error
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## Icons — bring your own
|
|
408
|
+
|
|
409
|
+
**This package ships no icons, and that is the point.** `StyledIcon` is a
|
|
410
|
+
sizing-and-colouring wrapper that renders *whatever node you hand it*, so you
|
|
411
|
+
choose the icon set and nothing about it leaks into the library. Lucide,
|
|
412
|
+
Heroicons, Font Awesome, Material Symbols, your designer's SVGs — all equally
|
|
413
|
+
supported, and you can mix them.
|
|
414
|
+
|
|
415
|
+
```tsx
|
|
416
|
+
import { StyledIcon } from "@stonedogcode/style";
|
|
417
|
+
import { Home } from "lucide-react";
|
|
418
|
+
|
|
419
|
+
<StyledIcon icon={<Home />} size="lg" />;
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### Building an icon set
|
|
423
|
+
|
|
424
|
+
An icon set is a few hundred near-identical wrappers, and hand-writing them is
|
|
425
|
+
how a set drifts — one forgets to forward `size`, another hardcodes a colour.
|
|
426
|
+
`createIcon` makes each one a line and forces them to agree:
|
|
427
|
+
|
|
428
|
+
```tsx
|
|
429
|
+
// icons.tsx — your own module, in your own repo
|
|
430
|
+
import { createIcon, createIconFromComponent } from "@stonedogcode/style";
|
|
431
|
+
import { Home, Trash2 } from "lucide-react";
|
|
432
|
+
|
|
433
|
+
export const StyledHome = createIcon("StyledHome", <Home />);
|
|
434
|
+
export const StyledTrash = createIconFromComponent("StyledTrash", Trash2);
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Use `createIconFromComponent` when the set exports one component per glyph
|
|
438
|
+
(Lucide, Heroicons, react-icons). It renders them at `width`/`height` 100% so
|
|
439
|
+
they fill the box `size` establishes — most sets default to 24px and would
|
|
440
|
+
otherwise ignore `size` entirely. Use `createIcon` when you have a node already.
|
|
441
|
+
|
|
442
|
+
### Sizing
|
|
443
|
+
|
|
444
|
+
`size` accepts `xs`, `sm`, `1x`, `md`, `lg`, `2x`, `xl`, `3x` … `10x` and sets a
|
|
445
|
+
square box in CSS px (`md` → 20, `lg` → 24, `2x` → 32). It always wins over a
|
|
446
|
+
height or width in a spread `style` prop, so sizing stays predictable.
|
|
447
|
+
|
|
448
|
+
Omit it and the app-wide `iconSize` from the provider applies — `2x` unless your
|
|
449
|
+
app says otherwise. Prefer omitting it: see "Retuning the scale for your
|
|
450
|
+
audience" above.
|
|
451
|
+
|
|
452
|
+
### Colouring
|
|
453
|
+
|
|
454
|
+
Two mechanisms, because icon libraries disagree about how they take a colour:
|
|
455
|
+
|
|
456
|
+
| Your icon set draws with… | What to do |
|
|
457
|
+
|---|---|
|
|
458
|
+
| `currentColor` — Lucide, Heroicons, Feather, Material Symbols, most SVGs | Nothing. `color` is set on the wrapper and inherits. |
|
|
459
|
+
| its own CSS variables — e.g. Font Awesome duotone | Map the published `--icon-*` properties, once. |
|
|
460
|
+
|
|
461
|
+
`StyledIcon` publishes `--icon-primary-color`, `--icon-secondary-color` and
|
|
462
|
+
`--icon-secondary-opacity` under **neutral names** so no icon library is baked
|
|
463
|
+
into this package. A set that wants different names needs one CSS rule:
|
|
464
|
+
|
|
465
|
+
```css
|
|
466
|
+
/* Font Awesome adapter — one rule, in your app */
|
|
467
|
+
.icon svg {
|
|
468
|
+
--fa-primary-color: var(--icon-primary-color);
|
|
469
|
+
--fa-secondary-color: var(--icon-secondary-color);
|
|
470
|
+
--fa-secondary-opacity: var(--icon-secondary-opacity, 0.4);
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Colours default to the theme tokens (`textMain`, `iconBgPrimary`), so an icon
|
|
475
|
+
with no explicit colour follows the host's theme and colour mode automatically.
|
|
476
|
+
Pass `color` / `secondaryColor` to override per call site.
|
|
477
|
+
|
|
478
|
+
### Accessibility
|
|
479
|
+
|
|
480
|
+
`title` is the whole interface, and the default is the one you want more often:
|
|
481
|
+
|
|
482
|
+
```tsx
|
|
483
|
+
<StyledIcon icon={<Trash2 />} /> {/* decorative: aria-hidden */}
|
|
484
|
+
<StyledIcon icon={<Trash2 />} title="Delete" /> {/* meaningful: role="img" + name */}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Give `title` **only** when the icon carries meaning no adjacent text already
|
|
488
|
+
conveys — an icon-only button, for instance. An icon sitting next to its own
|
|
489
|
+
label must stay untitled, or screen readers announce the name twice.
|
|
490
|
+
|
|
491
|
+
### Why it works this way
|
|
492
|
+
|
|
493
|
+
The components were extracted from an app built on a per-seat commercial icon
|
|
494
|
+
set whose artwork cannot be redistributed under this licence. Rather than pick a
|
|
495
|
+
replacement and impose it on everyone, the artwork was cut out entirely. Your
|
|
496
|
+
licensed set can live in a private package while the components that lay it out
|
|
497
|
+
stay open — which is exactly the arrangement the original app now uses.
|
|
498
|
+
|
|
499
|
+
## Navigation — `StyledSidebar`
|
|
500
|
+
|
|
501
|
+
A rail of tools, built for readers who navigate by reading words rather than by
|
|
502
|
+
decoding glyphs. Every item is **an icon *and* the tool's name** — there is no
|
|
503
|
+
icon-only rendering, not even collapsed. Full reasoning in
|
|
504
|
+
[PRD-0001](docs/prd/PRD-0001-styled-sidebar.md).
|
|
505
|
+
|
|
506
|
+
```tsx
|
|
507
|
+
import { StyledSidebar, type SidebarItem } from "@stonedogcode/style";
|
|
508
|
+
|
|
509
|
+
const tools: SidebarItem[] = [
|
|
510
|
+
{ id: "calendar", icon: <StyledCalendar />, label: "Calendar",
|
|
511
|
+
description: "Events & appointments", help: "Shows what is coming up." },
|
|
512
|
+
{ id: "notes", icon: <StyledNotes />, label: "Notes" },
|
|
513
|
+
];
|
|
514
|
+
|
|
515
|
+
<StyledSidebar
|
|
516
|
+
items={tools} // already ordered, already filtered
|
|
517
|
+
selectedId={selected}
|
|
518
|
+
onSelect={setSelected}
|
|
519
|
+
overflow="scroll" // or "paging"
|
|
520
|
+
emptyState="No tools match that search."
|
|
521
|
+
heading="TOOLS"
|
|
522
|
+
aria-label="Care Tools"
|
|
523
|
+
/>;
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
| Prop | Meaning |
|
|
527
|
+
|---|---|
|
|
528
|
+
| `items` | `SidebarItem[]` — `{ id, icon?, label, description?, help? }`. **Rendered exactly as given.** |
|
|
529
|
+
| `selectedId` / `onSelect` | Controlled selection. `onSelect(id)` reports a choice; navigation is yours. |
|
|
530
|
+
| `overflow` | `"scroll"` (default, uses `StyledScrollbar`) or `"paging"` (previous/next + "Page 2 of 4"). |
|
|
531
|
+
| `itemsPerPage` | Paging only; default 8. |
|
|
532
|
+
| `collapsed` / `onCollapsedChange` | Controlled collapse. Omit the handler and no collapse control renders. |
|
|
533
|
+
| `emptyState` | Rendered inside a live region when `items` is empty. |
|
|
534
|
+
| `heading` | e.g. `"TOOLS"`. |
|
|
535
|
+
| `aria-label` | Names the `navigation` landmark. Defaults to `"Tools"`. |
|
|
536
|
+
|
|
537
|
+
### Ordering, filtering and the search box are **yours**, not the component's
|
|
538
|
+
|
|
539
|
+
This is the load-bearing part of the API, so it is stated plainly: **`items`
|
|
540
|
+
arrive already ordered and already filtered. `StyledSidebar` does not sort, does
|
|
541
|
+
not filter, and owns no search field.**
|
|
542
|
+
|
|
543
|
+
- **Ordering** is a user preference the host stores and applies.
|
|
544
|
+
- **Filtering is policy** — name only or description too, fuzzy or exact,
|
|
545
|
+
accent-insensitive or not. Different products want different answers, and
|
|
546
|
+
baking one in would impose it on every future consumer.
|
|
547
|
+
- **A search input built here could not dictate.** Speech-to-text lives in the
|
|
548
|
+
host's own text input, behind its own engine selection and feature flag. This
|
|
549
|
+
package cannot import that and must not depend on any host. Leaving the field
|
|
550
|
+
outside is precisely what makes dictated search work.
|
|
551
|
+
|
|
552
|
+
Three behaviours exist to make that seam seamless, and a host gets them free:
|
|
553
|
+
|
|
554
|
+
- **Selection survives filtering.** A `selectedId` no longer present in `items`
|
|
555
|
+
stays selected — the reader is searching, not navigating away.
|
|
556
|
+
- **Paging resets when `items` changes**, so a narrowed list never strands
|
|
557
|
+
anyone on an empty page 3.
|
|
558
|
+
- **An empty `items` renders `emptyState` in a live region**, so a screen-reader
|
|
559
|
+
user learns the filter matched nothing instead of meeting a blank panel.
|
|
560
|
+
|
|
561
|
+
### What it guarantees
|
|
562
|
+
|
|
563
|
+
- **Tap targets:** 60px minimum on a tool row, 48px on the pager, collapse and
|
|
564
|
+
help controls — stated as `min-height`, so no density or font-scale change
|
|
565
|
+
erodes them.
|
|
566
|
+
- **Help opens on click, never hover** (via `StyledTooltip`'s `trigger="click"`,
|
|
567
|
+
which renders its own visible, focusable help control). Escape closes it and
|
|
568
|
+
returns focus. **Nothing anywhere in this component changes state on hover.**
|
|
569
|
+
- **Selection is never colour alone** — border, background *and* label weight,
|
|
570
|
+
plus `aria-current` for assistive technology.
|
|
571
|
+
- **Long names wrap** rather than spilling out of the rail.
|
|
572
|
+
- **No drag interaction at all** (WCAG 2.2 SC 2.5.7). A host that builds
|
|
573
|
+
reordering must provide a non-drag path.
|
|
574
|
+
|
|
575
|
+
Scroll mode needs a height to scroll inside: `StyledScrollbar` is
|
|
576
|
+
`flex: 1; min-height: 0; overflow: auto`, so give the sidebar's container a
|
|
577
|
+
height (`display: flex; flex-direction: column; height: …`). Unconstrained, the
|
|
578
|
+
rail simply grows — which is correct, and is not a bug.
|
|
579
|
+
|
|
580
|
+
## Adopting a component as it is migrated
|
|
581
|
+
|
|
582
|
+
Components move out of HopperGuard into this package one at a time (NEH-167).
|
|
583
|
+
Each lands as its own release, so consumers adopt on their own schedule rather
|
|
584
|
+
than waiting for a big-bang switch.
|
|
585
|
+
|
|
586
|
+
### Find out what is available
|
|
587
|
+
|
|
588
|
+
```bash
|
|
589
|
+
git -C packages/stonedog-style log --oneline main # what has landed
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
Every migration commit is `feat: migrate StyledX`. The commit body is the real
|
|
593
|
+
changelog: it says what the component does, **what was deliberately left
|
|
594
|
+
behind**, and any prop that was dropped. Read it before adopting — a migration
|
|
595
|
+
is rarely a pure move, because dead props and accessibility gaps get fixed on
|
|
596
|
+
the way through.
|
|
597
|
+
|
|
598
|
+
### HopperGuard
|
|
599
|
+
|
|
600
|
+
The app already consumes this package, so adopting a component is a pointer bump
|
|
601
|
+
plus a decision about the local file.
|
|
602
|
+
|
|
603
|
+
```bash
|
|
604
|
+
git -C packages/stonedog-style checkout main && git -C packages/stonedog-style pull
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
Then, for `apps/web/src/app/components/Styled/StyledX.tsx`:
|
|
608
|
+
|
|
609
|
+
**If the migrated component is a drop-in**, replace the file with a re-export.
|
|
610
|
+
Call sites stay untouched:
|
|
611
|
+
|
|
612
|
+
```tsx
|
|
613
|
+
export { StyledX as default, StyledX } from "@stonedogcode/style";
|
|
614
|
+
export type { StyledXProps } from "@stonedogcode/style";
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
**If the app needs behaviour the shared one deliberately does not have**, keep a
|
|
618
|
+
real wrapper that delegates. `StyledSpinner` is the worked example: the shared
|
|
619
|
+
one has no `spinLogo`, because a brand mark is not a primitive, so the app keeps
|
|
620
|
+
a component that renders its logo and falls through to the shared spinner
|
|
621
|
+
otherwise. That is not a shim — it is an app-level extension, and it should not
|
|
622
|
+
pretend to be one.
|
|
623
|
+
|
|
624
|
+
Then two PRs, in this order — the submodule pointer must be on `main` before the
|
|
625
|
+
app can resolve it:
|
|
626
|
+
|
|
627
|
+
1. **hopperguard** — bump the `packages/stonedog-style` gitlink.
|
|
628
|
+
2. **hopper-web** — swap the local file.
|
|
629
|
+
3. **hopperguard** — bump the `apps/web` gitlink.
|
|
630
|
+
|
|
631
|
+
### optima-filings / optima-cloud-saas
|
|
632
|
+
|
|
633
|
+
Nothing to unpick — these have no local copy to replace. Take the dependency
|
|
634
|
+
(see Install), then import:
|
|
635
|
+
|
|
636
|
+
```tsx
|
|
637
|
+
import { StyledSpinner } from "@stonedogcode/style";
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
`optima-filings` is public and AGPLv3 and ships a public Docker image, so it
|
|
641
|
+
uses a **permissive icon set** (Lucide) through the icon seam rather than the
|
|
642
|
+
private Font Awesome package. Everything else is shared. Both Optima repos run
|
|
643
|
+
their own `--optima-*` namespace via `cssVarPrefix` (NEH-170).
|
|
644
|
+
|
|
645
|
+
### Verify — the three checks that actually catch things
|
|
646
|
+
|
|
647
|
+
Learned the hard way; each one corresponds to a real bug that reached `main`.
|
|
648
|
+
|
|
649
|
+
1. **Type-check on a real `npm install`.** Not a symlinked `node_modules` from
|
|
650
|
+
another checkout — a stale copy there resolves to the wrong package and hides
|
|
651
|
+
dependency-graph breakage entirely. The unit tier cannot substitute: a missing
|
|
652
|
+
export is a *type* error, and jest does not type-check.
|
|
653
|
+
2. **Look at it at 375px.** Layout regressions surface on the narrowest screen
|
|
654
|
+
first, and jsdom has no layout engine, so no unit test will tell you.
|
|
655
|
+
3. **Read the migration commit for dropped props.** A prop removed upstream is
|
|
656
|
+
a type error at the call site — good. A prop that was *always* silently
|
|
657
|
+
ignored (`thickness`, `speed`, `color`, `emptyColor`, `logoSize` on the
|
|
658
|
+
spinner) is not, and its removal is a no-op you can adopt safely.
|
|
659
|
+
|
|
660
|
+
### If the component changed shape
|
|
661
|
+
|
|
662
|
+
Migrations fix things on the way through, so behaviour is occasionally
|
|
663
|
+
intentionally different. When it is, the commit says so explicitly. Two patterns
|
|
664
|
+
so far:
|
|
665
|
+
|
|
666
|
+
- **A prop was dropped because it never worked.** Adopt freely; nothing rendered
|
|
667
|
+
differently.
|
|
668
|
+
- **An accessibility gap was closed.** `StyledSpinner` gained `role="status"`, so
|
|
669
|
+
screen readers now announce it. Nothing visual changes, but a test asserting
|
|
670
|
+
the old silence will fail, and it should.
|
|
671
|
+
|
|
672
|
+
## Development
|
|
673
|
+
|
|
674
|
+
```bash
|
|
675
|
+
npm install # also runs panda codegen
|
|
676
|
+
npm run gate # codegen → typecheck → lint → tests. The merge bar.
|
|
677
|
+
npm test
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
`styled-system/` is generated and gitignored; regenerate with
|
|
681
|
+
`npm run panda:build`.
|
|
682
|
+
|
|
683
|
+
Tests run against the **real** generated `styled-system` rather than a mock, so
|
|
684
|
+
recipe output is assertable — see CLAUDE.md for why that took some doing.
|
|
685
|
+
|
|
686
|
+
### Design documents
|
|
687
|
+
|
|
688
|
+
Components with enough behaviour to argue about get a PRD under `docs/prd/`,
|
|
689
|
+
written before the component. They record what a component must do and — more
|
|
690
|
+
usefully — what it deliberately does not, so the next person does not re-open a
|
|
691
|
+
settled question.
|
|
692
|
+
|
|
693
|
+
| PRD | Component | Status |
|
|
694
|
+
|---|---|---|
|
|
695
|
+
| [PRD-0001](docs/prd/PRD-0001-styled-sidebar.md) | `StyledSidebar` | Shipped |
|
|
696
|
+
|
|
697
|
+
## License
|
|
698
|
+
|
|
699
|
+
[Apache-2.0](./LICENSE). See [NOTICE](./NOTICE) for attribution.
|