@stonedogcode/style 0.15.0 → 0.17.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.
@@ -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: "textPrimary",
84
+ color: "buttonTextAccent",
70
85
  _hover: {
71
86
  bg: "buttonBgSecondary",
72
- color: "textPrimary",
87
+ color: "buttonTextSecondary",
73
88
  },
74
89
  },
75
90
  outline: {
76
91
  bg: "buttonBgAccent",
77
- color: "textPrimary",
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
- color: "textPrimary",
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: "textPrimary",
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
- color: "textPrimary",
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
- color: "textPrimary",
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
- color: "textSecondary",
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
- color: "textPrimary",
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
- color: "textAccent",
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
- color: "textSecondary",
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: "textPrimary",
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
- color: "textPrimary",
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: "textPrimary",
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: "textSecondary",
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: "textSecondary",
164
+ color: "buttonTextSecondary",
138
165
  bg: "buttonBgSecondary",
139
166
  borderColor: "borderBgPrimary",
140
167
  "&::placeholder": {
141
- color: "textSecondary/60",
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
- color: "textSecondary",
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
  },
@@ -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
- color: "buttonTextSecondary",
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: "buttonTextSecondary",
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: "textSecondary",
61
+ color: "textPrimary",
40
62
  borderColor: "borderBgSecondary",
41
63
  border: "1px solid",
42
64
  },
43
65
  glass: {
44
66
  bg: "boxBgPrimary",
45
- color: "buttonTextPrimary",
67
+ color: "textPrimary",
46
68
  border: "1px solid",
47
69
  borderColor: "borderBgPrimary",
48
70
  },
49
71
  matte: {
50
72
  bg: "boxBgPrimary",
51
- color: "textSecondary",
73
+ color: "textPrimary",
52
74
  borderColor: "borderBgSecondary",
53
75
  border: "1px solid",
54
76
  },
55
77
  ghost: {
56
78
  bg: "boxBgPrimary",
57
- color: "buttonTextSecondary",
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: "textMain",
89
+ color: "textPrimary",
68
90
  border: "1px solid",
69
91
  borderColor: "borderBgPrimary",
70
92
  },
71
93
  unstyled: {
72
94
  bg: "boxBgPrimary",
73
- color: "textMain",
95
+ color: "textPrimary",
74
96
  },
75
97
  },
76
98
  },
