@stonedogcode/style 0.10.1 → 0.12.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/StyledText.tsx +2 -10
- package/src/config/font-size.ts +36 -0
- package/src/index.ts +3 -0
- package/src/preset/index.ts +3 -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-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
package/README.md
CHANGED
|
@@ -82,7 +82,8 @@ token reads one, and **a token whose property is undefined renders as nothing**
|
|
|
82
82
|
no fallback, no warning, no error. An app that skips this compiles, builds,
|
|
83
83
|
serves, and shows you a blank page.
|
|
84
84
|
|
|
85
|
-
There are **
|
|
85
|
+
There are **45** of them. Get the list at runtime rather than copying one — a
|
|
86
|
+
number in prose goes stale the day a token is added, and this one already had:
|
|
86
87
|
|
|
87
88
|
```ts
|
|
88
89
|
import { requiredCssCustomProperties } from "@stonedogcode/style/preset";
|
|
@@ -91,7 +92,57 @@ requiredCssCustomProperties(); // --hopper-* (default)
|
|
|
91
92
|
requiredCssCustomProperties("optima"); // --optima-*, if you set cssVarPrefix
|
|
92
93
|
```
|
|
93
94
|
|
|
94
|
-
|
|
95
|
+
### 8 colour tokens you do NOT have to define
|
|
96
|
+
|
|
97
|
+
`textMuted` and `textSubtle` are the **emphasis** axis — how important a piece
|
|
98
|
+
of text is, on whatever surface it sits. They are the one exception to the
|
|
99
|
+
no-fallback rule above, and they are not in `requiredCssCustomProperties()`.
|
|
100
|
+
|
|
101
|
+
They can have a default because *"like the surrounding text, but quieter"* has a
|
|
102
|
+
correct answer on every theme, which *"what colour is this surface?"* does not:
|
|
103
|
+
|
|
104
|
+
```css
|
|
105
|
+
color-mix(in srgb, currentColor 78%, transparent) /* textMuted */
|
|
106
|
+
color-mix(in srgb, currentColor 64%, transparent) /* textSubtle */
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`currentColor` inside a `color` declaration resolves to the **inherited** value,
|
|
110
|
+
so both follow whatever text they sit among — light theme, dark theme, or a
|
|
111
|
+
palette this package has never seen. Both clear WCAG AA against the surfaces
|
|
112
|
+
they pair with, measured in a browser rather than chosen by eye.
|
|
113
|
+
|
|
114
|
+
Define `--<prefix>-text-muted-text` / `--<prefix>-text-subtle-text` only if you
|
|
115
|
+
want a different step.
|
|
116
|
+
|
|
117
|
+
### ...and six more, for status
|
|
118
|
+
|
|
119
|
+
`boxSuccess` / `boxWarning` / `boxError` and their `borderSuccess` /
|
|
120
|
+
`borderWarning` / `borderError` are the chips `StyledAlert` paints on. They are
|
|
121
|
+
defaulted for the same reason and a slightly different one: **danger-red,
|
|
122
|
+
caution-amber and success-green are near-universal**, so a default is knowable
|
|
123
|
+
where "what colour is this surface?" is not.
|
|
124
|
+
|
|
125
|
+
The hue is fixed and the *lightness* is not, which is the split that matters —
|
|
126
|
+
red has to stay red to mean danger, but how light that red sits has to follow
|
|
127
|
+
the page:
|
|
128
|
+
|
|
129
|
+
```css
|
|
130
|
+
color-mix(in srgb, #dc2626 14%, transparent) /* boxError — tints the page */
|
|
131
|
+
#dc2626 /* borderError — a solid hue */
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The borders are solid because a translucent one cannot clear WCAG 1.4.11 (3:1
|
|
135
|
+
for a non-text boundary): at 45% they measured 1.72–2.05:1 against a dark page.
|
|
136
|
+
A saturated mid-tone clears 3:1 at both ends, which is what lets one value serve
|
|
137
|
+
a light theme and a dark one.
|
|
138
|
+
|
|
139
|
+
Define `--<prefix>-box-{success,warning,error}-{bg,border}` to use your own.
|
|
140
|
+
|
|
141
|
+
**Do not reach for `textSecondary` when you mean "muted".** That is the *surface*
|
|
142
|
+
axis — it means "text on the secondary surface" — and using it for emphasis
|
|
143
|
+
collapses two levels onto one colour.
|
|
144
|
+
|
|
145
|
+
A complete starter theme — all 45, nothing elided. Dark, and every text/surface
|
|
95
146
|
pair clears WCAG AA (measured: worst 5.17:1, ten of thirteen pairs at AAA), so
|
|
96
147
|
it is a legitimate starting point rather than a placeholder. Replace the values;
|
|
97
148
|
keep every key.
|
|
@@ -115,6 +166,7 @@ keep every key.
|
|
|
115
166
|
--hopper-text-pop-text: #38bdf8;
|
|
116
167
|
--hopper-text-error-text: #f87171;
|
|
117
168
|
--hopper-text-warning-text: #fbbf24;
|
|
169
|
+
--hopper-text-success-text: #4ade80;
|
|
118
170
|
|
|
119
171
|
/* Borders */
|
|
120
172
|
--hopper-box-primary-border: #475569;
|
|
@@ -193,7 +245,7 @@ the three ways this goes wrong silently:
|
|
|
193
245
|
```bash
|
|
194
246
|
npx panda cssgen --outfile styled-system/styles.css
|
|
195
247
|
|
|
196
|
-
# 1. Did the preset load? Expect ~
|
|
248
|
+
# 1. Did the preset load? Expect ~45 matches, not 0.
|
|
197
249
|
grep -c 'var(--hopper-' styled-system/styles.css
|
|
198
250
|
|
|
199
251
|
# 2. Did Panda parse the package's source? Expect ~240 classes, not ~0.
|
|
@@ -278,7 +330,7 @@ stonedogStylePreset({ cssVarPrefix: "acme" }); // → var(--acme-box-primary-bg)
|
|
|
278
330
|
|
|
279
331
|
The rename is total — every token re-points, and no `--hopper-*` reference
|
|
280
332
|
survives anywhere in the generated CSS. Choose it **before** you write a theme,
|
|
281
|
-
because it changes all
|
|
333
|
+
because it changes all 45 property names you have to define.
|
|
282
334
|
|
|
283
335
|
## Adopting it in a new app — a worked example
|
|
284
336
|
|
|
@@ -361,7 +413,7 @@ export default defineConfig({
|
|
|
361
413
|
|
|
362
414
|
```tsx
|
|
363
415
|
// 4. Your root — theme first, then the provider
|
|
364
|
-
import "./theme.css"; // the
|
|
416
|
+
import "./theme.css"; // the 45 properties, from step 3 above
|
|
365
417
|
import { StonedogStyleProvider } from "@stonedogcode/style";
|
|
366
418
|
|
|
367
419
|
export function Root({ children }) {
|
|
@@ -579,7 +631,7 @@ rail simply grows — which is correct, and is not a bug.
|
|
|
579
631
|
|
|
580
632
|
## Adopting a component as it is migrated
|
|
581
633
|
|
|
582
|
-
Components move out of HopperGuard into this package one at a time
|
|
634
|
+
Components move out of HopperGuard into this package one at a time.
|
|
583
635
|
Each lands as its own release, so consumers adopt on their own schedule rather
|
|
584
636
|
than waiting for a big-bang switch.
|
|
585
637
|
|
|
@@ -640,7 +692,7 @@ import { StyledSpinner } from "@stonedogcode/style";
|
|
|
640
692
|
`optima-filings` is public and AGPLv3 and ships a public Docker image, so it
|
|
641
693
|
uses a **permissive icon set** (Lucide) through the icon seam rather than the
|
|
642
694
|
private Font Awesome package. Everything else is shared. Both Optima repos run
|
|
643
|
-
their own `--optima-*` namespace via `cssVarPrefix
|
|
695
|
+
their own `--optima-*` namespace via `cssVarPrefix`.
|
|
644
696
|
|
|
645
697
|
### Verify — the three checks that actually catch things
|
|
646
698
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stonedogcode/style",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "A Panda CSS design system: a themeable Panda preset plus the React components built on it.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "StoneDogCode L.L.C.",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "git+https://github.com/stonedog-code/stonedog-style.git"
|
|
10
10
|
},
|
|
11
|
-
"//publishConfig": "A SCOPED package defaults to access: restricted. Publishing one without this succeeds, prints nothing unusual, and then 404s for every consumer — which reads as a missing package rather than as a private one. It was not needed while the name was unscoped (those default to public), so it is new as of
|
|
11
|
+
"//publishConfig": "A SCOPED package defaults to access: restricted. Publishing one without this succeeds, prints nothing unusual, and then 404s for every consumer — which reads as a missing package rather than as a private one. It was not needed while the name was unscoped (those default to public), so it is new as of the scope migration and it is the single thing most likely to be forgotten in one.",
|
|
12
12
|
"publishConfig": {
|
|
13
13
|
"access": "public"
|
|
14
14
|
},
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { alertRecipe } from "styled-system/recipes";
|
|
5
|
+
import { cx } from "styled-system/css";
|
|
6
|
+
|
|
7
|
+
/** The four things an alert can be. */
|
|
8
|
+
export type AlertStatus = "info" | "success" | "warning" | "error";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Which ARIA role each status takes, and why they are not all the same.
|
|
12
|
+
*
|
|
13
|
+
* The difference is whether the announcement **interrupts**. `alert` is an
|
|
14
|
+
* assertive live region: a screen reader abandons what it was saying to read it.
|
|
15
|
+
* That is right for a failure the user must deal with and wrong for a
|
|
16
|
+
* confirmation — being interrupted mid-sentence to be told something worked is
|
|
17
|
+
* hostile, and doing it for every status trains people to ignore the important
|
|
18
|
+
* ones.
|
|
19
|
+
*
|
|
20
|
+
* The original component had **no role at all**, so none of the four was
|
|
21
|
+
* announced. An alert that is not announced is not an alert (NEH-421).
|
|
22
|
+
*/
|
|
23
|
+
const ROLE_FOR_STATUS: Record<AlertStatus, "alert" | "status"> = {
|
|
24
|
+
error: "alert",
|
|
25
|
+
warning: "alert",
|
|
26
|
+
info: "status",
|
|
27
|
+
success: "status",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The non-colour signal for each status — WCAG 1.4.1 (Use of Colour), Level A.
|
|
32
|
+
*
|
|
33
|
+
* The original conveyed status by background colour alone. The `Indicator` slot
|
|
34
|
+
* existed and every call site left it empty, so the whole distinction was
|
|
35
|
+
* invisible to anyone who cannot separate four pale washes — which includes
|
|
36
|
+
* roughly one man in twelve, and anyone on a bad screen in daylight.
|
|
37
|
+
*
|
|
38
|
+
* These are text characters rather than icons on purpose. This package ships no
|
|
39
|
+
* artwork by policy, and a glyph inherits `currentColor` and the font scale for
|
|
40
|
+
* free, so the signal survives a font-size change and cannot end up a different
|
|
41
|
+
* colour from the message beside it.
|
|
42
|
+
*
|
|
43
|
+
* They are `aria-hidden`: the role above already tells a screen reader what kind
|
|
44
|
+
* of message this is, and reading "warning sign" before the text would be the
|
|
45
|
+
* same information twice.
|
|
46
|
+
*/
|
|
47
|
+
const GLYPH_FOR_STATUS: Record<AlertStatus, string> = {
|
|
48
|
+
info: "i",
|
|
49
|
+
success: "✓",
|
|
50
|
+
warning: "!",
|
|
51
|
+
error: "✕",
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export interface StyledAlertProps
|
|
55
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
|
56
|
+
/** Which kind of message this is. Decides colour, glyph AND announcement. */
|
|
57
|
+
status?: AlertStatus;
|
|
58
|
+
/** Optional heading, shown above the message in bold. */
|
|
59
|
+
title?: React.ReactNode;
|
|
60
|
+
/**
|
|
61
|
+
* Replace the built-in glyph.
|
|
62
|
+
*
|
|
63
|
+
* Pass a node to substitute your own icon, or `null` to render no indicator.
|
|
64
|
+
* `null` is deliberately possible and deliberately awkward to reach for: it
|
|
65
|
+
* puts the component back in breach of WCAG 1.4.1 unless the surrounding UI
|
|
66
|
+
* carries the signal some other way.
|
|
67
|
+
*/
|
|
68
|
+
indicator?: React.ReactNode;
|
|
69
|
+
children?: React.ReactNode;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A status banner.
|
|
74
|
+
*
|
|
75
|
+
* ```tsx
|
|
76
|
+
* <StyledAlert status="error" title="Something went wrong">
|
|
77
|
+
* We could not save your changes.
|
|
78
|
+
* </StyledAlert>
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export const StyledAlert = React.forwardRef<HTMLDivElement, StyledAlertProps>(
|
|
82
|
+
function StyledAlert(
|
|
83
|
+
{ status = "info", title, indicator, children, className, ...rest },
|
|
84
|
+
ref,
|
|
85
|
+
) {
|
|
86
|
+
const classes = alertRecipe({ status });
|
|
87
|
+
const glyph = indicator === undefined ? GLYPH_FOR_STATUS[status] : indicator;
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
ref={ref}
|
|
92
|
+
// `role` and `aria-live` together: the role carries the semantics, and
|
|
93
|
+
// the explicit `aria-live` is what makes an alert rendered into an
|
|
94
|
+
// already-present region announce when its CONTENT changes rather than
|
|
95
|
+
// only when it mounts. A banner that swaps "saving" for "failed" in
|
|
96
|
+
// place is the common case and the one that otherwise goes silent.
|
|
97
|
+
role={ROLE_FOR_STATUS[status]}
|
|
98
|
+
aria-live={ROLE_FOR_STATUS[status] === "alert" ? "assertive" : "polite"}
|
|
99
|
+
className={cx(classes.root, className)}
|
|
100
|
+
{...rest}
|
|
101
|
+
>
|
|
102
|
+
{glyph !== null && (
|
|
103
|
+
<span aria-hidden="true" className={classes.indicator}>
|
|
104
|
+
{glyph}
|
|
105
|
+
</span>
|
|
106
|
+
)}
|
|
107
|
+
<div className={classes.content}>
|
|
108
|
+
{title !== undefined && title !== null && (
|
|
109
|
+
<div className={classes.title}>{title}</div>
|
|
110
|
+
)}
|
|
111
|
+
{children !== undefined && children !== null && (
|
|
112
|
+
<div className={classes.description}>{children}</div>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
export default StyledAlert;
|
|
@@ -6,7 +6,7 @@ import { styled } from "styled-system/jsx";
|
|
|
6
6
|
import { css, cx } from "styled-system/css";
|
|
7
7
|
import StyledTooltip from "./StyledTooltip";
|
|
8
8
|
import { useFontSizeProfile } from "../config/style-config";
|
|
9
|
-
import { fontSizeMap } from "../config/font-size";
|
|
9
|
+
import { fontSizeMap, resolveFontSizeKey } from "../config/font-size";
|
|
10
10
|
import type { AllowedTextVariant } from "../config/types";
|
|
11
11
|
import { textRecipe } from "styled-system/recipes";
|
|
12
12
|
|
|
@@ -114,15 +114,7 @@ const StyledText = React.forwardRef<HTMLSpanElement, StyledTextProps>((props, re
|
|
|
114
114
|
} = props;
|
|
115
115
|
const fontSizeProfile = useFontSizeProfile();
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
if (size) {
|
|
119
|
-
finalSize = size;
|
|
120
|
-
} else if (fixedSize) {
|
|
121
|
-
finalSize = "md";
|
|
122
|
-
} else {
|
|
123
|
-
finalSize = fontSizeProfile;
|
|
124
|
-
}
|
|
125
|
-
|
|
117
|
+
const finalSize = resolveFontSizeKey({ size, fixedSize, profile: fontSizeProfile });
|
|
126
118
|
const fontSize = fontSizeMap[finalSize] || fontSizeMap.md;
|
|
127
119
|
|
|
128
120
|
const extraStyles: React.CSSProperties = {};
|
package/src/config/font-size.ts
CHANGED
|
@@ -62,6 +62,42 @@ export function getFontSizeLabel(size: string): string {
|
|
|
62
62
|
return fontSizeLabelMap[size] ?? size;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Which scale step a piece of text ends up at: caller → `fixedSize` → profile.
|
|
67
|
+
*
|
|
68
|
+
* The same precedence shape as `useResolvedVariant`, and here for the same
|
|
69
|
+
* reason — but it is a *pure function* rather than a branch inside `StyledText`
|
|
70
|
+
* specifically so the unit tier can assert it (NEH-406).
|
|
71
|
+
*
|
|
72
|
+
* That mattered more than it looks. The rule was only ever checked through a
|
|
73
|
+
* rendered `font-size`, and **jsdom cannot see one of these values at all**:
|
|
74
|
+
* every `fontSizeMap` entry is a `var(--font-sizes-*, …)` reference, jsdom's
|
|
75
|
+
* CSS parser rejects it against the `font-size` grammar, and the declaration is
|
|
76
|
+
* dropped — the element ends up with no `style` attribute whatsoever. So
|
|
77
|
+
* `toHaveStyle({ fontSize: <anything> })` compared "" with "" and passed for
|
|
78
|
+
* every possible expectation, including one asserting a size that had not been
|
|
79
|
+
* true since the scale moved.
|
|
80
|
+
*
|
|
81
|
+
* Splitting the rule out gives each tier a question it can actually answer:
|
|
82
|
+
* *which step wins* here, and *what does it measure* in the browser tier.
|
|
83
|
+
*
|
|
84
|
+
* `fixedSize` pins to `md` — used where a label must not grow with the profile,
|
|
85
|
+
* e.g. inside a fixed-height control it would otherwise clip.
|
|
86
|
+
*/
|
|
87
|
+
export function resolveFontSizeKey({
|
|
88
|
+
size,
|
|
89
|
+
fixedSize,
|
|
90
|
+
profile,
|
|
91
|
+
}: {
|
|
92
|
+
size?: string | undefined;
|
|
93
|
+
fixedSize?: boolean | undefined;
|
|
94
|
+
profile?: string | undefined;
|
|
95
|
+
}): string {
|
|
96
|
+
if (size) return size;
|
|
97
|
+
if (fixedSize) return "md";
|
|
98
|
+
return profile ?? "md";
|
|
99
|
+
}
|
|
100
|
+
|
|
65
101
|
/**
|
|
66
102
|
* The literal fallback inside a `fontSizeMap` entry, e.g. `"1rem"`.
|
|
67
103
|
*
|
package/src/index.ts
CHANGED
|
@@ -229,6 +229,9 @@ export type { StyledSearchProps } from "./components/StyledSearch";
|
|
|
229
229
|
export { default as StyledInputToggle } from "./components/StyledInputToggle";
|
|
230
230
|
export type { StyledInputToggleProps } from "./components/StyledInputToggle";
|
|
231
231
|
|
|
232
|
+
export { default as StyledAlert } from "./components/StyledAlert";
|
|
233
|
+
export type { StyledAlertProps, AlertStatus } from "./components/StyledAlert";
|
|
234
|
+
|
|
232
235
|
export { default as StyledInputRadio } from "./components/StyledInputRadio";
|
|
233
236
|
export type { StyledInputRadioProps, RadioItem, RadioVariant } from "./components/StyledInputRadio";
|
|
234
237
|
export { RADIO_VARIANTS } from "./components/StyledInputRadio";
|
package/src/preset/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
inputDropdownItemRecipe,
|
|
15
15
|
inputDropdownRecipe,
|
|
16
16
|
} from "./recipes/input-dropdown";
|
|
17
|
+
import { alertRecipe } from "./recipes/alert";
|
|
17
18
|
import { inputRadioRootRecipe } from "./recipes/input-radio";
|
|
18
19
|
import { inputTextRecipe } from "./recipes/input-text";
|
|
19
20
|
import { listRecipe } from "./recipes/list";
|
|
@@ -53,7 +54,7 @@ export interface StonedogStylePresetOptions {
|
|
|
53
54
|
/**
|
|
54
55
|
* Every recipe, keyed by the name it is exported under in `styled-system/recipes`.
|
|
55
56
|
*
|
|
56
|
-
*
|
|
57
|
+
* Five of these (`alertRecipe`, `listRecipe`, `menuRecipe`, `inputBoolRecipe`,
|
|
57
58
|
* `inputRadioRootRecipe`) are slot recipes declared with `defineSlotRecipe`.
|
|
58
59
|
* Panda accepts them here rather than under `slotRecipes` and generates them
|
|
59
60
|
* correctly — verified against HopperGuard's own generated output. Moving them
|
|
@@ -61,6 +62,7 @@ export interface StonedogStylePresetOptions {
|
|
|
61
62
|
* generated surface, so it is deliberately not done during extraction.
|
|
62
63
|
*/
|
|
63
64
|
const recipes = {
|
|
65
|
+
alertRecipe,
|
|
64
66
|
arrowRecipe,
|
|
65
67
|
boxRecipe,
|
|
66
68
|
buttonRecipe,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { defineSlotRecipe } from "@pandacss/dev";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A status banner: an icon, a title and a message, on a tinted chip.
|
|
5
|
+
*
|
|
6
|
+
* Extracted from HopperGuard, where all four statuses painted from the raw
|
|
7
|
+
* Panda palette (`red.50` / `red.200` / `red.700` and friends). Those ignore the
|
|
8
|
+
* theme and dark mode entirely and sit outside contrast validation, so on a dark
|
|
9
|
+
* theme the component rendered dark text on a light chip regardless of its
|
|
10
|
+
* surroundings — the NEH-278 family (NEH-421).
|
|
11
|
+
*
|
|
12
|
+
* Every colour here is now a token. Three of the four statuses needed tokens
|
|
13
|
+
* that did not exist; see `STATUS_SURFACE_TOKENS` for why those carry a default
|
|
14
|
+
* where the rest of the contract does not.
|
|
15
|
+
*/
|
|
16
|
+
export const alertRecipe = defineSlotRecipe({
|
|
17
|
+
className: "alert",
|
|
18
|
+
description: "A status banner — info, success, warning or error",
|
|
19
|
+
slots: ["root", "indicator", "content", "title", "description"],
|
|
20
|
+
base: {
|
|
21
|
+
root: {
|
|
22
|
+
position: "relative",
|
|
23
|
+
display: "flex",
|
|
24
|
+
alignItems: "flex-start",
|
|
25
|
+
gap: "3",
|
|
26
|
+
padding: "4",
|
|
27
|
+
borderRadius: "md",
|
|
28
|
+
borderWidth: "1px",
|
|
29
|
+
borderStyle: "solid",
|
|
30
|
+
// Stated, not inherited: a themed typeface otherwise reaches the page and
|
|
31
|
+
// stops at the edge of the component (NEH-289).
|
|
32
|
+
fontFamily: "body",
|
|
33
|
+
},
|
|
34
|
+
indicator: {
|
|
35
|
+
flexShrink: 0,
|
|
36
|
+
display: "flex",
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
justifyContent: "center",
|
|
39
|
+
// Sized to sit on the first line of the title rather than centred against
|
|
40
|
+
// the whole block, which drifts as the message grows.
|
|
41
|
+
width: "1.25em",
|
|
42
|
+
height: "1.5em",
|
|
43
|
+
lineHeight: "1",
|
|
44
|
+
fontSize: "lg",
|
|
45
|
+
// The glyph inherits the chip's text colour, so it never needs a colour of
|
|
46
|
+
// its own and can never disagree with the message beside it.
|
|
47
|
+
color: "inherit",
|
|
48
|
+
},
|
|
49
|
+
content: {
|
|
50
|
+
flex: "1",
|
|
51
|
+
minWidth: "0",
|
|
52
|
+
},
|
|
53
|
+
title: {
|
|
54
|
+
fontWeight: "bold",
|
|
55
|
+
},
|
|
56
|
+
description: {
|
|
57
|
+
display: "block",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
variants: {
|
|
61
|
+
status: {
|
|
62
|
+
info: {
|
|
63
|
+
root: {
|
|
64
|
+
backgroundColor: "boxInfo",
|
|
65
|
+
borderColor: "borderBgAccent",
|
|
66
|
+
color: "textMain",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
success: {
|
|
70
|
+
root: {
|
|
71
|
+
backgroundColor: "boxSuccess",
|
|
72
|
+
borderColor: "borderSuccess",
|
|
73
|
+
color: "textSuccess",
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
warning: {
|
|
77
|
+
root: {
|
|
78
|
+
backgroundColor: "boxWarning",
|
|
79
|
+
borderColor: "borderWarning",
|
|
80
|
+
color: "textWarning",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
error: {
|
|
84
|
+
root: {
|
|
85
|
+
backgroundColor: "boxError",
|
|
86
|
+
borderColor: "borderError",
|
|
87
|
+
color: "textError",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
defaultVariants: {
|
|
93
|
+
status: "info",
|
|
94
|
+
},
|
|
95
|
+
});
|
|
@@ -34,6 +34,12 @@ export const boxRecipe = defineRecipe({
|
|
|
34
34
|
},
|
|
35
35
|
link: {
|
|
36
36
|
bg: "boxBgPrimary",
|
|
37
|
+
// Its two siblings above, `solid` and `outline`, paint the same
|
|
38
|
+
// background and both state `textPrimary`; this one did not, so its
|
|
39
|
+
// text inherited from the page and could land unreadable on the same
|
|
40
|
+
// surface they render correctly on (NEH-441). Character for character
|
|
41
|
+
// the defect already fixed in `listRecipe` under NEH-167 cycle 9.
|
|
42
|
+
color: "textPrimary",
|
|
37
43
|
_hover: {
|
|
38
44
|
textDecoration: "underline",
|
|
39
45
|
},
|
|
@@ -45,11 +45,23 @@ export const buttonRecipe = defineRecipe({
|
|
|
45
45
|
},
|
|
46
46
|
outline: {
|
|
47
47
|
bg: "buttonBgAccent",
|
|
48
|
+
// Paints an accent background, so it must state the text colour that
|
|
49
|
+
// goes with it. Without this the label inherits whatever the page has,
|
|
50
|
+
// and under a theme whose surface sits at the same end of the scale as
|
|
51
|
+
// the inherited text it is unreadable — the NEH-278 family (NEH-441).
|
|
52
|
+
//
|
|
53
|
+
// `buttonTextAccent` exists in the token contract specifically to pair
|
|
54
|
+
// with `buttonBgAccent`, and every host already defines it, so this
|
|
55
|
+
// costs no host action. The hover state repaints the background, so it
|
|
56
|
+
// takes the matching hover pairing rather than letting the base colour
|
|
57
|
+
// ride along against a different surface.
|
|
58
|
+
color: "buttonTextAccent",
|
|
48
59
|
border: "2px solid",
|
|
49
60
|
borderRadius: 0,
|
|
50
61
|
_hover: {
|
|
51
62
|
border: "2px solid",
|
|
52
63
|
bg: "buttonBgAccentHover",
|
|
64
|
+
color: "buttonTextAccentHover",
|
|
53
65
|
},
|
|
54
66
|
},
|
|
55
67
|
aurora: {
|
|
@@ -3,7 +3,19 @@ import { defineRecipe } from "@pandacss/dev";
|
|
|
3
3
|
export const iconRecipe = defineRecipe({
|
|
4
4
|
className: "icon",
|
|
5
5
|
base: {
|
|
6
|
-
|
|
6
|
+
// A centring flex box, not `inline-block` (NEH-562). `StyledIcon` sizes
|
|
7
|
+
// this wrapper in pixels while the icon set sizes the glyph from
|
|
8
|
+
// `font-size` — two independent numbers — so on an inline-block wrapper the
|
|
9
|
+
// glyph sat on a text baseline rather than in the middle of the box, and
|
|
10
|
+
// Font Awesome's `vertical-align: -0.125em` pushed it a further ~2px down.
|
|
11
|
+
// That was a visible sag under the label on every icon-bearing button in
|
|
12
|
+
// the product. A flex item ignores `vertical-align`, so centring here fixes
|
|
13
|
+
// the whole class rather than one call site.
|
|
14
|
+
display: "inline-flex",
|
|
15
|
+
alignItems: "center",
|
|
16
|
+
justifyContent: "center",
|
|
17
|
+
// Still meaningful: it aligns this wrapper within a parent's line box, for
|
|
18
|
+
// the call sites that drop an icon into running text.
|
|
7
19
|
verticalAlign: "middle",
|
|
8
20
|
lineHeight: 1,
|
|
9
21
|
fontSize: "var(--font-sizes-2xl, 1.5rem)",
|
|
@@ -126,10 +126,20 @@ export const inputRadioRootRecipe = defineSlotRecipe({
|
|
|
126
126
|
border: "none",
|
|
127
127
|
},
|
|
128
128
|
},
|
|
129
|
+
// "none" means no chrome, not no surface: it drops the border and sits
|
|
130
|
+
// the item on the page's own background. That was written as a literal
|
|
131
|
+
// `white`, which is the page background of exactly one theme — under a
|
|
132
|
+
// dark one it painted a white slab, and no theme-aware text colour could
|
|
133
|
+
// be paired with it (a light `textPrimary` on it is white-on-white, the
|
|
134
|
+
// same NEH-278 illegibility in the other direction). `boxBgMain` is the
|
|
135
|
+
// token that means "the page surface", so the variant now follows the
|
|
136
|
+
// theme instead of contradicting it, and `textMain` is its documented
|
|
137
|
+
// partner in TEXT_BACKGROUND_PAIRS.
|
|
129
138
|
none: {
|
|
130
139
|
item: {
|
|
131
140
|
border: "none",
|
|
132
|
-
backgroundColor: "
|
|
141
|
+
backgroundColor: "boxBgMain",
|
|
142
|
+
color: "textMain",
|
|
133
143
|
},
|
|
134
144
|
},
|
|
135
145
|
},
|
|
@@ -13,6 +13,15 @@ export const menuRecipe = defineSlotRecipe({
|
|
|
13
13
|
gap: 3,
|
|
14
14
|
px: 4,
|
|
15
15
|
py: 2,
|
|
16
|
+
// Stated, not derived — the same reason `button`, `icon-button`,
|
|
17
|
+
// `input-bool`, `input-radio` and `input-surface` all state it. Height
|
|
18
|
+
// that emerges from padding plus the current font size moves whenever
|
|
19
|
+
// either does, and this recipe measured 34px at the default profile.
|
|
20
|
+
//
|
|
21
|
+
// A menu is a column of adjacent targets, so missing one does not fail:
|
|
22
|
+
// it performs the neighbour's action instead. An unexpected navigation
|
|
23
|
+
// is worse than a dead tap for the audience this system is built for.
|
|
24
|
+
minHeight: "48px",
|
|
16
25
|
borderRadius: "md",
|
|
17
26
|
cursor: "pointer",
|
|
18
27
|
_hover: {
|
|
@@ -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
|
}
|