You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/routes/test/air/button/+page.svelte

893 lines
27 KiB

<script lang="ts">
import { getContext } from 'svelte';
import { Button, ButtonIcon, ButtonLabel, Icon } from '$uix/air';
import type { AirDemoContext } from '../demo-context';
import { AIR_DEMO_CONTEXT_KEY } from '../demo-context';
import CodeExample from '../_components/code-example.svelte';
let variant = $state<'solid' | 'soft' | 'surface' | 'outline' | 'ghost' | 'plain'>('solid');
let color = $state<'primary' | 'neutral' | 'success' | 'warning' | 'danger' | 'info'>('primary');
let intent = $state<'' | 'primary' | 'neutral' | 'success' | 'warning' | 'danger' | 'info'>('');
let size = $state<'2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'>('md');
let rounded = $state<'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'>('md');
let block = $state(false);
let iconOnly = $state(false);
let loading = $state(false);
let spinnerPlacement = $state<'start' | 'end'>('start');
const demoContext = getContext<AirDemoContext>(AIR_DEMO_CONTEXT_KEY);
const t = (path: string, ...args: unknown[]) => demoContext?.t(path, ...args) ?? path;
const variants = ['solid', 'soft', 'surface', 'outline', 'ghost', 'plain'] as const;
const colors = ['primary', 'neutral', 'success', 'warning', 'danger', 'info'] as const;
const intents = ['', 'primary', 'neutral', 'success', 'warning', 'danger', 'info'] as const;
const sizes = ['2xs', 'xs', 'sm', 'md', 'lg', 'xl'] as const;
const roundedValues = ['sm', 'md', 'lg', 'xl', '2xl', 'full'] as const;
const placements = ['start', 'end'] as const;
type ButtonVariant = (typeof variants)[number];
type ButtonColor = (typeof colors)[number];
type ButtonIntent = Exclude<(typeof intents)[number], ''>;
type ButtonSize = (typeof sizes)[number];
type ButtonRounded = (typeof roundedValues)[number];
type SpinnerPlacement = (typeof placements)[number];
function buttonRootCode({
variant,
color,
intent,
size,
rounded,
loading,
loadingText,
iconOnly,
spinnerPlacement,
block,
ariaLabel,
label,
icon,
iconPosition = 'start'
}: {
variant?: ButtonVariant;
color?: ButtonColor;
intent?: ButtonIntent;
size?: ButtonSize;
rounded?: ButtonRounded;
loading?: boolean;
loadingText?: string;
iconOnly?: boolean;
spinnerPlacement?: SpinnerPlacement;
block?: boolean;
ariaLabel?: string;
label?: string;
icon?: string;
iconPosition?: 'start' | 'end';
}) {
const attrs = [
variant ? `variant="${variant}"` : '',
color && color !== 'primary' ? `color="${color}"` : '',
intent ? `intent="${intent}"` : '',
size ? `size="${size}"` : '',
rounded ? `rounded="${rounded}"` : '',
loading ? 'loading' : '',
loadingText ? `loadingText="${loadingText}"` : '',
iconOnly ? 'iconOnly' : '',
spinnerPlacement ? `spinnerPlacement="${spinnerPlacement}"` : '',
block ? 'block' : '',
ariaLabel ? `aria-label="${ariaLabel}"` : ''
].filter(Boolean);
const iconMarkup = icon ? `\n\t<ButtonIcon><Icon.${icon} size="100%" /></ButtonIcon>` : '';
const labelMarkup = !iconOnly && label ? `\n\t<ButtonLabel>${label}</ButtonLabel>` : '';
const children =
iconPosition === 'end' && iconMarkup && labelMarkup
? `${labelMarkup}${iconMarkup}`
: `${iconMarkup}${labelMarkup}`;
return `<Button ${attrs.join(' ')}>${children}\n</Button>`;
}
const playgroundCode = $derived(
buttonRootCode({
variant,
color,
intent: intent || undefined,
size,
rounded,
loading,
loadingText: t('air.button.saving'),
iconOnly,
spinnerPlacement,
block,
ariaLabel: iconOnly ? t('air.button.addProject') : undefined,
label: iconOnly ? undefined : t('air.button.continue'),
icon: 'Plus'
})
);
function iconExampleCode(kind: 'newProject' | 'search' | 'approve' | 'continue' | 'moreActions' | 'closeDialog') {
if (kind === 'newProject') return buttonRootCode({ variant: 'solid', color: 'primary', label: t('air.button.newProject'), icon: 'Plus' });
if (kind === 'search') return buttonRootCode({ variant: 'surface', color: 'info', label: t('air.button.search'), icon: 'Search', iconPosition: 'end' });
if (kind === 'approve') return buttonRootCode({ variant: 'outline', intent: 'success', label: t('air.button.approve'), icon: 'Check' });
if (kind === 'continue') return buttonRootCode({ variant: 'ghost', color: 'info', label: t('air.button.continue'), icon: 'ArrowRight', iconPosition: 'end' });
if (kind === 'moreActions') return buttonRootCode({ variant: 'outline', color: 'neutral', iconOnly: true, ariaLabel: t('air.button.moreActions'), icon: 'Ellipsis' });
return buttonRootCode({ variant: 'ghost', intent: 'danger', iconOnly: true, rounded: 'full', ariaLabel: t('air.button.closeDialog'), icon: 'X' });
}
function loadingExampleCode(kind: 'save' | 'upload' | 'wait') {
if (kind === 'save') return buttonRootCode({ loading: true, loadingText: t('air.button.saving'), intent: 'success', label: t('air.button.save'), icon: 'Check' });
if (kind === 'upload') return buttonRootCode({ loading: true, spinnerPlacement: 'end', loadingText: t('air.button.uploading'), color: 'info', variant: 'soft', label: t('air.button.upload'), icon: 'ArrowRight' });
return buttonRootCode({ loading: true, loadingText: t('air.button.pleaseWait'), color: 'neutral', variant: 'surface', iconOnly: true, ariaLabel: t('air.button.pleaseWait'), icon: 'LoaderCircle' });
}
function responsiveExampleCode(kind: 'grow' | 'adapt') {
if (kind === 'grow') {
return `<Button color="primary" size={{ base: 'sm', md: 'lg' }} rounded={{ base: 'md', md: 'full' }}>\n\t<ButtonIcon><Icon.ArrowRight size="100%" /></ButtonIcon>\n\t<ButtonLabel>${t('air.button.resizeMe')}</ButtonLabel>\n</Button>`;
}
return `<Button color="info" variant="surface" size={{ base: 'xs', lg: 'md' }} rounded={{ base: 'sm', lg: 'xl' }}>\n\t<ButtonLabel>${t('air.button.adaptiveAction')}</ButtonLabel>\n\t<ButtonIcon><Icon.ChevronRight size="100%" /></ButtonIcon>\n</Button>`;
}
function colorExampleCode(item: ButtonColor) {
return buttonRootCode({ color: item, variant: 'solid', label: item, icon: item === 'danger' ? 'X' : item === 'success' ? 'Check' : item === 'warning' ? 'Ellipsis' : item === 'info' ? 'Search' : item === 'neutral' ? 'ChevronRight' : 'Plus' });
}
const resolvedMode = $derived(intent || color);
</script>
<svelte:head>
<title>Air Button</title>
</svelte:head>
<div class="button-demo-page">
<header class="hero">
<div class="hero-copy">
<p class="eyebrow">Air / Button</p>
<h1>Primer componente faro</h1>
<p class="lead">
Valida densidad, tipografia, variantes, iconografia, radios, loading, color, intent y
comportamiento responsive sobre `terra.Button`.
</p>
<div class="hero-signals">
<span>6 variants</span>
<span>6 sizes</span>
<span>color + intent</span>
<span>Responsive props</span>
</div>
</div>
<div class="hero-aside">
<div>
<span>Contract</span>
<strong>terra.Button + air recipe</strong>
</div>
<div>
<span>Resolution</span>
<strong>{resolvedMode}</strong>
</div>
</div>
</header>
<section class="panel panel--feature">
<header class="section-header">
<h2>Playground</h2>
<p>
`color` elige la apariencia. `intent` comunica significado y, si existe, predomina sobre
`color`.
</p>
</header>
<div class="playground-grid">
<div class="controls">
<div class="control">
<span>Variant</span>
<div class="chips">
{#each variants as item}
<button type="button" class:chip={true} class:chip--active={variant === item} onclick={() => (variant = item)}>{item}</button>
{/each}
</div>
</div>
<div class="control">
<span>Color</span>
<div class="chips">
{#each colors as item}
<button type="button" class:chip={true} class:chip--active={color === item} onclick={() => (color = item)}>{item}</button>
{/each}
</div>
</div>
<div class="control">
<span>Intent</span>
<div class="chips">
{#each intents as item}
<button type="button" class:chip={true} class:chip--active={intent === item} onclick={() => (intent = item)}>{item || 'none'}</button>
{/each}
</div>
</div>
<div class="control">
<span>Size</span>
<div class="chips">
{#each sizes as item}
<button type="button" class:chip={true} class:chip--active={size === item} onclick={() => (size = item)}>{item}</button>
{/each}
</div>
</div>
<div class="control">
<span>Rounded</span>
<div class="chips">
{#each roundedValues as item}
<button type="button" class:chip={true} class:chip--active={rounded === item} onclick={() => (rounded = item)}>{item}</button>
{/each}
</div>
</div>
<div class="control">
<span>Spinner placement</span>
<div class="chips">
{#each placements as item}
<button type="button" class:chip={true} class:chip--active={spinnerPlacement === item} onclick={() => (spinnerPlacement = item)}>{item}</button>
{/each}
</div>
</div>
<div class="toggles">
<label class="toggle-row">
<input type="checkbox" bind:checked={block} />
<span>Block width</span>
</label>
<label class="toggle-row">
<input type="checkbox" bind:checked={iconOnly} />
<span>Icon only</span>
</label>
<label class="toggle-row">
<input type="checkbox" bind:checked={loading} />
<span>Loading</span>
</label>
</div>
</div>
<CodeExample title="Interactive recipe" code={playgroundCode}>
<div class="preview preview--hero">
<Button
{variant}
{color}
intent={intent || undefined}
{size}
{rounded}
{block}
{iconOnly}
{loading}
{spinnerPlacement}
loadingText={t('air.button.saving')}
aria-label={iconOnly ? t('air.button.addProject') : undefined}
>
<ButtonIcon>
<Icon.Plus size="100%" />
</ButtonIcon>
{#if !iconOnly}
<ButtonLabel>{t('air.button.continue')}</ButtonLabel>
{/if}
</Button>
</div>
</CodeExample>
</div>
</section>
<section class="panel">
<header class="section-header">
<h2>Label</h2>
<p>
`ButtonLabel` recibe texto ya resuelto. Si necesitas traduccion, resuelvela antes con
`t(...)` o con el traductor que use tu pantalla.
</p>
</header>
<div class="demo-grid demo-grid--states">
<CodeExample
title="Texto traducido antes del render"
code={`<Button>\n\t<ButtonLabel>{t('air.button.review')}</ButtonLabel>\n</Button>`}
>
<Button variant="surface" color="neutral">
<ButtonLabel>{t('air.button.review')}</ButtonLabel>
</Button>
</CodeExample>
<CodeExample
title="Texto plano"
code={`<Button>\n\t<ButtonLabel>Review</ButtonLabel>\n</Button>`}
>
<Button color="primary">
<ButtonLabel>Review</ButtonLabel>
</Button>
</CodeExample>
</div>
</section>
<section class="panel">
<header class="section-header">
<h2>Variants</h2>
<p>Las variantes base ya diferenciadas por peso visual y jerarquia.</p>
</header>
<div class="demo-grid demo-grid--variants">
{#each variants as item}
<div class="demo-card">
<Button variant={item}>
<ButtonLabel>{t('air.button.action')}</ButtonLabel>
</Button>
<span>{item}</span>
</div>
{/each}
</div>
</section>
<section class="panel">
<header class="section-header">
<h2>Colors</h2>
<p>`color` selecciona una paleta visual del sistema sin aportar significado extra.</p>
</header>
<div class="demo-grid demo-grid--variants">
{#each colors as item}
<CodeExample title={`${item} solid`} code={colorExampleCode(item)}>
<div class="preview preview--compact">
<Button color={item}>
<ButtonIcon>
{#if item === 'danger'}
<Icon.X size="100%" />
{:else if item === 'success'}
<Icon.Check size="100%" />
{:else if item === 'warning'}
<Icon.Ellipsis size="100%" />
{:else if item === 'info'}
<Icon.Search size="100%" />
{:else if item === 'neutral'}
<Icon.ChevronRight size="100%" />
{:else}
<Icon.Plus size="100%" />
{/if}
</ButtonIcon>
<ButtonLabel>{item}</ButtonLabel>
</Button>
</div>
</CodeExample>
{/each}
</div>
</section>
<section class="panel">
<header class="section-header">
<h2>Palette matrix</h2>
<p>
Lectura rapida para contrastar cada paleta en los pesos que mas nos importan. Sirve para
detectar enseguida si `solid`, `soft`, `surface`, `outline` o `ghost` se quedan cortos.
</p>
</header>
<div class="palette-matrix">
{#each colors as item}
<div class="palette-row">
<div class="palette-row__meta">
<span>{item}</span>
<strong>{item}</strong>
</div>
<div class="palette-row__actions">
<Button color={item} variant="solid" size="sm">
<ButtonIcon><Icon.Plus size="100%" /></ButtonIcon>
<ButtonLabel>Solid</ButtonLabel>
</Button>
<Button color={item} variant="soft" size="sm">
<ButtonIcon><Icon.Plus size="100%" /></ButtonIcon>
<ButtonLabel>Soft</ButtonLabel>
</Button>
<Button color={item} variant="surface" size="sm">
<ButtonIcon><Icon.Plus size="100%" /></ButtonIcon>
<ButtonLabel>Surface</ButtonLabel>
</Button>
<Button color={item} variant="outline" size="sm">
<ButtonIcon><Icon.Plus size="100%" /></ButtonIcon>
<ButtonLabel>Outline</ButtonLabel>
</Button>
<Button color={item} variant="ghost" size="sm">
<ButtonIcon><Icon.Plus size="100%" /></ButtonIcon>
<ButtonLabel>Ghost</ButtonLabel>
</Button>
</div>
</div>
{/each}
</div>
</section>
<section class="panel">
<header class="section-header">
<h2>Intent</h2>
<p>`intent` comunica semantica al usuario y siempre prevalece sobre `color`.</p>
</header>
<div class="demo-grid demo-grid--states">
<CodeExample title="Semantic success" code={buttonRootCode({ intent: 'success', label: t('air.button.approve'), icon: 'Check' })}>
<Button intent="success">
<ButtonIcon><Icon.Check size="100%" /></ButtonIcon>
<ButtonLabel>{t('air.button.approve')}</ButtonLabel>
</Button>
</CodeExample>
<CodeExample title="Semantic warning overrides color" code={buttonRootCode({ color: 'primary', intent: 'warning', variant: 'soft', label: t('air.button.review'), icon: 'Ellipsis' })}>
<Button color="primary" intent="warning" variant="soft">
<ButtonIcon><Icon.Ellipsis size="100%" /></ButtonIcon>
<ButtonLabel>{t('air.button.review')}</ButtonLabel>
</Button>
</CodeExample>
<CodeExample title="Semantic danger" code={buttonRootCode({ color: 'info', intent: 'danger', variant: 'outline', label: t('air.button.delete'), icon: 'X' })}>
<Button color="info" intent="danger" variant="outline">
<ButtonIcon><Icon.X size="100%" /></ButtonIcon>
<ButtonLabel>{t('air.button.delete')}</ButtonLabel>
</Button>
</CodeExample>
</div>
</section>
<section class="panel">
<header class="section-header">
<h2>Sizes</h2>
<p>
Escala completa de densidad: dos pasos por debajo de `sm`, un paso tactil por encima de
`lg`.
</p>
</header>
<div class="demo-grid demo-grid--sizes">
{#each sizes as item}
<div class="demo-card">
<Button size={item}>
<ButtonLabel>{t('air.button.buttonSize', item)}</ButtonLabel>
</Button>
<span>{item}</span>
</div>
{/each}
</div>
</section>
<section class="panel">
<header class="section-header">
<h2>Iconografia</h2>
<p>
La anatomia ya acepta `ButtonIcon` y `ButtonLabel`, con ejemplos claros de izquierda,
derecha e icon-only.
</p>
</header>
<div class="demo-grid demo-grid--states">
<CodeExample title="Primary with leading icon" code={iconExampleCode('newProject')}>
<Button variant="solid" color="primary">
<ButtonIcon><Icon.Plus size="100%" /></ButtonIcon>
<ButtonLabel>{t('air.button.newProject')}</ButtonLabel>
</Button>
</CodeExample>
<CodeExample title="Surface with trailing icon" code={iconExampleCode('search')}>
<Button variant="surface" color="info">
<ButtonLabel>{t('air.button.search')}</ButtonLabel>
<ButtonIcon><Icon.Search size="100%" /></ButtonIcon>
</Button>
</CodeExample>
<CodeExample title="Outline confirmation" code={iconExampleCode('approve')}>
<Button variant="outline" intent="success">
<ButtonIcon><Icon.Check size="100%" /></ButtonIcon>
<ButtonLabel>{t('air.button.approve')}</ButtonLabel>
</Button>
</CodeExample>
<CodeExample title="Ghost action" code={iconExampleCode('continue')}>
<Button variant="ghost" color="info">
<ButtonLabel>{t('air.button.continue')}</ButtonLabel>
<ButtonIcon><Icon.ArrowRight size="100%" /></ButtonIcon>
</Button>
</CodeExample>
<CodeExample title="Icon only utility" code={iconExampleCode('moreActions')}>
<Button variant="outline" color="neutral" iconOnly aria-label={t('air.button.moreActions')}>
<ButtonIcon><Icon.Ellipsis size="100%" /></ButtonIcon>
</Button>
</CodeExample>
<CodeExample title="Circular dismiss" code={iconExampleCode('closeDialog')}>
<Button variant="ghost" intent="danger" iconOnly rounded="full" aria-label={t('air.button.closeDialog')}>
<ButtonIcon><Icon.X size="100%" /></ButtonIcon>
</Button>
</CodeExample>
</div>
</section>
<section class="panel">
<header class="section-header">
<h2>Rounded</h2>
<p>El radio es una prop del sistema y siempre resuelve contra tokens de `air`.</p>
</header>
<div class="demo-grid demo-grid--sizes">
{#each roundedValues as item}
<div class="demo-card">
<Button rounded={item} variant="soft">
<ButtonLabel>{t('air.button.rounded', item)}</ButtonLabel>
</Button>
<span>{item}</span>
</div>
{/each}
</div>
</section>
<section class="panel">
<header class="section-header">
<h2>Responsive</h2>
<p>
`size` y `rounded` ya aceptan objeto responsive. Reduce o amplia el viewport para comprobar
el cambio.
</p>
</header>
<div class="demo-grid demo-grid--states">
<CodeExample title="Growth across breakpoints" code={responsiveExampleCode('grow')}>
<Button color="primary" size={{ base: 'sm', md: 'lg' }} rounded={{ base: 'md', md: 'full' }}>
<ButtonIcon><Icon.ArrowRight size="100%" /></ButtonIcon>
<ButtonLabel>{t('air.button.resizeMe')}</ButtonLabel>
</Button>
</CodeExample>
<CodeExample title="Adaptive surface action" code={responsiveExampleCode('adapt')}>
<Button color="info" variant="surface" size={{ base: 'xs', lg: 'md' }} rounded={{ base: 'sm', lg: 'xl' }}>
<ButtonLabel>{t('air.button.adaptiveAction')}</ButtonLabel>
<ButtonIcon><Icon.ChevronRight size="100%" /></ButtonIcon>
</Button>
</CodeExample>
</div>
</section>
<section class="panel">
<header class="section-header">
<h2>Loading</h2>
<p>Loading con spinner del sistema, texto estable, posicion configurable y variantes con icono.</p>
</header>
<div class="demo-grid demo-grid--states">
<CodeExample title="Loading confirmation" code={loadingExampleCode('save')}>
<Button loading loadingText={t('air.button.saving')} intent="success">
<ButtonIcon><Icon.Check size="100%" /></ButtonIcon>
<ButtonLabel>{t('air.button.save')}</ButtonLabel>
</Button>
</CodeExample>
<CodeExample title="Trailing spinner" code={loadingExampleCode('upload')}>
<Button loading spinnerPlacement="end" loadingText={t('air.button.uploading')} color="info" variant="soft">
<ButtonIcon><Icon.ArrowRight size="100%" /></ButtonIcon>
<ButtonLabel>{t('air.button.upload')}</ButtonLabel>
</Button>
</CodeExample>
<CodeExample title="Icon-only loading" code={loadingExampleCode('wait')}>
<Button loading loadingText={t('air.button.pleaseWait')} color="neutral" variant="surface" iconOnly aria-label={t('air.button.pleaseWait')}>
<ButtonIcon><Icon.LoaderCircle size="100%" /></ButtonIcon>
</Button>
</CodeExample>
</div>
</section>
<section class="panel">
<header class="section-header">
<h2>States</h2>
<p>Estados basicos para validar contraste y feedback visual.</p>
</header>
<div class="demo-grid demo-grid--states">
<Button>
<ButtonLabel>{t('air.button.default')}</ButtonLabel>
</Button>
<Button disabled color="neutral">
<ButtonLabel>{t('air.button.disabled')}</ButtonLabel>
</Button>
<Button href="/test/air" variant="outline" color="info">
<ButtonLabel>{t('air.button.asLink')}</ButtonLabel>
<ButtonIcon><Icon.ArrowRight size="100%" /></ButtonIcon>
</Button>
<Button block variant="soft" color="primary">
<ButtonIcon><Icon.Plus size="100%" /></ButtonIcon>
<ButtonLabel>{t('air.button.blockAction')}</ButtonLabel>
</Button>
</div>
</section>
</div>
<style>
.button-demo-page {
display: flex;
flex-direction: column;
gap: var(--air-space-5);
}
.hero {
display: grid;
grid-template-columns: minmax(0, 1.2fr) minmax(240px, 0.8fr);
gap: var(--air-space-5);
padding: var(--air-space-5);
border-radius: var(--air-radius-xl);
border: 1px solid color-mix(in srgb, var(--air-color-border) 84%, transparent);
background:
radial-gradient(circle at top left, color-mix(in srgb, var(--air-color-accent-muted) 24%, transparent), transparent 40%),
linear-gradient(180deg, color-mix(in srgb, var(--air-color-surface-raised) 96%, transparent), color-mix(in srgb, var(--air-color-surface) 76%, transparent));
box-shadow: var(--air-shadow-1);
}
.panel {
padding: var(--air-space-2) 0;
border-top: 1px solid color-mix(in srgb, var(--air-color-border) 70%, transparent);
}
.hero-copy {
display: flex;
flex-direction: column;
gap: var(--air-space-3);
}
.hero-aside {
display: flex;
flex-direction: column;
gap: var(--air-space-3);
align-self: stretch;
}
.hero-aside div {
display: flex;
flex-direction: column;
gap: var(--air-space-1);
padding: var(--air-space-4);
border-radius: var(--air-radius-lg);
border: 1px solid color-mix(in srgb, var(--air-color-border) 80%, transparent);
background: color-mix(in srgb, var(--air-color-surface) 84%, transparent);
}
.hero-aside span,
.hero-signals span,
.demo-card span {
font-family: var(--air-font-mono);
font-size: var(--air-font-size-xs);
line-height: var(--air-leading-ui);
}
.hero-aside span {
color: var(--air-color-text-secondary);
}
.hero-aside strong {
font-size: var(--air-font-size-sm);
line-height: var(--air-leading-text);
font-weight: var(--air-font-weight-medium);
}
.hero-signals {
display: flex;
flex-wrap: wrap;
gap: var(--air-space-2);
}
.hero-signals span {
padding: var(--air-space-1) var(--air-space-2);
border-radius: var(--air-radius-full);
border: 1px solid color-mix(in srgb, var(--air-color-border) 76%, transparent);
background: color-mix(in srgb, var(--air-color-surface-raised) 88%, transparent);
color: var(--air-color-text-secondary);
}
.panel--feature {
/* no extra bg needed when panel is just a separator */
}
.eyebrow {
margin: 0;
font-family: var(--air-font-mono);
font-size: var(--air-font-size-xs);
font-weight: var(--air-font-weight-medium);
letter-spacing: var(--air-tracking-wider);
text-transform: uppercase;
color: var(--air-color-accent-text);
}
.hero h1,
.section-header h2 {
margin: 0;
font-weight: var(--air-font-weight-semibold);
line-height: var(--air-leading-heading);
}
.hero h1 {
font-size: var(--air-font-size-2xl);
letter-spacing: var(--air-tracking-tight);
}
.lead,
.section-header p,
.demo-card span {
color: var(--air-color-text-secondary);
}
.lead,
.section-header p {
margin: 0;
font-size: var(--air-font-size-md);
line-height: var(--air-leading-text);
}
.section-header {
display: flex;
flex-direction: column;
gap: var(--air-space-2);
}
.playground-grid {
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: var(--air-space-5);
margin-top: var(--air-space-4);
}
.controls {
display: flex;
flex-direction: column;
gap: var(--air-space-4);
}
.control {
display: flex;
flex-direction: column;
gap: var(--air-space-2);
}
.control span,
.toggle-row {
font-size: var(--air-font-size-sm);
line-height: var(--air-leading-ui);
color: var(--air-color-text-secondary);
}
.chips {
display: flex;
flex-wrap: wrap;
gap: var(--air-space-2);
}
.chip {
border: 1px solid var(--air-color-border);
background: color-mix(in srgb, var(--air-color-surface) 82%, transparent);
color: var(--air-color-text-primary);
padding: var(--air-space-2) var(--air-space-3);
border-radius: var(--air-radius-full);
font: inherit;
font-size: var(--air-font-size-xs);
cursor: pointer;
}
.chip--active {
background: var(--air-color-accent);
border-color: var(--air-color-accent);
color: var(--air-color-content-on-solid);
}
.toggles {
display: flex;
gap: var(--air-space-4);
flex-wrap: wrap;
}
.toggle-row {
display: inline-flex;
align-items: center;
gap: var(--air-space-2);
}
.preview {
display: flex;
align-items: center;
justify-content: center;
padding: var(--air-space-4);
}
.preview--hero {
min-height: 200px;
background:
radial-gradient(circle at top, color-mix(in srgb, var(--air-color-accent-muted) 40%, transparent), transparent 58%);
}
.preview--compact {
min-height: 100px;
}
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: var(--air-space-4);
margin-top: var(--air-space-4);
}
.demo-grid :global(.code-example) {
height: 100%;
}
.demo-grid--variants {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.demo-grid--states {
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.palette-matrix {
display: flex;
flex-direction: column;
gap: var(--air-space-3);
margin-top: var(--air-space-4);
}
.palette-row {
display: grid;
grid-template-columns: minmax(110px, 140px) minmax(0, 1fr);
gap: var(--air-space-3);
padding: var(--air-space-3);
border-radius: var(--air-radius-lg);
border: 1px solid color-mix(in srgb, var(--air-color-border) 84%, transparent);
background:
linear-gradient(
90deg,
color-mix(in srgb, var(--air-color-surface-overlay) 90%, transparent),
color-mix(in srgb, var(--air-color-surface) 76%, transparent)
);
}
.palette-row__meta {
display: flex;
flex-direction: column;
justify-content: center;
gap: var(--air-space-1);
}
.palette-row__meta span {
font-family: var(--air-font-mono);
font-size: var(--air-font-size-xs);
line-height: var(--air-leading-ui);
text-transform: uppercase;
color: var(--air-color-text-secondary);
}
.palette-row__meta strong {
font-size: var(--air-font-size-sm);
line-height: var(--air-leading-text);
font-weight: var(--air-font-weight-semibold);
color: var(--air-color-text-primary);
}
.palette-row__actions {
display: flex;
flex-wrap: wrap;
gap: var(--air-space-2);
align-items: center;
}
.demo-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--air-space-3);
padding: var(--air-space-4);
border-radius: var(--air-radius-lg);
border: 1px solid color-mix(in srgb, var(--air-color-border) 80%, transparent);
background: var(--air-color-surface-raised);
min-height: 100px;
}
@media (max-width: 900px) {
.hero,
.playground-grid {
grid-template-columns: 1fr;
}
.palette-row {
grid-template-columns: 1fr;
}
}
</style>

Powered by TurnKey Linux.