@stonedogcode/style 0.10.1 → 0.13.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/README.md +59 -7
- package/package.json +2 -2
- package/src/components/StyledAlert.tsx +120 -0
- package/src/components/StyledConfetti.tsx +274 -0
- package/src/components/StyledFieldErrors.tsx +94 -0
- package/src/components/StyledForm.tsx +75 -0
- package/src/components/StyledLink.tsx +157 -0
- package/src/components/StyledPage.tsx +274 -0
- package/src/components/StyledTag.tsx +118 -0
- package/src/components/StyledText.tsx +2 -10
- package/src/config/font-size.ts +36 -0
- package/src/config/link-component.tsx +69 -0
- package/src/config/style-config.tsx +39 -1
- package/src/index.ts +45 -0
- package/src/preset/index.ts +31 -1
- package/src/preset/recipes/alert.ts +95 -0
- package/src/preset/recipes/box.ts +6 -0
- package/src/preset/recipes/button.ts +12 -0
- package/src/preset/recipes/icon.ts +13 -1
- package/src/preset/recipes/input-bool.ts +111 -10
- package/src/preset/recipes/input-radio.ts +11 -1
- package/src/preset/recipes/menu.ts +9 -0
- package/src/preset/recipes/stack.ts +7 -0
- package/src/preset/semantic-variables.ts +190 -9
|
@@ -72,6 +72,13 @@ export const stackRecipe = defineRecipe({
|
|
|
72
72
|
},
|
|
73
73
|
ghost: {
|
|
74
74
|
bg: "boxBgSecondary",
|
|
75
|
+
// Found by the new stylesheet guard, not by the NEH-441 sweep that
|
|
76
|
+
// preceded it — the sweep missed this one, which is the argument
|
|
77
|
+
// for having a guard rather than a one-off scan. `matte` directly
|
|
78
|
+
// above paints the identical `boxBgSecondary` and pairs it with
|
|
79
|
+
// `textSecondary`; this painted the same surface and left its text
|
|
80
|
+
// to inherit.
|
|
81
|
+
color: "textSecondary",
|
|
75
82
|
border: "none",
|
|
76
83
|
},
|
|
77
84
|
none: {
|
|
@@ -39,9 +39,28 @@ const COLOR_TOKENS: TokenMap = {
|
|
|
39
39
|
textPop: "text-pop-text",
|
|
40
40
|
textError: "text-error-text",
|
|
41
41
|
textWarning: "text-warning-text",
|
|
42
|
+
// Added NEH-519. The contract could express failure and caution but not
|
|
43
|
+
// success, so every consumer that needed one improvised — and the obvious
|
|
44
|
+
// substitute, `textAccent`, is wrong: accent is whatever the host theme sets
|
|
45
|
+
// it to, with nothing constraining it to read as positive, so a confirmation
|
|
46
|
+
// could land in an alarming colour. HopperGuard had a live site doing exactly
|
|
47
|
+
// this dance (an SSO panel reporting "ok" vs error) and had to settle for
|
|
48
|
+
// neutral text.
|
|
49
|
+
//
|
|
50
|
+
// Deliberately NOT in TEXT_BACKGROUND_PAIRS, matching textError and
|
|
51
|
+
// textWarning: a meaning-carrying colour appears on whatever surface the
|
|
52
|
+
// message happens to sit on, so there is no single pairing to contrast-check
|
|
53
|
+
// it against.
|
|
54
|
+
textSuccess: "text-success-text",
|
|
42
55
|
|
|
43
56
|
// Text on each surface. Pair these with the matching `boxBg*` — see
|
|
44
57
|
// TEXT_BACKGROUND_PAIRS for which goes with which.
|
|
58
|
+
//
|
|
59
|
+
// These are a SURFACE axis, not an emphasis one. `textSecondary` means "text
|
|
60
|
+
// on the secondary surface"; it does not mean "less important text". Reading
|
|
61
|
+
// it as the latter is the mistake NEH-519 records — it collapses two
|
|
62
|
+
// different emphasis levels onto one colour and loses a distinction the UI
|
|
63
|
+
// meant to draw. De-emphasis is EMPHASIS_TOKENS below.
|
|
45
64
|
textMain: "box-main-text",
|
|
46
65
|
textPrimary: "box-primary-text",
|
|
47
66
|
textSecondary: "box-secondary-text",
|
|
@@ -97,6 +116,131 @@ const COLOR_TOKENS: TokenMap = {
|
|
|
97
116
|
iconBgAccentHover: "icon-accent-hover-bg",
|
|
98
117
|
};
|
|
99
118
|
|
|
119
|
+
/**
|
|
120
|
+
* The emphasis axis: how important this text is, on whatever surface it sits.
|
|
121
|
+
*
|
|
122
|
+
* The contract had no such axis (NEH-519). It has a *surface* axis — `textMain`
|
|
123
|
+
* on `boxBgMain`, `textSecondary` on `boxBgSecondary` — and consumers reached
|
|
124
|
+
* for `textSecondary` when they meant "muted", which is a different question
|
|
125
|
+
* with a different answer. HopperGuard had a stepper wanting three levels at
|
|
126
|
+
* once:
|
|
127
|
+
*
|
|
128
|
+
* ```tsx
|
|
129
|
+
* color={active ? "fg" : done ? "fg.muted" : "fg.subtle"}
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* and the mapping collapsed "done" and "upcoming" onto one colour, losing a
|
|
133
|
+
* distinction that UI was drawing on purpose.
|
|
134
|
+
*
|
|
135
|
+
* ## The default is relative, which is what makes these adoptable
|
|
136
|
+
*
|
|
137
|
+
* Every other colour token is fallback-free by design: an undefined colour
|
|
138
|
+
* paints an invisible element, a louder and earlier bug than a wrong shade.
|
|
139
|
+
* That rule is right where "what colour is this?" has no answer without a
|
|
140
|
+
* theme.
|
|
141
|
+
*
|
|
142
|
+
* Emphasis is not that question. "Like the surrounding text, but quieter" has a
|
|
143
|
+
* correct answer on *every* theme, and `color-mix` states it directly:
|
|
144
|
+
* `currentColor` inside a `color` declaration resolves to the INHERITED colour
|
|
145
|
+
* (the property being computed is `color` itself), so the default de-emphasises
|
|
146
|
+
* whatever the text around it already is — light theme, dark theme, or a host
|
|
147
|
+
* palette nobody has seen.
|
|
148
|
+
*
|
|
149
|
+
* So these follow the font tokens' shape rather than the colours': they carry a
|
|
150
|
+
* fallback and stay OUT of `requiredCssCustomProperties()`. Putting them there
|
|
151
|
+
* would fail every existing host for no safety gain, and would move the
|
|
152
|
+
* `required === fallback-free colours` identity the contract test pins on both
|
|
153
|
+
* sides. This is the owner direction recorded on NEH-421 — a new token ships
|
|
154
|
+
* with a sensible default so every project can adopt it immediately — applied
|
|
155
|
+
* to the case where a default is genuinely knowable.
|
|
156
|
+
*
|
|
157
|
+
* ## The percentages are measured, not chosen
|
|
158
|
+
*
|
|
159
|
+
* Alpha de-emphasis trades contrast for hierarchy, and past some point it
|
|
160
|
+
* trades away legibility. `emphasis-contrast.ct.tsx` measures both tiers
|
|
161
|
+
* against the harness theme in a real browser and asserts they clear WCAG AA
|
|
162
|
+
* (4.5:1); the values below are what passed. A host that wants a stronger or
|
|
163
|
+
* weaker step defines the property.
|
|
164
|
+
*/
|
|
165
|
+
const EMPHASIS_TOKENS: Record<string, [suffix: string, fallback: string]> = {
|
|
166
|
+
/** Secondary information: still read, just not first. */
|
|
167
|
+
textMuted: ["text-muted-text", "color-mix(in srgb, currentColor 78%, transparent)"],
|
|
168
|
+
/** Furthest back — hints, placeholders, a step not yet reached. */
|
|
169
|
+
textSubtle: ["text-subtle-text", "color-mix(in srgb, currentColor 64%, transparent)"],
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Status SURFACES — the chip an alert or banner is painted on (NEH-421).
|
|
174
|
+
*
|
|
175
|
+
* The contract could already say what colour an error *message* is
|
|
176
|
+
* (`textError`), and had `boxInfo`, but nothing for a warning, error or success
|
|
177
|
+
* surface. So `StyledAlert` painted all four statuses from the raw palette —
|
|
178
|
+
* `red.50` / `red.200` / `red.700` and friends — which ignores the theme and
|
|
179
|
+
* dark mode entirely and sits outside contrast validation. On a dark theme
|
|
180
|
+
* those render dark text on a light chip regardless of surroundings.
|
|
181
|
+
*
|
|
182
|
+
* ## These carry defaults, and status is the reason
|
|
183
|
+
*
|
|
184
|
+
* The fallback-free rule is right where "what colour is this?" has no answer
|
|
185
|
+
* without a theme. **Danger-red, caution-amber and success-green are
|
|
186
|
+
* near-universal**, so the cost that rule imposes here — an invisible alert in
|
|
187
|
+
* any host that has not yet defined the property — is real, while the thing it
|
|
188
|
+
* protects against is not. A product wanting its own says so.
|
|
189
|
+
*
|
|
190
|
+
* It also dissolves the two-repo deadlock: without a default the package cannot
|
|
191
|
+
* add the token (the bump goes red until the hosts adopt) and the hosts cannot
|
|
192
|
+
* adopt until the bump lands. This is the owner direction recorded on NEH-421.
|
|
193
|
+
*
|
|
194
|
+
* ## The defaults are translucent on purpose
|
|
195
|
+
*
|
|
196
|
+
* A fixed `#fee2e2` chip is a light-theme chip; on a dark theme it is a glaring
|
|
197
|
+
* white slab. `color-mix` with `transparent` tints whatever surface the alert
|
|
198
|
+
* is placed on, so one default reads correctly in both — the same reasoning as
|
|
199
|
+
* EMPHASIS_TOKENS, applied to a background instead of a foreground.
|
|
200
|
+
*
|
|
201
|
+
* The hue is fixed and the *lightness* is not, which is the split that matters:
|
|
202
|
+
* red has to stay red to mean danger, but how light that red sits has to follow
|
|
203
|
+
* the page. `alert.ct.tsx` measures the paired text against each chip in both a
|
|
204
|
+
* light and a dark surrounding and asserts AA.
|
|
205
|
+
*/
|
|
206
|
+
const STATUS_SURFACE_TOKENS: Record<string, [suffix: string, fallback: string]> = {
|
|
207
|
+
boxSuccess: [
|
|
208
|
+
"box-success-bg",
|
|
209
|
+
"color-mix(in srgb, #16a34a 14%, transparent)",
|
|
210
|
+
],
|
|
211
|
+
boxWarning: [
|
|
212
|
+
"box-warning-bg",
|
|
213
|
+
"color-mix(in srgb, #d97706 16%, transparent)",
|
|
214
|
+
],
|
|
215
|
+
boxError: ["box-error-bg", "color-mix(in srgb, #dc2626 14%, transparent)"],
|
|
216
|
+
/**
|
|
217
|
+
* The border that goes with each chip — the **solid** hue, not a tint of it.
|
|
218
|
+
*
|
|
219
|
+
* The fill is deliberately faint, so the border is what makes the alert read
|
|
220
|
+
* as a container rather than as a colour accident. That means it has to clear
|
|
221
|
+
* WCAG 1.4.11 (3:1 for a non-text boundary) against the page, and a
|
|
222
|
+
* translucent border cannot: at 45% these measured **1.72–2.05:1** on a dark
|
|
223
|
+
* surface, which is the first thing `StyledAlert.ct.tsx` caught.
|
|
224
|
+
*
|
|
225
|
+
* A saturated mid-tone hue clears 3:1 at BOTH ends, which is what lets one
|
|
226
|
+
* value serve a light theme and a dark one without adapting:
|
|
227
|
+
*
|
|
228
|
+
* | | vs `#0f172a` | vs `#ffffff` |
|
|
229
|
+
* |---|---|---|
|
|
230
|
+
* | `#16a34a` | 4.37 | 3.79 |
|
|
231
|
+
* | `#d97706` | 4.63 | 3.58 |
|
|
232
|
+
* | `#dc2626` | 3.20 | 5.18 |
|
|
233
|
+
*
|
|
234
|
+
* So these are the one place in this file a bare literal colour is correct.
|
|
235
|
+
* It is a *hue carrying meaning* — red means danger to everyone — not a
|
|
236
|
+
* surface colour standing in for a theme, and the numbers above are why it
|
|
237
|
+
* does not need to follow the page the way the fill does.
|
|
238
|
+
*/
|
|
239
|
+
borderSuccess: ["box-success-border", "#16a34a"],
|
|
240
|
+
borderWarning: ["box-warning-border", "#d97706"],
|
|
241
|
+
borderError: ["box-error-border", "#dc2626"],
|
|
242
|
+
};
|
|
243
|
+
|
|
100
244
|
/**
|
|
101
245
|
* Host-provided *layout* properties, as `token name → [suffix, fallback]`.
|
|
102
246
|
*
|
|
@@ -253,9 +397,35 @@ export function getBackgroundForText(textToken: string): string | undefined {
|
|
|
253
397
|
return TEXT_BACKGROUND_PAIRS[textToken];
|
|
254
398
|
}
|
|
255
399
|
|
|
256
|
-
/**
|
|
400
|
+
/**
|
|
401
|
+
* Every colour token that carries a default, and is therefore NOT required of a
|
|
402
|
+
* host.
|
|
403
|
+
*
|
|
404
|
+
* Two groups with two different justifications, kept separate because their
|
|
405
|
+
* defaults obey different rules and a test has to be able to say which:
|
|
406
|
+
* emphasis is relative to the inherited colour, status fixes a hue and leaves
|
|
407
|
+
* the lightness relative.
|
|
408
|
+
*/
|
|
409
|
+
const DEFAULTED_COLOR_TOKENS = { ...EMPHASIS_TOKENS, ...STATUS_SURFACE_TOKENS };
|
|
410
|
+
|
|
411
|
+
/** Every Panda colour token this preset defines, defaulted ones included. */
|
|
257
412
|
export function colorTokenNames(): string[] {
|
|
258
|
-
return Object.keys(COLOR_TOKENS);
|
|
413
|
+
return [...Object.keys(COLOR_TOKENS), ...Object.keys(DEFAULTED_COLOR_TOKENS)];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** The de-emphasis tiers, which carry a fallback and are not required of a host. */
|
|
417
|
+
export function emphasisTokenNames(): string[] {
|
|
418
|
+
return Object.keys(EMPHASIS_TOKENS);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** The status chips and their borders, which also carry a fallback. */
|
|
422
|
+
export function statusSurfaceTokenNames(): string[] {
|
|
423
|
+
return Object.keys(STATUS_SURFACE_TOKENS);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** Every colour token a host may leave undefined. */
|
|
427
|
+
export function defaultedColorTokenNames(): string[] {
|
|
428
|
+
return Object.keys(DEFAULTED_COLOR_TOKENS);
|
|
259
429
|
}
|
|
260
430
|
|
|
261
431
|
/**
|
|
@@ -270,14 +440,25 @@ export function requiredCssCustomProperties(
|
|
|
270
440
|
return Object.values(COLOR_TOKENS).map((suffix) => `--${prefix}-${suffix}`);
|
|
271
441
|
}
|
|
272
442
|
|
|
273
|
-
/**
|
|
443
|
+
/**
|
|
444
|
+
* Panda colour-token definitions, bound to a custom-property prefix.
|
|
445
|
+
*
|
|
446
|
+
* Two shapes in one map, deliberately: the surface and meaning colours emit a
|
|
447
|
+
* bare `var(…)` with no fallback, while the emphasis tiers and the status chips
|
|
448
|
+
* emit `var(…, <default>)`. See `COLOR_TOKENS`, `EMPHASIS_TOKENS` and
|
|
449
|
+
* `STATUS_SURFACE_TOKENS` for why each asymmetry is correct rather than an
|
|
450
|
+
* oversight.
|
|
451
|
+
*/
|
|
274
452
|
export function createSemanticColors(
|
|
275
453
|
prefix: string = DEFAULT_CSS_VAR_PREFIX,
|
|
276
454
|
): Record<string, { value: string }> {
|
|
277
|
-
return
|
|
278
|
-
Object.
|
|
279
|
-
token,
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
455
|
+
return {
|
|
456
|
+
...Object.fromEntries(
|
|
457
|
+
Object.entries(COLOR_TOKENS).map(([token, suffix]) => [
|
|
458
|
+
token,
|
|
459
|
+
{ value: `var(--${prefix}-${suffix})` },
|
|
460
|
+
]),
|
|
461
|
+
),
|
|
462
|
+
...createFallbackTokens(DEFAULTED_COLOR_TOKENS, prefix),
|
|
463
|
+
};
|
|
283
464
|
}
|