diff --git a/src/components/api-badge/PlatformBadge.css b/src/components/api-badge/PlatformBadge.css index 4cf2c9477..516d7d9e6 100644 --- a/src/components/api-badge/PlatformBadge.css +++ b/src/components/api-badge/PlatformBadge.css @@ -1,175 +1,168 @@ /* - * Platform-specific color overrides for PlatformBadge. + * PlatformBadge styling. Single source of truth for platform color is + * platform-navigation/platform-colors.ts, whose hex values are mirrored + * below. If you change one, change both. * - * Overrides all Rspress Badge type variables (info, warning, danger) - * so the badge always renders in the platform's color. - * - * Color values match PLATFORM_CONFIG in src/components/api-status/constants.tsx: - * android → emerald - * ios → blue - * harmony → orange - * web_lynx → purple - * clay → teal - * clay_android → teal - * clay_ios → cyan - * clay_macos → indigo - * clay_windows → sky - * - * Light mode: Tailwind {color}-700 for text, {color}-500 at ~10% for bg. - * Dark mode: Tailwind {color}-400 for text, {color}-500 at ~15% for bg. + * Design intent: + * - Icon stays in full platform color so the badge is still scannable. + * - Background drops to a tinted 50/950 wash. Less ink, same signal. + * - The semantic variants default, *Only, No* read as three distinct + * things instead of "same chip, different word": + * default platform color, subtle bg + * .platform-badge--only adds an inset ring in the platform color + * .platform-badge--no drops platform color, mutes, strikes the label */ -/* android → emerald */ -.platform-badge-android { - --rp-container-info-text: #047857; - --rp-container-info-bg: #10b9811a; - --rp-container-warning-text: #047857; - --rp-container-warning-bg: #10b9811a; - --rp-container-danger-text: #047857; - --rp-container-danger-bg: #10b9811a; +/* ── Base: tighten Rspress's container variables to a quiet, tinted wash ── */ + +/* apple family (ios, macos) → zinc */ +.platform-badge-ios, +.platform-badge-clay_ios, +.platform-badge-macos, +.platform-badge-clay_macos { + --platform-badge-fg: #52525b; /* zinc-600 */ + --platform-badge-bg: #fafafa; /* zinc-50 */ + --platform-badge-ring: #d4d4d8; /* zinc-300 */ } -/* ios → blue */ -.platform-badge-ios { - --rp-container-info-text: #1d4ed8; - --rp-container-info-bg: #3b82f61a; - --rp-container-warning-text: #1d4ed8; - --rp-container-warning-bg: #3b82f61a; - --rp-container-danger-text: #1d4ed8; - --rp-container-danger-bg: #3b82f61a; +/* android → emerald */ +.platform-badge-android { + --platform-badge-fg: #059669; + --platform-badge-bg: #ecfdf5; + --platform-badge-ring: #a7f3d0; } -/* harmony → orange */ +/* harmony → rose */ .platform-badge-harmony { - --rp-container-info-text: #c2410c; - --rp-container-info-bg: #f973161a; - --rp-container-warning-text: #c2410c; - --rp-container-warning-bg: #f973161a; - --rp-container-danger-text: #c2410c; - --rp-container-danger-bg: #f973161a; + --platform-badge-fg: #e11d48; + --platform-badge-bg: #fff1f2; + --platform-badge-ring: #fecdd3; } -/* web_lynx → purple */ +/* web / web_lynx → orange */ +.platform-badge-web, .platform-badge-web_lynx { - --rp-container-info-text: #7e22ce; - --rp-container-info-bg: #a855f71a; - --rp-container-warning-text: #7e22ce; - --rp-container-warning-bg: #a855f71a; - --rp-container-danger-text: #7e22ce; - --rp-container-danger-bg: #a855f71a; + --platform-badge-fg: #ea580c; + --platform-badge-bg: #fff7ed; + --platform-badge-ring: #fed7aa; } -/* clay, clay_android → teal */ -.platform-badge-clay, -.platform-badge-clay_android { - --rp-container-info-text: #0f766e; - --rp-container-info-bg: #14b8a61a; - --rp-container-warning-text: #0f766e; - --rp-container-warning-bg: #14b8a61a; - --rp-container-danger-text: #0f766e; - --rp-container-danger-bg: #14b8a61a; -} - -/* clay_ios → cyan */ -.platform-badge-clay_ios { - --rp-container-info-text: #0e7490; - --rp-container-info-bg: #06b6d41a; - --rp-container-warning-text: #0e7490; - --rp-container-warning-bg: #06b6d41a; - --rp-container-danger-text: #0e7490; - --rp-container-danger-bg: #06b6d41a; -} - -/* clay_macos → indigo */ -.platform-badge-clay_macos { - --rp-container-info-text: #4338ca; - --rp-container-info-bg: #6366f11a; - --rp-container-warning-text: #4338ca; - --rp-container-warning-bg: #6366f11a; - --rp-container-danger-text: #4338ca; - --rp-container-danger-bg: #6366f11a; +/* windows (incl. clay_windows underlying surface) → sky for raw windows */ +.platform-badge-windows { + --platform-badge-fg: #0284c7; + --platform-badge-bg: #f0f9ff; + --platform-badge-ring: #bae6fd; } -/* clay_windows → sky */ +/* clay umbrella + variants → cyan (Clay/Desktop signature) */ +.platform-badge-clay, +.platform-badge-clay_android, .platform-badge-clay_windows { - --rp-container-info-text: #0369a1; - --rp-container-info-bg: #0ea5e91a; - --rp-container-warning-text: #0369a1; - --rp-container-warning-bg: #0ea5e91a; - --rp-container-danger-text: #0369a1; - --rp-container-danger-bg: #0ea5e91a; + --platform-badge-fg: #0891b2; + --platform-badge-bg: #ecfeff; + --platform-badge-ring: #a5f3fc; } -/* Dark mode */ -:where(html.rp-dark, html.dark) .platform-badge-android { - --rp-container-info-text: #34d399; - --rp-container-info-bg: #10b98126; - --rp-container-warning-text: #34d399; - --rp-container-warning-bg: #10b98126; - --rp-container-danger-text: #34d399; - --rp-container-danger-bg: #10b98126; +/* Dark mode bumps foreground to ~400 and swaps bg to a deep tinted wash. */ +:where(html.rp-dark, html.dark) .platform-badge-ios, +:where(html.rp-dark, html.dark) .platform-badge-clay_ios, +:where(html.rp-dark, html.dark) .platform-badge-macos, +:where(html.rp-dark, html.dark) .platform-badge-clay_macos { + --platform-badge-fg: #d4d4d8; + --platform-badge-bg: #27272a99; + --platform-badge-ring: #52525b; } -:where(html.rp-dark, html.dark) .platform-badge-ios { - --rp-container-info-text: #60a5fa; - --rp-container-info-bg: #3b82f626; - --rp-container-warning-text: #60a5fa; - --rp-container-warning-bg: #3b82f626; - --rp-container-danger-text: #60a5fa; - --rp-container-danger-bg: #3b82f626; +:where(html.rp-dark, html.dark) .platform-badge-android { + --platform-badge-fg: #34d399; + --platform-badge-bg: #022c2299; + --platform-badge-ring: #065f46; } :where(html.rp-dark, html.dark) .platform-badge-harmony { - --rp-container-info-text: #fb923c; - --rp-container-info-bg: #f9731626; - --rp-container-warning-text: #fb923c; - --rp-container-warning-bg: #f9731626; - --rp-container-danger-text: #fb923c; - --rp-container-danger-bg: #f9731626; + --platform-badge-fg: #fb7185; + --platform-badge-bg: #4c051599; + --platform-badge-ring: #9f1239; } +:where(html.rp-dark, html.dark) .platform-badge-web, :where(html.rp-dark, html.dark) .platform-badge-web_lynx { - --rp-container-info-text: #c084fc; - --rp-container-info-bg: #a855f726; - --rp-container-warning-text: #c084fc; - --rp-container-warning-bg: #a855f726; - --rp-container-danger-text: #c084fc; - --rp-container-danger-bg: #a855f726; + --platform-badge-fg: #fb923c; + --platform-badge-bg: #43140799; + --platform-badge-ring: #9a3412; +} + +:where(html.rp-dark, html.dark) .platform-badge-windows { + --platform-badge-fg: #38bdf8; + --platform-badge-bg: #082f4999; + --platform-badge-ring: #075985; } :where(html.rp-dark, html.dark) .platform-badge-clay, -:where(html.rp-dark, html.dark) .platform-badge-clay_android { - --rp-container-info-text: #2dd4bf; - --rp-container-info-bg: #14b8a626; - --rp-container-warning-text: #2dd4bf; - --rp-container-warning-bg: #14b8a626; - --rp-container-danger-text: #2dd4bf; - --rp-container-danger-bg: #14b8a626; +:where(html.rp-dark, html.dark) .platform-badge-clay_android, +:where(html.rp-dark, html.dark) .platform-badge-clay_windows { + --platform-badge-fg: #22d3ee; + --platform-badge-bg: #08344499; + --platform-badge-ring: #155e75; +} + +/* ── Apply our variables to Rspress's container slots ────────────────────── */ + +[class*='platform-badge-'] { + --rp-container-info-text: var(--platform-badge-fg); + --rp-container-info-bg: var(--platform-badge-bg); + --rp-container-warning-text: var(--platform-badge-fg); + --rp-container-warning-bg: var(--platform-badge-bg); + --rp-container-danger-text: var(--platform-badge-fg); + --rp-container-danger-bg: var(--platform-badge-bg); +} + +/* ── Vertical alignment ───────────────────────────────────────────────── + * Same fixed badge size everywhere. The only thing that varies between + * the body, the TOC list, and a heading is what the badge sits next to. + * Default inline-flex baseline alignment makes the chip cling to the + * surrounding text baseline, which looks fine in body but reads as + * "stranded at the bottom" next to a large heading. `vertical-align: + * middle` re-centers the chip on the line box, so the same chip aligns + * cleanly against a code chip in body text, a TOC list item, and a + * heading without changing size. + */ +[class*='platform-badge-'] .rp-badge { + vertical-align: middle; } -:where(html.rp-dark, html.dark) .platform-badge-clay_ios { - --rp-container-info-text: #22d3ee; - --rp-container-info-bg: #06b6d426; - --rp-container-warning-text: #22d3ee; - --rp-container-warning-bg: #06b6d426; - --rp-container-danger-text: #22d3ee; - --rp-container-danger-bg: #06b6d426; +[class*='platform-badge-'] .platform-badge__icon { + --icon-size: 0.9rem; } -:where(html.rp-dark, html.dark) .platform-badge-clay_macos { - --rp-container-info-text: #818cf8; - --rp-container-info-bg: #6366f126; - --rp-container-warning-text: #818cf8; - --rp-container-warning-bg: #6366f126; - --rp-container-danger-text: #818cf8; - --rp-container-danger-bg: #6366f126; +/* ── Variant: *Only, emphasized with an inset platform-tinted ring ─────── */ + +.platform-badge--only .rp-badge { + box-shadow: inset 0 0 0 1px var(--platform-badge-ring); } -:where(html.rp-dark, html.dark) .platform-badge-clay_windows { - --rp-container-info-text: #38bdf8; - --rp-container-info-bg: #0ea5e926; - --rp-container-warning-text: #38bdf8; - --rp-container-warning-bg: #0ea5e926; - --rp-container-danger-text: #38bdf8; - --rp-container-danger-bg: #0ea5e926; +/* ── Variant: No*, drops platform color, mutes, strikes the label ───────── */ + +.platform-badge--no { + /* Override the cascade above with neutral grays so "No iOS" doesn't read + the same as "iOS Only" at a glance. */ + --platform-badge-fg: #71717a; /* zinc-500 */ + --platform-badge-bg: #f4f4f5; /* zinc-100 */ +} + +:where(html.rp-dark, html.dark) .platform-badge--no { + --platform-badge-fg: #a1a1aa; /* zinc-400 */ + --platform-badge-bg: #18181b99; /* zinc-900 @ 60% */ +} + +.platform-badge--no .platform-badge__label { + text-decoration: line-through; + text-decoration-thickness: 1px; + text-decoration-color: currentColor; +} + +/* The icon also dims in the No* variant, since full saturation here would + fight the strikethrough and read as "supported but crossed out". */ +.platform-badge--no .platform-badge__icon { + opacity: 0.7; } diff --git a/src/components/api-badge/PlatformBadge.tsx b/src/components/api-badge/PlatformBadge.tsx index 63cd16109..8063341e6 100644 --- a/src/components/api-badge/PlatformBadge.tsx +++ b/src/components/api-badge/PlatformBadge.tsx @@ -24,11 +24,11 @@ function mapPlatformNameToIconName(platform: ExtendedPlatformName) { type ExtendedPlatformName = LCD.PlatformName | 'clay'; /** - * Maps a platform name to its full name. - * @param platform The platform name to map. - * @returns The full name of the given platform. + * Technical, stable platform name. Used to derive the exported component keys + * (`ClayOnly`, `NoHarmony`, etc.) so MDX imports stay valid no matter how the + * user-facing label moves around. */ -function mapPlatformNameToFullName(platform: ExtendedPlatformName) { +function mapPlatformNameToTechnicalName(platform: ExtendedPlatformName) { if (platform === 'clay') { return 'Clay'; } @@ -38,6 +38,18 @@ function mapPlatformNameToFullName(platform: ExtendedPlatformName) { return getFullPlatformName(platform); } +/** + * User-facing label rendered inside the badge. Bare `clay` renders as + * "Desktop" while the desktop branding is in flux. APIStatus and compat + * tables keep the technical name; this only affects the badge chip. + */ +function mapPlatformNameToLabel(platform: ExtendedPlatformName) { + if (platform === 'clay') { + return 'Desktop'; + } + return mapPlatformNameToTechnicalName(platform); +} + type BadgeProps = React.ComponentProps; type PlatformBadgeInnerProps = { @@ -46,10 +58,21 @@ type PlatformBadgeInnerProps = { type?: BadgeProps['type']; }; +// Maps the semantic `type` to a visual modifier class. The CSS in +// PlatformBadge.css decides what each modifier looks like: default keeps the +// platform color, `--only` emphasizes via a tinted ring, `--no` mutes the +// platform color and strikes the label so support/non-support don't read as +// the same chip in different words. +const TYPE_TO_MODIFIER: Record, string> = { + info: '', + warning: 'platform-badge--only', + danger: 'platform-badge--no', + tip: '', + note: '', +}; + /** * Internal component for rendering a platform badge. - * @param props The properties for the PlatformBadgeInner component. - * @returns A Badge component with platform-specific styling. * @internal */ function PlatformBadgeInner({ @@ -57,17 +80,15 @@ function PlatformBadgeInner({ badgeText, type = 'info', }: PlatformBadgeInnerProps) { + const modifier = TYPE_TO_MODIFIER[type] ?? ''; return ( - - {badgeText} + + {badgeText} ); @@ -95,7 +116,7 @@ export function PlatformBadge({ version, type = 'info', }: PlatformBadgeProps) { - const platformName = mapPlatformNameToFullName(platform); + const platformName = mapPlatformNameToLabel(platform); const badgeText = version ? `${platformName} ${version}+` : platformName; return ( @@ -126,7 +147,7 @@ function createPlatformOnlyComponent(platform: ExtendedPlatformName) { return ( ); @@ -143,7 +164,7 @@ function createNoPlatformComponent(platform: ExtendedPlatformName) { return ( ); @@ -154,7 +175,10 @@ function createNoPlatformComponent(platform: ExtendedPlatformName) { const generatedComponents: Record = {}; for (const platform of platformNames) { - const name = mapPlatformNameToFullName(platform) + // Component name derivation MUST use the technical name so `ClayOnly` and + // `NoClay` exports stay valid even while the user-facing label says + // "Desktop". Don't switch this to mapPlatformNameToLabel. + const name = mapPlatformNameToTechnicalName(platform) .split(' ') .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(''); diff --git a/src/components/api-status/APIStatusDashboard.tsx b/src/components/api-status/APIStatusDashboard.tsx index 42aa72a6d..0e9524346 100644 --- a/src/components/api-status/APIStatusDashboard.tsx +++ b/src/components/api-status/APIStatusDashboard.tsx @@ -345,17 +345,17 @@ export const APIItem: React.FC = ({
- {Icon && } + {Icon && } ; + icon: React.FC<{ className?: string; style?: React.CSSProperties }>; colors: { bg: string; border: string; @@ -14,11 +14,14 @@ export interface PlatformConfig { }; } -const makeIcon = (platformName: string): React.FC<{ className?: string }> => { - return ({ className }) => ( +const makeIcon = ( + platformName: string, +): React.FC<{ className?: string; style?: React.CSSProperties }> => { + return ({ className, style }) => ( ); }; diff --git a/src/components/api-table/compat-table/assets/icons/android.svg b/src/components/api-table/compat-table/assets/icons/android.svg index 873a2491a..fb4ed408b 100644 --- a/src/components/api-table/compat-table/assets/icons/android.svg +++ b/src/components/api-table/compat-table/assets/icons/android.svg @@ -1,4 +1,4 @@ - + diff --git a/src/components/home-comps/features/icon.tsx b/src/components/home-comps/features/icon.tsx index 9bedd36e1..864068613 100644 --- a/src/components/home-comps/features/icon.tsx +++ b/src/components/home-comps/features/icon.tsx @@ -2,22 +2,15 @@ import React from 'react'; import { withBase } from '@rspress/core/runtime'; import { cn } from '@/lib/utils'; import { PlatformSvg } from '@/components/platform-navigation/PlatformIcon'; +import { + PLATFORM_TINT, + type PlatformKey, +} from '@/components/platform-navigation/platform-colors'; import ReactLynxIcon from '@/components/api-table/compat-table/assets/icons/reactlynx.svg?react'; import VueLynxIcon from '@assets/home/vue-lynx-logo.svg?react'; import styles from './index.module.less'; -// Each platform gets a brand-adjacent hue, kept at matching tonal weight -// (Tailwind ~600 in light mode, ~400 in dark) so the row stays harmonious. -const PLATFORM_TINT: Record = { - ios: 'text-zinc-700 dark:text-zinc-300', - macos: 'text-zinc-700 dark:text-zinc-300', - android: 'text-emerald-600 dark:text-emerald-400', - harmony: 'text-rose-600 dark:text-rose-400', - web: 'text-orange-600 dark:text-orange-400', - windows: 'text-sky-600 dark:text-sky-400', -}; - -const PlatformIconWrapper = ({ platform }: { platform: string }) => ( +const PlatformIconWrapper = ({ platform }: { platform: PlatformKey }) => ( { const svgUrl = toIconUrl(platformName); return ( @@ -62,6 +64,7 @@ export const PlatformSvg = ({ style={{ maskImage: `url(${svgUrl})`, WebkitMaskImage: `url(${svgUrl})`, + ...style, }} /> ); diff --git a/src/components/platform-navigation/platform-colors.ts b/src/components/platform-navigation/platform-colors.ts new file mode 100644 index 000000000..0cb00be67 --- /dev/null +++ b/src/components/platform-navigation/platform-colors.ts @@ -0,0 +1,104 @@ +// Single source of truth for platform colors across the site. +// +// One hue per *brand family*, paired across light/dark so the row stays +// tonally even. Tailwind ~600 in light + ~400 in dark sits on the same +// perceived lightness, which is what lets the icons hum at the same volume. +// +// Brand mapping: +// apple family ios and macos -> zinc Apple system silver/space gray +// android -> emerald Android green +// harmony -> rose Huawei red, the brand cue +// web and web_lynx -> orange Lynx-on-web warmth +// windows -> sky Microsoft blue +// clay umbrella and variants -> cyan Clay/Desktop signature +// +// `clay_` reuses the underlying-platform icon glyph, so the color +// is the only thing keeping `ClayAndroidOnly` and `AndroidOnly` visually +// distinct. That's why every clay_* shares one Clay color rather than +// borrowing the underlying-platform color. + +export type PlatformKey = + | 'ios' + | 'macos' + | 'android' + | 'harmony' + | 'web' + | 'web_lynx' + | 'windows' + | 'clay' + | 'clay_ios' + | 'clay_android' + | 'clay_macos' + | 'clay_windows'; + +type PlatformHue = { + // Tailwind class string for icon/text tinting (light: ~600, dark: ~400). + tint: string; + // Solid hex pair for inline styles and CSS-variable overrides. + // light = Tailwind 600, dark = Tailwind 400. + hex: { light: string; dark: string }; + // Subtle container background pair used by PlatformBadge. + // light = Tailwind 50, dark = Tailwind 950 + 60% (mixed against bg by the + // browser via alpha). Keeps the badge calm so the icon does the signalling. + bg: { light: string; dark: string }; +}; + +const APPLE_HUE: PlatformHue = { + tint: 'text-zinc-700 dark:text-zinc-300', + hex: { light: '#52525b', dark: '#d4d4d8' }, + bg: { light: '#fafafa', dark: '#27272a99' }, +}; + +const ANDROID_HUE: PlatformHue = { + tint: 'text-emerald-600 dark:text-emerald-400', + hex: { light: '#059669', dark: '#34d399' }, + bg: { light: '#ecfdf5', dark: '#022c2299' }, +}; + +const HARMONY_HUE: PlatformHue = { + tint: 'text-rose-600 dark:text-rose-400', + hex: { light: '#e11d48', dark: '#fb7185' }, + bg: { light: '#fff1f2', dark: '#4c051599' }, +}; + +const WEB_HUE: PlatformHue = { + tint: 'text-orange-600 dark:text-orange-400', + hex: { light: '#ea580c', dark: '#fb923c' }, + bg: { light: '#fff7ed', dark: '#43140799' }, +}; + +const WINDOWS_HUE: PlatformHue = { + tint: 'text-sky-600 dark:text-sky-400', + hex: { light: '#0284c7', dark: '#38bdf8' }, + bg: { light: '#f0f9ff', dark: '#082f4999' }, +}; + +const CLAY_HUE: PlatformHue = { + tint: 'text-cyan-600 dark:text-cyan-400', + hex: { light: '#0891b2', dark: '#22d3ee' }, + bg: { light: '#ecfeff', dark: '#08344499' }, +}; + +export const PLATFORM_HUES: Record = { + ios: APPLE_HUE, + macos: APPLE_HUE, + android: ANDROID_HUE, + harmony: HARMONY_HUE, + web: WEB_HUE, + web_lynx: WEB_HUE, + windows: WINDOWS_HUE, + clay: CLAY_HUE, + clay_ios: CLAY_HUE, + clay_android: CLAY_HUE, + clay_macos: CLAY_HUE, + clay_windows: CLAY_HUE, +}; + +// Tailwind tint classes by platform key. Kept as a separate export because +// the homepage icon row only needs the tint string, not the full hue object. +export const PLATFORM_TINT: Record = Object.fromEntries( + (Object.keys(PLATFORM_HUES) as PlatformKey[]).map((key) => [ + key, + PLATFORM_HUES[key].tint, + ]), +) as Record;