@stonedogcode/style 0.13.0 → 0.16.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 +1 -1
- package/package.json +5 -5
- package/src/components/StyledLink.tsx +99 -2
- package/src/components/StyledPage.tsx +22 -0
- package/src/components/StyledTag.tsx +57 -20
- package/src/components/StyledTooltip.tsx +192 -17
- package/src/index.ts +2 -2
- package/src/preset/index.ts +24 -1
- package/src/preset/recipes/button.ts +34 -4
- package/src/preset/recipes/form.ts +6 -1
- package/src/preset/recipes/icon-button.ts +36 -5
- package/src/preset/recipes/input-bool.ts +19 -3
- package/src/preset/recipes/input-radio.ts +11 -3
- package/src/preset/recipes/input-surface.ts +33 -6
- package/src/preset/recipes/list.ts +7 -1
- package/src/preset/recipes/tag.ts +96 -0
- package/src/preset/recipes/tooltip.ts +30 -8
- package/src/preset/z-layers.ts +101 -0
package/src/preset/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "./recipes/separator";
|
|
26
26
|
import { stackRecipe } from "./recipes/stack";
|
|
27
27
|
import { stripedRecipe } from "./recipes/striped";
|
|
28
|
+
import { tagRecipe } from "./recipes/tag";
|
|
28
29
|
import { textRecipe } from "./recipes/text";
|
|
29
30
|
import { tooltipRecipe } from "./recipes/tooltip";
|
|
30
31
|
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
createSemanticFonts,
|
|
36
37
|
createSemanticSizes,
|
|
37
38
|
} from "./semantic-variables";
|
|
39
|
+
import { createZIndexTokens } from "./z-layers";
|
|
38
40
|
|
|
39
41
|
export interface StonedogStylePresetOptions {
|
|
40
42
|
/**
|
|
@@ -83,6 +85,7 @@ const recipes = {
|
|
|
83
85
|
separatorVerticalRecipe,
|
|
84
86
|
stackRecipe,
|
|
85
87
|
stripedRecipe,
|
|
88
|
+
tagRecipe,
|
|
86
89
|
textRecipe,
|
|
87
90
|
tooltipRecipe,
|
|
88
91
|
};
|
|
@@ -135,7 +138,7 @@ const staticCssAlignment = [
|
|
|
135
138
|
|
|
136
139
|
/**
|
|
137
140
|
* The @stonedogcode/style Panda preset: colour tokens, breakpoints, keyframes, and
|
|
138
|
-
* the
|
|
141
|
+
* the 23 recipes the component library is built on.
|
|
139
142
|
*
|
|
140
143
|
* Deliberately does NOT set `globalCss`, `preflight`, `include`, or `outdir` —
|
|
141
144
|
* those are application decisions, and a preset that quietly restyles `body` is
|
|
@@ -182,6 +185,19 @@ export function stonedogStylePreset(options: StonedogStylePresetOptions = {}) {
|
|
|
182
185
|
*/
|
|
183
186
|
fonts: createSemanticFonts(cssVarPrefix),
|
|
184
187
|
fontWeights: createSemanticFontWeights(cssVarPrefix),
|
|
188
|
+
/**
|
|
189
|
+
* Named stacking layers (NEH-830). Neither this preset nor either
|
|
190
|
+
* base Panda preset defined any, so `drawerRecipe`'s `zIndex:
|
|
191
|
+
* "modal"` resolved to nothing and was emitted as the literal
|
|
192
|
+
* `z-index: modal` — invalid CSS the browser discards, leaving that
|
|
193
|
+
* panel with no z-index at all.
|
|
194
|
+
*
|
|
195
|
+
* Unlike the colour tokens these carry no custom property: a layer
|
|
196
|
+
* is not a brand decision and there is nothing for a theme to
|
|
197
|
+
* restyle. A HOST overrides the numbers in its own config; see
|
|
198
|
+
* z-layers.ts for why the names live here and the values do not.
|
|
199
|
+
*/
|
|
200
|
+
zIndex: createZIndexTokens(),
|
|
185
201
|
},
|
|
186
202
|
keyframes: {
|
|
187
203
|
spin: {
|
|
@@ -248,6 +264,13 @@ export {
|
|
|
248
264
|
requiredCssCustomProperties,
|
|
249
265
|
} from "./semantic-variables";
|
|
250
266
|
|
|
267
|
+
export {
|
|
268
|
+
Z_LAYERS,
|
|
269
|
+
createZIndexTokens,
|
|
270
|
+
zIndexTokenNames,
|
|
271
|
+
type ZLayerName,
|
|
272
|
+
} from "./z-layers";
|
|
273
|
+
|
|
251
274
|
export { recipes as stonedogStyleRecipes };
|
|
252
275
|
|
|
253
276
|
/* ------------------------------------------------------------------------- *
|
|
@@ -37,10 +37,20 @@ export const buttonRecipe = defineRecipe({
|
|
|
37
37
|
variant: {
|
|
38
38
|
solid: {
|
|
39
39
|
bg: "buttonBgAccent",
|
|
40
|
-
|
|
40
|
+
// The same pairing `outline` states below, and for the same reason
|
|
41
|
+
// (NEH-441, NEH-796). `textPrimary` is the colour that goes on
|
|
42
|
+
// `boxBgPrimary`, not on an accent surface: against optima's light
|
|
43
|
+
// theme that pairing measures 2.43:1, below WCAG AA, while
|
|
44
|
+
// `buttonTextAccent` measures 7.34:1. The dark theme happens to
|
|
45
|
+
// survive it, which is what let a default variant ship unreadable.
|
|
46
|
+
//
|
|
47
|
+
// `TEXT_BACKGROUND_PAIRS` in `semantic-variables.ts` is the contract
|
|
48
|
+
// being obeyed: `buttonTextAccent` sits on `buttonBgAccent`, and
|
|
49
|
+
// `buttonTextSecondary` on the `buttonBgSecondary` the hover repaints.
|
|
50
|
+
color: "buttonTextAccent",
|
|
41
51
|
_hover: {
|
|
42
52
|
bg: "buttonBgSecondary",
|
|
43
|
-
color: "
|
|
53
|
+
color: "buttonTextSecondary",
|
|
44
54
|
},
|
|
45
55
|
},
|
|
46
56
|
outline: {
|
|
@@ -77,7 +87,15 @@ export const buttonRecipe = defineRecipe({
|
|
|
77
87
|
border: "2px solid",
|
|
78
88
|
borderRadius: "xl",
|
|
79
89
|
bg: "buttonBgAccent",
|
|
80
|
-
|
|
90
|
+
// The same correction `solid` took in NEH-796, which this variant was
|
|
91
|
+
// missed by twice over (NEH-877): the pairing sweep that landed with it
|
|
92
|
+
// was scoped to this recipe, but it also skipped every variant named
|
|
93
|
+
// `glass` — on the reasoning that a translucent surface should inherit.
|
|
94
|
+
// This one is not translucent. It paints an opaque `buttonBgAccent` and
|
|
95
|
+
// blurs what is BEHIND it, so it owes a paired foreground like any
|
|
96
|
+
// other accent surface: 2.43:1 with `textPrimary` against optima's
|
|
97
|
+
// light theme, 7.34:1 with this.
|
|
98
|
+
color: "buttonTextAccent",
|
|
81
99
|
boxShadow: "0 8px 32px rgba(0,0,0,0.2)",
|
|
82
100
|
backdropFilter: "blur(12px)",
|
|
83
101
|
WebkitBackdropFilter: "blur(12px)",
|
|
@@ -86,6 +104,13 @@ export const buttonRecipe = defineRecipe({
|
|
|
86
104
|
transition: "all 0.3s ease",
|
|
87
105
|
_hover: {
|
|
88
106
|
bg: "buttonBgSecondary",
|
|
107
|
+
// Stated, because the hover repaints a DIFFERENT surface (NEH-877).
|
|
108
|
+
// Without it the label rides its base `buttonTextAccent` onto a
|
|
109
|
+
// secondary background — 1.06:1 in optima's light theme, white on
|
|
110
|
+
// near-white, i.e. the label vanishing on hover. This is the same
|
|
111
|
+
// move `solid` made in NEH-796 and the reason a base-colour fix has
|
|
112
|
+
// to look at every pseudo-state that repaints beneath it.
|
|
113
|
+
color: "buttonTextSecondary",
|
|
89
114
|
borderColor: "rgba(255,255,255,0.4)",
|
|
90
115
|
boxShadow: "0 8px 32px rgba(0,0,0,0.3), inset 0 0 20px rgba(255,255,255,0.1)",
|
|
91
116
|
transform: "translateY(-1px)",
|
|
@@ -126,7 +151,12 @@ export const buttonRecipe = defineRecipe({
|
|
|
126
151
|
},
|
|
127
152
|
},
|
|
128
153
|
selected: {
|
|
129
|
-
|
|
154
|
+
// `textAccent`, not `textPrimary` — the same mispairing `solid` had
|
|
155
|
+
// (NEH-796). `boxBgAccent` is an accent surface and the contract's
|
|
156
|
+
// partner for it is `textAccent`; `textPrimary` is the colour for
|
|
157
|
+
// `boxBgPrimary`, a different surface. Adjacent instance of the defect
|
|
158
|
+
// the issue names, in the same recipe, so it is fixed here.
|
|
159
|
+
color: "textAccent",
|
|
130
160
|
border: "3px dashed black",
|
|
131
161
|
borderRadius: "xl",
|
|
132
162
|
bg: "boxBgAccent",
|
|
@@ -15,7 +15,12 @@ export const formRecipe = defineRecipe({
|
|
|
15
15
|
variant: {
|
|
16
16
|
solid: {
|
|
17
17
|
bg: "boxBgAccent",
|
|
18
|
-
|
|
18
|
+
// `textAccent`, not `textPrimary` (NEH-877). `textPrimary` is the
|
|
19
|
+
// contract's partner for `boxBgPrimary`, a different surface: against
|
|
20
|
+
// optima's light theme, whose accent surface is a near-black graphite,
|
|
21
|
+
// that pairing measures 1.17:1 — a form whose every label is the exact
|
|
22
|
+
// colour of the panel behind it. `textAccent` measures 15.27:1.
|
|
23
|
+
color: "textAccent",
|
|
19
24
|
borderColor: "borderBgPrimary",
|
|
20
25
|
"& > li:not(:last-child)": {
|
|
21
26
|
borderBottom: "1px solid",
|
|
@@ -64,23 +64,42 @@ export const buttonIconRecipe = defineRecipe({
|
|
|
64
64
|
},
|
|
65
65
|
},
|
|
66
66
|
variant: {
|
|
67
|
+
/**
|
|
68
|
+
* The NEH-796 pairing, applied to the recipe it was never applied to
|
|
69
|
+
* (NEH-877).
|
|
70
|
+
*
|
|
71
|
+
* `buttonRecipe` and this one are the same control in two shapes, and
|
|
72
|
+
* `iconButton` carried the identical defect in three variants: an accent
|
|
73
|
+
* fill labelled with `textPrimary`, the contract's partner for
|
|
74
|
+
* `boxBgPrimary`. Against optima's light theme that measures 2.43:1,
|
|
75
|
+
* below WCAG AA; `buttonTextAccent` measures 7.34:1. The dark theme
|
|
76
|
+
* clears AA either way, which is what let it ship.
|
|
77
|
+
*
|
|
78
|
+
* Each `_hover` that repaints a different surface states its own colour
|
|
79
|
+
* for the same reason — the base colour riding onto a secondary fill is
|
|
80
|
+
* the failure one state along.
|
|
81
|
+
*/
|
|
67
82
|
solid: {
|
|
68
83
|
bg: "buttonBgAccent",
|
|
69
|
-
color: "
|
|
84
|
+
color: "buttonTextAccent",
|
|
70
85
|
_hover: {
|
|
71
86
|
bg: "buttonBgSecondary",
|
|
72
|
-
color: "
|
|
87
|
+
color: "buttonTextSecondary",
|
|
73
88
|
},
|
|
74
89
|
},
|
|
75
90
|
outline: {
|
|
76
91
|
bg: "buttonBgAccent",
|
|
77
|
-
color: "
|
|
92
|
+
color: "buttonTextAccent",
|
|
78
93
|
border: "1px solid",
|
|
79
94
|
borderColor: "borderBgSecondary",
|
|
80
95
|
borderRadius: 0,
|
|
81
96
|
_hover: {
|
|
82
97
|
border: "2px solid",
|
|
83
98
|
bg: "buttonBgAccentHover",
|
|
99
|
+
// The hover surface has its own partner in the contract, and it is
|
|
100
|
+
// not the base one. Both are white in optima's palette today; the
|
|
101
|
+
// point is that a host is free to make them differ.
|
|
102
|
+
color: "buttonTextAccentHover",
|
|
84
103
|
},
|
|
85
104
|
},
|
|
86
105
|
aurora: {
|
|
@@ -98,7 +117,10 @@ export const buttonIconRecipe = defineRecipe({
|
|
|
98
117
|
borderColor: "black",
|
|
99
118
|
borderRadius: "xl",
|
|
100
119
|
bg: "buttonBgAccent",
|
|
101
|
-
|
|
120
|
+
// Not translucent, whatever the name suggests: the fill is an opaque
|
|
121
|
+
// `buttonBgAccent` and the blur applies to what is behind it. So it
|
|
122
|
+
// takes the accent partner like its neighbours above (NEH-877).
|
|
123
|
+
color: "buttonTextAccent",
|
|
102
124
|
boxShadow: "0 8px 32px rgba(0,0,0,0.2)",
|
|
103
125
|
backdropFilter: "blur(12px)",
|
|
104
126
|
WebkitBackdropFilter: "blur(12px)",
|
|
@@ -107,6 +129,10 @@ export const buttonIconRecipe = defineRecipe({
|
|
|
107
129
|
transition: "all 0.3s ease",
|
|
108
130
|
_hover: {
|
|
109
131
|
bg: "buttonBgSecondary",
|
|
132
|
+
// Stated because the hover repaints a different surface — otherwise
|
|
133
|
+
// the glyph keeps `buttonTextAccent` over a secondary fill, 1.06:1 in
|
|
134
|
+
// optima's light theme (NEH-877).
|
|
135
|
+
color: "buttonTextSecondary",
|
|
110
136
|
borderColor: "rgba(255,255,255,0.4)",
|
|
111
137
|
boxShadow: "0 8px 32px rgba(0,0,0,0.3), inset 0 0 20px rgba(255,255,255,0.1)",
|
|
112
138
|
transform: "translateY(-1px)",
|
|
@@ -140,11 +166,16 @@ export const buttonIconRecipe = defineRecipe({
|
|
|
140
166
|
fontWeight: "bold",
|
|
141
167
|
},
|
|
142
168
|
ghost: {
|
|
169
|
+
// The base stays `textPrimary`: a 50% accent fill is a BLEND with
|
|
170
|
+
// whatever is behind it, so the contract has no partner for it and
|
|
171
|
+
// `textPrimary` measures 5.9:1 over the light-theme blend. The hover
|
|
172
|
+
// takes the fill to full opacity, which IS a contract surface — and
|
|
173
|
+
// there `textPrimary` is the 2.43:1 pairing again (NEH-877).
|
|
143
174
|
bg: "buttonBgAccent/50",
|
|
144
175
|
color: "textPrimary",
|
|
145
176
|
_hover: {
|
|
146
177
|
bg: "buttonBgAccent",
|
|
147
|
-
color: "
|
|
178
|
+
color: "buttonTextAccent",
|
|
148
179
|
border: "1px solid",
|
|
149
180
|
borderColor: "gray.700",
|
|
150
181
|
},
|
|
@@ -103,7 +103,19 @@ export const inputBoolRecipe = defineSlotRecipe({
|
|
|
103
103
|
solid: {
|
|
104
104
|
control: {
|
|
105
105
|
bg: "buttonBgAccent",
|
|
106
|
-
|
|
106
|
+
/**
|
|
107
|
+
* The contract's partner for this fill (NEH-877), and INERT today.
|
|
108
|
+
*
|
|
109
|
+
* Neither `bg` nor `color` paints on a checkbox at `appearance:
|
|
110
|
+
* auto` — the note in `base` and NEH-234 are about exactly that — so
|
|
111
|
+
* this changes nothing anyone can see, and that is the honest reason
|
|
112
|
+
* to make it: the declarations are left in place per that note, and
|
|
113
|
+
* a declaration left in place should not be left WRONG. `textPrimary`
|
|
114
|
+
* is the partner of `boxBgPrimary`; the day option 2 on NEH-310
|
|
115
|
+
* (`appearance: none` plus a hand-drawn tick) makes these paint, it
|
|
116
|
+
* would paint at 2.43:1 against optima's light theme.
|
|
117
|
+
*/
|
|
118
|
+
color: "buttonTextAccent",
|
|
107
119
|
accentColor: "buttonBgPrimary",
|
|
108
120
|
boxShadow: "none",
|
|
109
121
|
},
|
|
@@ -111,7 +123,8 @@ export const inputBoolRecipe = defineSlotRecipe({
|
|
|
111
123
|
outline: {
|
|
112
124
|
control: {
|
|
113
125
|
bg: "buttonBgAccent",
|
|
114
|
-
|
|
126
|
+
// Same fill, same partner, same inertness as `solid` above.
|
|
127
|
+
color: "buttonTextAccent",
|
|
115
128
|
/**
|
|
116
129
|
* The SAME checked fill as `solid`, deliberately.
|
|
117
130
|
*
|
|
@@ -227,7 +240,10 @@ export const inputBoolRecipe = defineSlotRecipe({
|
|
|
227
240
|
},
|
|
228
241
|
ghost: {
|
|
229
242
|
control: {
|
|
230
|
-
|
|
243
|
+
// `buttonTextSecondary` is the partner of the fill below;
|
|
244
|
+
// `textSecondary` belongs to `boxBgSecondary` (NEH-877). Inert for
|
|
245
|
+
// the same reason as `solid`.
|
|
246
|
+
color: "buttonTextSecondary",
|
|
231
247
|
bg: "buttonBgSecondary",
|
|
232
248
|
accentColor: "buttonBgPrimary",
|
|
233
249
|
// The thinnest ring in the set, in the secondary border colour.
|
|
@@ -82,7 +82,11 @@ export const inputRadioRootRecipe = defineSlotRecipe({
|
|
|
82
82
|
solid: {
|
|
83
83
|
item: {
|
|
84
84
|
bg: "boxBgAccent",
|
|
85
|
-
|
|
85
|
+
// `textAccent` is the contract's partner for this surface;
|
|
86
|
+
// `textPrimary` belongs to `boxBgPrimary` and measures 1.17:1 on it
|
|
87
|
+
// against optima's light theme, whose accent surface is a near-black
|
|
88
|
+
// graphite (NEH-877). This is the DEFAULT variant of this recipe.
|
|
89
|
+
color: "textAccent",
|
|
86
90
|
borderColor: "borderBgPrimary",
|
|
87
91
|
},
|
|
88
92
|
},
|
|
@@ -116,12 +120,16 @@ export const inputRadioRootRecipe = defineSlotRecipe({
|
|
|
116
120
|
matte: {
|
|
117
121
|
item: {
|
|
118
122
|
bg: "buttonBgSecondary",
|
|
119
|
-
|
|
123
|
+
// Was `textAccent` — the partner of `boxBgAccent`, a surface this
|
|
124
|
+
// variant does not paint. 1.14:1 in optima's light theme, where the
|
|
125
|
+
// accent text is white and this fill is near-white (NEH-877).
|
|
126
|
+
color: "buttonTextSecondary",
|
|
120
127
|
},
|
|
121
128
|
},
|
|
122
129
|
ghost: {
|
|
123
130
|
item: {
|
|
124
|
-
|
|
131
|
+
// `textSecondary` is `boxBgSecondary`'s partner, not this fill's.
|
|
132
|
+
color: "buttonTextSecondary",
|
|
125
133
|
bg: "buttonBgSecondary",
|
|
126
134
|
border: "none",
|
|
127
135
|
},
|
|
@@ -66,12 +66,31 @@ export const inputSurfaceBase = {
|
|
|
66
66
|
* The variant map. Every variant states its own `bg`, `color`, and
|
|
67
67
|
* `borderColor` rather than inheriting some and not others — that asymmetry is
|
|
68
68
|
* what let the two recipes disagree without either looking wrong on its own.
|
|
69
|
+
*
|
|
70
|
+
* ## The colour has to be the surface's PARTNER, not just a colour (NEH-877)
|
|
71
|
+
*
|
|
72
|
+
* Four of these paired a surface with the wrong contract token, and because
|
|
73
|
+
* `inputTextRecipe` and `inputDropdownRecipe` both spread this map, each one was
|
|
74
|
+
* eight offending selectors rather than four. `solid` and `glass` paint
|
|
75
|
+
* `boxBgAccent` and said `textPrimary` — the partner of `boxBgPrimary` — which
|
|
76
|
+
* measures **1.17:1** against optima's light theme, a control whose typed value
|
|
77
|
+
* is the exact colour of the field. `matte` and `ghost` paint
|
|
78
|
+
* `buttonBgSecondary` and said `textSecondary`, the partner of `boxBgSecondary`.
|
|
79
|
+
*
|
|
80
|
+
* **The placeholder moves with the text**, which is the part easy to miss here:
|
|
81
|
+
* `inputSurfaceBase` sets `&::placeholder { color: textPrimary }` for the
|
|
82
|
+
* base's own `textPrimary`, so a variant that repaints the surface and only
|
|
83
|
+
* fixes `color` leaves the placeholder on the old pairing — the same defect,
|
|
84
|
+
* surviving in the one piece of text an empty field actually shows.
|
|
69
85
|
*/
|
|
70
86
|
export const inputSurfaceVariants = {
|
|
71
87
|
solid: {
|
|
72
88
|
bg: "boxBgAccent",
|
|
73
|
-
color: "
|
|
89
|
+
color: "textAccent",
|
|
74
90
|
borderColor: "borderBgPrimary",
|
|
91
|
+
"&::placeholder": {
|
|
92
|
+
color: "textAccent",
|
|
93
|
+
},
|
|
75
94
|
},
|
|
76
95
|
outline: {
|
|
77
96
|
// Stated, not omitted. An outline control is meant to show whatever is
|
|
@@ -97,12 +116,15 @@ export const inputSurfaceVariants = {
|
|
|
97
116
|
borderStyle: "solid",
|
|
98
117
|
borderRadius: "xl",
|
|
99
118
|
bg: "boxBgAccent",
|
|
100
|
-
|
|
119
|
+
// An opaque accent fill with a blur applied to what is behind it — not a
|
|
120
|
+
// translucent surface, whatever the name says, so it takes the accent
|
|
121
|
+
// partner like `solid` (NEH-877).
|
|
122
|
+
color: "textAccent",
|
|
101
123
|
borderColor: "borderBgSecondary",
|
|
102
124
|
backdropFilter: "blur(12px)",
|
|
103
125
|
fontWeight: "normal",
|
|
104
126
|
_placeholder: {
|
|
105
|
-
color: "
|
|
127
|
+
color: "textAccent",
|
|
106
128
|
opacity: 0.8,
|
|
107
129
|
},
|
|
108
130
|
_focusVisible: {
|
|
@@ -130,15 +152,20 @@ export const inputSurfaceVariants = {
|
|
|
130
152
|
},
|
|
131
153
|
matte: {
|
|
132
154
|
bg: "buttonBgSecondary",
|
|
133
|
-
color: "
|
|
155
|
+
color: "buttonTextSecondary",
|
|
134
156
|
borderColor: "borderBgPrimary",
|
|
157
|
+
// Otherwise the placeholder keeps the base's `textPrimary` while the value
|
|
158
|
+
// above it moves — see the note on this map (NEH-877).
|
|
159
|
+
"&::placeholder": {
|
|
160
|
+
color: "buttonTextSecondary",
|
|
161
|
+
},
|
|
135
162
|
},
|
|
136
163
|
ghost: {
|
|
137
|
-
color: "
|
|
164
|
+
color: "buttonTextSecondary",
|
|
138
165
|
bg: "buttonBgSecondary",
|
|
139
166
|
borderColor: "borderBgPrimary",
|
|
140
167
|
"&::placeholder": {
|
|
141
|
-
color: "
|
|
168
|
+
color: "buttonTextSecondary/60",
|
|
142
169
|
},
|
|
143
170
|
},
|
|
144
171
|
none: {
|
|
@@ -164,12 +164,18 @@ export const listRecipe = defineSlotRecipe({
|
|
|
164
164
|
},
|
|
165
165
|
ghost: {
|
|
166
166
|
item: {
|
|
167
|
-
|
|
167
|
+
// The partner of `buttonBgSecondary` (NEH-877). `textSecondary`
|
|
168
|
+
// belongs to `boxBgSecondary`, a surface this variant does not paint.
|
|
169
|
+
color: "buttonTextSecondary",
|
|
168
170
|
bg: "buttonBgSecondary",
|
|
169
171
|
borderRadius: "md",
|
|
170
172
|
cursor: "pointer",
|
|
171
173
|
_hover: {
|
|
172
174
|
bg: "boxBgAccent",
|
|
175
|
+
// The hover repaints an accent surface, so the row must take that
|
|
176
|
+
// surface's partner rather than ride the base colour onto it —
|
|
177
|
+
// 1.14:1 in optima's light theme if it did.
|
|
178
|
+
color: "textAccent",
|
|
173
179
|
},
|
|
174
180
|
},
|
|
175
181
|
},
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { defineRecipe } from "@pandacss/dev";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A small label, in one of six tones.
|
|
5
|
+
*
|
|
6
|
+
* ## Why this is a recipe and not `css()` in the component
|
|
7
|
+
*
|
|
8
|
+
* `StyledTag` painted itself with an inline `css()` call, which was right while
|
|
9
|
+
* it had exactly one appearance. A tone chosen at runtime is a different
|
|
10
|
+
* problem: Panda's extractor reads source text, so `tone={STATUS_COLOR[status]}`
|
|
11
|
+
* resolves to nothing and emits no rule — while the class name still lands in
|
|
12
|
+
* the DOM. The result is an unstyled tag, with no build error and no console
|
|
13
|
+
* warning.
|
|
14
|
+
*
|
|
15
|
+
* Recipes are the escape from that, because `staticCssRecipes` in
|
|
16
|
+
* `preset/index.ts` forces **every variant of every recipe** into the
|
|
17
|
+
* stylesheet. So a tone computed from a status map is covered for free, which is
|
|
18
|
+
* how the consuming apps actually use this (NEH-721: HopperGuard drives 109 of
|
|
19
|
+
* its 155 tags from `STATUS_COLOR[item.status]` and friends).
|
|
20
|
+
*
|
|
21
|
+
* ## The tones reuse StyledAlert's vocabulary exactly
|
|
22
|
+
*
|
|
23
|
+
* `info` / `success` / `warning` / `error` are `AlertStatus`, and each pairs the
|
|
24
|
+
* **same tokens** the alert recipe pairs. Two status vocabularies in one package
|
|
25
|
+
* — one for banners and a different one for tags — is how a product ends up with
|
|
26
|
+
* a green that means "success" in one place and "active" in another.
|
|
27
|
+
*
|
|
28
|
+
* Two tones are additional rather than borrowed:
|
|
29
|
+
*
|
|
30
|
+
* - **`neutral`** is the historical default and stays the default, so every
|
|
31
|
+
* existing call site renders exactly as it did before this variant existed.
|
|
32
|
+
* - **`accent`** has no alert equivalent, because an alert is always *about*
|
|
33
|
+
* something being fine or not. A tag is often just a category — a label, a
|
|
34
|
+
* type, a group — and forcing those into `info` would make "informational"
|
|
35
|
+
* mean nothing.
|
|
36
|
+
*
|
|
37
|
+
* ## No border, unlike the alert
|
|
38
|
+
*
|
|
39
|
+
* The alert recipe pairs each background with a `border*` token. A tag is small
|
|
40
|
+
* and usually appears in groups; a 1px edge on each turns a row of five into
|
|
41
|
+
* visual noise, and the tinted background already separates it from the page.
|
|
42
|
+
* The border tokens stay available if a consumer disagrees.
|
|
43
|
+
*/
|
|
44
|
+
export const tagRecipe = defineRecipe({
|
|
45
|
+
className: "tag",
|
|
46
|
+
base: {
|
|
47
|
+
display: "inline-flex",
|
|
48
|
+
alignItems: "center",
|
|
49
|
+
gap: "1",
|
|
50
|
+
paddingInline: "2",
|
|
51
|
+
/*
|
|
52
|
+
* Vertical padding is deliberately absent: the height comes from the line
|
|
53
|
+
* box and the horizontal padding, so a tag tracks the font scale instead of
|
|
54
|
+
* needing a re-tune whenever it moves.
|
|
55
|
+
*/
|
|
56
|
+
borderRadius: "md",
|
|
57
|
+
/*
|
|
58
|
+
* Not a tap target. A plain tag is not interactive, so the 48px floor does
|
|
59
|
+
* not apply to it — the remove BUTTON inside `StyledTag` is, and states its
|
|
60
|
+
* own.
|
|
61
|
+
*/
|
|
62
|
+
fontSize: "sm",
|
|
63
|
+
whiteSpace: "nowrap",
|
|
64
|
+
},
|
|
65
|
+
variants: {
|
|
66
|
+
tone: {
|
|
67
|
+
neutral: {
|
|
68
|
+
backgroundColor: "boxBgSecondary",
|
|
69
|
+
color: "textSecondary",
|
|
70
|
+
},
|
|
71
|
+
info: {
|
|
72
|
+
backgroundColor: "boxInfo",
|
|
73
|
+
color: "textMain",
|
|
74
|
+
},
|
|
75
|
+
success: {
|
|
76
|
+
backgroundColor: "boxSuccess",
|
|
77
|
+
color: "textSuccess",
|
|
78
|
+
},
|
|
79
|
+
warning: {
|
|
80
|
+
backgroundColor: "boxWarning",
|
|
81
|
+
color: "textWarning",
|
|
82
|
+
},
|
|
83
|
+
error: {
|
|
84
|
+
backgroundColor: "boxError",
|
|
85
|
+
color: "textError",
|
|
86
|
+
},
|
|
87
|
+
accent: {
|
|
88
|
+
backgroundColor: "boxBgAccent",
|
|
89
|
+
color: "textMain",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
defaultVariants: {
|
|
94
|
+
tone: "neutral",
|
|
95
|
+
},
|
|
96
|
+
});
|
|
@@ -1,12 +1,34 @@
|
|
|
1
1
|
import { defineRecipe } from "@pandacss/dev";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Every variant here paints the SAME surface, so every variant owes the same
|
|
5
|
+
* text colour (NEH-877).
|
|
6
|
+
*
|
|
7
|
+
* `boxBgPrimary` is the background of all nine, base included — the variants
|
|
8
|
+
* differ by border and nothing else. Yet seven of them named a text token
|
|
9
|
+
* belonging to some other surface: `buttonTextSecondary`, `textSecondary`,
|
|
10
|
+
* `textMain`, `buttonTextPrimary`. The contract's partner for `boxBgPrimary` is
|
|
11
|
+
* `textPrimary`, and there is no reading of these variants under which a
|
|
12
|
+
* different one is intended — they are copy-paste drift, not design.
|
|
13
|
+
*
|
|
14
|
+
* Measured against optima-cloud-saas's light theme, the worst was `glass` at
|
|
15
|
+
* **1.05:1** — `buttonTextPrimary` is white and `boxBgPrimary` is #f8f9fa, so
|
|
16
|
+
* that tooltip rendered white text on a near-white panel and said nothing at
|
|
17
|
+
* all. The dark theme reads it at 17.84:1, which is why nobody saw it.
|
|
18
|
+
* `textPrimary` measures 16.92:1 light and 15.02:1 dark.
|
|
19
|
+
*
|
|
20
|
+
* A tooltip is the one surface where this class of defect is invisible in
|
|
21
|
+
* review: it appears on hover, over the thing it describes, and an empty-looking
|
|
22
|
+
* one reads as a tooltip that has not opened yet.
|
|
23
|
+
*/
|
|
3
24
|
export const tooltipRecipe = defineRecipe({
|
|
4
25
|
className: "tooltip",
|
|
5
26
|
base: {
|
|
6
27
|
position: "absolute",
|
|
7
28
|
zIndex: 9999,
|
|
8
29
|
pointerEvents: "none",
|
|
9
|
-
|
|
30
|
+
// The partner of the `boxBgPrimary` this base paints (NEH-877).
|
|
31
|
+
color: "textPrimary",
|
|
10
32
|
padding: "2px",
|
|
11
33
|
borderRadius: "md",
|
|
12
34
|
fontSize: "var(--font-sizes-lg, 1rem)",
|
|
@@ -25,7 +47,7 @@ export const tooltipRecipe = defineRecipe({
|
|
|
25
47
|
variant: {
|
|
26
48
|
solid: {
|
|
27
49
|
bg: "boxBgPrimary",
|
|
28
|
-
color: "
|
|
50
|
+
color: "textPrimary",
|
|
29
51
|
borderColor: "borderBgPrimary",
|
|
30
52
|
},
|
|
31
53
|
outline: {
|
|
@@ -36,25 +58,25 @@ export const tooltipRecipe = defineRecipe({
|
|
|
36
58
|
},
|
|
37
59
|
aurora: {
|
|
38
60
|
bg: "boxBgPrimary",
|
|
39
|
-
color: "
|
|
61
|
+
color: "textPrimary",
|
|
40
62
|
borderColor: "borderBgSecondary",
|
|
41
63
|
border: "1px solid",
|
|
42
64
|
},
|
|
43
65
|
glass: {
|
|
44
66
|
bg: "boxBgPrimary",
|
|
45
|
-
color: "
|
|
67
|
+
color: "textPrimary",
|
|
46
68
|
border: "1px solid",
|
|
47
69
|
borderColor: "borderBgPrimary",
|
|
48
70
|
},
|
|
49
71
|
matte: {
|
|
50
72
|
bg: "boxBgPrimary",
|
|
51
|
-
color: "
|
|
73
|
+
color: "textPrimary",
|
|
52
74
|
borderColor: "borderBgSecondary",
|
|
53
75
|
border: "1px solid",
|
|
54
76
|
},
|
|
55
77
|
ghost: {
|
|
56
78
|
bg: "boxBgPrimary",
|
|
57
|
-
color: "
|
|
79
|
+
color: "textPrimary",
|
|
58
80
|
},
|
|
59
81
|
link: {
|
|
60
82
|
bg: "boxBgPrimary",
|
|
@@ -64,13 +86,13 @@ export const tooltipRecipe = defineRecipe({
|
|
|
64
86
|
},
|
|
65
87
|
none: {
|
|
66
88
|
bg: "boxBgPrimary",
|
|
67
|
-
color: "
|
|
89
|
+
color: "textPrimary",
|
|
68
90
|
border: "1px solid",
|
|
69
91
|
borderColor: "borderBgPrimary",
|
|
70
92
|
},
|
|
71
93
|
unstyled: {
|
|
72
94
|
bg: "boxBgPrimary",
|
|
73
|
-
color: "
|
|
95
|
+
color: "textPrimary",
|
|
74
96
|
},
|
|
75
97
|
},
|
|
76
98
|
},
|