@@ -154,13 +154,30 @@ const COLOR_TOKENS: TokenMap = {
154
154
  * with a sensible default so every project can adopt it immediately — applied
155
155
  * to the case where a default is genuinely knowable.
156
156
  *
157
- * ## The percentages are measured, not chosen
157
+ * ## `textMuted` is measured. `textSubtle` is still only chosen (NEH-974)
158
158
  *
159
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.
160
+ * trades away legibility, so these percentages want measuring rather than
161
+ * picking.
162
+ *
163
+ * This comment claimed both tiers were measured by a file called
164
+ * `emphasis-contrast.ct.tsx`, **and no such file has ever existed** — the whole
165
+ * repo contains exactly one reference to that name, this one. A documented
166
+ * guard nobody implemented is worse than an absent one: it is a guard everybody
167
+ * believes in, and it is what made "the values below are what passed" read as a
168
+ * measurement rather than as a guess.
169
+ *
170
+ * What is true today: `components/StyledFieldHelp.contrast.ct.tsx` measures
171
+ * **`textMuted`** in a real browser, composited over the surface it actually
172
+ * paints on rather than over the page, and asserts WCAG 1.4.3 AA (4.5:1). On
173
+ * the harness theme it clears comfortably — 11.7:1 on the page, 9.1:1 on an
174
+ * opaque card, 8.2:1 on a translucent chip over that card.
175
+ *
176
+ * **`textSubtle` at 64% is not measured anywhere.** It is the tier closer to
177
+ * the legibility floor, so it is the one that needed the check more. Tracked;
178
+ * do not restore the claim that it passed something.
179
+ *
180
+ * A host that wants a stronger or weaker step defines the property.
164
181
  */
165
182
  const EMPHASIS_TOKENS: Record<string, [suffix: string, fallback: string]> = {
166
183
  /** Secondary information: still read, just not first. */
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The stacking-order vocabulary: names for the layers a UI actually has.
3
+ *
4
+ * ## The bug this exists to fix
5
+ *
6
+ * `drawerRecipe` has said `zIndex: "modal"` since it was extracted, and there
7
+ * has never been a `zIndex` token scale for `"modal"` to resolve against —
8
+ * neither here nor in either base Panda preset. Panda passes an unknown token
9
+ * through as a **literal**, so the generated stylesheet said
10
+ *
11
+ * .drawer { position: fixed; z-index: modal; }
12
+ *
13
+ * `modal` is not a valid `z-index` value, so the browser discards the whole
14
+ * declaration. **The drawer panel has never had a z-index at all.** No build
15
+ * error, no console warning, no type error — the class is in the DOM and the
16
+ * rule behind it is one line shorter than it looks.
17
+ *
18
+ * This is precisely the defect class CLAUDE.md records under "Token
19
+ * compliance", one property along from `bg: "buttonBgHover"`. The colour half
20
+ * is guarded by `token-contract.test.ts`; the z-index half is now guarded
21
+ * beside it.
22
+ *
23
+ * ## This package owns the NAMES. The host owns the NUMBERS
24
+ *
25
+ * A z-index ladder is an application concern — it encodes which of *that
26
+ * product's* surfaces may cover which, and no two products agree. So the
27
+ * values below are conventional defaults chosen to be sane for a fresh
28
+ * consumer, and a host is expected to override them:
29
+ *
30
+ * ```ts
31
+ * // the host's panda.config.ts
32
+ * theme: { extend: { tokens: { zIndex: { modal: { value: 99999 } } } } }
33
+ * ```
34
+ *
35
+ * Overriding a value keeps the name, and the name is the part that makes the
36
+ * order reviewable. A literal at a call site expresses nothing; `zIndex:
37
+ * "menu"` says what the element *is*, and a reader can check it against this
38
+ * table without opening a second file.
39
+ *
40
+ * ## The ladder
41
+ *
42
+ * Ascending, and the ORDER is the contract — not the numbers:
43
+ *
44
+ * hide behind its own box (decorative pseudo-elements)
45
+ * base the ordinary flow
46
+ * raised lifted within its own stacking context
47
+ * docked a bar or rail pinned inside a region
48
+ * sticky a sticky header inside a scroll region
49
+ * banner page-level chrome above sticky content
50
+ * surface a page surface that fills the viewport
51
+ * dialog a modal dialog and its scrim
52
+ * menu menus, drawer scrims, docked panels — the floating band's floor
53
+ * popover a popover that has to clear an open menu
54
+ * overlay a full-viewport cover: a splash, a loading shade
55
+ * toast toasts, pickers, transient chrome
56
+ * modal a drawer or modal panel that must clear everything but a tip
57
+ * tooltip the top of the application
58
+ *
59
+ * **A dialog sits LOW on purpose.** Menus, toasts, tooltips and dropdowns all
60
+ * have to be able to open *on* a dialog, so every one of them is above it.
61
+ * Raising the dialog to "win" is the change that looks right and breaks every
62
+ * control opened inside one.
63
+ */
64
+
65
+ /**
66
+ * The layer names, in ascending order, with this package's default values.
67
+ *
68
+ * Exported as plain numbers as well as tokens because a **portalled** element
69
+ * usually sets its z-index from an inline `style`, and an inline style cannot
70
+ * name a Panda token. Both readings have to come from one place or they drift.
71
+ */
72
+ export const Z_LAYERS = {
73
+ hide: -1,
74
+ base: 0,
75
+ raised: 1,
76
+ docked: 10,
77
+ sticky: 20,
78
+ banner: 50,
79
+ surface: 100,
80
+ dialog: 200,
81
+ menu: 300,
82
+ popover: 400,
83
+ overlay: 500,
84
+ toast: 600,
85
+ modal: 700,
86
+ tooltip: 800,
87
+ } as const;
88
+
89
+ export type ZLayerName = keyof typeof Z_LAYERS;
90
+
91
+ /** Every layer name. Useful to a consumer's guard test. */
92
+ export function zIndexTokenNames(): ZLayerName[] {
93
+ return Object.keys(Z_LAYERS) as ZLayerName[];
94
+ }
95
+
96
+ /** The scale in the shape Panda's `theme.extend.tokens.zIndex` wants. */
97
+ export function createZIndexTokens(): Record<string, { value: number }> {
98
+ return Object.fromEntries(
99
+ Object.entries(Z_LAYERS).map(([token, value]) => [token, { value }]),
100
+ );
101
+ }