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.
800 lines
25 KiB
800 lines
25 KiB
|
6 months ago
|
<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>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,
|
||
|
|
.panel {
|
||
|
|
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: color-mix(in srgb, var(--air-color-surface-raised) 94%, transparent);
|
||
|
|
box-shadow: var(--air-shadow-1);
|
||
|
|
}
|
||
|
|
|
||
|
|
.hero {
|
||
|
|
display: grid;
|
||
|
|
grid-template-columns: minmax(0, 1.2fr) minmax(240px, 0.8fr);
|
||
|
|
gap: var(--air-space-5);
|
||
|
|
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));
|
||
|
|
}
|
||
|
|
|
||
|
|
.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 {
|
||
|
|
background:
|
||
|
|
linear-gradient(180deg, color-mix(in srgb, var(--air-color-accent-muted) 10%, transparent), transparent),
|
||
|
|
color-mix(in srgb, var(--air-color-surface-raised) 95%, transparent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.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-5);
|
||
|
|
border-radius: var(--air-radius-lg);
|
||
|
|
background: color-mix(in srgb, var(--air-color-surface) 82%, transparent);
|
||
|
|
border: 1px solid color-mix(in srgb, var(--air-color-border) 82%, transparent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.preview--hero {
|
||
|
|
min-height: 220px;
|
||
|
|
background:
|
||
|
|
radial-gradient(circle at top, color-mix(in srgb, var(--air-color-accent-muted) 52%, transparent), transparent 62%),
|
||
|
|
color-mix(in srgb, var(--air-color-surface) 82%, transparent);
|
||
|
|
}
|
||
|
|
|
||
|
|
.preview--compact {
|
||
|
|
min-height: 140px;
|
||
|
|
}
|
||
|
|
|
||
|
|
.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));
|
||
|
|
}
|
||
|
|
|
||
|
|
.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) 84%, transparent);
|
||
|
|
background: color-mix(in srgb, var(--air-color-surface) 82%, transparent);
|
||
|
|
min-height: 120px;
|
||
|
|
}
|
||
|
|
|
||
|
|
@media (max-width: 900px) {
|
||
|
|
.hero,
|
||
|
|
.playground-grid {
|
||
|
|
grid-template-columns: 1fr;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
|