@stonedogcode/style 0.20.0 → 0.20.2
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stonedogcode/style",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.2",
|
|
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.",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"gate": "npm run type-check && npm run lint && npm run test",
|
|
49
49
|
"//publish:stonedog-style": "Publish to npm, end to end. Run from a terminal, interactively — npm prompts for the 2FA one-time password and the login flow needs a browser. It refuses a checkout that is detached, dirty, or behind origin/main: a submodule sits detached at the consumer's gitlink by default, and publishing from one commit behind ships a tarball missing the very thing you are publishing for while looking like a success (it did, on 2026-08-04, without TitleLogo.tsx). Runs the gate, prints the tarball listing, then proves the result by installing from the registry into a temp directory.",
|
|
50
50
|
"publish:stonedog-style": "bash scripts/publish-package.sh",
|
|
51
|
+
"publishnpm": "npm run publish:stonedog-style",
|
|
51
52
|
"version:bump:patch": "npm version patch --no-git-tag-version",
|
|
52
53
|
"version:bump:minor": "npm version minor --no-git-tag-version",
|
|
53
54
|
"audit": "npm audit --audit-level=moderate"
|
|
@@ -68,25 +68,80 @@ const PandaItemGroup = styled("div", {
|
|
|
68
68
|
});
|
|
69
69
|
const PandaItem = styled("div", { base: { position: "relative" } });
|
|
70
70
|
|
|
71
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Chakra's `Float` — the delete control at the item's top-end corner.
|
|
73
|
+
*
|
|
74
|
+
* The `translate(40%, -40%)` this used to carry is gone (NEH-1117). It centred
|
|
75
|
+
* the control ON the corner, so roughly half of it hung outside the preview it
|
|
76
|
+
* belonged to — measured in Chromium at every viewport, the control's top edge
|
|
77
|
+
* sat at **y = -7.2**, above the top of the page. That is tolerable while the
|
|
78
|
+
* control is 16px and merely untidy; at the 48px hit area below it would put
|
|
79
|
+
* two neighbouring previews' targets into the same few pixels, which is the
|
|
80
|
+
* sharper half of what the issue reports.
|
|
81
|
+
*
|
|
82
|
+
* So the hit area is anchored inside the tile instead, and the visible chip
|
|
83
|
+
* sits in its top-end corner — which lands the chip in very nearly the place
|
|
84
|
+
* it occupied before, without anything overhanging.
|
|
85
|
+
*/
|
|
72
86
|
const PandaFloat = styled("div", {
|
|
73
87
|
base: {
|
|
74
88
|
position: "absolute",
|
|
75
89
|
top: "0",
|
|
76
90
|
right: "0",
|
|
77
|
-
transform: "translate(40%, -40%)",
|
|
78
91
|
},
|
|
79
92
|
});
|
|
80
93
|
|
|
81
94
|
/**
|
|
95
|
+
* The hit area — and only the hit area.
|
|
96
|
+
*
|
|
82
97
|
* `boxSize="4"` and `layerStyle="fill.solid"` were Chakra props with no Panda
|
|
83
|
-
* equivalent
|
|
84
|
-
*
|
|
98
|
+
* equivalent; their effect is reproduced as real styles. What was NOT
|
|
99
|
+
* reproduced was a tap target: `boxSize="4"` is **16x16 CSS px**, a third of
|
|
100
|
+
* this package's stated 48x48 floor, and it came across the extraction
|
|
101
|
+
* unchanged (NEH-1116) because changing it changes HopperGuard's rendering.
|
|
102
|
+
* This is that change.
|
|
103
|
+
*
|
|
104
|
+
* The split is `StyledTag`'s, which solves the same tension: the BUTTON is the
|
|
105
|
+
* target and carries no appearance at all, and the chip inside it is what a
|
|
106
|
+
* reader sees. Sizing the visible circle to 48px instead would put a control
|
|
107
|
+
* half the width of the 96px preview on top of it.
|
|
108
|
+
*
|
|
109
|
+
* `StyledTag` needs negative block margin to stop the target growing its own
|
|
110
|
+
* tag; here the control is absolutely positioned, so it is already out of
|
|
111
|
+
* flow and cannot drag the preview's layout with it whatever size it is. The
|
|
112
|
+
* component test asserts the preview stays 96x96 rather than trusting that.
|
|
113
|
+
*/
|
|
114
|
+
const PandaDeleteTrigger = styled("button", {
|
|
115
|
+
base: {
|
|
116
|
+
display: "inline-flex",
|
|
117
|
+
// Top-end, not centred: the chip keeps the corner position it has always
|
|
118
|
+
// had, and the extra target grows inwards over the preview — which is
|
|
119
|
+
// decorative, and the only direction with room.
|
|
120
|
+
alignItems: "flex-start",
|
|
121
|
+
justifyContent: "flex-end",
|
|
122
|
+
// The house floor, stated rather than left to emerge from whatever glyph
|
|
123
|
+
// a consumer passes — see CLAUDE.md. 48 rather than WCAG 2.5.5 AAA's 44,
|
|
124
|
+
// because the standard is calibrated for the general population and this
|
|
125
|
+
// library's largest consumer serves an often-elderly, sometimes
|
|
126
|
+
// motor-impaired audience.
|
|
127
|
+
minWidth: "48px",
|
|
128
|
+
minHeight: "48px",
|
|
129
|
+
padding: "0",
|
|
130
|
+
background: "transparent",
|
|
131
|
+
border: "none",
|
|
132
|
+
cursor: "pointer",
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* What a reader actually sees: the small round chip that used to BE the
|
|
138
|
+
* button. Same size, same tokens, same appearance — it is now a passenger
|
|
139
|
+
* inside a target big enough to hit.
|
|
85
140
|
*
|
|
86
141
|
* The colours are tokens, never literals, so the control follows the host's
|
|
87
142
|
* theme and colour mode.
|
|
88
143
|
*/
|
|
89
|
-
const
|
|
144
|
+
const PandaDeleteChip = styled("span", {
|
|
90
145
|
base: {
|
|
91
146
|
display: "inline-flex",
|
|
92
147
|
alignItems: "center",
|
|
@@ -94,7 +149,6 @@ const PandaDeleteTrigger = styled("button", {
|
|
|
94
149
|
width: "4",
|
|
95
150
|
height: "4",
|
|
96
151
|
borderRadius: "full",
|
|
97
|
-
cursor: "pointer",
|
|
98
152
|
lineHeight: "1",
|
|
99
153
|
bg: "boxBgPrimary",
|
|
100
154
|
color: "textPrimary",
|
|
@@ -207,9 +261,7 @@ const FileUploadPreviewOnly = ({
|
|
|
207
261
|
aria-label={`Remove ${file.name}`}
|
|
208
262
|
onClick={() => removeFile(file)}
|
|
209
263
|
>
|
|
210
|
-
{removeIcon
|
|
211
|
-
<span aria-hidden="true">{removeIcon}</span>
|
|
212
|
-
) : null}
|
|
264
|
+
<PandaDeleteChip aria-hidden="true">{removeIcon}</PandaDeleteChip>
|
|
213
265
|
</PandaDeleteTrigger>
|
|
214
266
|
</PandaFloat>
|
|
215
267
|
</PandaItem>
|
|
@@ -226,6 +226,59 @@ const StyledTooltip: React.FC<StyledTooltipProps> = ({
|
|
|
226
226
|
*/
|
|
227
227
|
const [focusableAncestor, setFocusableAncestor] = useState<HTMLElement | null>(null);
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* Whether the layout effect below has yet decided WHERE the click-mode help
|
|
231
|
+
* control belongs (NEH-965).
|
|
232
|
+
*
|
|
233
|
+
* The control is a real `<button>`, and until the trigger has been measured
|
|
234
|
+
* the component cannot know whether it is standing inside one. Rendering it
|
|
235
|
+
* optimistically and moving it afterwards is not an option: React validates
|
|
236
|
+
* DOM nesting at *render* time, against the React tree, so a single pass
|
|
237
|
+
* with `<button>` inside `<button>` warns and — on a server-rendered host
|
|
238
|
+
* like hopper-web — produces a hydration error, whatever the DOM ends up
|
|
239
|
+
* looking like a moment later.
|
|
240
|
+
*
|
|
241
|
+
* So the first pass renders no control at all and the second one puts it
|
|
242
|
+
* where it belongs. Both effects here are layout effects, so that settles
|
|
243
|
+
* before paint and before hydration compares anything: there is no frame in
|
|
244
|
+
* which a reader could see the control missing, and no server/client
|
|
245
|
+
* mismatch, because the server also renders nothing.
|
|
246
|
+
*/
|
|
247
|
+
const [helpPlacementSettled, setHelpPlacementSettled] = useState(false);
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The element the click-mode help control is rendered into when the trigger
|
|
251
|
+
* sits inside something focusable (NEH-965).
|
|
252
|
+
*
|
|
253
|
+
* `HelpTrigger` renders beside the child, inside the trigger wrapper. When
|
|
254
|
+
* the tooltipped thing is an icon inside an icon button that lands the
|
|
255
|
+
* control *inside* that button:
|
|
256
|
+
*
|
|
257
|
+
* <button aria-label="Expand"> <- the consumer's control
|
|
258
|
+
* <div> <- TooltipTrigger
|
|
259
|
+
* <svg aria-hidden />
|
|
260
|
+
* <button aria-label="More information">?</button> <- invalid
|
|
261
|
+
*
|
|
262
|
+
* `<button>` cannot be a descendant of `<button>`, and this is not a
|
|
263
|
+
* preference anybody opted into: `isClick` is `trigger === "click" ||
|
|
264
|
+
* !canHover`, so it is the DEFAULT rendering on every phone and tablet.
|
|
265
|
+
*
|
|
266
|
+
* Simply not rendering the control would trade invalid HTML for an
|
|
267
|
+
* unreachable explanation — on a device that cannot hover there is no hover,
|
|
268
|
+
* and tapping the button activates it rather than explaining it. So the
|
|
269
|
+
* control moves *out* instead: a span inserted immediately after the
|
|
270
|
+
* focusable ancestor, portalled into. Valid HTML, still visible, still
|
|
271
|
+
* tappable, still in the tab sequence, and it scrolls with the page because
|
|
272
|
+
* it sits in normal flow rather than being positioned over anything.
|
|
273
|
+
*
|
|
274
|
+
* `inline-flex` on the host rather than `display: contents`: contents would
|
|
275
|
+
* let the control participate in the ancestor's parent layout directly, but
|
|
276
|
+
* it was removed from the accessibility tree by browsers this package's
|
|
277
|
+
* audience is still using, and an invisible help control is the bug we are
|
|
278
|
+
* fixing.
|
|
279
|
+
*/
|
|
280
|
+
const [helpHost, setHelpHost] = useState<HTMLElement | null>(null);
|
|
281
|
+
|
|
229
282
|
/**
|
|
230
283
|
* True when something else — a descendant or an ancestor — already puts this
|
|
231
284
|
* trigger's content in the tab sequence. When it does, the trigger must add
|
|
@@ -287,6 +340,10 @@ const StyledTooltip: React.FC<StyledTooltipProps> = ({
|
|
|
287
340
|
? null
|
|
288
341
|
: node?.parentElement?.closest<HTMLElement>(FOCUSABLE_SELECTOR) ?? null;
|
|
289
342
|
setFocusableAncestor((prev) => (prev === ancestor ? prev : ancestor));
|
|
343
|
+
// Measured — the next render may place the help control (NEH-965). Set
|
|
344
|
+
// before the `!node` bail so a trigger that never mounted a node does not
|
|
345
|
+
// leave click mode permanently without its control.
|
|
346
|
+
setHelpPlacementSettled(true);
|
|
290
347
|
|
|
291
348
|
if (!node) return;
|
|
292
349
|
|
|
@@ -299,9 +356,21 @@ const StyledTooltip: React.FC<StyledTooltipProps> = ({
|
|
|
299
356
|
.join(" ")
|
|
300
357
|
.replace(/\s+/g, " ")
|
|
301
358
|
.trim();
|
|
359
|
+
// With a focusable ancestor the help control is rendered OUTSIDE it
|
|
360
|
+
// (NEH-965), so the ancestor no longer supplies context by containment and
|
|
361
|
+
// the control has to carry the subject in its own name. "More information"
|
|
362
|
+
// sitting on its own next to a button called "Expand" names nothing —
|
|
363
|
+
// which is the twenty-identical-controls problem NEH-769 fixed for the
|
|
364
|
+
// inline case, reappearing one level up.
|
|
365
|
+
const ancestorText = ancestor
|
|
366
|
+
? (ancestor.getAttribute("aria-label") ?? ancestor.textContent ?? "")
|
|
367
|
+
.replace(/\s+/g, " ")
|
|
368
|
+
.trim()
|
|
369
|
+
: "";
|
|
370
|
+
const subject = ownText || ancestorText;
|
|
302
371
|
// Long enough to distinguish twenty controls, short enough that a screen
|
|
303
372
|
// reader does not read a paragraph before the reader can act on it.
|
|
304
|
-
setSubjectLabel(
|
|
373
|
+
setSubjectLabel(subject.length > 80 ? `${subject.slice(0, 80).trimEnd()}…` : subject);
|
|
305
374
|
|
|
306
375
|
// parentElement, not the node itself: closest() would match our own
|
|
307
376
|
// aria-label once we set one, and the answer would flip every render.
|
|
@@ -361,6 +430,67 @@ const StyledTooltip: React.FC<StyledTooltipProps> = ({
|
|
|
361
430
|
};
|
|
362
431
|
}, [focusableAncestor, isClick, show, hide]);
|
|
363
432
|
|
|
433
|
+
/**
|
|
434
|
+
* Insert the host for the portalled help control, immediately after the
|
|
435
|
+
* focusable ancestor (NEH-965). A layout effect, so the control is in place
|
|
436
|
+
* before paint.
|
|
437
|
+
*/
|
|
438
|
+
useLayoutEffect(() => {
|
|
439
|
+
if (!isClick || !focusableAncestor || typeof document === "undefined") {
|
|
440
|
+
setHelpHost(null);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const host = document.createElement("span");
|
|
444
|
+
// Named so a consumer reading the DOM can see whose node this is, and so a
|
|
445
|
+
// test can assert the control landed outside the ancestor rather than
|
|
446
|
+
// merely that it exists somewhere.
|
|
447
|
+
host.setAttribute("data-stonedog-tooltip-help-host", "");
|
|
448
|
+
host.style.display = "inline-flex";
|
|
449
|
+
host.style.verticalAlign = "middle";
|
|
450
|
+
focusableAncestor.after(host);
|
|
451
|
+
setHelpHost(host);
|
|
452
|
+
return () => {
|
|
453
|
+
host.remove();
|
|
454
|
+
setHelpHost(null);
|
|
455
|
+
};
|
|
456
|
+
}, [isClick, focusableAncestor]);
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Keep the host beside its ancestor when React MOVES the ancestor.
|
|
460
|
+
*
|
|
461
|
+
* The host is a node this component inserts, not one React renders, so React
|
|
462
|
+
* does not move it with the ancestor: reordering a row of icon buttons
|
|
463
|
+
* leaves every help control behind at its old index. Measured before fixing,
|
|
464
|
+
* two buttons swapped:
|
|
465
|
+
*
|
|
466
|
+
* before BTN(Expand) HOST(Help: Expand) BTN(Collapse) HOST(Help: Collapse)
|
|
467
|
+
* after HOST(Help: Expand) BTN(Collapse) HOST(Help: Collapse) BTN(Expand)
|
|
468
|
+
*
|
|
469
|
+
* — every control now beside the wrong button, which for an accessibility
|
|
470
|
+
* affordance is worse than the nesting it replaced.
|
|
471
|
+
*
|
|
472
|
+
* Re-running the effect above cannot catch this. Its dependency is the
|
|
473
|
+
* ancestor NODE, and a moved node is the same node, so nothing changes and
|
|
474
|
+
* nothing re-runs. Hence a deliberately dependency-free layout effect: it
|
|
475
|
+
* re-asserts the placement on every commit, before paint, and costs two DOM
|
|
476
|
+
* property reads on a path that only exists in click mode inside a focusable
|
|
477
|
+
* ancestor.
|
|
478
|
+
*
|
|
479
|
+
* Scope worth stating rather than implying: this follows a move that
|
|
480
|
+
* RE-RENDERS this component, which is what a list reorder does. A subtree
|
|
481
|
+
* memoised so hard that React moves it without rendering it would not be
|
|
482
|
+
* followed. A `MutationObserver` on the parent would cover that too, and is
|
|
483
|
+
* deliberately not here — its callback is a microtask, so it could not be
|
|
484
|
+
* asserted in the tier that can see this at all, and an untested guard is
|
|
485
|
+
* the thing this package's rules are most emphatic about.
|
|
486
|
+
*/
|
|
487
|
+
useLayoutEffect(() => {
|
|
488
|
+
if (!helpHost || !focusableAncestor) return;
|
|
489
|
+
if (!focusableAncestor.isConnected) return;
|
|
490
|
+
if (helpHost.previousSibling === focusableAncestor) return;
|
|
491
|
+
focusableAncestor.after(helpHost);
|
|
492
|
+
});
|
|
493
|
+
|
|
364
494
|
useLayoutEffect(() => {
|
|
365
495
|
if (visible && triggerRef.current && tooltipRef.current) {
|
|
366
496
|
const triggerRect = triggerRef.current.getBoundingClientRect();
|
|
@@ -566,19 +696,40 @@ const StyledTooltip: React.FC<StyledTooltipProps> = ({
|
|
|
566
696
|
const resolvedHelpLabel =
|
|
567
697
|
helpLabel ?? (subjectLabel ? `Help: ${subjectLabel}` : "More information");
|
|
568
698
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
699
|
+
/**
|
|
700
|
+
* The click-mode control — rendered only once its home is known, see
|
|
701
|
+
* `helpPlacementSettled`.
|
|
702
|
+
*
|
|
703
|
+
* `stopPropagation` is load-bearing rather than defensive. A React portal
|
|
704
|
+
* bubbles its events through the REACT tree, not the DOM tree, so once this
|
|
705
|
+
* control is portalled out of the icon button it is still, as far as React
|
|
706
|
+
* is concerned, inside it — and a press on "?" would fire the button's own
|
|
707
|
+
* `onClick`. That is precisely the collision this fix exists to remove, so
|
|
708
|
+
* it is stopped for the inline case too: pressing "?" asks for an
|
|
709
|
+
* explanation, and must never also do the thing being explained.
|
|
710
|
+
*/
|
|
711
|
+
const helpControl =
|
|
712
|
+
isClick && helpPlacementSettled ? (
|
|
713
|
+
<HelpTrigger
|
|
714
|
+
ref={helpRef}
|
|
715
|
+
type="button"
|
|
716
|
+
side={helpGoesFirst ? "before" : "after"}
|
|
717
|
+
aria-label={resolvedHelpLabel}
|
|
718
|
+
aria-expanded={visible}
|
|
719
|
+
aria-controls={visible ? tooltipId : undefined}
|
|
720
|
+
onMouseDown={(event: React.MouseEvent) => event.stopPropagation()}
|
|
721
|
+
onClick={(event: React.MouseEvent) => {
|
|
722
|
+
event.stopPropagation();
|
|
723
|
+
setVisible((open) => !open);
|
|
724
|
+
}}
|
|
725
|
+
>
|
|
726
|
+
?
|
|
727
|
+
</HelpTrigger>
|
|
728
|
+
) : null;
|
|
729
|
+
|
|
730
|
+
// Inside a focusable ancestor the control is portalled out to `helpHost`;
|
|
731
|
+
// everywhere else it stays where it has always been, beside the child.
|
|
732
|
+
const inlineHelpControl = focusableAncestor ? null : helpControl;
|
|
582
733
|
|
|
583
734
|
return (
|
|
584
735
|
<>
|
|
@@ -628,10 +779,11 @@ const StyledTooltip: React.FC<StyledTooltipProps> = ({
|
|
|
628
779
|
aria-describedby={!insideFocusable && visible ? tooltipId : undefined}
|
|
629
780
|
{...rest}
|
|
630
781
|
>
|
|
631
|
-
{helpGoesFirst &&
|
|
782
|
+
{helpGoesFirst && inlineHelpControl}
|
|
632
783
|
{children}
|
|
633
|
-
{!helpGoesFirst &&
|
|
784
|
+
{!helpGoesFirst && inlineHelpControl}
|
|
634
785
|
</TooltipTrigger>
|
|
786
|
+
{helpControl && helpHost && createPortal(helpControl, helpHost)}
|
|
635
787
|
{visible && typeof document !== "undefined" &&
|
|
636
788
|
createPortal(
|
|
637
789
|
<TooltipContent
|
|
@@ -133,13 +133,24 @@ export const buttonRecipe = defineRecipe({
|
|
|
133
133
|
pointerEvents: "none",
|
|
134
134
|
},
|
|
135
135
|
},
|
|
136
|
+
/**
|
|
137
|
+
* The same correction `iconButton`'s `matte` takes, for the same reason
|
|
138
|
+
* (NEH-881) — see the long note there.
|
|
139
|
+
*
|
|
140
|
+
* NEH-881 named only `iconButton`. These two recipes are the same control
|
|
141
|
+
* in two shapes and their `matte` blocks were byte-identical, including
|
|
142
|
+
* the inert `bgGradient` that kept the pairing guard from inspecting
|
|
143
|
+
* either of them. Fixing one and leaving the other would ship the defect
|
|
144
|
+
* under a different class name, so both move together: the same 8-of-18
|
|
145
|
+
* measurement applies unchanged, because it is a property of the surface
|
|
146
|
+
* and the text token, not of the shape of the control.
|
|
147
|
+
*/
|
|
136
148
|
matte: {
|
|
137
|
-
bgGradient: "linear(to-b, gray.800, gray.900)",
|
|
138
149
|
borderColor: "gray.700",
|
|
139
150
|
borderWidth: "1px",
|
|
140
151
|
boxShadow: "md",
|
|
141
152
|
bg: "buttonBgAccent",
|
|
142
|
-
color: "
|
|
153
|
+
color: "buttonTextAccent",
|
|
143
154
|
borderRadius: "lg",
|
|
144
155
|
fontWeight: "bold",
|
|
145
156
|
},
|
|
@@ -155,13 +155,50 @@ export const buttonIconRecipe = defineRecipe({
|
|
|
155
155
|
pointerEvents: "none",
|
|
156
156
|
},
|
|
157
157
|
},
|
|
158
|
+
/**
|
|
159
|
+
* The accent pairing again, and the gradient that was hiding it
|
|
160
|
+
* (NEH-881).
|
|
161
|
+
*
|
|
162
|
+
* This variant was filed as a different defect class: a FIXED dark
|
|
163
|
+
* `gray.800`→`gray.900` gradient painting over the accent fill in every
|
|
164
|
+
* theme, with a themed `textPrimary` glyph left dark-on-dark on top of
|
|
165
|
+
* it. The proposed fix was a deliberate literal `color: "white"`, as
|
|
166
|
+
* `box` and `input-bool` had already taken for their own `matte`.
|
|
167
|
+
*
|
|
168
|
+
* **There is no gradient.** `bgGradient` is Chakra v2 syntax; Panda has
|
|
169
|
+
* no such utility and no `linear()` shorthand, so it emitted
|
|
170
|
+
* `background-image: linear(to-b, gray.800, gray.900)` — not valid CSS,
|
|
171
|
+
* discarded at parse time by every engine. Chromium reads the element
|
|
172
|
+
* back as `background-image: none`, and the `background` shorthand
|
|
173
|
+
* emitted just above it had already reset `background-image` in any case.
|
|
174
|
+
*
|
|
175
|
+
* So the surface here has always been plain `buttonBgAccent`, and this is
|
|
176
|
+
* the ordinary NEH-796 / NEH-877 mispairing after all — `textPrimary` is
|
|
177
|
+
* the contract's partner for `boxBgPrimary`, not for an accent fill.
|
|
178
|
+
* Measured over nine host themes in both modes, 18 pairs: `textPrimary`
|
|
179
|
+
* is below WCAG AA on 8 of them, every one in dark mode, the worst at
|
|
180
|
+
* 2.15:1 (white on `#F59E0B`). `buttonTextAccent` is below AA on 0 of the
|
|
181
|
+
* 18, and reaches 9.78:1 on that same worst pair.
|
|
182
|
+
*
|
|
183
|
+
* Had `color: "white"` been taken as filed, it would have pinned the
|
|
184
|
+
* failing half of that measurement in place under a comment calling it
|
|
185
|
+
* deliberate — the shape where a test makes a bug unfixable by review.
|
|
186
|
+
*
|
|
187
|
+
* The dead declaration goes too, and that is not tidying. The pairing
|
|
188
|
+
* sweep in `variant-contrast-pairing.test.ts` reads `background-image`
|
|
189
|
+
* before `background`, so while the invalid gradient was emitted the
|
|
190
|
+
* guard took it as this variant's surface, found the token contract said
|
|
191
|
+
* nothing about it, and skipped the variant entirely. Removing it is what
|
|
192
|
+
* puts `matte` back under the guard that already covers its five
|
|
193
|
+
* siblings — verified by planting the old colour back and watching that
|
|
194
|
+
* guard name both selectors.
|
|
195
|
+
*/
|
|
158
196
|
matte: {
|
|
159
|
-
bgGradient: "linear(to-b, gray.800, gray.900)",
|
|
160
197
|
borderColor: "gray.700",
|
|
161
198
|
borderWidth: "1px",
|
|
162
199
|
boxShadow: "md",
|
|
163
200
|
bg: "buttonBgAccent",
|
|
164
|
-
color: "
|
|
201
|
+
color: "buttonTextAccent",
|
|
165
202
|
borderRadius: "lg",
|
|
166
203
|
fontWeight: "bold",
|
|
167
204
|
},
|