Compare commits
2
Commits
aedef6daeb
...
2c51c35fe8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c51c35fe8
|
||
|
|
19fab4a783
|
@@ -26,6 +26,9 @@
|
|||||||
* "Ghost / Large / Default" is the one cell in the matrix with no padding
|
* "Ghost / Large / Default" is the one cell in the matrix with no padding
|
||||||
while its own Hover and Disabled cells have it - an authoring slip that
|
while its own Hover and Disabled cells have it - an authoring slip that
|
||||||
would make the button jump on hover. Padding is kept on all ghost states.
|
would make the button jump on hover. Padding is kept on all ghost states.
|
||||||
|
* Ghost is repainted to the Claude Design drafts rather than to the Figma
|
||||||
|
frame: text-only throughout, no hover fill, no lift, widening gap. See
|
||||||
|
the block below and the ghost tokens in semantic.css.
|
||||||
* Disabled uses the opaque `-bg-disabled` / `-fg-disabled` tokens rather than
|
* Disabled uses the opaque `-bg-disabled` / `-fg-disabled` tokens rather than
|
||||||
Figma's 40% opacity, so the page never shows through a disabled control.
|
Figma's 40% opacity, so the page never shows through a disabled control.
|
||||||
`--control-opacity-disabled` still holds the 0.4 if that is ever wanted.
|
`--control-opacity-disabled` still holds the 0.4 if that is ever wanted.
|
||||||
@@ -190,11 +193,28 @@
|
|||||||
--sh-btn-border-disabled: var(--interactive-ghost-border-disabled);
|
--sh-btn-border-disabled: var(--interactive-ghost-border-disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Figma gives the ghost md/lg cells radius.md while every other cell uses
|
/* GHOST IS A TEXT LINK, NOT A BUTTON.
|
||||||
radius.sm - reproduced rather than normalised. */
|
The drafts draw "All updates ->" and "What we do ->" as plain text that
|
||||||
.sh-btn[data-variant="ghost"][data-size="md"],
|
turns accent green while the label-to-arrow gap opens from 8px to 12px.
|
||||||
.sh-btn[data-variant="ghost"][data-size="lg"] {
|
So the ghost variant opts out of the 3px lift the filled controls take -
|
||||||
border-radius: var(--radius-control-lg);
|
nothing that has no box should appear to rise off the page - and animates
|
||||||
|
its gap instead. (The old Figma-quirk override that gave ghost md/lg
|
||||||
|
radius.md is gone with --radius-control-lg; every control is 4px.) */
|
||||||
|
.sh-btn[data-variant="ghost"] .sh-btn__content {
|
||||||
|
transition:
|
||||||
|
opacity var(--motion-duration-fast) var(--motion-ease-standard),
|
||||||
|
gap var(--motion-duration-base) var(--motion-ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.sh-btn[data-variant="ghost"]:hover:not(:disabled):not([aria-disabled="true"]):not([data-loading]) {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sh-btn[data-variant="ghost"]:hover:not(:disabled):not([aria-disabled="true"]):not([data-loading])
|
||||||
|
.sh-btn__content {
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------
|
/* ---------------------------------------------------------------------
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ export function CardCTA({
|
|||||||
data-surface="dark"
|
data-surface="dark"
|
||||||
className={cx(
|
className={cx(
|
||||||
"sh-card relative flex flex-col gap-lg rounded-surface p-10",
|
"sh-card relative flex flex-col gap-lg rounded-surface p-10",
|
||||||
"bg-surface-accent text-text-on-accent shadow-[var(--elevation-popover)]",
|
/* The drafts rest nothing on a shadow - a panel is a fill and an edge,
|
||||||
|
and the black is spent on what floats. --elevation-card is that
|
||||||
|
resting step: none on the dark surface, soft on the light one. */
|
||||||
|
"bg-surface-accent text-text-on-accent shadow-[var(--elevation-card)]",
|
||||||
"sm:flex-row sm:items-center",
|
"sm:flex-row sm:items-center",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export function CardContent({
|
|||||||
light on both surfaces - text-on-accent is the only always-white
|
light on both surfaces - text-on-accent is the only always-white
|
||||||
role, text-primary would go dark on the light surface. */}
|
role, text-primary would go dark on the light surface. */}
|
||||||
{badge ? (
|
{badge ? (
|
||||||
<span className="absolute start-md top-md rounded-control-lg bg-overlay-scrim px-xs py-2xs label-md text-text-on-accent backdrop-blur-[4px]">
|
<span className="absolute start-md top-md rounded-chip bg-overlay-scrim px-xs py-2xs label-md text-text-on-accent backdrop-blur-[4px]">
|
||||||
{badge}
|
{badge}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
STYLING RULE for this whole directory:
|
STYLING RULE for this whole directory:
|
||||||
* colour and radius come from the tier-2 Tailwind bridge
|
* colour and radius come from the tier-2 Tailwind bridge
|
||||||
(bg-surface-raised, text-text-accent, border-border-subtle,
|
(bg-surface-raised, text-text-accent, border-border-subtle,
|
||||||
rounded-surface, ...) - never a hex, never a tier-1 primitive;
|
rounded-card, ...) - never a hex, never a tier-1 primitive;
|
||||||
* tokens without a bridge utility are spent as arbitrary values,
|
* tokens without a bridge utility are spent as arbitrary values,
|
||||||
e.g. shadow-[var(--elevation-card)];
|
e.g. shadow-[var(--elevation-card)];
|
||||||
* spacing uses the named scale (p-lg = 24, gap-md = 16) so the Figma
|
* spacing uses the named scale (p-lg = 24, gap-md = 16) so the Figma
|
||||||
@@ -143,8 +143,10 @@ export interface CardShellProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The surface every card shares: raised fill, hairline border, 12px corner
|
* The surface every card shares: raised fill, hairline border and the 4px
|
||||||
* (Figma radius/lg) and the card elevation.
|
* corner the design drafts draw. `--elevation-card` is the drafts' resting
|
||||||
|
* shadow, which on the dark surface is none at all - the hairline does the
|
||||||
|
* separating, and the black is kept for the hover state in card.css.
|
||||||
*/
|
*/
|
||||||
export function CardShell({
|
export function CardShell({
|
||||||
as = "article",
|
as = "article",
|
||||||
@@ -160,7 +162,7 @@ export function CardShell({
|
|||||||
"sh-card",
|
"sh-card",
|
||||||
linked && "sh-card--linked",
|
linked && "sh-card--linked",
|
||||||
"relative flex bg-[var(--surface-card)] text-text-primary",
|
"relative flex bg-[var(--surface-card)] text-text-primary",
|
||||||
"rounded-surface border border-border-subtle shadow-[var(--elevation-card)]",
|
"rounded-card border border-border-subtle shadow-[var(--elevation-card)]",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export function CardStat({
|
|||||||
is falling. */
|
is falling. */
|
||||||
<span
|
<span
|
||||||
className={cx(
|
className={cx(
|
||||||
"inline-flex shrink-0 items-center gap-2xs rounded-control-lg px-xs py-2xs",
|
"inline-flex shrink-0 items-center gap-2xs rounded-chip px-xs py-2xs",
|
||||||
"label-md text-text-on-accent",
|
"label-md text-text-on-accent",
|
||||||
isDown ? "bg-surface-danger" : "bg-surface-accent",
|
isDown ? "bg-surface-danger" : "bg-surface-accent",
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
CARD - mechanics that cannot be expressed as a utility
|
CARD - mechanics that cannot be expressed as a utility
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
Everything token-driven lives in the TSX as a tier-2 utility
|
Everything token-driven lives in the TSX as a tier-2 utility
|
||||||
(bg-surface-raised, text-text-accent, rounded-surface, ...). This file only
|
(bg-surface-raised, text-text-accent, rounded-card, ...). This file only
|
||||||
holds the three things a utility cannot do:
|
holds the three things a utility cannot do:
|
||||||
|
|
||||||
1. paint an exported SVG asset in the current token colour (mask),
|
1. paint an exported SVG asset in the current token colour (mask),
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
component rather than by app/app.css, so it is not scoped by anything else.
|
component rather than by app/app.css, so it is not scoped by anything else.
|
||||||
|
|
||||||
Everything lives in @layer components so a Tailwind utility passed through
|
Everything lives in @layer components so a Tailwind utility passed through
|
||||||
`className` (`rounded-surface`, `text-text-secondary`, ...) still wins.
|
`className` (`rounded-card`, `text-text-secondary`, ...) still wins.
|
||||||
The layer statement is a defensive no-op when app.css has already declared
|
The layer statement is a defensive no-op when app.css has already declared
|
||||||
the same order, and makes this file correct on its own if it ever loads first.
|
the same order, and makes this file correct on its own if it ever loads first.
|
||||||
========================================================================= */
|
========================================================================= */
|
||||||
@@ -88,17 +88,29 @@
|
|||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
HOVER / FOCUS AFFORDANCES
|
HOVER / FOCUS AFFORDANCES
|
||||||
------------------------------------------------------------------------ */
|
------------------------------------------------------------------------ */
|
||||||
|
/* The drafts hover a card three ways at once: the hairline turns accent
|
||||||
|
green, the card rises, and a deep black lands under it. The lift is
|
||||||
|
pointer-only - a touch "hover" would leave the card raised after the tap -
|
||||||
|
and it is dropped entirely under prefers-reduced-motion below, because a
|
||||||
|
transform still lands instantly once app.css has flattened the duration. */
|
||||||
.sh-card--linked {
|
.sh-card--linked {
|
||||||
transition:
|
transition:
|
||||||
box-shadow var(--motion-duration-base) var(--motion-ease-standard),
|
box-shadow var(--motion-duration-base) var(--motion-ease-standard),
|
||||||
border-color var(--motion-duration-base) var(--motion-ease-standard);
|
border-color var(--motion-duration-base) var(--motion-ease-standard),
|
||||||
|
transform var(--motion-duration-base) var(--motion-ease-standard);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sh-card--linked:hover {
|
.sh-card--linked:hover {
|
||||||
border-color: var(--border-default);
|
border-color: var(--border-accent);
|
||||||
box-shadow: var(--elevation-popover);
|
box-shadow: var(--elevation-popover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.sh-card--linked:hover {
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.sh-card--linked:hover .sh-card-title,
|
.sh-card--linked:hover .sh-card-title,
|
||||||
.sh-card--linked:has(.sh-card-link:focus-visible) .sh-card-title {
|
.sh-card--linked:has(.sh-card-link:focus-visible) .sh-card-title {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
@@ -116,6 +128,7 @@
|
|||||||
/* app.css already neutralises transition durations under reduced motion, but a
|
/* app.css already neutralises transition durations under reduced motion, but a
|
||||||
transform still lands instantly - drop it entirely. */
|
transform still lands instantly - drop it entirely. */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.sh-card--linked:hover,
|
||||||
.sh-card--linked:hover .sh-card-media img {
|
.sh-card--linked:hover .sh-card-media img {
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
background-color: var(--field-bg);
|
background-color: var(--field-bg);
|
||||||
color: var(--field-fg);
|
color: var(--field-fg);
|
||||||
border: var(--control-border-width) solid var(--field-border);
|
border: var(--control-border-width) solid var(--field-border);
|
||||||
border-radius: var(--radius-control-lg);
|
border-radius: var(--radius-control);
|
||||||
padding-inline: calc(var(--control-padding-inline-md) - var(--control-border-width));
|
padding-inline: calc(var(--control-padding-inline-md) - var(--control-border-width));
|
||||||
padding-block: calc(var(--control-padding-block-md) - var(--control-border-width));
|
padding-block: calc(var(--control-padding-block-md) - var(--control-border-width));
|
||||||
text-align: start;
|
text-align: start;
|
||||||
|
|||||||
@@ -436,10 +436,14 @@ export function DesktopNav({
|
|||||||
className={cx(
|
className={cx(
|
||||||
"body-sm-bold flex cursor-pointer items-center gap-2xs",
|
"body-sm-bold flex cursor-pointer items-center gap-2xs",
|
||||||
"whitespace-nowrap pb-[2px] text-start",
|
"whitespace-nowrap pb-[2px] text-start",
|
||||||
"text-[var(--text-primary)]",
|
/* The drafts rest a nav label in text/secondary and
|
||||||
"hover:text-[var(--text-accent)]",
|
brighten it to primary under the pointer; the section
|
||||||
"aria-expanded:text-[var(--text-accent)]",
|
the reader is in stays primary and is marked by the
|
||||||
"data-[current=true]:text-[var(--text-accent)]",
|
accent rule below, not by green text. */
|
||||||
|
"text-[var(--text-secondary)]",
|
||||||
|
"hover:text-[var(--text-primary)]",
|
||||||
|
"aria-expanded:text-[var(--text-primary)]",
|
||||||
|
"data-[current=true]:text-[var(--text-primary)]",
|
||||||
HOVER_TRANSITION,
|
HOVER_TRANSITION,
|
||||||
FOCUS_RING,
|
FOCUS_RING,
|
||||||
)}
|
)}
|
||||||
@@ -512,9 +516,10 @@ export function DesktopNav({
|
|||||||
onFocus={closeGroup}
|
onFocus={closeGroup}
|
||||||
className={cx(
|
className={cx(
|
||||||
"body-sm-bold whitespace-nowrap pb-[2px] no-underline",
|
"body-sm-bold whitespace-nowrap pb-[2px] no-underline",
|
||||||
"text-[var(--text-primary)]",
|
/* Same rest/hover/current treatment as a group trigger. */
|
||||||
"hover:text-[var(--text-accent)]",
|
"text-[var(--text-secondary)]",
|
||||||
"aria-[current=page]:text-[var(--text-accent)]",
|
"hover:text-[var(--text-primary)]",
|
||||||
|
"aria-[current=page]:text-[var(--text-primary)]",
|
||||||
HOVER_TRANSITION,
|
HOVER_TRANSITION,
|
||||||
FOCUS_RING,
|
FOCUS_RING,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
type KeyboardEvent as ReactKeyboardEvent,
|
type KeyboardEvent as ReactKeyboardEvent,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
|
|
||||||
import { EuBadge } from "../EuBadge";
|
import { EuBadge } from "../EuBadge";
|
||||||
@@ -28,6 +29,18 @@ const FOCUSABLE_SELECTOR = [
|
|||||||
'[tabindex]:not([tabindex="-1"])',
|
'[tabindex]:not([tabindex="-1"])',
|
||||||
].join(", ");
|
].join(", ");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The hamburger and its close twin. The glyph is the drafts' 24px box, which is
|
||||||
|
* well under the 44x44 WCAG 2.5.5 target size, so the control is a 44px square
|
||||||
|
* around it. The negative inline-end margin gives back the 10px of padding that
|
||||||
|
* would otherwise push the glyph off the page gutter the wordmark opposite it
|
||||||
|
* lines up with - logical, so it mirrors in Arabic.
|
||||||
|
*/
|
||||||
|
const TAP_TARGET = cx(
|
||||||
|
"-me-[0.625rem] inline-flex size-11 shrink-0 cursor-pointer",
|
||||||
|
"items-center justify-center rounded-control",
|
||||||
|
);
|
||||||
|
|
||||||
function focusablesIn(root: HTMLElement): HTMLElement[] {
|
function focusablesIn(root: HTMLElement): HTMLElement[] {
|
||||||
return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter(
|
return Array.from(root.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter(
|
||||||
(element) => !element.hasAttribute("hidden") && element.getClientRects().length > 0,
|
(element) => !element.hasAttribute("hidden") && element.getClientRects().length > 0,
|
||||||
@@ -55,6 +68,23 @@ export interface MobileNavProps {
|
|||||||
* `inert` (so assistive technology cannot browse the page under an aria-modal
|
* `inert` (so assistive technology cannot browse the page under an aria-modal
|
||||||
* dialog either), the page behind cannot scroll, Escape closes it, and focus
|
* dialog either), the page behind cannot scroll, Escape closes it, and focus
|
||||||
* returns to the hamburger on close.
|
* returns to the hamburger on close.
|
||||||
|
*
|
||||||
|
* THE DRAWER IS PORTALLED TO <body>, AND HAS TO BE.
|
||||||
|
* The bar it hangs off paints the drafts' translucent ground with
|
||||||
|
* `backdrop-filter: blur(14px)`, and a filtered element becomes the containing
|
||||||
|
* block for every `position: fixed` descendant (CSS Filter Effects 1 s.6.1).
|
||||||
|
* Rendered in place, `fixed inset-0` therefore resolved against the 64px bar
|
||||||
|
* rather than the viewport: the panel painted a 64px strip and its menu spilled
|
||||||
|
* over the page with no background behind it. The header also carries
|
||||||
|
* `z-index: var(--layer-header)`, which is a stacking context, so an
|
||||||
|
* `aria-modal` dialog nested inside it could never rise above anything painted
|
||||||
|
* above the bar either. A portal fixes both at once, and it is where a modal
|
||||||
|
* dialog belongs regardless.
|
||||||
|
*
|
||||||
|
* The portal is client-only, so the panel joins the DOM on mount. Nothing is
|
||||||
|
* lost: the trigger is a button with an onClick, so without JavaScript the
|
||||||
|
* drawer could not be opened anyway, and every link in it is already in the
|
||||||
|
* server-rendered DesktopNav markup.
|
||||||
*/
|
*/
|
||||||
export function MobileNav({
|
export function MobileNav({
|
||||||
items,
|
items,
|
||||||
@@ -69,6 +99,9 @@ export function MobileNav({
|
|||||||
className,
|
className,
|
||||||
}: MobileNavProps) {
|
}: MobileNavProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
/* The portal target only exists in a browser, and the first client render has
|
||||||
|
to match the server's, so the panel is mounted one commit later. */
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||||
const closeRef = useRef<HTMLButtonElement>(null);
|
const closeRef = useRef<HTMLButtonElement>(null);
|
||||||
const drawerRef = useRef<HTMLDivElement>(null);
|
const drawerRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -81,6 +114,8 @@ export function MobileNav({
|
|||||||
|
|
||||||
const close = useCallback(() => setOpen(false), []);
|
const close = useCallback(() => setOpen(false), []);
|
||||||
|
|
||||||
|
useEffect(() => setMounted(true), []);
|
||||||
|
|
||||||
/* Focus in on open, and back to the hamburger on close. */
|
/* Focus in on open, and back to the hamburger on close. */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
@@ -94,11 +129,11 @@ export function MobileNav({
|
|||||||
|
|
||||||
/* The drawer is aria-modal, so the rest of the page has to be inert as well -
|
/* The drawer is aria-modal, so the rest of the page has to be inert as well -
|
||||||
otherwise a screen reader still browses the header, main and footer behind
|
otherwise a screen reader still browses the header, main and footer behind
|
||||||
it. Rather than naming those landmarks (the drawer is itself rendered
|
it. Rather than naming those landmarks, every ancestor up to <body> has its
|
||||||
inside the header, so inerting the header would inert the drawer), every
|
OTHER children inerted; from the portal that is simply body's other
|
||||||
ancestor up to <body> has its OTHER children inerted. `inert` is set as an
|
children, which is exactly the header, main and footer. `inert` is set as
|
||||||
attribute so no DOM-lib typing is needed, and an element that was already
|
an attribute so no DOM-lib typing is needed, and an element that was
|
||||||
inert is skipped so the restore never clears someone else's flag.
|
already inert is skipped so the restore never clears someone else's flag.
|
||||||
|
|
||||||
This runs after the focus effect above on purpose: focus is already inside
|
This runs after the focus effect above on purpose: focus is already inside
|
||||||
the drawer by the time the element it came from goes inert. */
|
the drawer by the time the element it came from goes inert. */
|
||||||
@@ -215,24 +250,7 @@ export function MobileNav({
|
|||||||
FOCUS_RING,
|
FOCUS_RING,
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
const panel = (
|
||||||
<>
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
aria-expanded={open}
|
|
||||||
aria-controls={drawerId}
|
|
||||||
aria-label={open ? strings.closeMenu : strings.openMenu}
|
|
||||||
onClick={() => setOpen((value) => !value)}
|
|
||||||
className={cx(
|
|
||||||
"inline-flex shrink-0 cursor-pointer items-center justify-center rounded-control xl:hidden",
|
|
||||||
FOCUS_RING,
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<MenuBars />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
id={drawerId}
|
id={drawerId}
|
||||||
ref={drawerRef}
|
ref={drawerRef}
|
||||||
@@ -256,10 +274,7 @@ export function MobileNav({
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label={strings.closeMenu}
|
aria-label={strings.closeMenu}
|
||||||
onClick={close}
|
onClick={close}
|
||||||
className={cx(
|
className={cx(TAP_TARGET, FOCUS_RING)}
|
||||||
"inline-flex shrink-0 cursor-pointer items-center justify-center rounded-control",
|
|
||||||
FOCUS_RING,
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<MenuBars open />
|
<MenuBars open />
|
||||||
</button>
|
</button>
|
||||||
@@ -341,6 +356,23 @@ export function MobileNav({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
ref={triggerRef}
|
||||||
|
type="button"
|
||||||
|
aria-expanded={open}
|
||||||
|
aria-controls={drawerId}
|
||||||
|
aria-label={open ? strings.closeMenu : strings.openMenu}
|
||||||
|
onClick={() => setOpen((value) => !value)}
|
||||||
|
className={cx(TAP_TARGET, "xl:hidden", FOCUS_RING, className)}
|
||||||
|
>
|
||||||
|
<MenuBars />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{mounted ? createPortal(panel, document.body) : null}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -409,7 +409,7 @@ export function ConsentCheckbox({
|
|||||||
required
|
required
|
||||||
aria-invalid={error ? true : undefined}
|
aria-invalid={error ? true : undefined}
|
||||||
aria-describedby={describedBy}
|
aria-describedby={describedBy}
|
||||||
className="mt-2xs size-md shrink-0 rounded-[var(--radius-control)] border border-border-strong accent-[var(--interactive-primary-bg)]"
|
className="mt-2xs size-md shrink-0 rounded-chip border border-border-strong accent-[var(--interactive-primary-bg)]"
|
||||||
/>
|
/>
|
||||||
<label htmlFor={name} className="body-sm text-text-primary">
|
<label htmlFor={name} className="body-sm text-text-primary">
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@@ -563,27 +563,24 @@ export function PageHero({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* The colour band (478:16360): three blocks flush under the
|
{/* The colour band (478:16360, redrawn in the drafts' home
|
||||||
picture, 6px tall. Figma lays out 390 / 495 / 363 inside a
|
hero): three blocks flush under the picture, 6px tall, split
|
||||||
1244-wide clipping frame, so the last block loses 4px and the
|
390 : 495 : 363 and carried as flex-grow ratios so the split
|
||||||
split that is actually drawn is 390 : 495 : 359 - carried here
|
holds at every width instead of only at the drafts' 1244.
|
||||||
as flex-grow ratios so it holds at every width instead of only
|
|
||||||
at 1244. The three fills are `background/accent`,
|
The fills are `background/accent`, `background/inverse` and
|
||||||
`background/inverse` and `background/danger`, which is why the
|
`background/danger` - the drafts paint the middle block
|
||||||
middle block reads white on the dark hero and near-black on a
|
#F8F9FA, so the stripe reads green / white / red on the dark
|
||||||
light surface.
|
hero and flips to green / near-black / red on a light
|
||||||
|
surface.
|
||||||
|
|
||||||
Purely decorative, so it is hidden from assistive technology;
|
Purely decorative, so it is hidden from assistive technology;
|
||||||
as an ordinary flex row it mirrors in Arabic along with the
|
as an ordinary flex row it mirrors in Arabic along with the
|
||||||
rest of the page. */}
|
rest of the page. */}
|
||||||
<div aria-hidden="true" className="sh-bar-grow flex h-[6px] w-full">
|
<div aria-hidden="true" className="sh-bar-grow flex h-[6px] w-full">
|
||||||
<span className="basis-0 grow-[390] bg-[var(--surface-accent)]" />
|
<span className="basis-0 grow-[390] bg-[var(--surface-accent)]" />
|
||||||
{/* The middle block is the raised dark, not the inverse:
|
<span className="basis-0 grow-[495] bg-[var(--surface-inverse)]" />
|
||||||
the redesign drops pure white from the page entirely, so
|
<span className="basis-0 grow-[363] bg-[var(--surface-danger)]" />
|
||||||
the stripe reads green / dark / red on the near-black
|
|
||||||
band instead of green / white / red. */}
|
|
||||||
<span className="basis-0 grow-[495] bg-[var(--surface-raised)]" />
|
|
||||||
<span className="basis-0 grow-[359] bg-[var(--surface-danger)]" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -792,7 +792,7 @@ export function FacetGroup({
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={cx(
|
className={cx(
|
||||||
"mt-2xs flex size-[0.875rem] shrink-0 items-center justify-center",
|
"mt-2xs flex size-[0.875rem] shrink-0 items-center justify-center",
|
||||||
"rounded-[var(--radius-control)] border",
|
"rounded-chip border",
|
||||||
option.isCurrent
|
option.isCurrent
|
||||||
? "border-border-accent"
|
? "border-border-accent"
|
||||||
: "border-border-default",
|
: "border-border-default",
|
||||||
|
|||||||
@@ -466,7 +466,7 @@ function StreamDisclosure({
|
|||||||
|
|
||||||
<h3 className="flex-1 heading-h3">{copy.title}</h3>
|
<h3 className="flex-1 heading-h3">{copy.title}</h3>
|
||||||
|
|
||||||
<span className="hidden shrink-0 rounded-[var(--radius-control)] bg-surface-accent-subtle px-xs py-2xs label-md uppercase text-text-accent sm:inline-block">
|
<span className="hidden shrink-0 rounded-chip bg-surface-accent-subtle px-xs py-2xs label-md uppercase text-text-accent sm:inline-block">
|
||||||
{copy.partners}
|
{copy.partners}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|||||||
@@ -638,7 +638,7 @@ function MonthCell({ day, locale }: { day: CalendarDay; locale: Locale }) {
|
|||||||
to={entry.href}
|
to={entry.href}
|
||||||
prefetch="intent"
|
prefetch="intent"
|
||||||
className={[
|
className={[
|
||||||
"rounded-[var(--radius-control)] border-s-2 border-s-[var(--border-accent)]",
|
"rounded-chip border-s-2 border-s-[var(--border-accent)]",
|
||||||
"bg-surface-accent-subtle px-2xs py-2xs body-sm-bold text-text-primary",
|
"bg-surface-accent-subtle px-2xs py-2xs body-sm-bold text-text-primary",
|
||||||
"transition-colors duration-[var(--motion-duration-fast)]",
|
"transition-colors duration-[var(--motion-duration-fast)]",
|
||||||
"ease-[var(--motion-ease-standard)] hover:text-text-accent",
|
"ease-[var(--motion-ease-standard)] hover:text-text-accent",
|
||||||
|
|||||||
@@ -664,7 +664,7 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
|||||||
key={member.name}
|
key={member.name}
|
||||||
className="sh-lift sh-reveal sh-reveal--in flex h-full flex-col items-start gap-sm rounded-card border border-[var(--border-subtle)] bg-[var(--surface-card)] p-xl hover:border-[var(--border-accent)]"
|
className="sh-lift sh-reveal sh-reveal--in flex h-full flex-col items-start gap-sm rounded-card border border-[var(--border-subtle)] bg-[var(--surface-card)] p-xl hover:border-[var(--border-accent)]"
|
||||||
>
|
>
|
||||||
<span className="rounded-control bg-[var(--surface-accent-subtle)] px-xs py-2xs label-md uppercase text-[var(--text-accent)]">
|
<span className="rounded-chip bg-[var(--surface-accent-subtle)] px-xs py-2xs label-md uppercase text-[var(--text-accent)]">
|
||||||
{member.role}
|
{member.role}
|
||||||
</span>
|
</span>
|
||||||
<h3 className="heading-h3 text-[var(--text-primary)]">{member.name}</h3>
|
<h3 className="heading-h3 text-[var(--text-primary)]">{member.name}</h3>
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ function NewsMetaRow({ entry }: { entry: NewsListItem }) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center gap-sm">
|
<div className="flex flex-wrap items-center gap-sm">
|
||||||
{entry.tag ? (
|
{entry.tag ? (
|
||||||
<span className="rounded-[var(--radius-control)] bg-surface-accent-subtle px-xs py-2xs label-md uppercase text-text-accent">
|
<span className="rounded-chip bg-surface-accent-subtle px-xs py-2xs label-md uppercase text-text-accent">
|
||||||
{entry.tag}
|
{entry.tag}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -515,7 +515,7 @@ function ResourceRow({
|
|||||||
{entry.fileKind ? (
|
{entry.fileKind ? (
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="flex h-14 w-11 shrink-0 items-center justify-center rounded-[var(--radius-control)] border border-border-default bg-surface-sunken label-md text-text-accent"
|
className="flex h-14 w-11 shrink-0 items-center justify-center rounded-chip border border-border-default bg-surface-sunken label-md text-text-accent"
|
||||||
>
|
>
|
||||||
{entry.fileKind}
|
{entry.fileKind}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ A Tailwind 4 `@theme static` block. Raw ramps only, named by value:
|
|||||||
`--font-weight-regular..black`
|
`--font-weight-regular..black`
|
||||||
* space `--spacing-2xs..5xl` - radius `--radius-none..full` -
|
* space `--spacing-2xs..5xl` - radius `--radius-none..full` -
|
||||||
border width `--border-width-sm..xl` - opacity `--opacity-0..100`
|
border width `--border-width-sm..xl` - opacity `--opacity-0..100`
|
||||||
* shadow `--shadow-xs..xl` - duration `--duration-instant..slower` -
|
* shadow `--shadow-xs..xl` plus the dark-ground steps `--shadow-dark-lg|xl` -
|
||||||
|
duration `--duration-instant..slower` -
|
||||||
easing `--ease-standard | decelerate | accelerate | emphasized`
|
easing `--ease-standard | decelerate | accelerate | emphasized`
|
||||||
* z ladder `--z-0..70` - containers `--container-prose | content | wide`
|
* z ladder `--z-0..70` - containers `--container-prose | content | wide`
|
||||||
|
|
||||||
@@ -90,6 +91,25 @@ Plain CSS custom properties whose value is always `var(--<primitive>)`. Families
|
|||||||
`--focus-ring-*` `--overlay-*` `--radius-*` `--control-*` `--layout-*`
|
`--focus-ring-*` `--overlay-*` `--radius-*` `--control-*` `--layout-*`
|
||||||
`--elevation-*` `--motion-*` `--layer-*` `--type-body-base-*`
|
`--elevation-*` `--motion-*` `--layer-*` `--type-body-base-*`
|
||||||
|
|
||||||
|
Two families are worth knowing by heart, because both were retuned to the
|
||||||
|
Claude Design drafts rather than to the older Figma Button frame:
|
||||||
|
|
||||||
|
* **Shape.** The drafts draw three corners, not five. `--radius-chip` (2px) for
|
||||||
|
a tag, thumbnail, flag or checkbox; `--radius-control` / `--radius-card` /
|
||||||
|
`--radius-surface` (all 4px) for a button, field, menu, card or panel;
|
||||||
|
`--radius-pill` for a filter chip or status pill. Nothing in the drafts is
|
||||||
|
8px or 12px, which is why the old `--radius-control-lg` is gone. The tier-1
|
||||||
|
ramp still carries `--radius-md` / `--radius-lg` - only the meanings moved.
|
||||||
|
* **Elevation.** A shadow is a fraction of the ground it falls on, so
|
||||||
|
`--elevation-card`, `--elevation-popover` and `--elevation-modal` are
|
||||||
|
declared **per surface**. On the dark surface a resting card has no shadow at
|
||||||
|
all (the drafts separate with a hairline) and the deep black - tier 1's
|
||||||
|
`--shadow-dark-lg` / `--shadow-dark-xl`, the drafts' own
|
||||||
|
`0 20px 44px rgba(0,0,0,.5)` and `0 24px 48px rgba(0,0,0,.6)` - is spent on
|
||||||
|
what actually floats: a card under the pointer, the nav dropdown. The light
|
||||||
|
surface keeps the soft ramp. Only `--elevation-flat` and `--elevation-raised`
|
||||||
|
are surface-independent.
|
||||||
|
|
||||||
Interactive tokens are fully regular - for each of the five Figma button
|
Interactive tokens are fully regular - for each of the five Figma button
|
||||||
variants (`primary`, `secondary`, `dark`, `danger`, `ghost`) there is
|
variants (`primary`, `secondary`, `dark`, `danger`, `ghost`) there is
|
||||||
|
|
||||||
@@ -163,12 +183,14 @@ Filenames are case-sensitive in production - match the table exactly.
|
|||||||
|
|
||||||
## Known deviation from Figma
|
## Known deviation from Figma
|
||||||
|
|
||||||
The published Ghost button hover paints `background/accent` (`#008764`) and keeps
|
**Ghost is a text link, not a button.** The published Figma Ghost cell fills on
|
||||||
`base/turquoise-dark` (`#006b4f`) as the label - 1.40:1, which fails WCAG AA.
|
hover (`background/accent` behind a `base/turquoise-dark` label - 1.40:1, a
|
||||||
`--interactive-ghost-fg-hover` is therefore `--color-white` on **both** surfaces
|
WCAG AA failure). The Claude Design drafts never fill a ghost control: every
|
||||||
(4.52:1, passes AA for body text). The two lines are marked `(a11y)` in
|
"All updates ->" / "What we do ->" is plain text in `--text-primary` that turns
|
||||||
`semantic.css`; revert them to `var(--color-green-700)` for pixel-exact parity
|
accent green under the pointer while the label-to-arrow gap opens from 8px to
|
||||||
if the designer prefers the failure.
|
12px. The drafts win, so the ghost tokens carry no fill in any state, the
|
||||||
|
variant opts out of the 3px lift in `Button.css`, and the old a11y workaround
|
||||||
|
is no longer needed.
|
||||||
|
|
||||||
## Contrast audit
|
## Contrast audit
|
||||||
|
|
||||||
@@ -179,25 +201,26 @@ bold) and for non-text UI boundaries (1.4.11).
|
|||||||
|
|
||||||
### Dark surface (`:root`, `[data-surface="dark"]`)
|
### Dark surface (`:root`, `[data-surface="dark"]`)
|
||||||
|
|
||||||
| text token | page `#1d1d1b` | raised `#2e2e2c` | sunken `#0b0b0a` | muted (white 8%) | accent-subtle (green 24%) | danger-subtle (red 24%) |
|
| text token | page `#1d1d1b` | raised `#141413` | sunken `#0b0b0a` | muted (white 8%) | accent-subtle (green 24%) | danger-subtle (red 24%) |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| `--text-primary` neutral-50 | 16.02 | 12.91 | 18.68 | 12.71 | 12.40 | 12.86 |
|
| `--text-primary` neutral-50 | 16.02 | 17.49 | 18.68 | 12.71 | 12.40 | 12.86 |
|
||||||
| `--text-secondary` neutral-300 | 9.46 | 7.63 | 11.04 | 7.51 | 7.32 | 7.60 |
|
| `--text-secondary` neutral-300 | 9.46 | 10.33 | 11.04 | 7.51 | 7.32 | 7.60 |
|
||||||
| `--text-tertiary` neutral-400 | 5.56 | 4.48 | 6.48 | 4.41 | 4.30 | 4.46 |
|
| `--text-tertiary` neutral-400 | 5.56 | 6.07 | 6.48 | 4.41 | 4.30 | 4.46 |
|
||||||
| `--text-accent` / `--text-link` green-400 | 6.02 | 4.85 | 7.02 | 4.78 | 4.66 | 4.83 |
|
| `--text-accent` / `--text-link` green-400 | 6.02 | 6.57 | 7.02 | 4.78 | 4.66 | 4.83 |
|
||||||
| `--text-link-hover` green-300 | 8.56 | 6.90 | 9.99 | 6.80 | 6.63 | 6.87 |
|
| `--text-link-hover` green-300 | 8.56 | 9.35 | 9.99 | 6.80 | 6.63 | 6.87 |
|
||||||
| `--text-danger` red-400 | 5.39 | 4.34 | 6.29 | 4.28 | 4.17 | 4.33 |
|
| `--text-danger` red-400 | 5.39 | 5.89 | 6.29 | 4.28 | 4.17 | 4.33 |
|
||||||
| `--text-disabled` neutral-500 | 2.36 | 1.91 | 2.76 | 1.88 | 1.83 | 1.90 |
|
| `--text-disabled` neutral-500 | 2.36 | 2.58 | 2.76 | 1.88 | 1.83 | 1.90 |
|
||||||
|
|
||||||
`--text-primary`, `--text-secondary` and `--text-link-hover` pass AA everywhere.
|
`--text-primary`, `--text-secondary` and `--text-link-hover` pass AA everywhere.
|
||||||
Everything below them is only cleared against `--surface-page` and
|
Everything below them is only cleared against `--surface-page` and
|
||||||
`--surface-sunken`, which is where the design actually uses them:
|
`--surface-sunken`, which is where the design actually uses them:
|
||||||
|
|
||||||
|
* `--surface-raised` is the drafts' `#141413`, DARKER than the page, so every
|
||||||
|
role gains contrast on it rather than losing it - the whole column clears AA.
|
||||||
* `--text-tertiary`, `--text-accent`, `--text-link` and `--text-danger` land
|
* `--text-tertiary`, `--text-accent`, `--text-link` and `--text-danger` land
|
||||||
between 4.17 and 4.85 on `--surface-raised` and on the three translucent
|
between 4.17 and 4.78 on the three translucent fills - AA for large text,
|
||||||
fills - AA for large text, **not** for body text. Card bodies use
|
**not** for body text. Card bodies use `--text-secondary`; keep
|
||||||
`--text-secondary`; keep tertiary/accent/danger body copy on the page or
|
tertiary/accent/danger body copy on an opaque surface, or raise it a step.
|
||||||
sunken surface, or raise the text a step.
|
|
||||||
* `--text-disabled` is 2.36:1 by design. WCAG 1.4.3 exempts the label of a
|
* `--text-disabled` is 2.36:1 by design. WCAG 1.4.3 exempts the label of a
|
||||||
disabled control, so this is deliberate and must not be repointed - it is
|
disabled control, so this is deliberate and must not be repointed - it is
|
||||||
the only token allowed below 3:1.
|
the only token allowed below 3:1.
|
||||||
@@ -250,16 +273,18 @@ Everything below them is only cleared against `--surface-page` and
|
|||||||
| dark white / `bg-hover` black | 21.00 |
|
| dark white / `bg-hover` black | 21.00 |
|
||||||
| dark `fg-disabled` neutral-300 / `bg-disabled` neutral-600 (dark surface) | 5.39 |
|
| dark `fg-disabled` neutral-300 / `bg-disabled` neutral-600 (dark surface) | 5.39 |
|
||||||
| dark `fg-disabled` neutral-200 / `bg-disabled` neutral-500 (light surface) | 4.91 |
|
| dark `fg-disabled` neutral-200 / `bg-disabled` neutral-500 (light surface) | 4.91 |
|
||||||
| **ghost `fg-hover` white / `bg-hover` green-600** (was green-900, 2.62) | **4.52** |
|
| ghost `fg` neutral-50 / dark page (no fill in any state) | 16.02 |
|
||||||
| ghost white / `bg-active` green-700 | 6.53 |
|
| ghost `fg-hover` green-400 / dark page | 6.02 |
|
||||||
| ghost `fg` green-400 / dark page | 6.02 |
|
| ghost `fg` neutral-800 / light page | 16.88 |
|
||||||
| ghost `fg` green-600 / light page | 4.52 |
|
| ghost `fg-hover` green-700 / light page | 6.53 |
|
||||||
|
| **ghost `fg-disabled` neutral-500 / dark page** | **2.36** |
|
||||||
|
| **ghost `fg-disabled` neutral-400 / light page** | **3.04** |
|
||||||
| secondary `fg` neutral-50 / dark page | 16.02 |
|
| secondary `fg` neutral-50 / dark page | 16.02 |
|
||||||
| secondary `fg` neutral-800 / light page | 16.88 |
|
| secondary `fg` neutral-800 / light page | 16.88 |
|
||||||
| **secondary `fg-disabled` neutral-500 / dark page** | **2.36** |
|
| **secondary `fg-disabled` neutral-500 / dark page** | **2.36** |
|
||||||
| **secondary `fg-disabled` neutral-400 / light page** | **3.04** |
|
| **secondary `fg-disabled` neutral-400 / light page** | **3.04** |
|
||||||
|
|
||||||
The four bolded disabled rows are the WCAG 1.4.3 exemption again - a disabled
|
The bolded disabled rows are the WCAG 1.4.3 exemption again - a disabled
|
||||||
control has no contrast minimum. Every enabled pair clears 4.5:1.
|
control has no contrast minimum. Every enabled pair clears 4.5:1.
|
||||||
|
|
||||||
### Fields and boundaries
|
### Fields and boundaries
|
||||||
|
|||||||
@@ -210,12 +210,25 @@
|
|||||||
|
|
||||||
/* --------------------------------------------------------------------
|
/* --------------------------------------------------------------------
|
||||||
SHADOW ramp
|
SHADOW ramp
|
||||||
|
|
||||||
|
Two ramps, because a shadow is a fraction of the ground it falls on.
|
||||||
|
The soft steps are tuned for a white page; on the drafts' near-black
|
||||||
|
ground a 12-28% black is simply invisible, so the deep steps below carry
|
||||||
|
the two shadows the drafts actually draw:
|
||||||
|
card hover 0 20px 44px rgba(0,0,0,0.5)
|
||||||
|
nav dropdown 0 24px 48px rgba(0,0,0,0.6)
|
||||||
|
`semantic.css` picks the ramp per surface via --elevation-*.
|
||||||
-------------------------------------------------------------------- */
|
-------------------------------------------------------------------- */
|
||||||
--shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.06);
|
--shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.06);
|
||||||
--shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.08);
|
--shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.08);
|
||||||
--shadow-md: 0 4px 12px -2px rgb(0 0 0 / 0.12), 0 2px 4px -2px rgb(0 0 0 / 0.08);
|
--shadow-md: 0 4px 12px -2px rgb(0 0 0 / 0.12), 0 2px 4px -2px rgb(0 0 0 / 0.08);
|
||||||
--shadow-lg: 0 12px 28px -6px rgb(0 0 0 / 0.18), 0 4px 8px -4px rgb(0 0 0 / 0.1);
|
--shadow-lg: 0 12px 28px -6px rgb(0 0 0 / 0.18), 0 4px 8px -4px rgb(0 0 0 / 0.1);
|
||||||
--shadow-xl: 0 24px 48px -12px rgb(0 0 0 / 0.28);
|
--shadow-xl: 0 24px 48px -12px rgb(0 0 0 / 0.28);
|
||||||
|
|
||||||
|
/* [drafts] the deep steps, for a shadow falling on the dark ground */
|
||||||
|
--shadow-dark-lg: 0 20px 44px rgb(0 0 0 / 0.5);
|
||||||
|
--shadow-dark-xl: 0 24px 48px rgb(0 0 0 / 0.6);
|
||||||
|
|
||||||
--shadow-none: 0 0 #0000;
|
--shadow-none: 0 0 #0000;
|
||||||
|
|
||||||
/* --------------------------------------------------------------------
|
/* --------------------------------------------------------------------
|
||||||
|
|||||||
@@ -121,19 +121,18 @@
|
|||||||
--interactive-danger-border-disabled: var(--color-transparent);
|
--interactive-danger-border-disabled: var(--color-transparent);
|
||||||
|
|
||||||
/* --- interactive: ghost (text-only) -------------------------------------
|
/* --- interactive: ghost (text-only) -------------------------------------
|
||||||
DEVIATION FROM FIGMA (a11y). The published component paints the hover
|
The drafts never fill a ghost control. Every "All updates ->" / "What we
|
||||||
background/accent (#008764) and keeps base/turquoise-dark (#006b4f) as
|
do ->" link is plain text in --text-primary that turns accent green under
|
||||||
the label - 1.40:1, far below WCAG AA. Both surfaces therefore use white
|
the pointer while the label-to-arrow gap opens; the fill the old Figma
|
||||||
on the accent fill, which is 4.52:1 and passes AA for body text. The two
|
Button frame painted on hover (background/accent behind a green label,
|
||||||
lines marked (a11y) are the only ones that differ from Figma; reverting
|
1.40:1) is gone, and with it the a11y deviation it forced. */
|
||||||
them to var(--color-green-700) restores pixel parity and the failure. */
|
|
||||||
--interactive-ghost-bg: var(--color-transparent);
|
--interactive-ghost-bg: var(--color-transparent);
|
||||||
--interactive-ghost-bg-hover: var(--color-green-600);
|
--interactive-ghost-bg-hover: var(--color-transparent);
|
||||||
--interactive-ghost-bg-active: var(--color-green-700);
|
--interactive-ghost-bg-active: var(--color-transparent);
|
||||||
--interactive-ghost-bg-disabled: var(--color-transparent);
|
--interactive-ghost-bg-disabled: var(--color-transparent);
|
||||||
--interactive-ghost-fg: var(--color-green-400);
|
--interactive-ghost-fg: var(--color-neutral-50);
|
||||||
--interactive-ghost-fg-hover: var(--color-white); /* (a11y) 4.52:1 on green-600; figma: green-700 = 1.40:1 */
|
--interactive-ghost-fg-hover: var(--color-green-400); /* 6.02:1 on the page ground */
|
||||||
--interactive-ghost-fg-disabled: var(--color-green-400);
|
--interactive-ghost-fg-disabled: var(--color-neutral-500);
|
||||||
--interactive-ghost-border: var(--color-transparent);
|
--interactive-ghost-border: var(--color-transparent);
|
||||||
--interactive-ghost-border-hover: var(--color-transparent);
|
--interactive-ghost-border-hover: var(--color-transparent);
|
||||||
--interactive-ghost-border-disabled: var(--color-transparent);
|
--interactive-ghost-border-disabled: var(--color-transparent);
|
||||||
@@ -165,6 +164,14 @@
|
|||||||
they carry the same value on either surface. */
|
they carry the same value on either surface. */
|
||||||
--overlay-picture-shade: var(--color-black-a20);
|
--overlay-picture-shade: var(--color-black-a20);
|
||||||
--overlay-picture-tint: var(--color-green-200-a12);
|
--overlay-picture-tint: var(--color-green-200-a12);
|
||||||
|
|
||||||
|
/* --- elevation ----------------------------------------------------------
|
||||||
|
The drafts give a resting card a hairline and no shadow at all, and keep
|
||||||
|
the black for what genuinely floats: a card under the pointer and the
|
||||||
|
navigation dropdown. */
|
||||||
|
--elevation-card: var(--shadow-none);
|
||||||
|
--elevation-popover: var(--shadow-dark-lg);
|
||||||
|
--elevation-modal: var(--shadow-dark-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
@@ -262,14 +269,14 @@
|
|||||||
--interactive-danger-border-hover: var(--color-transparent);
|
--interactive-danger-border-hover: var(--color-transparent);
|
||||||
--interactive-danger-border-disabled: var(--color-transparent);
|
--interactive-danger-border-disabled: var(--color-transparent);
|
||||||
|
|
||||||
/* --- interactive: ghost (text-only) - see the a11y note above ---------- */
|
/* --- interactive: ghost (text-only) - see the note above --------------- */
|
||||||
--interactive-ghost-bg: var(--color-transparent);
|
--interactive-ghost-bg: var(--color-transparent);
|
||||||
--interactive-ghost-bg-hover: var(--color-green-600);
|
--interactive-ghost-bg-hover: var(--color-transparent);
|
||||||
--interactive-ghost-bg-active: var(--color-green-700);
|
--interactive-ghost-bg-active: var(--color-transparent);
|
||||||
--interactive-ghost-bg-disabled: var(--color-transparent);
|
--interactive-ghost-bg-disabled: var(--color-transparent);
|
||||||
--interactive-ghost-fg: var(--color-green-600);
|
--interactive-ghost-fg: var(--color-neutral-800);
|
||||||
--interactive-ghost-fg-hover: var(--color-white); /* (a11y) 4.52:1 on green-600; figma: green-700 = 1.40:1 */
|
--interactive-ghost-fg-hover: var(--color-green-700); /* 6.53:1 on white */
|
||||||
--interactive-ghost-fg-disabled: var(--color-green-600);
|
--interactive-ghost-fg-disabled: var(--color-neutral-400);
|
||||||
--interactive-ghost-border: var(--color-transparent);
|
--interactive-ghost-border: var(--color-transparent);
|
||||||
--interactive-ghost-border-hover: var(--color-transparent);
|
--interactive-ghost-border-hover: var(--color-transparent);
|
||||||
--interactive-ghost-border-disabled: var(--color-transparent);
|
--interactive-ghost-border-disabled: var(--color-transparent);
|
||||||
@@ -299,6 +306,11 @@
|
|||||||
they carry the same value on either surface. */
|
they carry the same value on either surface. */
|
||||||
--overlay-picture-shade: var(--color-black-a20);
|
--overlay-picture-shade: var(--color-black-a20);
|
||||||
--overlay-picture-tint: var(--color-green-200-a12);
|
--overlay-picture-tint: var(--color-green-200-a12);
|
||||||
|
|
||||||
|
/* --- elevation - the soft ramp; the deep black belongs on the dark ground */
|
||||||
|
--elevation-card: var(--shadow-md);
|
||||||
|
--elevation-popover: var(--shadow-lg);
|
||||||
|
--elevation-modal: var(--shadow-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
@@ -308,7 +320,7 @@
|
|||||||
------------------------------------------------------------------------ */
|
------------------------------------------------------------------------ */
|
||||||
:root {
|
:root {
|
||||||
/* --- shape -------------------------------------------------------------
|
/* --- shape -------------------------------------------------------------
|
||||||
--radius-control / --radius-control-lg / --radius-card / --radius-surface
|
--radius-chip / --radius-control / --radius-card / --radius-surface
|
||||||
/ --radius-pill are declared in the @theme block near the bottom of this
|
/ --radius-pill are declared in the @theme block near the bottom of this
|
||||||
file so that they also generate rounded-* utilities. They are tier-2
|
file so that they also generate rounded-* utilities. They are tier-2
|
||||||
tokens like everything else here. */
|
tokens like everything else here. */
|
||||||
@@ -348,12 +360,13 @@
|
|||||||
--layout-content-max: var(--container-content);
|
--layout-content-max: var(--container-content);
|
||||||
--layout-prose-max: var(--container-prose);
|
--layout-prose-max: var(--container-prose);
|
||||||
|
|
||||||
/* --- elevation ---------------------------------------------------------- */
|
/* --- elevation ----------------------------------------------------------
|
||||||
|
Only the two flattest steps are surface-independent. A shadow is a
|
||||||
|
fraction of the ground it falls on, so --elevation-card / -popover /
|
||||||
|
-modal are declared inside each surface block above: the dark surface
|
||||||
|
takes the drafts' deep black, the light one keeps the soft ramp. */
|
||||||
--elevation-flat: var(--shadow-none);
|
--elevation-flat: var(--shadow-none);
|
||||||
--elevation-raised: var(--shadow-sm);
|
--elevation-raised: var(--shadow-sm);
|
||||||
--elevation-card: var(--shadow-md);
|
|
||||||
--elevation-popover: var(--shadow-lg);
|
|
||||||
--elevation-modal: var(--shadow-xl);
|
|
||||||
|
|
||||||
/* --- focus ring geometry ------------------------------------------------- */
|
/* --- focus ring geometry ------------------------------------------------- */
|
||||||
--focus-ring-width: var(--border-width-lg);
|
--focus-ring-width: var(--border-width-lg);
|
||||||
@@ -430,7 +443,6 @@
|
|||||||
--color-interactive-dark-hover: var(--interactive-dark-bg-hover);
|
--color-interactive-dark-hover: var(--interactive-dark-bg-hover);
|
||||||
--color-interactive-danger: var(--interactive-danger-bg);
|
--color-interactive-danger: var(--interactive-danger-bg);
|
||||||
--color-interactive-danger-hover: var(--interactive-danger-bg-hover);
|
--color-interactive-danger-hover: var(--interactive-danger-bg-hover);
|
||||||
--color-interactive-ghost-hover: var(--interactive-ghost-bg-hover);
|
|
||||||
|
|
||||||
--color-field-bg: var(--field-bg);
|
--color-field-bg: var(--field-bg);
|
||||||
--color-field-border: var(--field-border);
|
--color-field-border: var(--field-border);
|
||||||
@@ -443,13 +455,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Shape semantics that also need to exist as Tailwind utilities
|
/* Shape semantics that also need to exist as Tailwind utilities
|
||||||
(rounded-control / rounded-card / rounded-surface). They alias tier 1
|
(rounded-chip / rounded-control / rounded-card / rounded-surface). They
|
||||||
directly - never a tier-2 name, which would self-reference. */
|
alias tier 1 directly - never a tier-2 name, which would self-reference.
|
||||||
|
|
||||||
|
THE DRAFTS DRAW THREE CORNERS, NOT FIVE. Every panel, card, button, input
|
||||||
|
and menu in the Claude Design drafts is 4px; every tag, thumbnail, flag and
|
||||||
|
checkbox is 2px; filter chips and the status pill are fully round. Nothing
|
||||||
|
in the drafts is 8px or 12px, so `--radius-card` and `--radius-surface` come
|
||||||
|
down to radius/sm and the old `--radius-control-lg` (radius/md, a quirk of
|
||||||
|
the Figma Button frame's ghost cells) is gone. The tier-1 ramp is untouched:
|
||||||
|
only the meanings moved. */
|
||||||
@theme static {
|
@theme static {
|
||||||
--radius-control: var(--radius-sm);
|
--radius-chip: var(--radius-xs); /* 2px - tags, thumbnails, checkboxes */
|
||||||
--radius-control-lg: var(--radius-md);
|
--radius-control: var(--radius-sm); /* 4px - buttons, fields, menu items */
|
||||||
--radius-card: var(--radius-md);
|
--radius-card: var(--radius-sm); /* 4px */
|
||||||
--radius-surface: var(--radius-lg);
|
--radius-surface: var(--radius-sm); /* 4px */
|
||||||
--radius-pill: var(--radius-full);
|
--radius-pill: var(--radius-full);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user