parent
b30092dd06
commit
5eda0c6aef
@ -1,7 +1,10 @@
|
||||
import type { LingString } from '$lib/ling/lib/types';
|
||||
|
||||
export const AIR_DEMO_CONTEXT_KEY = Symbol('air-demo-context');
|
||||
|
||||
export type AirDemoContext = {
|
||||
localeId: () => string;
|
||||
dir: () => 'ltr' | 'rtl';
|
||||
t: (path: string, ...args: unknown[]) => string;
|
||||
tv: (value: LingString) => string;
|
||||
};
|
||||
|
||||
@ -0,0 +1,524 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { Dialog, Field } from '$uix/air';
|
||||
import type { AirDialogSize } from '$uix/air';
|
||||
import type { LingString } from '$lib/ling/lib/types';
|
||||
import type { AirDemoContext } from '../demo-context';
|
||||
import { AIR_DEMO_CONTEXT_KEY } from '../demo-context';
|
||||
import CodeExample from '../_components/code-example.svelte';
|
||||
|
||||
const demoContext = getContext<AirDemoContext>(AIR_DEMO_CONTEXT_KEY);
|
||||
const tv = (value: LingString) => demoContext?.tv(value) ?? String(value ?? '');
|
||||
|
||||
let playgroundOpen = $state(false);
|
||||
let playgroundSize = $state<AirDialogSize>('md');
|
||||
let profileOpen = $state(false);
|
||||
|
||||
const sizes = ['sm', 'md', 'lg', 'full'] as const;
|
||||
|
||||
const playgroundCode = $derived(
|
||||
[
|
||||
`<Dialog.Root bind:open={open}>`,
|
||||
` <Dialog.Trigger class="demo-trigger">Open dialog</Dialog.Trigger>`,
|
||||
` <Dialog.Portal>`,
|
||||
` <Dialog.Overlay />`,
|
||||
` <Dialog.Content size="${playgroundSize}">`,
|
||||
` <Dialog.Header>`,
|
||||
` <Dialog.Title>Dialog title</Dialog.Title>`,
|
||||
` <Dialog.Close />`,
|
||||
` </Dialog.Header>`,
|
||||
` <Dialog.Description>Short supporting copy.</Dialog.Description>`,
|
||||
` <Dialog.Footer>`,
|
||||
` <Dialog.Close>Cancelar</Dialog.Close>`,
|
||||
` <button>Continuar</button>`,
|
||||
` </Dialog.Footer>`,
|
||||
` </Dialog.Content>`,
|
||||
` </Dialog.Portal>`,
|
||||
`</Dialog.Root>`
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
const sizeCode = `<Dialog.Content size="sm">...</Dialog.Content>
|
||||
<Dialog.Content size="md">...</Dialog.Content>
|
||||
<Dialog.Content size="lg">...</Dialog.Content>
|
||||
<Dialog.Content size="full">...</Dialog.Content>`;
|
||||
|
||||
const formCode = `<Dialog.Root bind:open={profileOpen}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay />
|
||||
<Dialog.Content size="lg">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Edit profile</Dialog.Title>
|
||||
<Dialog.Close />
|
||||
</Dialog.Header>
|
||||
<Dialog.Description>Fields, helper text and actions compose normally inside Dialog.</Dialog.Description>
|
||||
<Field.Root>...</Field.Root>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>`;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Dialog · Air</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="dialog-demo-page">
|
||||
<header class="hero">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Air / Dialog</p>
|
||||
<h1>{tv({ es: 'Dialog sobre terra.Dialog', en: 'Dialog over terra.Dialog', ar: 'Dialog فوق terra.Dialog' })}</h1>
|
||||
<p class="lead">
|
||||
{tv({
|
||||
es: '`Dialog` reutiliza el wiring de `terra` para foco, portal y estado, y `air` aterriza la superficie modal, el overlay y la jerarquia visual del contenido.',
|
||||
en: '`Dialog` reuses `terra` wiring for focus, portal and state, and `air` lands the modal surface, overlay and content hierarchy.',
|
||||
ar: 'يعيد `Dialog` استخدام wiring الخاص بـ `terra` للتركيز والبوابة والحالة، بينما يحدد `air` سطح النافذة والطبقة المتراكبة وتسلسل المحتوى.',
|
||||
})}
|
||||
</p>
|
||||
<div class="hero-signals">
|
||||
<span>{tv({ es: 'portal + overlay', en: 'portal + overlay', ar: 'بوابة + طبقة' })}</span>
|
||||
<span>{tv({ es: 'motion contextual', en: 'context motion', ar: 'حركة سياقية' })}</span>
|
||||
<span>{tv({ es: 'size responsive', en: 'responsive size', ar: 'حجم متجاوب' })}</span>
|
||||
<span>{tv({ es: 'header + footer', en: 'header + footer', ar: 'رأس + تذييل' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-aside">
|
||||
<div>
|
||||
<span>{tv({ es: 'API', en: 'API', ar: 'الواجهة' })}</span>
|
||||
<strong>Root · Trigger · Portal · Overlay · Content · Header · Footer · Title · Description · Close</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{tv({ es: 'DOM', en: 'DOM', ar: 'DOM' })}</span>
|
||||
<strong>{tv({
|
||||
es: 'overlay + dialog panel portaled, con title/description enlazados por terra',
|
||||
en: 'portaled overlay + dialog panel, with title/description linked by terra',
|
||||
ar: 'طبقة ولوحة حوار عبر بوابة، مع ربط title و description بواسطة terra',
|
||||
})}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="panel panel--feature">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Playground', en: 'Playground', ar: 'ساحة التجريب' })}</h2>
|
||||
<p>
|
||||
{tv({
|
||||
es: '`size` vive en `Dialog.Content` porque el panel sale a un portal y no puede heredar dimensiones desde `Root`.',
|
||||
en: '`size` lives on `Dialog.Content` because the panel is portaled and cannot inherit dimensions from `Root`.',
|
||||
ar: 'توجد `size` داخل `Dialog.Content` لأن اللوحة تخرج عبر بوابة ولا يمكنها وراثة الأبعاد من `Root`.',
|
||||
})}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="playground-grid">
|
||||
<div class="controls">
|
||||
<div class="control">
|
||||
<span>{tv({ es: 'Size', en: 'Size', ar: 'الحجم' })}</span>
|
||||
<div class="chips">
|
||||
{#each sizes as item}
|
||||
<button
|
||||
type="button"
|
||||
class:chip={true}
|
||||
class:chip--active={playgroundSize === item}
|
||||
onclick={() => (playgroundSize = item)}
|
||||
>{item}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<span>{tv({ es: 'State', en: 'State', ar: 'الحالة' })}</span>
|
||||
<div class="chips">
|
||||
<button
|
||||
type="button"
|
||||
class:chip={true}
|
||||
class:chip--active={playgroundOpen}
|
||||
onclick={() => (playgroundOpen = !playgroundOpen)}
|
||||
>{playgroundOpen ? tv({ es: 'abierto', en: 'open', ar: 'مفتوح' }) : tv({ es: 'cerrado', en: 'closed', ar: 'مغلق' })}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CodeExample title={tv({ es: 'Playground de Dialog', en: 'Dialog playground', ar: 'ساحة Dialog' })} code={playgroundCode}>
|
||||
<div class="preview-stack">
|
||||
<Dialog.Root bind:open={playgroundOpen}>
|
||||
<Dialog.Trigger class="demo-action demo-action--primary">{tv({ es: 'Abrir diálogo', en: 'Open dialog', ar: 'افتح الحوار' })}</Dialog.Trigger>
|
||||
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay />
|
||||
<Dialog.Content size={playgroundSize}>
|
||||
<Dialog.Header>
|
||||
<div class="stack-tight">
|
||||
<Dialog.Title>{tv({ es: 'Invitar compañero', en: 'Invite teammate', ar: 'دعوة زميل' })}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
{tv({
|
||||
es: 'Trae un nuevo colaborador al workspace con el mismo lenguaje visual usado por Field y Button.',
|
||||
en: 'Bring a new collaborator into the workspace with the same visual language used by Field and Button.',
|
||||
ar: 'أضف متعاوناً جديداً إلى مساحة العمل بنفس اللغة البصرية المستخدمة في Field و Button.',
|
||||
})}
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
<Dialog.Close />
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="dialog-body">
|
||||
<p class="body-copy">
|
||||
{tv({
|
||||
es: 'El panel conserva foco y escape desde `terra`, mientras `air` controla elevación, spacing y ritmo tipográfico.',
|
||||
en: 'The panel keeps focus and escape handling from `terra`, while `air` controls elevation, spacing and type rhythm.',
|
||||
ar: 'تحتفظ اللوحة بالتركيز ومعالجة Escape من `terra`، بينما يتحكم `air` في الارتفاع والمسافات والإيقاع الطباعي.',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close>{tv({ es: 'Cancelar', en: 'Cancel', ar: 'إلغاء' })}</Dialog.Close>
|
||||
<button type="button" class="demo-action demo-action--primary">
|
||||
{tv({ es: 'Enviar invitación', en: 'Send invite', ar: 'إرسال الدعوة' })}
|
||||
</button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</div>
|
||||
</CodeExample>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Sizes', en: 'Sizes', ar: 'الأحجام' })}</h2>
|
||||
<p>{tv({ es: 'Los tamaños gobiernan anchura y padding del panel, sin tocar la jerarquia interna.', en: 'Sizes govern panel width and padding without touching internal hierarchy.', ar: 'تتحكم الأحجام في عرض اللوحة وحشوها دون المساس بالهرمية الداخلية.' })}</p>
|
||||
</header>
|
||||
|
||||
<CodeExample title={tv({ es: 'Tamaños de Dialog', en: 'Dialog sizes', ar: 'أحجام Dialog' })} code={sizeCode}>
|
||||
<div class="size-grid">
|
||||
{#each sizes as item}
|
||||
<article class="size-card">
|
||||
<p class="size-title">{item}</p>
|
||||
<div class="size-shell" data-size={item}>
|
||||
<div class="size-surface" data-size={item}>
|
||||
<div class="stack-tight">
|
||||
<p class="size-heading">Dialog {item}</p>
|
||||
<p class="size-copy">
|
||||
{tv({
|
||||
es: `Previsualización de superficie para ${item}. El componente real sale a portal; esta card solo replica anchura y spacing.`,
|
||||
en: `Surface preview for ${item}. The real component is portaled; this card mirrors width and spacing only.`,
|
||||
ar: `معاينة سطح للحجم ${item}. المكوّن الحقيقي يُعرض عبر بوابة؛ وهذه البطاقة تعكس العرض والمسافات فقط.`,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
</CodeExample>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Composición con formularios', en: 'Form composition', ar: 'التركيب مع النماذج' })}</h2>
|
||||
<p>{tv({ es: '`Dialog` compone bien con `Field` porque ambos comparten spacing, bordes y foco del tema.', en: '`Dialog` composes well with `Field` because both share theme spacing, borders and focus.', ar: 'يتكامل `Dialog` جيداً مع `Field` لأنهما يشتركان في المسافات والحدود وحالة التركيز الخاصة بالنسق.' })}</p>
|
||||
</header>
|
||||
|
||||
<CodeExample title={tv({ es: 'Dialog con fields', en: 'Dialog with fields', ar: 'Dialog مع الحقول' })} code={formCode}>
|
||||
<div class="preview-stack">
|
||||
<Dialog.Root bind:open={profileOpen}>
|
||||
<Dialog.Trigger class="demo-action">{tv({ es: 'Editar perfil', en: 'Edit profile', ar: 'تعديل الملف الشخصي' })}</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay />
|
||||
<Dialog.Content size="lg">
|
||||
<Dialog.Header>
|
||||
<div class="stack-tight">
|
||||
<Dialog.Title>{tv({ es: 'Editar perfil', en: 'Edit profile', ar: 'تعديل الملف الشخصي' })}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
{tv({ es: 'Actualiza los datos de la cuenta que ve el resto del equipo.', en: 'Update the account details shown to the rest of the team.', ar: 'حدّث تفاصيل الحساب المعروضة لبقية الفريق.' })}
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
<Dialog.Close />
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="form-stack">
|
||||
<Field.Root>
|
||||
<Field.Label>{tv({ es: 'Nombre', en: 'Name', ar: 'الاسم' })}</Field.Label>
|
||||
<Field.Control>
|
||||
<Field.Input placeholder="Ada Lovelace" />
|
||||
</Field.Control>
|
||||
<Field.HelperText>{tv({ es: 'Visible en menciones y actividad.', en: 'Shown in mentions and activity.', ar: 'يظهر في الإشارات والنشاط.' })}</Field.HelperText>
|
||||
</Field.Root>
|
||||
|
||||
<Field.Root>
|
||||
<Field.Label>Email</Field.Label>
|
||||
<Field.Control>
|
||||
<Field.Input type="email" placeholder="ada@engine.example" />
|
||||
</Field.Control>
|
||||
<Field.HelperText>{tv({ es: 'Usado para invitaciones y notificaciones.', en: 'Used for invites and notifications.', ar: 'يُستخدم للدعوات والإشعارات.' })}</Field.HelperText>
|
||||
</Field.Root>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close>{tv({ es: 'Descartar', en: 'Discard', ar: 'تجاهل' })}</Dialog.Close>
|
||||
<button type="button" class="demo-action demo-action--primary">
|
||||
{tv({ es: 'Guardar cambios', en: 'Save changes', ar: 'حفظ التغييرات' })}
|
||||
</button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</div>
|
||||
</CodeExample>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dialog-demo-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--air-space-6);
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.85fr);
|
||||
gap: var(--air-space-6);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 var(--air-space-2);
|
||||
font: 600 var(--air-font-size-xs) / 1 var(--air-font-mono);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--air-color-content-secondary);
|
||||
}
|
||||
|
||||
.hero h1,
|
||||
.section-header h2,
|
||||
.size-heading,
|
||||
.size-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin: var(--air-space-3) 0 0;
|
||||
max-width: 62ch;
|
||||
color: var(--air-color-content-secondary);
|
||||
font-size: var(--air-font-size-md);
|
||||
line-height: var(--air-leading-prose);
|
||||
}
|
||||
|
||||
.hero-signals,
|
||||
.chips,
|
||||
.preview-stack,
|
||||
.stack-tight,
|
||||
.controls,
|
||||
.form-stack {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.hero-signals,
|
||||
.chips {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hero-signals {
|
||||
gap: var(--air-space-2);
|
||||
margin-top: var(--air-space-4);
|
||||
}
|
||||
|
||||
.hero-signals span,
|
||||
.hero-aside span,
|
||||
.control > span,
|
||||
.size-title {
|
||||
font-family: var(--air-font-mono);
|
||||
font-size: var(--air-font-size-xs);
|
||||
color: var(--air-color-content-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.hero-signals span,
|
||||
.hero-aside > div,
|
||||
.panel,
|
||||
.size-card {
|
||||
border: 1px solid color-mix(in srgb, var(--air-color-border-default) 82%, transparent);
|
||||
background: color-mix(in srgb, var(--air-color-surface-raised) 92%, transparent);
|
||||
}
|
||||
|
||||
.hero-signals span,
|
||||
.chip,
|
||||
.hero-aside > div,
|
||||
.panel,
|
||||
.size-card,
|
||||
.size-surface,
|
||||
.demo-action {
|
||||
border-radius: var(--air-radius-lg);
|
||||
}
|
||||
|
||||
.hero-signals span {
|
||||
padding: 0.45rem 0.7rem;
|
||||
}
|
||||
|
||||
.hero-aside {
|
||||
display: grid;
|
||||
gap: var(--air-space-3);
|
||||
}
|
||||
|
||||
.hero-aside > div {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
padding: var(--air-space-4);
|
||||
}
|
||||
|
||||
.hero-aside strong {
|
||||
font-size: var(--air-font-size-sm);
|
||||
line-height: var(--air-leading-prose);
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: grid;
|
||||
gap: var(--air-space-5);
|
||||
padding: var(--air-space-5);
|
||||
}
|
||||
|
||||
.panel--feature {
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--air-color-surface-overlay) 80%, transparent), transparent 55%),
|
||||
color-mix(in srgb, var(--air-color-surface-raised) 94%, transparent);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.section-header p,
|
||||
.body-copy,
|
||||
.size-copy {
|
||||
margin: 0;
|
||||
color: var(--air-color-content-secondary);
|
||||
line-height: var(--air-leading-prose);
|
||||
}
|
||||
|
||||
.playground-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 320px) minmax(0, 1fr);
|
||||
gap: var(--air-space-5);
|
||||
}
|
||||
|
||||
.controls,
|
||||
.preview-stack,
|
||||
.form-stack {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.controls,
|
||||
.preview-stack,
|
||||
.form-stack,
|
||||
.stack-tight {
|
||||
gap: var(--air-space-3);
|
||||
}
|
||||
|
||||
.control {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.chips {
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.chip {
|
||||
border: 1px solid color-mix(in srgb, var(--air-color-border-default) 82%, transparent);
|
||||
background: transparent;
|
||||
color: var(--air-color-content-secondary);
|
||||
padding: 0.45rem 0.8rem;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--air-duration-fast) var(--air-ease-default),
|
||||
border-color var(--air-duration-fast) var(--air-ease-default),
|
||||
color var(--air-duration-fast) var(--air-ease-default);
|
||||
}
|
||||
|
||||
.chip--active {
|
||||
background: color-mix(in srgb, var(--air-color-primary-track) 82%, transparent);
|
||||
border-color: var(--air-color-primary-border);
|
||||
color: var(--air-color-content-primary);
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
display: grid;
|
||||
gap: var(--air-space-3);
|
||||
}
|
||||
|
||||
.demo-action {
|
||||
min-height: var(--air-control-height-sm);
|
||||
padding-inline: var(--air-space-3);
|
||||
border: var(--air-border-width) solid color-mix(in srgb, var(--air-color-border-default) 82%, transparent);
|
||||
background: transparent;
|
||||
color: var(--air-color-content-primary);
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--air-duration-fast) var(--air-ease-default),
|
||||
border-color var(--air-duration-fast) var(--air-ease-default),
|
||||
color var(--air-duration-fast) var(--air-ease-default);
|
||||
}
|
||||
|
||||
.demo-action--primary {
|
||||
background: var(--air-color-primary-solid);
|
||||
border-color: var(--air-color-primary-solid);
|
||||
color: var(--air-color-primary-contrast);
|
||||
}
|
||||
|
||||
.size-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--air-space-4);
|
||||
}
|
||||
|
||||
.size-card {
|
||||
display: grid;
|
||||
gap: var(--air-space-3);
|
||||
padding: var(--air-space-4);
|
||||
}
|
||||
|
||||
.size-shell {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: var(--air-space-3);
|
||||
background: color-mix(in srgb, var(--air-color-overlay) 18%, transparent);
|
||||
border-radius: var(--air-radius-xl);
|
||||
}
|
||||
|
||||
.size-surface {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
width: min(100%, 560px);
|
||||
padding: var(--air-space-5);
|
||||
border: var(--air-border-width) solid color-mix(in srgb, var(--air-color-border-default) 82%, transparent);
|
||||
background: var(--air-color-surface-overlay);
|
||||
box-shadow: var(--air-shadow-3);
|
||||
}
|
||||
|
||||
.size-shell[data-size='sm'] .size-surface { max-width: 420px; padding: var(--air-space-4); }
|
||||
.size-shell[data-size='md'] .size-surface { max-width: 560px; padding: var(--air-space-5); }
|
||||
.size-shell[data-size='lg'] .size-surface { max-width: 760px; padding: var(--air-space-6); }
|
||||
.size-shell[data-size='full'] .size-surface { max-width: none; width: 100%; min-height: 240px; }
|
||||
|
||||
.size-heading {
|
||||
font-family: var(--air-font-heading);
|
||||
font-size: var(--air-font-size-lg);
|
||||
line-height: var(--air-leading-heading);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero,
|
||||
.playground-grid,
|
||||
.size-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,423 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { Button, Drawer, Field } from '$uix/air';
|
||||
import type { AirDrawerSide, AirDrawerSize } from '$uix/air';
|
||||
import type { LingString } from '$lib/ling/lib/types';
|
||||
import type { AirDemoContext } from '../demo-context';
|
||||
import { AIR_DEMO_CONTEXT_KEY } from '../demo-context';
|
||||
import CodeExample from '../_components/code-example.svelte';
|
||||
|
||||
const demoContext = getContext<AirDemoContext>(AIR_DEMO_CONTEXT_KEY);
|
||||
const tv = (value: LingString) => demoContext?.tv(value) ?? String(value ?? '');
|
||||
|
||||
let playgroundOpen = $state(false);
|
||||
let playgroundSide = $state<AirDrawerSide>('right');
|
||||
let playgroundSize = $state<AirDrawerSize>('md');
|
||||
let projectOpen = $state(false);
|
||||
|
||||
const sides = ['right', 'left', 'bottom', 'top'] as const;
|
||||
const sizes = ['sm', 'md', 'lg', 'full'] as const;
|
||||
|
||||
const playgroundCode = $derived(
|
||||
`<Drawer.Root bind:open={open}>
|
||||
<Drawer.Trigger>Abrir panel</Drawer.Trigger>
|
||||
<Drawer.Portal>
|
||||
<Drawer.Overlay />
|
||||
<Drawer.Content side="${playgroundSide}" size="${playgroundSize}">
|
||||
<Drawer.Header>
|
||||
<Drawer.Title>Workspace activity</Drawer.Title>
|
||||
<Drawer.Close />
|
||||
</Drawer.Header>
|
||||
</Drawer.Content>
|
||||
</Drawer.Portal>
|
||||
</Drawer.Root>`
|
||||
);
|
||||
|
||||
const formCode = `<Drawer.Content side="right" size="lg">...</Drawer.Content>`;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Drawer · Air</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="drawer-demo-page">
|
||||
<header class="hero">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Air / Drawer</p>
|
||||
<h1>{tv({
|
||||
es: 'Panel lateral sobre terra.Drawer',
|
||||
en: 'Panel over terra.Drawer',
|
||||
ar: 'لوحة فوق terra.Drawer',
|
||||
})}</h1>
|
||||
<p class="lead">{tv({
|
||||
es: '`Drawer` comparte la base accesible con `Dialog`, pero aterriza otra lectura espacial: panel anclado a un borde con side y tamaño explícitos.',
|
||||
en: '`Drawer` shares the accessible base with `Dialog`, but lands a different spatial reading: a panel anchored to an edge with explicit side and size.',
|
||||
ar: 'يشترك `Drawer` في الأساس القابل للوصول مع `Dialog`، لكنه يقدّم قراءة مكانية مختلفة: لوحة مثبتة على حافة مع side و size صريحتين.',
|
||||
})}</p>
|
||||
<div class="hero-signals">
|
||||
<span>{tv({ es: 'edge anchored', en: 'edge anchored', ar: 'مثبت على الحافة' })}</span>
|
||||
<span>{tv({ es: 'top · right · bottom · left', en: 'top · right · bottom · left', ar: 'أعلى · يمين · أسفل · يسار' })}</span>
|
||||
<span>{tv({ es: 'shared close language', en: 'shared close language', ar: 'لغة إغلاق مشتركة' })}</span>
|
||||
<span>{tv({ es: 'context motion', en: 'context motion', ar: 'حركة سياقية' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-aside">
|
||||
<div>
|
||||
<span>{tv({ es: 'API', en: 'API', ar: 'الواجهة' })}</span>
|
||||
<strong>Root · Trigger · Portal · Overlay · Content · Header · Footer · Title · Description · Close</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>DOM</span>
|
||||
<strong>{tv({
|
||||
es: 'panel portaled con data-side y data-state',
|
||||
en: 'portaled panel with data-side and data-state',
|
||||
ar: 'لوحة عبر بوابة مع data-side و data-state',
|
||||
})}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{tv({ es: 'Use case', en: 'Use case', ar: 'الاستخدام' })}</span>
|
||||
<strong>{tv({
|
||||
es: 'inspector, settings, quick edit, command sidecar',
|
||||
en: 'inspector, settings, quick edit, command sidecar',
|
||||
ar: 'المعاين والإعدادات والتحرير السريع ولوحة الأوامر الجانبية',
|
||||
})}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="panel panel--feature">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Playground', en: 'Playground', ar: 'ساحة التجريب' })}</h2>
|
||||
<p>{tv({
|
||||
es: '`side` y `size` viven en `Drawer.Content` porque el panel real sale a portal y necesita resolver su geometría en esa pieza.',
|
||||
en: '`side` and `size` live on `Drawer.Content` because the real panel is portaled and needs to resolve its geometry there.',
|
||||
ar: 'تعيش `side` و `size` داخل `Drawer.Content` لأن اللوحة الحقيقية تُعرض عبر بوابة وتحتاج إلى حل هندستها هناك.',
|
||||
})}</p>
|
||||
</header>
|
||||
|
||||
<div class="playground-grid">
|
||||
<div class="controls">
|
||||
<div class="control">
|
||||
<span>{tv({ es: 'Side', en: 'Side', ar: 'الجهة' })}</span>
|
||||
<div class="chips">
|
||||
{#each sides as item}
|
||||
<button
|
||||
type="button"
|
||||
class:chip={true}
|
||||
class:chip--active={playgroundSide === item}
|
||||
onclick={() => (playgroundSide = item)}
|
||||
>{item}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<span>{tv({ es: 'Size', en: 'Size', ar: 'الحجم' })}</span>
|
||||
<div class="chips">
|
||||
{#each sizes as item}
|
||||
<button
|
||||
type="button"
|
||||
class:chip={true}
|
||||
class:chip--active={playgroundSize === item}
|
||||
onclick={() => (playgroundSize = item)}
|
||||
>{item}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CodeExample title={tv({ es: 'Drawer playground', en: 'Drawer playground', ar: 'ساحة Drawer' })} code={playgroundCode}>
|
||||
<div class="preview-stack">
|
||||
<Drawer.Root bind:open={playgroundOpen}>
|
||||
<Drawer.Trigger>
|
||||
<Button variant="surface" color="neutral">{tv({ es: 'Abrir panel', en: 'Open panel', ar: 'فتح اللوحة' })}</Button>
|
||||
</Drawer.Trigger>
|
||||
|
||||
<Drawer.Portal>
|
||||
<Drawer.Overlay />
|
||||
<Drawer.Content side={playgroundSide} size={playgroundSize}>
|
||||
<Drawer.Header>
|
||||
<div class="stack-tight">
|
||||
<Drawer.Title>{tv({ es: 'Actividad del workspace', en: 'Workspace activity', ar: 'نشاط مساحة العمل' })}</Drawer.Title>
|
||||
<Drawer.Description>{tv({
|
||||
es: 'Ideal para contenido que acompaña a la pantalla principal sin bloquearla del todo.',
|
||||
en: 'Ideal for content that accompanies the main screen without fully blocking it.',
|
||||
ar: 'مثالي للمحتوى الذي يرافق الشاشة الرئيسية من دون حجبها بالكامل.',
|
||||
})}</Drawer.Description>
|
||||
</div>
|
||||
<Drawer.Close />
|
||||
</Drawer.Header>
|
||||
|
||||
<div class="body-stack">
|
||||
<div class="activity-card">
|
||||
<strong>{tv({ es: 'Despliegue completado', en: 'Deployment completed', ar: 'اكتمل النشر' })}</strong>
|
||||
<p>{tv({ es: 'La build de producción terminó hace 2 minutos sin errores.', en: 'The production build finished 2 minutes ago with no errors.', ar: 'انتهى بناء الإنتاج قبل دقيقتين من دون أخطاء.' })}</p>
|
||||
</div>
|
||||
<div class="activity-card">
|
||||
<strong>{tv({ es: 'Nueva incidencia asignada', en: 'New incident assigned', ar: 'تم إسناد حادثة جديدة' })}</strong>
|
||||
<p>{tv({ es: 'El panel lateral funciona bien como sidecar de seguimiento.', en: 'The side panel works well as a tracking sidecar.', ar: 'تعمل اللوحة الجانبية جيداً كلوحة متابعة جانبية.' })}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Drawer.Footer>
|
||||
<Drawer.Close>{tv({ es: 'Cerrar', en: 'Close', ar: 'إغلاق' })}</Drawer.Close>
|
||||
<Button variant="solid" color="primary">{tv({ es: 'Ver detalle', en: 'View details', ar: 'عرض التفاصيل' })}</Button>
|
||||
</Drawer.Footer>
|
||||
</Drawer.Content>
|
||||
</Drawer.Portal>
|
||||
</Drawer.Root>
|
||||
</div>
|
||||
</CodeExample>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Composición con formularios', en: 'Form composition', ar: 'التركيب مع النماذج' })}</h2>
|
||||
<p>{tv({
|
||||
es: 'Un drawer es un buen contenedor para edición rápida: no te saca del contexto pero admite formularios completos.',
|
||||
en: 'A drawer is a good container for quick editing: it keeps you in context while still admitting full forms.',
|
||||
ar: 'يُعد drawer حاوية جيدة للتحرير السريع: يبقيك ضمن السياق مع قدرته على احتواء نماذج كاملة.',
|
||||
})}</p>
|
||||
</header>
|
||||
|
||||
<CodeExample title={tv({ es: 'Drawer con fields', en: 'Drawer with fields', ar: 'Drawer مع الحقول' })} code={formCode}>
|
||||
<div class="preview-stack">
|
||||
<Drawer.Root bind:open={projectOpen}>
|
||||
<Drawer.Trigger>
|
||||
<Button variant="surface" color="neutral">{tv({ es: 'Editar proyecto', en: 'Edit project', ar: 'تحرير المشروع' })}</Button>
|
||||
</Drawer.Trigger>
|
||||
<Drawer.Portal>
|
||||
<Drawer.Overlay />
|
||||
<Drawer.Content side="right" size="lg">
|
||||
<Drawer.Header>
|
||||
<div class="stack-tight">
|
||||
<Drawer.Title>{tv({ es: 'Editar proyecto', en: 'Edit project', ar: 'تحرير المشروع' })}</Drawer.Title>
|
||||
<Drawer.Description>{tv({ es: 'Los mismos `Field` del sistema componen aquí sin cambiar receta visual.', en: 'The same system `Field`s compose here without changing the visual recipe.', ar: 'تتكوّن حقول النظام نفسها هنا من دون تغيير الوصفة البصرية.' })}</Drawer.Description>
|
||||
</div>
|
||||
<Drawer.Close />
|
||||
</Drawer.Header>
|
||||
|
||||
<div class="form-stack">
|
||||
<Field.Root>
|
||||
<Field.Label>{tv({ es: 'Nombre', en: 'Name', ar: 'الاسم' })}</Field.Label>
|
||||
<Field.Control>
|
||||
<Field.Input placeholder="Apollo Mission Control" />
|
||||
</Field.Control>
|
||||
<Field.HelperText>{tv({ es: 'Visible en el sidebar del workspace.', en: 'Shown in the workspace sidebar.', ar: 'يظهر في الشريط الجانبي لمساحة العمل.' })}</Field.HelperText>
|
||||
</Field.Root>
|
||||
|
||||
<Field.Root>
|
||||
<Field.Label>{tv({ es: 'Slug', en: 'Slug', ar: 'المعرّف' })}</Field.Label>
|
||||
<Field.Control>
|
||||
<Field.Input placeholder="apollo-control" />
|
||||
</Field.Control>
|
||||
</Field.Root>
|
||||
</div>
|
||||
|
||||
<Drawer.Footer>
|
||||
<Drawer.Close>{tv({ es: 'Cancelar', en: 'Cancel', ar: 'إلغاء' })}</Drawer.Close>
|
||||
<Button variant="solid" color="primary">{tv({ es: 'Guardar', en: 'Save', ar: 'حفظ' })}</Button>
|
||||
</Drawer.Footer>
|
||||
</Drawer.Content>
|
||||
</Drawer.Portal>
|
||||
</Drawer.Root>
|
||||
</div>
|
||||
</CodeExample>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.drawer-demo-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--air-space-6);
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.85fr);
|
||||
gap: var(--air-space-6);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 var(--air-space-2);
|
||||
font: 600 var(--air-font-size-xs) / 1 var(--air-font-mono);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--air-color-content-secondary);
|
||||
}
|
||||
|
||||
.hero h1,
|
||||
.section-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin: var(--air-space-3) 0 0;
|
||||
max-width: 62ch;
|
||||
color: var(--air-color-content-secondary);
|
||||
font-size: var(--air-font-size-md);
|
||||
line-height: var(--air-leading-prose);
|
||||
}
|
||||
|
||||
.hero-signals,
|
||||
.chips,
|
||||
.preview-stack,
|
||||
.controls,
|
||||
.stack-tight,
|
||||
.body-stack,
|
||||
.form-stack {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.hero-signals,
|
||||
.chips {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hero-signals {
|
||||
gap: var(--air-space-2);
|
||||
margin-top: var(--air-space-4);
|
||||
}
|
||||
|
||||
.hero-signals span,
|
||||
.hero-aside span,
|
||||
.control > span {
|
||||
font-family: var(--air-font-mono);
|
||||
font-size: var(--air-font-size-xs);
|
||||
color: var(--air-color-content-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.hero-signals span,
|
||||
.hero-aside > div,
|
||||
.panel,
|
||||
.activity-card {
|
||||
border: 1px solid color-mix(in srgb, var(--air-color-border-default) 82%, transparent);
|
||||
background: color-mix(in srgb, var(--air-color-surface-raised) 92%, transparent);
|
||||
}
|
||||
|
||||
.hero-signals span,
|
||||
.chip,
|
||||
.hero-aside > div,
|
||||
.panel,
|
||||
.activity-card {
|
||||
border-radius: var(--air-radius-lg);
|
||||
}
|
||||
|
||||
.hero-signals span {
|
||||
padding: 0.45rem 0.7rem;
|
||||
}
|
||||
|
||||
.hero-aside {
|
||||
display: grid;
|
||||
gap: var(--air-space-3);
|
||||
}
|
||||
|
||||
.hero-aside > div {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
padding: var(--air-space-4);
|
||||
}
|
||||
|
||||
.hero-aside strong {
|
||||
font-size: var(--air-font-size-sm);
|
||||
line-height: var(--air-leading-prose);
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: grid;
|
||||
gap: var(--air-space-5);
|
||||
padding: var(--air-space-5);
|
||||
}
|
||||
|
||||
.panel--feature {
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--air-color-surface-overlay) 80%, transparent), transparent 55%),
|
||||
color-mix(in srgb, var(--air-color-surface-raised) 94%, transparent);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.section-header p,
|
||||
.activity-card p {
|
||||
margin: 0;
|
||||
color: var(--air-color-content-secondary);
|
||||
line-height: var(--air-leading-prose);
|
||||
}
|
||||
|
||||
.playground-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 320px) minmax(0, 1fr);
|
||||
gap: var(--air-space-5);
|
||||
}
|
||||
|
||||
.controls,
|
||||
.preview-stack,
|
||||
.stack-tight,
|
||||
.body-stack,
|
||||
.form-stack {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.controls,
|
||||
.preview-stack,
|
||||
.stack-tight,
|
||||
.body-stack,
|
||||
.form-stack {
|
||||
gap: var(--air-space-3);
|
||||
}
|
||||
|
||||
.control {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.chips {
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.chip {
|
||||
border: 1px solid color-mix(in srgb, var(--air-color-border-default) 82%, transparent);
|
||||
background: transparent;
|
||||
color: var(--air-color-content-secondary);
|
||||
padding: 0.45rem 0.8rem;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--air-duration-fast) var(--air-ease-default),
|
||||
border-color var(--air-duration-fast) var(--air-ease-default),
|
||||
color var(--air-duration-fast) var(--air-ease-default);
|
||||
}
|
||||
|
||||
.chip--active {
|
||||
background: color-mix(in srgb, var(--air-color-primary-track) 82%, transparent);
|
||||
border-color: var(--air-color-primary-border);
|
||||
color: var(--air-color-content-primary);
|
||||
}
|
||||
|
||||
.activity-card {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
padding: var(--air-space-4);
|
||||
}
|
||||
|
||||
.activity-card strong {
|
||||
font-size: var(--air-font-size-sm);
|
||||
line-height: var(--air-leading-ui);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.hero,
|
||||
.playground-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,485 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { Field, Icon } from '$uix/air';
|
||||
import type { AirFieldSize, AirFieldVariant } from '$uix/air';
|
||||
import type { LingString } from '$lib/ling/lib/types';
|
||||
import type { AirDemoContext } from '../demo-context';
|
||||
import { AIR_DEMO_CONTEXT_KEY } from '../demo-context';
|
||||
import CodeExample from '../_components/code-example.svelte';
|
||||
|
||||
const demoContext = getContext<AirDemoContext>(AIR_DEMO_CONTEXT_KEY);
|
||||
const tv = (value: LingString) => demoContext?.tv(value) ?? String(value ?? '');
|
||||
|
||||
type DemoFieldState = 'idle' | 'error' | 'disabled' | 'readonly' | 'loading';
|
||||
|
||||
let size = $state<AirFieldSize>('md');
|
||||
let variant = $state<AirFieldVariant>('outline');
|
||||
let fieldState = $state<DemoFieldState>('idle');
|
||||
let required = $state(false);
|
||||
let value = $state('');
|
||||
|
||||
const sizes = ['xs', 'sm', 'md', 'lg'] as const;
|
||||
const variants = ['outline', 'ghost'] as const;
|
||||
const states = ['idle', 'error', 'disabled', 'readonly', 'loading'] as const;
|
||||
|
||||
const playgroundCode = $derived(
|
||||
[
|
||||
`<Field.Root`,
|
||||
size !== 'md' ? ` size="${size}"` : '',
|
||||
variant !== 'outline' ? ` variant="${variant}"` : '',
|
||||
fieldState !== 'idle' ? ` state="${fieldState}"` : '',
|
||||
required ? ` required` : '',
|
||||
`>`,
|
||||
` <Field.Label>Email</Field.Label>`,
|
||||
` <Field.Control>`,
|
||||
` <Field.Prefix><Icon.Mail size="sm" /></Field.Prefix>`,
|
||||
` <Field.Input type="email" placeholder="name@company.com" />`,
|
||||
` </Field.Control>`,
|
||||
` <Field.HelperText>Usa tu correo profesional.</Field.HelperText>`,
|
||||
` <Field.Error>Introduce un email valido.</Field.Error>`,
|
||||
`</Field.Root>`
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
);
|
||||
|
||||
const sizesCode = `<Field.Root size="xs">...</Field.Root>
|
||||
<Field.Root size="sm">...</Field.Root>
|
||||
<Field.Root size="md">...</Field.Root>
|
||||
<Field.Root size="lg">...</Field.Root>`;
|
||||
|
||||
const statesCode = `<Field.Root state="idle">...</Field.Root>
|
||||
<Field.Root state="error">...</Field.Root>
|
||||
<Field.Root state="disabled">...</Field.Root>
|
||||
<Field.Root state="readonly">...</Field.Root>
|
||||
<Field.Root state="loading">...</Field.Root>`;
|
||||
|
||||
const adornmentsCode = `<Field.Root>
|
||||
<Field.Label>Price</Field.Label>
|
||||
<Field.Control>
|
||||
<Field.Prefix>€</Field.Prefix>
|
||||
<Field.Input placeholder="0.00" />
|
||||
<Field.Suffix>EUR</Field.Suffix>
|
||||
</Field.Control>
|
||||
</Field.Root>`;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Field · Air</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="field-demo-page">
|
||||
<header class="hero">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Air / Field</p>
|
||||
<h1>{tv({ es: 'Field sobre terra.Field', en: 'Field over terra.Field', ar: 'Field فوق terra.Field' })}</h1>
|
||||
<p class="lead">
|
||||
{tv({
|
||||
es: '`Field` compone el layout y la tipografia del campo; `terra.Field` sigue resolviendo ids, aria y estado. La caja visual vive en `Field.Control` y el elemento real en `Field.Input`.',
|
||||
en: '`Field` composes field layout and typography; `terra.Field` still resolves ids, aria and state. The visual box lives in `Field.Control` and the real element in `Field.Input`.',
|
||||
ar: 'يقوم `Field` ببناء تخطيط الحقل وطباعة النص، بينما يواصل `terra.Field` حل المعرفات وخصائص aria والحالة. الصندوق البصري يوجد في `Field.Control` والعنصر الحقيقي في `Field.Input`.',
|
||||
})}
|
||||
</p>
|
||||
<div class="hero-signals">
|
||||
<span>{tv({ es: 'size + variant', en: 'size + variant', ar: 'الحجم + المتغير' })}</span>
|
||||
<span>{tv({ es: 'terra aria wiring', en: 'terra aria wiring', ar: 'ربط aria من terra' })}</span>
|
||||
<span>{tv({ es: 'control + prefix/suffix', en: 'control + prefix/suffix', ar: 'control + prefix/suffix' })}</span>
|
||||
<span>{tv({ es: 'helper + error', en: 'helper + error', ar: 'مساعدة + خطأ' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-aside">
|
||||
<div>
|
||||
<span>{tv({ es: 'API', en: 'API', ar: 'الواجهة' })}</span>
|
||||
<strong>Root · Label · Control · Input · Prefix · Suffix · HelperText · Error</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{tv({ es: 'DOM', en: 'DOM', ar: 'DOM' })}</span>
|
||||
<strong>{tv({ es: 'div[data-field] > label + div[data-field-control] + p helper/error', en: 'div[data-field] > label + div[data-field-control] + p helper/error', ar: 'div[data-field] > label + div[data-field-control] + p helper/error' })}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="panel panel--feature">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Playground', en: 'Playground', ar: 'ساحة التجريب' })}</h2>
|
||||
<p>
|
||||
{tv({
|
||||
es: '`Field.Root` gobierna densidad y variant; `Field.Control` dibuja la caja visual del control.',
|
||||
en: '`Field.Root` governs density and variant; `Field.Control` draws the visual control box.',
|
||||
ar: 'يتحكم `Field.Root` في الكثافة والمتغير، بينما يرسم `Field.Control` الصندوق البصري للحقل.',
|
||||
})}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="playground-grid">
|
||||
<div class="controls">
|
||||
<div class="control">
|
||||
<span>{tv({ es: 'Size', en: 'Size', ar: 'الحجم' })}</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>{tv({ es: 'Variant', en: 'Variant', ar: 'المتغير' })}</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>{tv({ es: 'State', en: 'State', ar: 'الحالة' })}</span>
|
||||
<div class="chips">
|
||||
{#each states as item}
|
||||
<button
|
||||
type="button"
|
||||
class:chip={true}
|
||||
class:chip--active={fieldState === item}
|
||||
onclick={() => (fieldState = item)}>{item}</button
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<span>{tv({ es: 'Flags', en: 'Flags', ar: 'العلامات' })}</span>
|
||||
<div class="chips">
|
||||
<button
|
||||
type="button"
|
||||
class:chip={true}
|
||||
class:chip--active={required}
|
||||
onclick={() => (required = !required)}
|
||||
>{tv({ es: 'required', en: 'required', ar: 'إلزامي' })}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CodeExample title={tv({ es: 'Playground de Field', en: 'Field playground', ar: 'ساحة Field' })} code={playgroundCode}>
|
||||
<div class="preview-stack">
|
||||
<Field.Root {size} {variant} {required} state={fieldState}>
|
||||
<Field.Label>Email</Field.Label>
|
||||
<Field.Control>
|
||||
<Field.Prefix><Icon.Mail size="sm" /></Field.Prefix>
|
||||
<Field.Input type="email" bind:value placeholder="name@company.com" />
|
||||
<Field.Suffix><Icon.BadgeCheck size="sm" /></Field.Suffix>
|
||||
</Field.Control>
|
||||
<Field.HelperText>{tv({ es: 'Usa tu correo profesional.', en: 'Use your work email.', ar: 'استخدم بريدك المهني.' })}</Field.HelperText>
|
||||
<Field.Error>{tv({ es: 'Introduce un email valido.', en: 'Enter a valid email.', ar: 'أدخل بريداً إلكترونياً صالحاً.' })}</Field.Error>
|
||||
</Field.Root>
|
||||
|
||||
{#if value}
|
||||
<p class="value-hint">
|
||||
<span class="value-label">{tv({ es: 'value:', en: 'value:', ar: 'القيمة:' })}</span>
|
||||
<code class="value-code">{value}</code>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</CodeExample>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Sizes', en: 'Sizes', ar: 'الأحجام' })}</h2>
|
||||
<p>{tv({ es: 'La densidad de `Field` sigue la misma escala que `Input` para no romper composición.', en: '`Field` density follows the same scale as `Input` to preserve composition.', ar: 'تتبع كثافة `Field` نفس مقياس `Input` حتى لا تنكسر البنية.' })}</p>
|
||||
</header>
|
||||
|
||||
<CodeExample title={tv({ es: 'Tamaños de Field', en: 'Field sizes', ar: 'أحجام Field' })} code={sizesCode}>
|
||||
<div class="stack">
|
||||
{#each sizes as item}
|
||||
<Field.Root size={item}>
|
||||
<Field.Label>{item} {tv({ es: 'field', en: 'field', ar: 'حقل' })}</Field.Label>
|
||||
<Field.Control>
|
||||
<Field.Input placeholder={`Field size ${item}`} />
|
||||
</Field.Control>
|
||||
<Field.HelperText>{tv({ es: `Texto de ayuda para ${item}.`, en: `Helper text for ${item}.`, ar: `نص مساعد لـ ${item}.` })}</Field.HelperText>
|
||||
</Field.Root>
|
||||
{/each}
|
||||
</div>
|
||||
</CodeExample>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'States', en: 'States', ar: 'الحالات' })}</h2>
|
||||
<p>{tv({ es: 'El estado sale de `terra.Field` y `air` solo lo refleja visualmente.', en: 'State comes from `terra.Field` and `air` only reflects it visually.', ar: 'تأتي الحالة من `terra.Field` ويعكسها `air` بصرياً فقط.' })}</p>
|
||||
</header>
|
||||
|
||||
<CodeExample title={tv({ es: 'Estados de Field', en: 'Field states', ar: 'حالات Field' })} code={statesCode}>
|
||||
<div class="states-grid">
|
||||
{#each states as item}
|
||||
<div class="state-card">
|
||||
<p class="state-title">{item}</p>
|
||||
<Field.Root state={item}>
|
||||
<Field.Label>Email</Field.Label>
|
||||
<Field.Control>
|
||||
<Field.Input placeholder={`State ${item}`} value={item === 'readonly' ? 'readonly value' : ''} />
|
||||
</Field.Control>
|
||||
<Field.HelperText>{tv({ es: 'Texto de ayuda.', en: 'Helper copy.', ar: 'نص مساعد.' })}</Field.HelperText>
|
||||
<Field.Error>{tv({ es: 'Texto de error.', en: 'Error copy.', ar: 'نص خطأ.' })}</Field.Error>
|
||||
</Field.Root>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CodeExample>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Adornments', en: 'Adornments', ar: 'الزخارف' })}</h2>
|
||||
<p>
|
||||
{tv({ es: '`Field.Prefix` y `Field.Suffix` son decorativos y viven dentro de `Field.Control`.', en: '`Field.Prefix` and `Field.Suffix` are decorative and live inside `Field.Control`.', ar: 'يُعد `Field.Prefix` و `Field.Suffix` عناصر زخرفية ويعيشان داخل `Field.Control`.' })}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<CodeExample title={tv({ es: 'Adornments de Field', en: 'Field adornments', ar: 'زخارف Field' })} code={adornmentsCode}>
|
||||
<div class="states-grid">
|
||||
<Field.Root>
|
||||
<Field.Label>{tv({ es: 'Precio', en: 'Price', ar: 'السعر' })}</Field.Label>
|
||||
<Field.Control>
|
||||
<Field.Prefix>€</Field.Prefix>
|
||||
<Field.Input placeholder="0.00" />
|
||||
<Field.Suffix>EUR</Field.Suffix>
|
||||
</Field.Control>
|
||||
</Field.Root>
|
||||
|
||||
<Field.Root variant="ghost">
|
||||
<Field.Label>{tv({ es: 'Buscar', en: 'Search', ar: 'بحث' })}</Field.Label>
|
||||
<Field.Control>
|
||||
<Field.Prefix><Icon.Search size="sm" /></Field.Prefix>
|
||||
<Field.Input type="search" placeholder={tv({ es: 'Buscar proyectos', en: 'Search projects', ar: 'ابحث في المشاريع' })} />
|
||||
<Field.Suffix><Icon.Calendar size="sm" /></Field.Suffix>
|
||||
</Field.Control>
|
||||
</Field.Root>
|
||||
</div>
|
||||
</CodeExample>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.field-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.15fr) minmax(260px, 0.85fr);
|
||||
gap: var(--air-space-5);
|
||||
background:
|
||||
radial-gradient(circle at top left, color-mix(in srgb, var(--air-color-accent-muted) 22%, 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,
|
||||
.hero-aside,
|
||||
.controls,
|
||||
.control,
|
||||
.preview-stack,
|
||||
.stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hero-copy,
|
||||
.hero-aside {
|
||||
gap: var(--air-space-3);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.eyebrow,
|
||||
.hero-signals span,
|
||||
.hero-aside span,
|
||||
.state-title,
|
||||
.value-label {
|
||||
font-family: var(--air-font-mono);
|
||||
font-size: var(--air-font-size-xs);
|
||||
line-height: var(--air-leading-ui);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0;
|
||||
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 {
|
||||
margin: 0;
|
||||
font-size: var(--air-font-size-md);
|
||||
line-height: var(--air-leading-text);
|
||||
color: var(--air-color-text-secondary);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.playground-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1.1fr 1fr;
|
||||
gap: var(--air-space-5);
|
||||
margin-top: var(--air-space-4);
|
||||
}
|
||||
|
||||
.controls {
|
||||
gap: var(--air-space-4);
|
||||
}
|
||||
|
||||
.control {
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.control > span,
|
||||
.state-title {
|
||||
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-surface-raised);
|
||||
}
|
||||
|
||||
.preview-stack,
|
||||
.stack {
|
||||
gap: var(--air-space-4);
|
||||
}
|
||||
|
||||
.states-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: var(--air-space-4);
|
||||
}
|
||||
|
||||
.state-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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) 84%, transparent);
|
||||
}
|
||||
|
||||
.value-hint {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--air-space-2);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.value-code {
|
||||
font-family: var(--air-font-mono);
|
||||
font-size: var(--air-font-size-xs);
|
||||
color: var(--air-color-accent-text);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero,
|
||||
.playground-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,276 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { airLingNode } from '$uix/air';
|
||||
import { getAir } from '$uix/air/config';
|
||||
import type { LingString } from '$lib/ling/lib/types';
|
||||
import type { AirDemoContext } from '../demo-context';
|
||||
import { AIR_DEMO_CONTEXT_KEY } from '../demo-context';
|
||||
import { AIR_DEMO_AIR_LING_NODE } from '../demo-locale';
|
||||
import CodeExample from '../_components/code-example.svelte';
|
||||
|
||||
const air = getAir();
|
||||
const demoContext = getContext<AirDemoContext>(AIR_DEMO_CONTEXT_KEY);
|
||||
const t = (path: string, ...args: unknown[]) => demoContext?.t(path, ...args) ?? path;
|
||||
const tv = (value: LingString) => demoContext?.tv(value) ?? String(value ?? '');
|
||||
|
||||
const localeId = $derived(demoContext?.localeId() ?? 'es-ES');
|
||||
const dir = $derived(demoContext?.dir() ?? 'ltr');
|
||||
const translatorLocale = $derived(air.translator.current.getLocale());
|
||||
|
||||
const officialInlineEntries = [
|
||||
{ key: 'air.dialog.close', value: '#?air.dialog.close|Cerrar diálogo' },
|
||||
{ key: 'air.code-block.copy', value: '#?air.code-block.copy|Copiar' },
|
||||
{ key: 'air.code-block.copied', value: '#?air.code-block.copied|Copiado' },
|
||||
{ key: 'air.code-block.copy-aria', value: '#?air.code-block.copy-aria|Copiar código' },
|
||||
] as const;
|
||||
|
||||
const demoPathEntries = [
|
||||
{ key: 'air.button.review', args: [] },
|
||||
{ key: 'air.button.buttonSize', args: ['md'] },
|
||||
{ key: 'air.spinner.loadingWith', args: ['bars'] },
|
||||
{ key: 'air.icon.all', args: [] },
|
||||
] as const;
|
||||
|
||||
const bridgeCode = `const ling = createLing({}, 'es');
|
||||
extendTerraTranslationModule(ling, AIR_DEMO_TERRA_LING_NODE);
|
||||
extendAirTranslationModule(ling);
|
||||
ling.extend('air', AIR_DEMO_AIR_LING_NODE);
|
||||
|
||||
const translator = createTerraTranslator({
|
||||
\tlocale: () => ling.getLocale(),
|
||||
\ttranslate: () => ling.ts,
|
||||
\ttranslatePath: () => (path, ...args) => translateAirDemoLingPath(ling, path, ...args),
|
||||
\truntime: () => ling,
|
||||
});`;
|
||||
|
||||
const officialRows = $derived(
|
||||
officialInlineEntries.map((entry) => ({
|
||||
...entry,
|
||||
resolved: air.translator.current.translate(entry.value),
|
||||
}))
|
||||
);
|
||||
|
||||
const demoRows = $derived(
|
||||
demoPathEntries.map((entry) => ({
|
||||
...entry,
|
||||
resolved: air.translator.current.translatePath(entry.key, ...entry.args),
|
||||
}))
|
||||
);
|
||||
|
||||
const officialAirSchema = $derived(JSON.stringify(airLingNode, null, 2));
|
||||
const demoAirSchema = $derived(JSON.stringify(AIR_DEMO_AIR_LING_NODE, null, 2));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Air Ling</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="air-ling-page">
|
||||
<header class="hero">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Air / Ling</p>
|
||||
<h1>{t('air.demo.ling.title')}</h1>
|
||||
<p class="lead">{t('air.demo.ling.lead')}</p>
|
||||
</div>
|
||||
|
||||
<div class="hero-meta">
|
||||
<div>
|
||||
<span>{t('air.demo.ling.uiLocale')}</span>
|
||||
<strong>{localeId}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{t('air.demo.ling.lingLocale')}</span>
|
||||
<strong>{translatorLocale}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{t('air.demo.ling.direction')}</span>
|
||||
<strong>{dir}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{t('air.demo.ling.namespaces')}</span>
|
||||
<strong>terra + air</strong>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<header class="section-header">
|
||||
<h2>{t('air.demo.ling.bridge.title')}</h2>
|
||||
<p>{t('air.demo.ling.bridge.lead')}</p>
|
||||
</header>
|
||||
|
||||
<CodeExample title={t('air.demo.ling.bridge.codeTitle')} code={bridgeCode} defaultOpen={true}>
|
||||
<div class="bridge-note">
|
||||
<strong>{t('air.demo.ling.result')}</strong>
|
||||
<p>{t('air.demo.ling.resultLead')}</p>
|
||||
</div>
|
||||
</CodeExample>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header class="section-header">
|
||||
<h2>{t('air.demo.ling.official.title')}</h2>
|
||||
<p>{t('air.demo.ling.official.lead')}</p>
|
||||
</header>
|
||||
|
||||
<div class="cards">
|
||||
{#each officialRows as row}
|
||||
<article class="card">
|
||||
<span>{row.key}</span>
|
||||
<strong>{row.resolved}</strong>
|
||||
<code>{row.value}</code>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header class="section-header">
|
||||
<h2>{t('air.demo.ling.demo.title')}</h2>
|
||||
<p>{t('air.demo.ling.demo.lead')}</p>
|
||||
</header>
|
||||
|
||||
<div class="cards">
|
||||
{#each demoRows as row}
|
||||
<article class="card">
|
||||
<span>{row.key}</span>
|
||||
<strong>{row.resolved}</strong>
|
||||
<code>{row.args.length ? row.args.join(', ') : t('air.demo.ling.noParams')}</code>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="schema-grid">
|
||||
<CodeExample title={t('air.demo.ling.schemaOfficial')} code={officialAirSchema} />
|
||||
<CodeExample title={t('air.demo.ling.schemaDemo')} code={demoAirSchema} />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.air-ling-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) 22%, 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) 78%, transparent));
|
||||
}
|
||||
|
||||
.hero-copy,
|
||||
.hero-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--air-space-3);
|
||||
}
|
||||
|
||||
.hero-meta div,
|
||||
.card,
|
||||
.bridge-note {
|
||||
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);
|
||||
}
|
||||
|
||||
.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,
|
||||
.bridge-note p {
|
||||
margin: 0;
|
||||
font-size: var(--air-font-size-md);
|
||||
line-height: var(--air-leading-text);
|
||||
color: var(--air-color-text-secondary);
|
||||
}
|
||||
|
||||
.hero-meta span,
|
||||
.card span,
|
||||
.card code {
|
||||
font-family: var(--air-font-mono);
|
||||
font-size: var(--air-font-size-xs);
|
||||
line-height: var(--air-leading-ui);
|
||||
}
|
||||
|
||||
.hero-meta span,
|
||||
.card span,
|
||||
.card code {
|
||||
color: var(--air-color-text-secondary);
|
||||
}
|
||||
|
||||
.hero-meta strong,
|
||||
.card strong,
|
||||
.bridge-note strong {
|
||||
font-size: var(--air-font-size-sm);
|
||||
line-height: var(--air-leading-text);
|
||||
font-weight: var(--air-font-weight-medium);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--air-space-2);
|
||||
margin-bottom: var(--air-space-4);
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
|
||||
gap: var(--air-space-4);
|
||||
}
|
||||
|
||||
.schema-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--air-space-4);
|
||||
}
|
||||
|
||||
.schema-grid :global(.code-example) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero,
|
||||
.schema-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,456 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { Select } from '$uix/air';
|
||||
import type { AirSelectSize, AirSelectVariant, AirSelectMultipleDisplay } from '$uix/air';
|
||||
import type { LingString } from '$lib/ling/lib/types';
|
||||
import type { AirDemoContext } from '../demo-context';
|
||||
import { AIR_DEMO_CONTEXT_KEY } from '../demo-context';
|
||||
import CodeExample from '../_components/code-example.svelte';
|
||||
|
||||
type Item = { value: string; label: LingString; disabled?: boolean };
|
||||
type Group = { heading: LingString; items: Item[] };
|
||||
|
||||
const demoContext = getContext<AirDemoContext>(AIR_DEMO_CONTEXT_KEY);
|
||||
const tv = (value: LingString) => demoContext?.tv(value) ?? String(value ?? '');
|
||||
|
||||
const teams: Item[] = [
|
||||
{ value: 'design', label: { es: 'Design', en: 'Design', ar: 'التصميم' } },
|
||||
{ value: 'product', label: { es: 'Producto', en: 'Product', ar: 'المنتج' } },
|
||||
{ value: 'platform', label: { es: 'Platform', en: 'Platform', ar: 'المنصة' } },
|
||||
{ value: 'ops', label: { es: 'Ops', en: 'Ops', ar: 'العمليات' }, disabled: true }
|
||||
];
|
||||
|
||||
const stacks: Group[] = [
|
||||
{
|
||||
heading: { es: 'Frontend', en: 'Frontend', ar: 'الواجهة الأمامية' },
|
||||
items: [
|
||||
{ value: 'svelte', label: 'Svelte' },
|
||||
{ value: 'react', label: 'React' },
|
||||
{ value: 'vue', label: 'Vue' }
|
||||
]
|
||||
},
|
||||
{
|
||||
heading: { es: 'Backend', en: 'Backend', ar: 'الخلفية' },
|
||||
items: [
|
||||
{ value: 'node', label: 'Node' },
|
||||
{ value: 'go', label: 'Go' },
|
||||
{ value: 'rust', label: 'Rust' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
let value = $state('product');
|
||||
let multiCountValue = $state(['svelte', 'node']);
|
||||
let multiChipsValue = $state(['react', 'go', 'rust']);
|
||||
let size = $state<AirSelectSize>('md');
|
||||
let variant = $state<AirSelectVariant>('outline');
|
||||
let chipsMax = $state(2);
|
||||
let invalid = $state(false);
|
||||
let disabled = $state(false);
|
||||
|
||||
const sizes = ['xs', 'sm', 'md', 'lg'] as const;
|
||||
const variants = ['outline', 'ghost'] as const;
|
||||
const teamItems = $derived(teams.map((item) => ({
|
||||
value: item.value,
|
||||
label: tv(item.label),
|
||||
disabled: item.disabled
|
||||
})));
|
||||
|
||||
function labelFor(items: Item[], currentValue: string) {
|
||||
return items.find((item) => item.value === currentValue)?.label;
|
||||
}
|
||||
|
||||
function labelForGroups(groups: Group[], currentValue: string) {
|
||||
return groups.flatMap((group) => group.items).find((item) => item.value === currentValue)?.label;
|
||||
}
|
||||
|
||||
const stackItems = $derived(
|
||||
stacks.flatMap((group) =>
|
||||
group.items.map((item) => ({ value: item.value, label: tv(item.label), disabled: item.disabled }))
|
||||
)
|
||||
);
|
||||
|
||||
const playgroundCode = $derived(
|
||||
`<Select.Root type="single" value="${value}">
|
||||
<Select.Trigger size="${size}" variant="${variant}" invalid={${invalid}}>
|
||||
${String(tv(labelFor(teams, value) ?? { es: 'Selecciona equipo', en: 'Select team', ar: 'اختر الفريق' }))}
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Portal>
|
||||
<Select.Content sideOffset={8}>
|
||||
<Select.Viewport>
|
||||
<!-- items -->
|
||||
</Select.Viewport>
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select.Root>`
|
||||
);
|
||||
|
||||
const groupedCode = `<Select.Root type="multiple" value={['svelte', 'node']} items={stackItems}>
|
||||
<Select.Trigger size="sm" placeholder="Select stacks" multipleDisplay="count" />
|
||||
<Select.Portal>
|
||||
<Select.Content sideOffset={8}>
|
||||
<Select.Viewport>
|
||||
<Select.Group>
|
||||
<Select.GroupHeading>Frontend</Select.GroupHeading>
|
||||
<Select.Item value="svelte">Svelte</Select.Item>
|
||||
</Select.Group>
|
||||
</Select.Viewport>
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select.Root>`;
|
||||
|
||||
const chipsCode = $derived(`<Select.Root type="multiple" value={['react', 'go', 'rust']} items={stackItems}>
|
||||
<Select.Trigger size="sm" placeholder="Select stacks" multipleDisplay="chips" maxChips={${chipsMax}} />
|
||||
<Select.Portal>
|
||||
<Select.Content sideOffset={8}>...</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select.Root>`);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Select · Air</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="select-demo-page">
|
||||
<header class="hero">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Air / Select</p>
|
||||
<h1>{tv({
|
||||
es: 'Dropdown composable sobre terra.Select',
|
||||
en: 'Composable dropdown over terra.Select',
|
||||
ar: 'قائمة منسدلة قابلة للتركيب فوق terra.Select'
|
||||
})}</h1>
|
||||
<p class="lead">
|
||||
{tv({
|
||||
es: '`Select` reutiliza el wiring accesible de `terra` para trigger, viewport, roving highlight y selección, mientras `air` aterriza el lenguaje visual del control y del panel.',
|
||||
en: '`Select` reuses `terra` accessible wiring for trigger, viewport, roving highlight and selection, while `air` lands the control and panel visual language.',
|
||||
ar: 'يعيد `Select` استخدام البنية القابلة للوصول في `terra` للزر ولوحة العرض والإبراز المتحرك والاختيار، بينما يحدد `air` اللغة البصرية للحقل واللوحة.'
|
||||
})}
|
||||
</p>
|
||||
<div class="hero-signals">
|
||||
<span>{tv({ es: 'trigger + content', en: 'trigger + content', ar: 'زر + محتوى' })}</span>
|
||||
<span>{tv({ es: 'single select', en: 'single select', ar: 'اختيار مفرد' })}</span>
|
||||
<span>{tv({ es: 'grouped items', en: 'grouped items', ar: 'عناصر مجمعة' })}</span>
|
||||
<span>{tv({ es: 'outline · ghost', en: 'outline · ghost', ar: 'outline · ghost' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="panel panel--feature">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Playground', en: 'Playground', ar: 'ساحة التجريب' })}</h2>
|
||||
<p>{tv({
|
||||
es: 'El trigger hereda la gramática de Input, mientras el panel aterriza una superficie de overlay con items navegables.',
|
||||
en: 'The trigger inherits Input grammar, while the panel lands an overlay surface with navigable items.',
|
||||
ar: 'يرث الزر قواعد Input، بينما تقدّم اللوحة سطح overlay بعناصر قابلة للتنقل.'
|
||||
})}</p>
|
||||
</header>
|
||||
|
||||
<div class="playground-grid">
|
||||
<div class="controls">
|
||||
<div class="control">
|
||||
<span>{tv({ es: 'Variant', en: 'Variant', ar: 'النمط' })}</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>{tv({ es: 'Size', en: 'Size', ar: 'الحجم' })}</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>{tv({ es: 'Max chips', en: 'Max chips', ar: 'أقصى عدد للرقائق' })}</span>
|
||||
<div class="chips">
|
||||
{#each [1, 2, 3, 4] as item}
|
||||
<button
|
||||
type="button"
|
||||
class:chip={true}
|
||||
class:chip--active={chipsMax === item}
|
||||
onclick={() => (chipsMax = item)}
|
||||
>{item}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="toggle">
|
||||
<input type="checkbox" bind:checked={invalid} />
|
||||
<span>{tv({ es: 'invalid', en: 'invalid', ar: 'غير صالح' })}</span>
|
||||
</label>
|
||||
|
||||
<label class="toggle">
|
||||
<input type="checkbox" bind:checked={disabled} />
|
||||
<span>{tv({ es: 'disabled', en: 'disabled', ar: 'معطل' })}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<CodeExample title={tv({ es: 'Select playground', en: 'Select playground', ar: 'ساحة Select' })} code={playgroundCode}>
|
||||
<div class="preview-stack">
|
||||
<Select.Root type="single" bind:value items={teamItems}>
|
||||
<Select.Trigger
|
||||
{size}
|
||||
{variant}
|
||||
{invalid}
|
||||
{disabled}
|
||||
placeholder={tv({ es: 'Selecciona equipo', en: 'Select team', ar: 'اختر الفريق' })}
|
||||
/>
|
||||
|
||||
<Select.Portal>
|
||||
<Select.Content sideOffset={8}>
|
||||
<Select.Viewport>
|
||||
{#each teams as item}
|
||||
<Select.Item value={item.value} label={tv(item.label)} disabled={item.disabled}>
|
||||
{tv(item.label)}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Viewport>
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</CodeExample>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Multiple display', en: 'Multiple display', ar: 'عرض الاختيار المتعدد' })}</h2>
|
||||
<p>{tv({
|
||||
es: 'El multi-select necesita una estrategia de lectura clara. Aquí se ven dos: contador compacto y chips dentro del trigger.',
|
||||
en: 'Multi-select needs a clear reading strategy. Here are two: compact count and chips inside the trigger.',
|
||||
ar: 'يحتاج الاختيار المتعدد إلى طريقة قراءة واضحة. هنا يظهر نمطان: عدّاد مختصر ورقائق داخل الزر.'
|
||||
})}</p>
|
||||
</header>
|
||||
|
||||
<div class="multi-grid">
|
||||
<CodeExample title={tv({ es: 'Multiple · count', en: 'Multiple · count', ar: 'متعدد · عداد' })} code={groupedCode}>
|
||||
<Select.Root type="multiple" bind:value={multiCountValue} allowDeselect items={stackItems}>
|
||||
<Select.Trigger
|
||||
size="sm"
|
||||
placeholder={tv({ es: 'Selecciona stacks', en: 'Select stacks', ar: 'اختر التقنيات' })}
|
||||
multipleDisplay={'count' satisfies AirSelectMultipleDisplay}
|
||||
/>
|
||||
|
||||
<Select.Portal>
|
||||
<Select.Content sideOffset={8}>
|
||||
<Select.Viewport>
|
||||
{#each stacks as group}
|
||||
<Select.Group>
|
||||
<Select.GroupHeading>{tv(group.heading)}</Select.GroupHeading>
|
||||
{#each group.items as item}
|
||||
<Select.Item value={item.value} label={tv(item.label)}>
|
||||
{tv(item.label)}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Group>
|
||||
{/each}
|
||||
</Select.Viewport>
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select.Root>
|
||||
</CodeExample>
|
||||
|
||||
<CodeExample title={tv({ es: 'Multiple · chips', en: 'Multiple · chips', ar: 'متعدد · رقائق' })} code={chipsCode}>
|
||||
<Select.Root type="multiple" bind:value={multiChipsValue} allowDeselect items={stackItems}>
|
||||
<Select.Trigger
|
||||
size="sm"
|
||||
placeholder={tv({ es: 'Selecciona stacks', en: 'Select stacks', ar: 'اختر التقنيات' })}
|
||||
multipleDisplay={'chips' satisfies AirSelectMultipleDisplay}
|
||||
maxChips={chipsMax}
|
||||
/>
|
||||
|
||||
<Select.Portal>
|
||||
<Select.Content sideOffset={8}>
|
||||
<Select.Viewport>
|
||||
{#each stacks as group}
|
||||
<Select.Group>
|
||||
<Select.GroupHeading>{tv(group.heading)}</Select.GroupHeading>
|
||||
{#each group.items as item}
|
||||
<Select.Item value={item.value} label={tv(item.label)}>
|
||||
{tv(item.label)}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Group>
|
||||
{/each}
|
||||
</Select.Viewport>
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select.Root>
|
||||
</CodeExample>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.select-demo-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--air-space-6);
|
||||
}
|
||||
|
||||
.hero,
|
||||
.panel {
|
||||
display: grid;
|
||||
gap: var(--air-space-5);
|
||||
}
|
||||
|
||||
.hero {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 var(--air-space-2);
|
||||
font: 600 var(--air-font-size-xs) / 1 var(--air-font-mono);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--air-color-content-secondary);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lead,
|
||||
.section-header p {
|
||||
margin: 0;
|
||||
color: var(--air-color-content-secondary);
|
||||
line-height: var(--air-leading-prose);
|
||||
}
|
||||
|
||||
.hero-signals,
|
||||
.controls,
|
||||
.chips,
|
||||
.preview-stack {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.hero-signals,
|
||||
.chips {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hero-signals {
|
||||
gap: var(--air-space-2);
|
||||
margin-top: var(--air-space-4);
|
||||
}
|
||||
|
||||
.hero-signals span,
|
||||
.control > span {
|
||||
font-family: var(--air-font-mono);
|
||||
font-size: var(--air-font-size-xs);
|
||||
color: var(--air-color-content-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.hero-signals span,
|
||||
.chip,
|
||||
.panel {
|
||||
border-radius: var(--air-radius-lg);
|
||||
}
|
||||
|
||||
.hero-signals span,
|
||||
.panel {
|
||||
border: 1px solid color-mix(in srgb, var(--air-color-border-default) 82%, transparent);
|
||||
background: color-mix(in srgb, var(--air-color-surface-raised) 92%, transparent);
|
||||
}
|
||||
|
||||
.hero-signals span {
|
||||
padding: 0.45rem 0.7rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: var(--air-space-5);
|
||||
}
|
||||
|
||||
.panel--feature {
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--air-color-surface-overlay) 80%, transparent), transparent 55%),
|
||||
color-mix(in srgb, var(--air-color-surface-raised) 94%, transparent);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.playground-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 320px) minmax(0, 1fr);
|
||||
gap: var(--air-space-5);
|
||||
}
|
||||
|
||||
.multi-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--air-space-4);
|
||||
}
|
||||
|
||||
.controls,
|
||||
.preview-stack {
|
||||
flex-direction: column;
|
||||
gap: var(--air-space-3);
|
||||
}
|
||||
|
||||
.control {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.chips {
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.chip {
|
||||
border: 1px solid color-mix(in srgb, var(--air-color-border-default) 82%, transparent);
|
||||
background: transparent;
|
||||
color: var(--air-color-content-secondary);
|
||||
padding: 0.45rem 0.8rem;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--air-duration-fast) var(--air-ease-default),
|
||||
border-color var(--air-duration-fast) var(--air-ease-default),
|
||||
color var(--air-duration-fast) var(--air-ease-default);
|
||||
}
|
||||
|
||||
.chip--active {
|
||||
background: color-mix(in srgb, var(--air-color-primary-track) 82%, transparent);
|
||||
border-color: var(--air-color-primary-border);
|
||||
color: var(--air-color-content-primary);
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--air-space-2);
|
||||
font-size: var(--air-font-size-sm);
|
||||
color: var(--air-color-content-secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.playground-grid,
|
||||
.multi-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,515 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { Button, Switch } from '$uix/air';
|
||||
import type { AirSwitchColor, AirSwitchSize } from '$uix/air';
|
||||
import type { LingString } from '$lib/ling/lib/types';
|
||||
import type { AirDemoContext } from '../demo-context';
|
||||
import { AIR_DEMO_CONTEXT_KEY } from '../demo-context';
|
||||
import CodeExample from '../_components/code-example.svelte';
|
||||
|
||||
const demoContext = getContext<AirDemoContext>(AIR_DEMO_CONTEXT_KEY);
|
||||
const tv = (value: LingString) => demoContext?.tv(value) ?? String(value ?? '');
|
||||
|
||||
let playgroundChecked = $state(true);
|
||||
let playgroundDisabled = $state(false);
|
||||
let playgroundSize = $state<AirSwitchSize>('md');
|
||||
let playgroundColor = $state<AirSwitchColor>('primary');
|
||||
|
||||
let notifications = $state(true);
|
||||
let marketing = $state(false);
|
||||
let security = $state(true);
|
||||
|
||||
const sizes = ['sm', 'md', 'lg'] as const;
|
||||
const colors = ['primary', 'neutral', 'success', 'warning', 'danger', 'info'] as const;
|
||||
|
||||
const playgroundCode = $derived(
|
||||
`<Switch.Root checked={${playgroundChecked}} size="${playgroundSize}" color="${playgroundColor}" disabled={${playgroundDisabled}}>
|
||||
<Switch.Thumb />
|
||||
</Switch.Root>`
|
||||
);
|
||||
|
||||
const listCode = `<Switch.Root bind:checked={notifications} color="success">
|
||||
<Switch.Thumb />
|
||||
</Switch.Root>`;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Switch · Air</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="switch-demo-page">
|
||||
<header class="hero">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Air / Switch</p>
|
||||
<h1>{tv({
|
||||
es: 'Toggle binario sobre terra.Switch',
|
||||
en: 'Binary toggle over terra.Switch',
|
||||
ar: 'مفتاح ثنائي فوق terra.Switch',
|
||||
})}</h1>
|
||||
<p class="lead">{tv({
|
||||
es: '`Switch` reutiliza la semántica accesible de `terra` y `air` aterriza geometría, color, foco y feedback del sistema para cambios on/off.',
|
||||
en: '`Switch` reuses `terra` accessible semantics and `air` lands geometry, color, focus and system feedback for on/off changes.',
|
||||
ar: 'يعيد `Switch` استخدام الدلالة القابلة للوصول من `terra` بينما يحدد `air` الهندسة واللون والتركيز وتغذية النظام الراجعة لحالات التشغيل والإيقاف.',
|
||||
})}</p>
|
||||
<div class="hero-signals">
|
||||
<span>{tv({ es: 'feedback sound', en: 'feedback sound', ar: 'صوت التغذية الراجعة' })}</span>
|
||||
<span>{tv({ es: 'focus ring común', en: 'shared focus ring', ar: 'حلقة تركيز مشتركة' })}</span>
|
||||
<span>{tv({ es: 'size responsive', en: 'responsive size', ar: 'حجم متجاوب' })}</span>
|
||||
<span>{tv({ es: '6 familias de color', en: '6 color families', ar: '6 عائلات لونية' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-aside">
|
||||
<div>
|
||||
<span>{tv({ es: 'API', en: 'API', ar: 'الواجهة' })}</span>
|
||||
<strong>Root · Thumb</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>DOM</span>
|
||||
<strong>{tv({
|
||||
es: 'button[role="switch"] + thumb + input oculto opcional',
|
||||
en: 'button[role="switch"] + thumb + optional hidden input',
|
||||
ar: 'button[role="switch"] + thumb + input مخفي اختياري',
|
||||
})}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{tv({ es: 'Behavior', en: 'Behavior', ar: 'السلوك' })}</span>
|
||||
<strong>{tv({
|
||||
es: 'feedback enter/exit al cambiar checked',
|
||||
en: 'feedback enter/exit on checked change',
|
||||
ar: 'feedback enter/exit عند تغيير checked',
|
||||
})}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="panel panel--feature">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Playground', en: 'Playground', ar: 'ساحة التجريب' })}</h2>
|
||||
<p>{tv({
|
||||
es: 'El wrapper de `air` añade tamaño responsive, familias de color y feedback, pero el contrato accesible sigue siendo el de `terra.Switch`.',
|
||||
en: '`air` wrapper adds responsive size, color families and feedback, while the accessible contract stays the one from `terra.Switch`.',
|
||||
ar: 'يضيف غلاف `air` الحجم المتجاوب والعائلات اللونية والتغذية الراجعة، بينما يبقى العقد القابل للوصول هو عقد `terra.Switch`.',
|
||||
})}</p>
|
||||
</header>
|
||||
|
||||
<div class="playground-grid">
|
||||
<div class="controls">
|
||||
<div class="control">
|
||||
<span>{tv({ es: 'Size', en: 'Size', ar: 'الحجم' })}</span>
|
||||
<div class="chips">
|
||||
{#each sizes as item}
|
||||
<button
|
||||
type="button"
|
||||
class:chip={true}
|
||||
class:chip--active={playgroundSize === item}
|
||||
onclick={() => (playgroundSize = item)}
|
||||
>{item}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<span>{tv({ es: 'Color', en: 'Color', ar: 'اللون' })}</span>
|
||||
<div class="chips">
|
||||
{#each colors as item}
|
||||
<button
|
||||
type="button"
|
||||
class:chip={true}
|
||||
class:chip--active={playgroundColor === item}
|
||||
onclick={() => (playgroundColor = item)}
|
||||
>{item}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<span>{tv({ es: 'State', en: 'State', ar: 'الحالة' })}</span>
|
||||
<div class="chips">
|
||||
<button
|
||||
type="button"
|
||||
class:chip={true}
|
||||
class:chip--active={playgroundChecked}
|
||||
onclick={() => (playgroundChecked = !playgroundChecked)}
|
||||
>{playgroundChecked ? 'on' : 'off'}</button>
|
||||
<button
|
||||
type="button"
|
||||
class:chip={true}
|
||||
class:chip--active={playgroundDisabled}
|
||||
onclick={() => (playgroundDisabled = !playgroundDisabled)}
|
||||
>{playgroundDisabled ? 'disabled' : 'enabled'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CodeExample title={tv({ es: 'Switch playground', en: 'Switch playground', ar: 'ساحة Switch' })} code={playgroundCode}>
|
||||
<div class="preview-stack">
|
||||
<div class="preview-row">
|
||||
<Switch.Root
|
||||
bind:checked={playgroundChecked}
|
||||
size={playgroundSize}
|
||||
color={playgroundColor}
|
||||
disabled={playgroundDisabled}
|
||||
>
|
||||
<Switch.Thumb />
|
||||
</Switch.Root>
|
||||
|
||||
<div class="state-copy">
|
||||
<strong>{playgroundChecked ? 'checked' : 'unchecked'}</strong>
|
||||
<p>{tv({
|
||||
es: 'El tamaño mueve geometría y thumb. El color solo aparece en estado checked.',
|
||||
en: 'Size drives geometry and thumb. Color only appears in the checked state.',
|
||||
ar: 'يقود الحجم الهندسة والعنصر الداخلي. ويظهر اللون فقط في حالة التفعيل.',
|
||||
})}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CodeExample>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Tamaños y colores', en: 'Sizes and colors', ar: 'الأحجام والألوان' })}</h2>
|
||||
<p>{tv({
|
||||
es: 'Los tamaños gobiernan rail y thumb; las familias gobiernan solo el estado activo.',
|
||||
en: 'Sizes govern rail and thumb; families govern only the active state.',
|
||||
ar: 'تتحكم الأحجام في المسار والعنصر الداخلي؛ وتتحكم العائلات في الحالة النشطة فقط.',
|
||||
})}</p>
|
||||
</header>
|
||||
|
||||
<div class="matrix">
|
||||
{#each sizes as size}
|
||||
<CodeExample title={`Switch ${size}`} code={`<Switch.Root size="${size}" color="success"><Switch.Thumb /></Switch.Root>`}>
|
||||
<div class="matrix-row">
|
||||
{#each colors as color}
|
||||
<div class="matrix-item">
|
||||
<Switch.Root checked size={size} {color}>
|
||||
<Switch.Thumb />
|
||||
</Switch.Root>
|
||||
<span>{color}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</CodeExample>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Composición en listas', en: 'List composition', ar: 'التركيب داخل القوائم' })}</h2>
|
||||
<p>{tv({
|
||||
es: 'El caso típico no es un switch flotando solo, sino una preferencia con nombre, descripción y estado persistente.',
|
||||
en: 'The typical case is not a floating switch alone, but a preference with name, description and persistent state.',
|
||||
ar: 'الحالة المعتادة ليست مفتاحاً منفرداً، بل تفضيلاً مع اسم ووصف وحالة مستمرة.',
|
||||
})}</p>
|
||||
</header>
|
||||
|
||||
<CodeExample title={tv({ es: 'Preferencias', en: 'Preferences', ar: 'التفضيلات' })} code={listCode}>
|
||||
<div class="settings-list">
|
||||
<div class="setting-row">
|
||||
<div class="setting-copy">
|
||||
<strong>{tv({ es: 'Notificaciones push', en: 'Push notifications', ar: 'إشعارات فورية' })}</strong>
|
||||
<p>{tv({ es: 'Alertas en tiempo real cuando cambian incidencias asignadas.', en: 'Real-time alerts when assigned incidents change.', ar: 'تنبيهات فورية عند تغيّر الحوادث المعيّنة.' })}</p>
|
||||
</div>
|
||||
<Switch.Root bind:checked={notifications} color="success" name="notifications">
|
||||
<Switch.Thumb />
|
||||
</Switch.Root>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-copy">
|
||||
<strong>{tv({ es: 'Correos de marketing', en: 'Marketing email', ar: 'رسائل التسويق' })}</strong>
|
||||
<p>{tv({ es: 'Novedades y lanzamientos del producto.', en: 'Product updates and launches.', ar: 'مستجدات وإطلاقات المنتج.' })}</p>
|
||||
</div>
|
||||
<Switch.Root bind:checked={marketing} color="warning" name="marketing">
|
||||
<Switch.Thumb />
|
||||
</Switch.Root>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div class="setting-copy">
|
||||
<strong>{tv({ es: 'Alertas de seguridad', en: 'Security alerts', ar: 'تنبيهات الأمان' })}</strong>
|
||||
<p>{tv({ es: 'Actividad sospechosa, dispositivos nuevos e inicios de sesión.', en: 'Suspicious activity, new devices and sign-ins.', ar: 'نشاط مريب وأجهزة جديدة وتسجيلات الدخول.' })}</p>
|
||||
</div>
|
||||
<Switch.Root bind:checked={security} color="danger" name="security">
|
||||
<Switch.Thumb />
|
||||
</Switch.Root>
|
||||
</div>
|
||||
</div>
|
||||
</CodeExample>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Control externo', en: 'External control', ar: 'تحكم خارجي' })}</h2>
|
||||
<p>{tv({
|
||||
es: 'El switch sigue siendo bindable, así que puede participar en flujos controlados sin perder su semántica.',
|
||||
en: 'The switch stays bindable, so it can participate in controlled flows without losing its semantics.',
|
||||
ar: 'يبقى المفتاح قابلاً للربط، لذلك يمكنه المشاركة في تدفقات متحكم بها من دون فقدان دلالته.',
|
||||
})}</p>
|
||||
</header>
|
||||
|
||||
<div class="external-control">
|
||||
<Button
|
||||
variant="surface"
|
||||
color={playgroundChecked ? 'neutral' : 'primary'}
|
||||
onclick={() => (playgroundChecked = !playgroundChecked)}
|
||||
>
|
||||
{playgroundChecked
|
||||
? tv({ es: 'Desactivar playground', en: 'Disable playground', ar: 'تعطيل ساحة التجريب' })
|
||||
: tv({ es: 'Activar playground', en: 'Enable playground', ar: 'تفعيل ساحة التجريب' })}
|
||||
</Button>
|
||||
|
||||
<Switch.Root bind:checked={playgroundChecked} size="lg" color="info">
|
||||
<Switch.Thumb />
|
||||
</Switch.Root>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.switch-demo-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--air-space-6);
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.85fr);
|
||||
gap: var(--air-space-6);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 var(--air-space-2);
|
||||
font: 600 var(--air-font-size-xs) / 1 var(--air-font-mono);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--air-color-content-secondary);
|
||||
}
|
||||
|
||||
.hero h1,
|
||||
.section-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin: var(--air-space-3) 0 0;
|
||||
max-width: 62ch;
|
||||
color: var(--air-color-content-secondary);
|
||||
font-size: var(--air-font-size-md);
|
||||
line-height: var(--air-leading-prose);
|
||||
}
|
||||
|
||||
.hero-signals,
|
||||
.chips,
|
||||
.preview-stack,
|
||||
.controls {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.hero-signals,
|
||||
.chips {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hero-signals {
|
||||
gap: var(--air-space-2);
|
||||
margin-top: var(--air-space-4);
|
||||
}
|
||||
|
||||
.hero-signals span,
|
||||
.hero-aside span,
|
||||
.control > span,
|
||||
.matrix-item span {
|
||||
font-family: var(--air-font-mono);
|
||||
font-size: var(--air-font-size-xs);
|
||||
color: var(--air-color-content-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.hero-signals span,
|
||||
.hero-aside > div,
|
||||
.panel {
|
||||
border: 1px solid color-mix(in srgb, var(--air-color-border-default) 82%, transparent);
|
||||
background: color-mix(in srgb, var(--air-color-surface-raised) 92%, transparent);
|
||||
}
|
||||
|
||||
.hero-signals span,
|
||||
.chip,
|
||||
.hero-aside > div,
|
||||
.panel {
|
||||
border-radius: var(--air-radius-lg);
|
||||
}
|
||||
|
||||
.hero-signals span {
|
||||
padding: 0.45rem 0.7rem;
|
||||
}
|
||||
|
||||
.hero-aside {
|
||||
display: grid;
|
||||
gap: var(--air-space-3);
|
||||
}
|
||||
|
||||
.hero-aside > div {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
padding: var(--air-space-4);
|
||||
}
|
||||
|
||||
.hero-aside strong {
|
||||
font-size: var(--air-font-size-sm);
|
||||
line-height: var(--air-leading-prose);
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: grid;
|
||||
gap: var(--air-space-5);
|
||||
padding: var(--air-space-5);
|
||||
}
|
||||
|
||||
.panel--feature {
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--air-color-surface-overlay) 80%, transparent), transparent 55%),
|
||||
color-mix(in srgb, var(--air-color-surface-raised) 94%, transparent);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.section-header p,
|
||||
.state-copy p,
|
||||
.setting-copy p {
|
||||
margin: 0;
|
||||
color: var(--air-color-content-secondary);
|
||||
line-height: var(--air-leading-prose);
|
||||
}
|
||||
|
||||
.playground-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 320px) minmax(0, 1fr);
|
||||
gap: var(--air-space-5);
|
||||
}
|
||||
|
||||
.controls,
|
||||
.preview-stack {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.controls,
|
||||
.preview-stack {
|
||||
gap: var(--air-space-3);
|
||||
}
|
||||
|
||||
.control {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.chips {
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.chip {
|
||||
border: 1px solid color-mix(in srgb, var(--air-color-border-default) 82%, transparent);
|
||||
background: transparent;
|
||||
color: var(--air-color-content-secondary);
|
||||
padding: 0.45rem 0.8rem;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--air-duration-fast) var(--air-ease-default),
|
||||
border-color var(--air-duration-fast) var(--air-ease-default),
|
||||
color var(--air-duration-fast) var(--air-ease-default);
|
||||
}
|
||||
|
||||
.chip--active {
|
||||
background: color-mix(in srgb, var(--air-color-primary-track) 82%, transparent);
|
||||
border-color: var(--air-color-primary-border);
|
||||
color: var(--air-color-content-primary);
|
||||
}
|
||||
|
||||
.preview-row,
|
||||
.setting-row,
|
||||
.external-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--air-space-4);
|
||||
}
|
||||
|
||||
.state-copy {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.state-copy strong,
|
||||
.setting-copy strong {
|
||||
font-size: var(--air-font-size-sm);
|
||||
line-height: var(--air-leading-ui);
|
||||
}
|
||||
|
||||
.matrix {
|
||||
display: grid;
|
||||
gap: var(--air-space-4);
|
||||
}
|
||||
|
||||
.matrix-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--air-space-4);
|
||||
}
|
||||
|
||||
.matrix-item {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: var(--air-space-2);
|
||||
min-width: 72px;
|
||||
}
|
||||
|
||||
.settings-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
padding: var(--air-space-3) 0;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--air-color-border-default) 72%, transparent);
|
||||
}
|
||||
|
||||
.setting-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.setting-copy {
|
||||
display: grid;
|
||||
gap: var(--air-space-1);
|
||||
}
|
||||
|
||||
.external-control {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.hero,
|
||||
.playground-grid,
|
||||
.preview-row,
|
||||
.setting-row,
|
||||
.external-control {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.preview-row,
|
||||
.setting-row,
|
||||
.external-control {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,489 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { Tabs } from '$uix/air';
|
||||
import type { AirTabsSize, AirTabsVariant } from '$uix/air';
|
||||
import type { LingString } from '$lib/ling/lib/types';
|
||||
import type { AirDemoContext } from '../demo-context';
|
||||
import { AIR_DEMO_CONTEXT_KEY } from '../demo-context';
|
||||
import CodeExample from '../_components/code-example.svelte';
|
||||
|
||||
const demoContext = getContext<AirDemoContext>(AIR_DEMO_CONTEXT_KEY);
|
||||
const tv = (value: LingString) => demoContext?.tv(value) ?? String(value ?? '');
|
||||
|
||||
let playgroundValue = $state('overview');
|
||||
let playgroundSize = $state<AirTabsSize>('md');
|
||||
let playgroundVariant = $state<AirTabsVariant>('line');
|
||||
let playgroundOrientation = $state<'horizontal' | 'vertical'>('horizontal');
|
||||
let playgroundActivation = $state<'automatic' | 'manual'>('automatic');
|
||||
|
||||
let verticalValue = $state('metrics');
|
||||
|
||||
const sizes = ['xs', 'sm', 'md', 'lg'] as const;
|
||||
const variants = ['line', 'surface', 'pills'] as const;
|
||||
const orientations = ['horizontal', 'vertical'] as const;
|
||||
const activationModes = ['automatic', 'manual'] as const;
|
||||
|
||||
const tabsCode = $derived(
|
||||
`<Tabs.Root
|
||||
value="${playgroundValue}"
|
||||
size="${playgroundSize}"
|
||||
variant="${playgroundVariant}"
|
||||
orientation="${playgroundOrientation}"
|
||||
activationMode="${playgroundActivation}"
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
|
||||
<Tabs.Trigger value="activity">Activity</Tabs.Trigger>
|
||||
<Tabs.Trigger value="settings">Settings</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content value="overview">...</Tabs.Content>
|
||||
<Tabs.Content value="activity">...</Tabs.Content>
|
||||
<Tabs.Content value="settings">...</Tabs.Content>
|
||||
</Tabs.Root>`
|
||||
);
|
||||
|
||||
const verticalCode = `<Tabs.Root value="metrics" orientation="vertical" variant="surface" size="sm">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="metrics">Metrics</Tabs.Trigger>
|
||||
<Tabs.Trigger value="members">Members</Tabs.Trigger>
|
||||
<Tabs.Trigger value="logs">Logs</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content value="metrics">...</Tabs.Content>
|
||||
<Tabs.Content value="members">...</Tabs.Content>
|
||||
<Tabs.Content value="logs">...</Tabs.Content>
|
||||
</Tabs.Root>`;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Tabs · Air</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="tabs-demo-page">
|
||||
<header class="hero">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Air / Tabs</p>
|
||||
<h1>{tv({ es: 'Navegación tabulada sobre terra.Tabs', en: 'Tabbed navigation over terra.Tabs', ar: 'تنقل بعلامات تبويب فوق terra.Tabs' })}</h1>
|
||||
<p class="lead">
|
||||
{tv({
|
||||
es: '`Tabs` reutiliza el wiring accesible de `terra` para roving focus, activación y relación trigger/panel, y `air` aterriza densidad, variantes y lectura visual del estado activo.',
|
||||
en: '`Tabs` reuses `terra` accessible wiring for roving focus, activation and trigger/panel relationships, while `air` lands density, variants and the visual reading of active state.',
|
||||
ar: 'يعيد `Tabs` استخدام البنية القابلة للوصول في `terra` للتركيز المتحرك والتفعيل وعلاقة الزر باللوحة، بينما يحدد `air` الكثافة والأنماط وقراءة الحالة النشطة.',
|
||||
})}
|
||||
</p>
|
||||
<div class="hero-signals">
|
||||
<span>{tv({ es: 'navigation sound', en: 'navigation sound', ar: 'صوت التنقل' })}</span>
|
||||
<span>{tv({ es: 'roving focus', en: 'roving focus', ar: 'تركيز متحرك' })}</span>
|
||||
<span>{tv({ es: 'horizontal + vertical', en: 'horizontal + vertical', ar: 'أفقي + عمودي' })}</span>
|
||||
<span>{tv({ es: 'line · surface · pills', en: 'line · surface · pills', ar: 'line · surface · pills' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-aside">
|
||||
<div>
|
||||
<span>{tv({ es: 'API', en: 'API', ar: 'الواجهة' })}</span>
|
||||
<strong>Root · List · Trigger · Content</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>DOM</span>
|
||||
<strong>{tv({
|
||||
es: 'div[data-tabs] + tablist + tab triggers + tabpanel',
|
||||
en: 'div[data-tabs] + tablist + tab triggers + tabpanel',
|
||||
ar: 'div[data-tabs] + tablist + tab triggers + tabpanel',
|
||||
})}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{tv({ es: 'Semántica', en: 'Semantic', ar: 'الدلالة' })}</span>
|
||||
<strong>{tv({
|
||||
es: 'navigation al cambiar de tab',
|
||||
en: 'navigation on tab change',
|
||||
ar: 'navigation عند تغيير التبويب',
|
||||
})}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="panel panel--feature">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Playground', en: 'Playground', ar: 'ساحة التجريب' })}</h2>
|
||||
<p>
|
||||
{tv({
|
||||
es: 'La receta visual vive en `variant` y `size`, mientras `terra` sigue gobernando activación, teclado y relación ARIA.',
|
||||
en: 'The visual recipe lives in `variant` and `size`, while `terra` still governs activation, keyboard handling and ARIA relationships.',
|
||||
ar: 'تعيش الوصفة البصرية في `variant` و `size`، بينما يواصل `terra` إدارة التفعيل ولوحة المفاتيح وعلاقات ARIA.',
|
||||
})}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="playground-grid">
|
||||
<div class="controls">
|
||||
<div class="control">
|
||||
<span>{tv({ es: 'Variant', en: 'Variant', ar: 'النمط' })}</span>
|
||||
<div class="chips">
|
||||
{#each variants as item}
|
||||
<button
|
||||
type="button"
|
||||
class:chip={true}
|
||||
class:chip--active={playgroundVariant === item}
|
||||
onclick={() => (playgroundVariant = item)}
|
||||
>{item}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<span>{tv({ es: 'Size', en: 'Size', ar: 'الحجم' })}</span>
|
||||
<div class="chips">
|
||||
{#each sizes as item}
|
||||
<button
|
||||
type="button"
|
||||
class:chip={true}
|
||||
class:chip--active={playgroundSize === item}
|
||||
onclick={() => (playgroundSize = item)}
|
||||
>{item}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<span>{tv({ es: 'Orientation', en: 'Orientation', ar: 'الاتجاه' })}</span>
|
||||
<div class="chips">
|
||||
{#each orientations as item}
|
||||
<button
|
||||
type="button"
|
||||
class:chip={true}
|
||||
class:chip--active={playgroundOrientation === item}
|
||||
onclick={() => (playgroundOrientation = item)}
|
||||
>{item}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<span>{tv({ es: 'Activation', en: 'Activation', ar: 'التفعيل' })}</span>
|
||||
<div class="chips">
|
||||
{#each activationModes as item}
|
||||
<button
|
||||
type="button"
|
||||
class:chip={true}
|
||||
class:chip--active={playgroundActivation === item}
|
||||
onclick={() => (playgroundActivation = item)}
|
||||
>{item}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CodeExample title={tv({ es: 'Tabs playground', en: 'Tabs playground', ar: 'ساحة Tabs' })} code={tabsCode}>
|
||||
<div class="preview-stack">
|
||||
<Tabs.Root
|
||||
bind:value={playgroundValue}
|
||||
size={playgroundSize}
|
||||
variant={playgroundVariant}
|
||||
orientation={playgroundOrientation}
|
||||
activationMode={playgroundActivation}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="overview">{tv({ es: 'Resumen', en: 'Overview', ar: 'نظرة عامة' })}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="activity">{tv({ es: 'Actividad', en: 'Activity', ar: 'النشاط' })}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="settings">{tv({ es: 'Ajustes', en: 'Settings', ar: 'الإعدادات' })}</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content value="overview">
|
||||
<div class="content-stack">
|
||||
<h3>{tv({ es: 'Resumen del workspace', en: 'Workspace overview', ar: 'نظرة عامة على مساحة العمل' })}</h3>
|
||||
<p>{tv({
|
||||
es: 'El estado activo debe leerse rápido sin romper la jerarquía tipográfica del contenido.',
|
||||
en: 'The active state should read quickly without breaking the content typography hierarchy.',
|
||||
ar: 'يجب أن تُقرأ الحالة النشطة بسرعة من دون كسر الهرمية الطباعية للمحتوى.',
|
||||
})}</p>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="activity">
|
||||
<div class="content-stack">
|
||||
<h3>{tv({ es: 'Actividad reciente', en: 'Recent activity', ar: 'النشاط الأخير' })}</h3>
|
||||
<p>{tv({
|
||||
es: 'En modo automático el foco ya cambia la tab; en manual hace falta Enter o Space.',
|
||||
en: 'In automatic mode focus already changes the tab; in manual mode Enter or Space is required.',
|
||||
ar: 'في الوضع التلقائي يغيّر التركيز التبويب مباشرة؛ أما في الوضع اليدوي فيلزم Enter أو Space.',
|
||||
})}</p>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="settings">
|
||||
<div class="content-stack">
|
||||
<h3>{tv({ es: 'Preferencias', en: 'Preferences', ar: 'التفضيلات' })}</h3>
|
||||
<p>{tv({
|
||||
es: 'La receta de `air` toca densidad, fondo, borde y foco; `terra` mantiene el contrato accesible.',
|
||||
en: '`air` recipe touches density, background, border and focus; `terra` keeps the accessible contract.',
|
||||
ar: 'تضبط وصفة `air` الكثافة والخلفية والحدود والتركيز؛ بينما يحافظ `terra` على العقد القابل للوصول.',
|
||||
})}</p>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</div>
|
||||
</CodeExample>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Variantes', en: 'Variants', ar: 'الأنماط' })}</h2>
|
||||
<p>{tv({
|
||||
es: 'Las variantes no cambian el wiring; solo la lectura visual de la navegación.',
|
||||
en: 'Variants do not change the wiring; they only change the visual reading of navigation.',
|
||||
ar: 'لا تغيّر الأنماط البنية الأساسية؛ بل تغيّر فقط القراءة البصرية للتنقل.',
|
||||
})}</p>
|
||||
</header>
|
||||
|
||||
<div class="variant-grid">
|
||||
{#each variants as variant}
|
||||
<CodeExample title={`Tabs ${variant}`} code={`<Tabs.Root variant="${variant}">...</Tabs.Root>`}>
|
||||
<Tabs.Root value="one" variant={variant} size="sm">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="one">{tv({ es: 'Uno', en: 'One', ar: 'واحد' })}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="two">{tv({ es: 'Dos', en: 'Two', ar: 'اثنان' })}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="three">{tv({ es: 'Tres', en: 'Three', ar: 'ثلاثة' })}</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="one">
|
||||
<p class="mini-copy">{tv({
|
||||
es: `Lectura visual de la variante ${variant}.`,
|
||||
en: `${variant} variant visual reading.`,
|
||||
ar: `القراءة البصرية للنمط ${variant}.`,
|
||||
})}</p>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="two"></Tabs.Content>
|
||||
<Tabs.Content value="three"></Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</CodeExample>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Vertical', en: 'Vertical', ar: 'عمودي' })}</h2>
|
||||
<p>{tv({
|
||||
es: 'La orientación vertical debe seguir siendo legible en desktop y caer con dignidad a una columna en móvil.',
|
||||
en: 'Vertical orientation should stay readable on desktop and gracefully collapse to one column on mobile.',
|
||||
ar: 'يجب أن تبقى البنية العمودية مقروءة على سطح المكتب وأن تنهار بسلاسة إلى عمود واحد على الجوال.',
|
||||
})}</p>
|
||||
</header>
|
||||
|
||||
<CodeExample title={tv({ es: 'Tabs verticales', en: 'Vertical tabs', ar: 'علامات تبويب عمودية' })} code={verticalCode}>
|
||||
<Tabs.Root bind:value={verticalValue} orientation="vertical" variant="surface" size="sm">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="metrics">{tv({ es: 'Métricas', en: 'Metrics', ar: 'المقاييس' })}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="members">{tv({ es: 'Equipo', en: 'Team', ar: 'الفريق' })}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="logs">{tv({ es: 'Logs', en: 'Logs', ar: 'السجلات' })}</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content value="metrics">
|
||||
<div class="content-stack">
|
||||
<h3>{tv({ es: 'Métricas', en: 'Metrics', ar: 'المقاييس' })}</h3>
|
||||
<p>{tv({ es: 'Ideal para paneles con navegación local persistente.', en: 'Ideal for dashboards with persistent local navigation.', ar: 'مثالي للوحات التحكم ذات التنقل المحلي المستمر.' })}</p>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="members">
|
||||
<div class="content-stack">
|
||||
<h3>{tv({ es: 'Equipo', en: 'Team', ar: 'الفريق' })}</h3>
|
||||
<p>{tv({ es: 'El rail lateral mantiene jerarquía sin convertirse en un menú pesado.', en: 'The side rail keeps hierarchy without turning into a heavy menu.', ar: 'يحافظ الشريط الجانبي على الهرمية من دون أن يتحول إلى قائمة ثقيلة.' })}</p>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="logs">
|
||||
<div class="content-stack">
|
||||
<h3>{tv({ es: 'Logs', en: 'Logs', ar: 'السجلات' })}</h3>
|
||||
<p>{tv({ es: 'La receta usa las mismas escalas de spacing, borde y foco del resto de controles.', en: 'The recipe uses the same spacing, border and focus scales as the rest of the controls.', ar: 'تستخدم الوصفة مقاييس المسافات والحدود والتركيز نفسها مثل بقية عناصر التحكم.' })}</p>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</CodeExample>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.tabs-demo-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--air-space-6);
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.85fr);
|
||||
gap: var(--air-space-6);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 var(--air-space-2);
|
||||
font: 600 var(--air-font-size-xs) / 1 var(--air-font-mono);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--air-color-content-secondary);
|
||||
}
|
||||
|
||||
.hero h1,
|
||||
.section-header h2,
|
||||
.content-stack h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin: var(--air-space-3) 0 0;
|
||||
max-width: 64ch;
|
||||
color: var(--air-color-content-secondary);
|
||||
font-size: var(--air-font-size-md);
|
||||
line-height: var(--air-leading-prose);
|
||||
}
|
||||
|
||||
.hero-signals,
|
||||
.controls,
|
||||
.chips,
|
||||
.preview-stack,
|
||||
.content-stack,
|
||||
.hero-aside {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.hero-signals,
|
||||
.chips {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hero-signals {
|
||||
gap: var(--air-space-2);
|
||||
margin-top: var(--air-space-4);
|
||||
}
|
||||
|
||||
.hero-signals span,
|
||||
.hero-aside span,
|
||||
.control > span {
|
||||
font-family: var(--air-font-mono);
|
||||
font-size: var(--air-font-size-xs);
|
||||
color: var(--air-color-content-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.hero-signals span,
|
||||
.hero-aside > div,
|
||||
.panel {
|
||||
border: 1px solid color-mix(in srgb, var(--air-color-border-default) 82%, transparent);
|
||||
background: color-mix(in srgb, var(--air-color-surface-raised) 92%, transparent);
|
||||
}
|
||||
|
||||
.hero-signals span,
|
||||
.chip,
|
||||
.hero-aside > div,
|
||||
.panel {
|
||||
border-radius: var(--air-radius-lg);
|
||||
}
|
||||
|
||||
.hero-signals span {
|
||||
padding: 0.45rem 0.7rem;
|
||||
}
|
||||
|
||||
.hero-aside {
|
||||
flex-direction: column;
|
||||
gap: var(--air-space-3);
|
||||
}
|
||||
|
||||
.hero-aside > div {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
padding: var(--air-space-4);
|
||||
}
|
||||
|
||||
.hero-aside strong {
|
||||
font-size: var(--air-font-size-sm);
|
||||
line-height: var(--air-leading-prose);
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: grid;
|
||||
gap: var(--air-space-5);
|
||||
padding: var(--air-space-5);
|
||||
}
|
||||
|
||||
.panel--feature {
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--air-color-surface-overlay) 80%, transparent), transparent 55%),
|
||||
color-mix(in srgb, var(--air-color-surface-raised) 94%, transparent);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.section-header p,
|
||||
.content-stack p,
|
||||
.mini-copy {
|
||||
margin: 0;
|
||||
color: var(--air-color-content-secondary);
|
||||
line-height: var(--air-leading-prose);
|
||||
}
|
||||
|
||||
.playground-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(240px, 320px) minmax(0, 1fr);
|
||||
gap: var(--air-space-5);
|
||||
}
|
||||
|
||||
.controls,
|
||||
.preview-stack,
|
||||
.content-stack {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.controls,
|
||||
.preview-stack,
|
||||
.content-stack {
|
||||
gap: var(--air-space-3);
|
||||
}
|
||||
|
||||
.control {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.chips {
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.chip {
|
||||
border: 1px solid color-mix(in srgb, var(--air-color-border-default) 82%, transparent);
|
||||
background: transparent;
|
||||
color: var(--air-color-content-secondary);
|
||||
padding: 0.45rem 0.8rem;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--air-duration-fast) var(--air-ease-default),
|
||||
border-color var(--air-duration-fast) var(--air-ease-default),
|
||||
color var(--air-duration-fast) var(--air-ease-default);
|
||||
}
|
||||
|
||||
.chip--active {
|
||||
background: color-mix(in srgb, var(--air-color-primary-track) 82%, transparent);
|
||||
border-color: var(--air-color-primary-border);
|
||||
color: var(--air-color-content-primary);
|
||||
}
|
||||
|
||||
.variant-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--air-space-4);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.hero,
|
||||
.playground-grid,
|
||||
.variant-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,587 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import { Button, Toast, toast, Icon } from '$uix/air';
|
||||
import type { AirToastPosition } from '$uix/air';
|
||||
import type { LingString } from '$lib/ling/lib/types';
|
||||
import type { AirDemoContext } from '../demo-context';
|
||||
import { AIR_DEMO_CONTEXT_KEY } from '../demo-context';
|
||||
import CodeExample from '../_components/code-example.svelte';
|
||||
|
||||
const demoContext = getContext<AirDemoContext>(AIR_DEMO_CONTEXT_KEY);
|
||||
const tv = (value: LingString) => demoContext?.tv(value) ?? String(value ?? '');
|
||||
|
||||
let position = $state<AirToastPosition>('bottom-right');
|
||||
|
||||
const positions: AirToastPosition[] = [
|
||||
'top-left',
|
||||
'top-center',
|
||||
'top-right',
|
||||
'bottom-left',
|
||||
'bottom-center',
|
||||
'bottom-right',
|
||||
];
|
||||
|
||||
function fireDefault() {
|
||||
toast('Archivo guardado correctamente');
|
||||
}
|
||||
|
||||
function fireSuccess() {
|
||||
toast.success('Invitación enviada al equipo');
|
||||
}
|
||||
|
||||
function fireError() {
|
||||
toast.error('No se pudo conectar al servidor');
|
||||
}
|
||||
|
||||
function fireWarning() {
|
||||
toast.warning('El workspace se está quedando sin espacio');
|
||||
}
|
||||
|
||||
function fireInfo() {
|
||||
toast.info('Nueva versión disponible');
|
||||
}
|
||||
|
||||
function fireRich() {
|
||||
toast.success('La subida terminó correctamente', {
|
||||
title: 'Archivo listo',
|
||||
description: '2.4 MB transferidos en 1.2 s. Ya puedes compartir el enlace.',
|
||||
});
|
||||
}
|
||||
|
||||
function fireAction() {
|
||||
let id = '';
|
||||
id = toast.error('No se pudo enviar el mensaje', {
|
||||
title: 'Error de red',
|
||||
action: {
|
||||
label: 'Reintentar',
|
||||
onClick: () => {
|
||||
toast.dismiss(id);
|
||||
toast.success('Mensaje enviado');
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function firePersistent() {
|
||||
toast('Esta notificación no se cierra sola', {
|
||||
duration: 0,
|
||||
title: 'Toast persistente',
|
||||
description: 'Útil para procesos que necesitan confirmación manual.',
|
||||
});
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
toast.clear();
|
||||
}
|
||||
|
||||
function toastPreviewMeta(type: 'default' | 'success' | 'error' | 'warning' | 'info') {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return {
|
||||
title: 'Success',
|
||||
message: 'Invitación enviada',
|
||||
icon: Icon.BadgeCheck,
|
||||
};
|
||||
case 'error':
|
||||
return {
|
||||
title: 'Error',
|
||||
message: 'No se pudo conectar',
|
||||
icon: Icon.CircleAlert,
|
||||
};
|
||||
case 'warning':
|
||||
return {
|
||||
title: 'Warning',
|
||||
message: 'Queda poco espacio',
|
||||
icon: Icon.TriangleAlert,
|
||||
};
|
||||
case 'info':
|
||||
return {
|
||||
title: 'Info',
|
||||
message: 'Nueva versión disponible',
|
||||
icon: Icon.BadgeInfo,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
title: 'Default',
|
||||
message: 'Archivo guardado',
|
||||
icon: Icon.BellRing,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const launcherPreviews = [
|
||||
{ type: 'default', ...toastPreviewMeta('default'), onClick: fireDefault, className: 'toast-launcher--neutral' },
|
||||
{ type: 'success', ...toastPreviewMeta('success'), onClick: fireSuccess, className: 'toast-launcher--success' },
|
||||
{ type: 'error', ...toastPreviewMeta('error'), onClick: fireError, className: 'toast-launcher--error' },
|
||||
{ type: 'warning', ...toastPreviewMeta('warning'), onClick: fireWarning, className: 'toast-launcher--warning' },
|
||||
{ type: 'info', ...toastPreviewMeta('info'), onClick: fireInfo, className: 'toast-launcher--info' },
|
||||
] as const;
|
||||
|
||||
const apiCode = $derived(`<Toast.Toaster position="${position}" />
|
||||
|
||||
toast.success('Invitación enviada');
|
||||
toast.error('No se pudo conectar', {
|
||||
title: 'Error de red',
|
||||
action: { label: 'Reintentar', onClick: retry }
|
||||
});`);
|
||||
|
||||
const customCode = `<Toast.Toaster position="top-right">
|
||||
{#snippet children({ item })}
|
||||
<Toast.Message>{item.message}</Toast.Message>
|
||||
{/snippet}
|
||||
</Toast.Toaster>`;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Toast · Air</title>
|
||||
</svelte:head>
|
||||
|
||||
<Toast.Toaster {position} />
|
||||
|
||||
<div class="toast-demo-page">
|
||||
<header class="hero">
|
||||
<div class="hero-copy">
|
||||
<p class="eyebrow">Air / Toast</p>
|
||||
<h1>{tv({
|
||||
es: 'Toast sobre terra.Toast',
|
||||
en: 'Toast over terra.Toast',
|
||||
ar: 'Toast فوق terra.Toast',
|
||||
})}</h1>
|
||||
<p class="lead">
|
||||
{tv({
|
||||
es: '`Toast` reutiliza la store y el ciclo de vida de `terra`, y `air` aterriza superficie, color semántico, cierre, acción y jerarquía tipográfica.',
|
||||
en: '`Toast` reuses the store and lifecycle from `terra`, and `air` lands surface, semantic color, close action and type hierarchy.',
|
||||
ar: 'يعيد `Toast` استخدام المخزن ودورة الحياة من `terra`، بينما يحدد `air` السطح واللون الدلالي والإغلاق وتسلسل النص.',
|
||||
})}
|
||||
</p>
|
||||
<div class="hero-signals">
|
||||
<span>{tv({ es: 'store API', en: 'store API', ar: 'واجهة store' })}</span>
|
||||
<span>{tv({ es: 'notification semantics', en: 'notification semantics', ar: 'دلالات الإشعار' })}</span>
|
||||
<span>{tv({ es: 'semantic variants', en: 'semantic variants', ar: 'تنويعات دلالية' })}</span>
|
||||
<span>{tv({ es: 'action + dismiss', en: 'action + dismiss', ar: 'إجراء + إغلاق' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hero-aside">
|
||||
<div>
|
||||
<span>{tv({ es: 'API', en: 'API', ar: 'الواجهة' })}</span>
|
||||
<strong>Toaster · Root · Title · Message · Description · Action · Close · toast(...)</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span>{tv({ es: 'DOM', en: 'DOM', ar: 'DOM' })}</span>
|
||||
<strong>{tv({
|
||||
es: 'stack portaled con store global, partes tipadas y semántica de notificación ya conectada a behavior',
|
||||
en: 'portaled stack with global store, typed parts and notification semantics already wired to behavior',
|
||||
ar: 'مكدس عبر بوابة مع مخزن عام وأجزاء مكتوبة ودلالات إشعار موصولة بالفعل إلى behavior',
|
||||
})}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Playground', en: 'Playground', ar: 'ساحة التجريب' })}</h2>
|
||||
<p>{tv({
|
||||
es: 'La API principal sigue siendo `toast(...)`. El `Toaster` solo decide dónde vive el stack y cómo se presenta.',
|
||||
en: 'The main API remains `toast(...)`. `Toaster` only decides where the stack lives and how it looks.',
|
||||
ar: 'تظل الواجهة الرئيسية هي `toast(...)`. ويحدد `Toaster` فقط مكان المكدس وكيفية عرضه.',
|
||||
})}</p>
|
||||
</header>
|
||||
|
||||
<div class="playground-grid">
|
||||
<div class="controls">
|
||||
<div class="control">
|
||||
<span>{tv({ es: 'Tipos', en: 'Types', ar: 'الأنواع' })}</span>
|
||||
<div class="button-cloud">
|
||||
{#each launcherPreviews as item}
|
||||
<button type="button" class={`toast-launcher ${item.className}`} onclick={item.onClick}>
|
||||
<span class="toast-launcher__status" aria-hidden="true">
|
||||
<item.icon size="sm" />
|
||||
</span>
|
||||
<span class="toast-launcher__main">
|
||||
<span class="toast-launcher__title">{item.title}</span>
|
||||
<span class="toast-launcher__message">{item.message}</span>
|
||||
</span>
|
||||
<span class="toast-launcher__close" aria-hidden="true">
|
||||
<Icon.X size="sm" />
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<span>{tv({ es: 'Contenido rico', en: 'Rich content', ar: 'محتوى غني' })}</span>
|
||||
<div class="button-cloud">
|
||||
<Button variant="surface" color="neutral" onclick={fireRich}>
|
||||
{tv({ es: 'Título + descripción', en: 'Title + description', ar: 'عنوان + وصف' })}
|
||||
</Button>
|
||||
<Button variant="surface" color="danger" onclick={fireAction}>
|
||||
{tv({ es: 'Acción', en: 'Action', ar: 'إجراء' })}
|
||||
</Button>
|
||||
<Button variant="surface" color="neutral" onclick={firePersistent}>
|
||||
{tv({ es: 'Persistente', en: 'Persistent', ar: 'مستمر' })}
|
||||
</Button>
|
||||
<Button variant="plain" color="danger" onclick={clearAll}>
|
||||
{tv({ es: 'Limpiar todos', en: 'Clear all', ar: 'مسح الكل' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<span>{tv({ es: 'Posición', en: 'Position', ar: 'الموضع' })}</span>
|
||||
<div class="chips">
|
||||
{#each positions as item}
|
||||
<button
|
||||
type="button"
|
||||
class:chip={true}
|
||||
class:chip--active={position === item}
|
||||
onclick={() => {
|
||||
position = item;
|
||||
toast.info(`Posición: ${item}`);
|
||||
}}
|
||||
>{item}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CodeExample
|
||||
title={tv({ es: 'API de Toast', en: 'Toast API', ar: 'واجهة Toast' })}
|
||||
code={apiCode}
|
||||
>
|
||||
<div class="preview-note">
|
||||
<p>{tv({
|
||||
es: 'Dispara varios toasts y cambia la posición. El stack real se renderiza fuera de la página mediante portal.',
|
||||
en: 'Fire several toasts and change position. The real stack is rendered outside the page through a portal.',
|
||||
ar: 'أطلق عدة إشعارات وغيّر الموضع. يتم عرض المكدس الحقيقي خارج الصفحة عبر بوابة.',
|
||||
})}</p>
|
||||
</div>
|
||||
</CodeExample>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<header class="section-header">
|
||||
<h2>{tv({ es: 'Render custom por item', en: 'Custom item rendering', ar: 'عرض مخصص لكل عنصر' })}</h2>
|
||||
<p>{tv({
|
||||
es: 'El `Toaster` también acepta un snippet con `{ item }`, por si una app necesita reordenar la anatomía sin tocar la store.',
|
||||
en: '`Toaster` also accepts a `{ item }` snippet when an app needs to reorder the anatomy without touching the store.',
|
||||
ar: 'يقبل `Toaster` أيضاً مقتطف `{ item }` إذا احتاجت التطبيق إلى إعادة ترتيب البنية دون لمس المخزن.',
|
||||
})}</p>
|
||||
</header>
|
||||
|
||||
<CodeExample
|
||||
title={tv({ es: 'Snippet custom', en: 'Custom snippet', ar: 'مقتطف مخصص' })}
|
||||
code={customCode}
|
||||
>
|
||||
<div class="preview-note">
|
||||
<p>{tv({
|
||||
es: 'La demo principal usa el render por defecto, pero la API deja abierta la puerta a layouts más editoriales o más productivos.',
|
||||
en: 'The main demo uses the default renderer, but the API still leaves room for more editorial or more product-like layouts.',
|
||||
ar: 'تستخدم المعاينة الرئيسية العارض الافتراضي، لكن الواجهة تترك الباب مفتوحاً لتخطيطات أكثر تحريريّة أو أكثر إنتاجية.',
|
||||
})}</p>
|
||||
</div>
|
||||
</CodeExample>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.toast-demo-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--air-space-6);
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.85fr);
|
||||
gap: var(--air-space-6);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 var(--air-space-2);
|
||||
font: 600 var(--air-font-size-xs) / 1 var(--air-font-mono);
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--air-color-content-secondary);
|
||||
}
|
||||
|
||||
.hero h1,
|
||||
.section-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lead {
|
||||
margin: var(--air-space-3) 0 0;
|
||||
max-width: 62ch;
|
||||
color: var(--air-color-content-secondary);
|
||||
font-size: var(--air-font-size-md);
|
||||
line-height: var(--air-leading-prose);
|
||||
}
|
||||
|
||||
.hero-signals,
|
||||
.button-cloud,
|
||||
.chips,
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.hero-signals {
|
||||
gap: var(--air-space-2);
|
||||
margin-top: var(--air-space-4);
|
||||
}
|
||||
|
||||
.hero-signals span,
|
||||
.hero-aside span,
|
||||
.control > span {
|
||||
font-family: var(--air-font-mono);
|
||||
font-size: var(--air-font-size-xs);
|
||||
color: var(--air-color-content-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.hero-signals span,
|
||||
.hero-aside > div,
|
||||
.panel,
|
||||
.preview-note {
|
||||
border: 1px solid color-mix(in srgb, var(--air-color-border-default) 82%, transparent);
|
||||
background: color-mix(in srgb, var(--air-color-surface-raised) 92%, transparent);
|
||||
border-radius: var(--air-radius-lg);
|
||||
}
|
||||
|
||||
.hero-signals span {
|
||||
padding: 0.45rem 0.7rem;
|
||||
}
|
||||
|
||||
.hero-aside {
|
||||
display: grid;
|
||||
gap: var(--air-space-3);
|
||||
}
|
||||
|
||||
.hero-aside > div {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
padding: var(--air-space-4);
|
||||
}
|
||||
|
||||
.hero-aside strong {
|
||||
font-size: var(--air-font-size-sm);
|
||||
line-height: var(--air-leading-prose);
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: grid;
|
||||
gap: var(--air-space-5);
|
||||
padding: var(--air-space-5);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.section-header p,
|
||||
.preview-note p {
|
||||
margin: 0;
|
||||
color: var(--air-color-content-secondary);
|
||||
line-height: var(--air-leading-prose);
|
||||
}
|
||||
|
||||
.playground-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(260px, 360px) minmax(0, 1fr);
|
||||
gap: var(--air-space-5);
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
gap: var(--air-space-4);
|
||||
}
|
||||
|
||||
.control {
|
||||
display: grid;
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.button-cloud,
|
||||
.chips {
|
||||
gap: var(--air-space-2);
|
||||
}
|
||||
|
||||
.toast-launcher {
|
||||
--_toast-launcher-surface: var(--air-toast-default-surface);
|
||||
--_toast-launcher-border: var(--air-toast-default-border);
|
||||
--_toast-launcher-accent: var(--air-toast-default-accent);
|
||||
--_toast-launcher-status-bg: var(--air-toast-default-status-bg);
|
||||
--_toast-launcher-color: var(--air-toast-default-status-color);
|
||||
--_toast-launcher-action-hover-bg: var(--air-toast-default-action-hover-bg);
|
||||
|
||||
inline-size: min(100%, 320px);
|
||||
min-height: var(--air-control-height-lg);
|
||||
padding: var(--air-space-3) var(--air-space-4);
|
||||
display: inline-grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: start;
|
||||
justify-content: stretch;
|
||||
gap: var(--air-space-3);
|
||||
border: var(--air-border-width) solid var(--_toast-launcher-border);
|
||||
border-inline-start: var(--air-toast-accent-width) solid var(--_toast-launcher-accent);
|
||||
border-radius: var(--air-toast-root-radius);
|
||||
background: var(--_toast-launcher-surface);
|
||||
box-shadow: var(--air-toast-root-shadow);
|
||||
color: var(--air-toast-message-color);
|
||||
font: inherit;
|
||||
font-size: var(--air-toast-message-font-size);
|
||||
font-weight: var(--air-toast-message-font-weight);
|
||||
line-height: var(--air-toast-message-line-height);
|
||||
text-align: start;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--air-duration-fast) var(--air-ease-default),
|
||||
border-color var(--air-duration-fast) var(--air-ease-default),
|
||||
color var(--air-duration-fast) var(--air-ease-default),
|
||||
box-shadow var(--air-duration-fast) var(--air-ease-default),
|
||||
transform var(--air-duration-fast) var(--air-ease-default);
|
||||
}
|
||||
|
||||
.toast-launcher__status {
|
||||
inline-size: var(--air-toast-status-size);
|
||||
block-size: var(--air-toast-status-size);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--air-toast-status-radius);
|
||||
background: var(--_toast-launcher-status-bg);
|
||||
color: var(--_toast-launcher-color);
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--_toast-launcher-accent) 22%, transparent);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.toast-launcher__main {
|
||||
min-inline-size: 0;
|
||||
display: grid;
|
||||
gap: var(--air-toast-root-stack-gap);
|
||||
}
|
||||
|
||||
.toast-launcher__title {
|
||||
font-family: var(--air-toast-title-font-family);
|
||||
font-size: var(--air-toast-title-font-size);
|
||||
font-weight: var(--air-toast-title-font-weight);
|
||||
line-height: var(--air-toast-title-line-height);
|
||||
color: var(--air-toast-title-color);
|
||||
}
|
||||
|
||||
.toast-launcher__message {
|
||||
font-family: var(--air-toast-message-font-family);
|
||||
font-size: var(--air-toast-message-font-size);
|
||||
font-weight: var(--air-toast-message-font-weight);
|
||||
line-height: var(--air-toast-message-line-height);
|
||||
color: var(--air-toast-message-color);
|
||||
}
|
||||
|
||||
.toast-launcher__close {
|
||||
inline-size: var(--air-toast-close-size);
|
||||
block-size: var(--air-toast-close-size);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: var(--air-toast-close-border-width) solid var(--air-toast-close-border);
|
||||
border-radius: var(--air-toast-close-radius);
|
||||
background: var(--air-toast-close-bg);
|
||||
color: var(--air-toast-close-color);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.toast-launcher:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 16px 34px rgb(17 14 10 / 0.14), 0 6px 10px rgb(17 14 10 / 0.06);
|
||||
}
|
||||
|
||||
.toast-launcher:focus-visible {
|
||||
outline: none;
|
||||
box-shadow:
|
||||
0 0 0 var(--air-focus-ring-offset) var(--air-color-surface-default),
|
||||
0 0 0 calc(var(--air-focus-ring-offset) + var(--air-focus-ring-width))
|
||||
var(--air-focus-ring-color);
|
||||
}
|
||||
|
||||
.toast-launcher--neutral {
|
||||
--_toast-launcher-surface: var(--air-toast-default-surface);
|
||||
--_toast-launcher-border: var(--air-toast-default-border);
|
||||
--_toast-launcher-accent: var(--air-toast-default-accent);
|
||||
--_toast-launcher-status-bg: var(--air-toast-default-status-bg);
|
||||
--_toast-launcher-color: var(--air-toast-default-status-color);
|
||||
--_toast-launcher-action-hover-bg: var(--air-toast-default-action-hover-bg);
|
||||
}
|
||||
|
||||
.toast-launcher--success {
|
||||
--_toast-launcher-surface: var(--air-toast-success-surface);
|
||||
--_toast-launcher-border: var(--air-toast-success-border);
|
||||
--_toast-launcher-accent: var(--air-toast-success-accent);
|
||||
--_toast-launcher-status-bg: var(--air-toast-success-status-bg);
|
||||
--_toast-launcher-color: var(--air-toast-success-status-color);
|
||||
--_toast-launcher-action-hover-bg: var(--air-toast-success-action-hover-bg);
|
||||
}
|
||||
|
||||
.toast-launcher--error {
|
||||
--_toast-launcher-surface: var(--air-toast-error-surface);
|
||||
--_toast-launcher-border: var(--air-toast-error-border);
|
||||
--_toast-launcher-accent: var(--air-toast-error-accent);
|
||||
--_toast-launcher-status-bg: var(--air-toast-error-status-bg);
|
||||
--_toast-launcher-color: var(--air-toast-error-status-color);
|
||||
--_toast-launcher-action-hover-bg: var(--air-toast-error-action-hover-bg);
|
||||
}
|
||||
|
||||
.toast-launcher--warning {
|
||||
--_toast-launcher-surface: var(--air-toast-warning-surface);
|
||||
--_toast-launcher-border: var(--air-toast-warning-border);
|
||||
--_toast-launcher-accent: var(--air-toast-warning-accent);
|
||||
--_toast-launcher-status-bg: var(--air-toast-warning-status-bg);
|
||||
--_toast-launcher-color: var(--air-toast-warning-status-color);
|
||||
--_toast-launcher-action-hover-bg: var(--air-toast-warning-action-hover-bg);
|
||||
}
|
||||
|
||||
.toast-launcher--info {
|
||||
--_toast-launcher-surface: var(--air-toast-info-surface);
|
||||
--_toast-launcher-border: var(--air-toast-info-border);
|
||||
--_toast-launcher-accent: var(--air-toast-info-accent);
|
||||
--_toast-launcher-status-bg: var(--air-toast-info-status-bg);
|
||||
--_toast-launcher-color: var(--air-toast-info-status-color);
|
||||
--_toast-launcher-action-hover-bg: var(--air-toast-info-action-hover-bg);
|
||||
}
|
||||
|
||||
.chip {
|
||||
border: 1px solid color-mix(in srgb, var(--air-color-border-default) 82%, transparent);
|
||||
border-radius: var(--air-radius-full);
|
||||
background: transparent;
|
||||
color: var(--air-color-content-secondary);
|
||||
padding: 0.45rem 0.8rem;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--air-duration-fast) var(--air-ease-default),
|
||||
border-color var(--air-duration-fast) var(--air-ease-default),
|
||||
color var(--air-duration-fast) var(--air-ease-default);
|
||||
}
|
||||
|
||||
.chip--active {
|
||||
background: color-mix(in srgb, var(--air-color-primary-track) 82%, transparent);
|
||||
border-color: var(--air-color-primary-border);
|
||||
color: var(--air-color-content-primary);
|
||||
}
|
||||
|
||||
.preview-note {
|
||||
padding: var(--air-space-4);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero,
|
||||
.playground-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,401 @@
|
||||
# AIR_DOCUMENTATION_AUDIT
|
||||
|
||||
Auditoría editorial y arquitectónica de la documentación de `air`.
|
||||
|
||||
Su objetivo es dejar claro:
|
||||
|
||||
- qué documentos son normativos hoy
|
||||
- cuáles son derivados útiles pero no mandan
|
||||
- cuáles están desalineados con la implementación real
|
||||
- y qué piezas conviene fusionar, deprecar o mantener como histórico
|
||||
|
||||
Este archivo no redefine la arquitectura. Ordena el corpus documental existente.
|
||||
|
||||
---
|
||||
|
||||
## 1. Diagnóstico general
|
||||
|
||||
La documentación de `air` ha mejorado mucho en arquitectura, pero ya empieza a mostrar
|
||||
un patrón típico de sistemas en consolidación:
|
||||
|
||||
- el núcleo conceptual es fuerte
|
||||
- la continuidad ya está bien resuelta
|
||||
- pero hay documentos derivados que todavía describen versiones anteriores del sistema
|
||||
- y algunas piezas nuevas no están todavía integradas en la jerarquía oficial de lectura
|
||||
|
||||
La situación no es de caos, pero sí de **ramificación documental no cerrada**.
|
||||
|
||||
En términos prácticos:
|
||||
|
||||
- hoy ya existe una base normativa suficientemente buena para continuar
|
||||
- pero no todo documento bajo `src/uix/air/` tiene el mismo nivel de autoridad
|
||||
- y varias especificaciones han quedado parcialmente obsoletas tras los cambios en contratos, tipografía, motion y estructura de carpetas
|
||||
|
||||
---
|
||||
|
||||
## 2. Criterio de clasificación
|
||||
|
||||
Cada documento se clasifica en una de estas categorías:
|
||||
|
||||
- **Normativo**
|
||||
fuente principal. Debe mantenerse alineado con la implementación real.
|
||||
|
||||
- **Derivado activo**
|
||||
útil y vigente, pero subordinado a los normativos.
|
||||
|
||||
- **Histórico / de transición**
|
||||
conserva contexto, pero no debe usarse como fuente de decisión nueva.
|
||||
|
||||
- **Desalineado / candidato a deprecación**
|
||||
hoy describe una arquitectura o un estado ya superado total o parcialmente.
|
||||
|
||||
---
|
||||
|
||||
## 3. Resultado de la auditoría
|
||||
|
||||
### 3.1 Núcleo normativo recomendado
|
||||
|
||||
Estos documentos deben considerarse la base oficial de diseño y arquitectura:
|
||||
|
||||
- [AIR_ARCHITECTURE.md](./AIR_ARCHITECTURE.md)
|
||||
- [AIR_COLOR_SYSTEM.md](./AIR_COLOR_SYSTEM.md)
|
||||
- [AIR_CONTRACT_ARCHITECTURE.md](./AIR_CONTRACT_ARCHITECTURE.md)
|
||||
- [AIR_VISUAL_LANGUAGE.md](./AIR_VISUAL_LANGUAGE.md)
|
||||
- [AIR_CONTINUITY.md](./AIR_CONTINUITY.md)
|
||||
|
||||
Razón:
|
||||
|
||||
- cubren la frontera `terra -> air -> theme`
|
||||
- cubren el sistema de color en 3 capas
|
||||
- cubren la arquitectura de contratos
|
||||
- cubren el lenguaje visual base
|
||||
- y cubren el estado real del repo y cómo continuar
|
||||
|
||||
### 3.2 Derivados activos útiles
|
||||
|
||||
Estos documentos siguen siendo valiosos, pero no deberían mandar por encima del núcleo:
|
||||
|
||||
- [AIR_TYPOGRAPHY_SPEC.md](./AIR_TYPOGRAPHY_SPEC.md)
|
||||
- [color-system-guide.md](./color-system-guide.md)
|
||||
|
||||
Razón:
|
||||
|
||||
- `AIR_TYPOGRAPHY_SPEC.md` documenta bien el módulo tipográfico y sus decisiones de implementación
|
||||
- `color-system-guide.md` sigue siendo una buena base conceptual fundacional del sistema de color
|
||||
|
||||
### 3.3 Históricos o de transición
|
||||
|
||||
Estos documentos pueden conservarse, pero deben quedar explícitamente relegados:
|
||||
|
||||
- [air-arquitectura.md](./air-arquitectura.md)
|
||||
|
||||
Razón:
|
||||
|
||||
- conserva contexto valioso del momento en que `air` todavía estaba más mezclado con motion/sound/config
|
||||
- pero ya no refleja bien la arquitectura actual de contratos, carpetas y responsabilidades
|
||||
|
||||
### 3.4 Desalineados o candidatos claros a deprecación
|
||||
|
||||
Estos documentos hoy no deberían guiar decisiones nuevas sin una revisión fuerte:
|
||||
|
||||
- [AIR_IMPLEMENTATION_GUIDE.md](./AIR_IMPLEMENTATION_GUIDE.md)
|
||||
- [AIR_MOTION_V2.md](./AIR_MOTION_V2.md)
|
||||
- [AIR_SEMANTICS.md](./AIR_SEMANTICS.md)
|
||||
- [AIR_SOUND.md](./AIR_SOUND.md)
|
||||
|
||||
Razón general:
|
||||
|
||||
- describen una versión de `air` mucho más centrada en semánticas de motion/sound como capa vertebral
|
||||
- hablan de APIs, carpetas y mecanismos que no son el centro del sistema actual
|
||||
- y en varios casos no están integrados en la ruta de lectura ni en la continuidad oficial
|
||||
|
||||
No significa que todo su contenido sea inválido. Significa que **ya no son autoridad estable**.
|
||||
|
||||
---
|
||||
|
||||
## 4. Hallazgos concretos por documento
|
||||
|
||||
### [AIR_ARCHITECTURE.md](./AIR_ARCHITECTURE.md)
|
||||
|
||||
Estado:
|
||||
|
||||
- **Normativo**
|
||||
|
||||
Fortalezas:
|
||||
|
||||
- define bien la frontera entre `terra`, `air` y tema
|
||||
- deja claras las responsabilidades de cada capa
|
||||
- establece reglas fuertes sobre anatomía pública y `data-*`
|
||||
|
||||
Problemas detectados:
|
||||
|
||||
- todavía nombra [tokens/contract.css](./tokens/contract.css) como forma natural del contrato visual
|
||||
- la arquitectura real ya se ha movido a [contracts/contract.css](./contracts/contract.css)
|
||||
|
||||
Acción recomendada:
|
||||
|
||||
- **actualizar**
|
||||
|
||||
### [AIR_COLOR_SYSTEM.md](./AIR_COLOR_SYSTEM.md)
|
||||
|
||||
Estado:
|
||||
|
||||
- **Normativo**
|
||||
|
||||
Fortalezas:
|
||||
|
||||
- muy alineado con el modelo real de 3 capas
|
||||
- fija bien la regla de que los componentes no consumen primitives
|
||||
- documenta correctamente `intent` vs `color`
|
||||
|
||||
Problemas detectados:
|
||||
|
||||
- el texto dice que los temas “solo deberían tocar Capa 2”, pero con la arquitectura actual existe también una extensión opcional de primitives en contrato avanzado
|
||||
|
||||
Acción recomendada:
|
||||
|
||||
- **ajuste menor de redacción**
|
||||
|
||||
### [AIR_CONTRACT_ARCHITECTURE.md](./AIR_CONTRACT_ARCHITECTURE.md)
|
||||
|
||||
Estado:
|
||||
|
||||
- **Normativo**
|
||||
|
||||
Fortalezas:
|
||||
|
||||
- es el documento que más ha madurado la arquitectura real
|
||||
- separa bien contrato de tema, contrato de componente y contrato avanzado
|
||||
- formaliza muy bien público vs privado con `--air-*` y `--_air-*`
|
||||
|
||||
Problemas detectados:
|
||||
|
||||
- mínimos
|
||||
|
||||
Acción recomendada:
|
||||
|
||||
- **mantener como fuente principal**
|
||||
|
||||
### [AIR_VISUAL_LANGUAGE.md](./AIR_VISUAL_LANGUAGE.md)
|
||||
|
||||
Estado:
|
||||
|
||||
- **Normativo**, pero parcialmente desalineado
|
||||
|
||||
Fortalezas:
|
||||
|
||||
- fija muy bien el tono visual de `air`
|
||||
- el apartado de superficies, foco, densidad y jerarquía sigue siendo valioso
|
||||
|
||||
Problemas detectados:
|
||||
|
||||
- todavía describe la tipografía base como “una sola sans + una sola mono”
|
||||
- asigna `Instrument Sans` también a headings
|
||||
- no refleja la decisión ya tomada de usar `Lora` para `heading` y `display`
|
||||
- sigue hablando de `tokens/contract.css` en lugar de `contracts/contract.css`
|
||||
|
||||
Acción recomendada:
|
||||
|
||||
- **actualizar pronto**
|
||||
|
||||
### [AIR_CONTINUITY.md](./AIR_CONTINUITY.md)
|
||||
|
||||
Estado:
|
||||
|
||||
- **Normativo operativo**
|
||||
|
||||
Fortalezas:
|
||||
|
||||
- resuelve muy bien el handoff del sistema
|
||||
- refleja bastante bien el estado de implementación
|
||||
- hace de mapa de entrada real para continuar
|
||||
|
||||
Problemas detectados:
|
||||
|
||||
- sigue diciendo que el estado esperado de `npm run check` son `22 warnings`
|
||||
- hoy eso ya no coincide con el repo
|
||||
|
||||
Acción recomendada:
|
||||
|
||||
- **actualizar el estado de warnings y revisar periódicamente**
|
||||
|
||||
### [AIR_TYPOGRAPHY_SPEC.md](./AIR_TYPOGRAPHY_SPEC.md)
|
||||
|
||||
Estado:
|
||||
|
||||
- **Derivado activo**
|
||||
|
||||
Fortalezas:
|
||||
|
||||
- es bastante útil para el módulo tipográfico
|
||||
- documenta decisiones de implementación de forma concreta
|
||||
|
||||
Problemas detectados:
|
||||
|
||||
- su escala alta (`44 / 52 / 60`) ya no coincide con la decisión actual consolidada en tema base (`24 / 36 / 52` en el tramo alto)
|
||||
- contiene decisiones históricas que ya no reflejan exactamente el estado actual
|
||||
|
||||
Acción recomendada:
|
||||
|
||||
- **mantener, pero reescribir o recortar la parte de escala para que no compita con la continuidad**
|
||||
|
||||
### [AIR_IMPLEMENTATION_GUIDE.md](./AIR_IMPLEMENTATION_GUIDE.md)
|
||||
|
||||
Estado:
|
||||
|
||||
- **Desalineado**
|
||||
|
||||
Problemas detectados:
|
||||
|
||||
- empuja una arquitectura en la que motion y sound parecen parte obligatoria del checklist de cualquier componente
|
||||
- depende de utilidades y carpetas (`getAir()`, `internal/behavior`, semánticas obligatorias) que ya no son el centro del `air` actual
|
||||
- da demasiado peso a una capa expresiva que hoy no está consolidada como contrato base del sistema
|
||||
|
||||
Acción recomendada:
|
||||
|
||||
- **deprecar o reescribir desde cero**
|
||||
|
||||
### [AIR_MOTION_V2.md](./AIR_MOTION_V2.md)
|
||||
|
||||
Estado:
|
||||
|
||||
- **Desalineado / excesivamente específico**
|
||||
|
||||
Problemas detectados:
|
||||
|
||||
- es muy detallado y útil en abstracto, pero ahora mismo no actúa como documento integrado en la arquitectura principal
|
||||
- habla de `_static.css`, estructuras y rutas que no reflejan exactamente el estado real actual
|
||||
- compite con `AIR_VISUAL_LANGUAGE.md` en el espacio conceptual de motion
|
||||
|
||||
Acción recomendada:
|
||||
|
||||
- **fusionar lo esencial en el documento normativo correcto o marcarlo como spec futura**
|
||||
|
||||
### [AIR_SEMANTICS.md](./AIR_SEMANTICS.md)
|
||||
|
||||
Estado:
|
||||
|
||||
- **Desalineado**
|
||||
|
||||
Problemas detectados:
|
||||
|
||||
- describe un sistema de semánticas muy ambicioso y rico, pero no forma parte hoy del núcleo documental por el que se gobierna `air`
|
||||
- su nivel de implementación declarada no coincide claramente con lo que la continuidad presenta como estado actual
|
||||
- puede inducir a pensar que `air` está gobernado principalmente por semánticas de interaction/sound, cuando hoy el núcleo real está en contratos, color, tema y componentes
|
||||
|
||||
Acción recomendada:
|
||||
|
||||
- **bajar a documento histórico o reencuadrar como visión futura**
|
||||
|
||||
### [AIR_SOUND.md](./AIR_SOUND.md)
|
||||
|
||||
Estado:
|
||||
|
||||
- **Desalineado / exploratorio**
|
||||
|
||||
Problemas detectados:
|
||||
|
||||
- define un sistema de sonido completo, pero hoy no está integrado en la continuidad como parte central del estado del repo
|
||||
- su nivel de especificidad es muy alto para una capa cuyo estatus real no está consolidado
|
||||
|
||||
Acción recomendada:
|
||||
|
||||
- **mover a visión futura o exploración**
|
||||
|
||||
### [air-arquitectura.md](./air-arquitectura.md)
|
||||
|
||||
Estado:
|
||||
|
||||
- **Histórico**
|
||||
|
||||
Problemas detectados:
|
||||
|
||||
- mezcla una arquitectura previa de `air` con motion/sound/config como centro del sistema
|
||||
- usa rutas y estructuras que ya no son la referencia real
|
||||
|
||||
Acción recomendada:
|
||||
|
||||
- **mantener solo como histórico explícito**
|
||||
|
||||
---
|
||||
|
||||
## 5. Mapa editorial recomendado
|
||||
|
||||
La jerarquía documental de `air` debería leerse así:
|
||||
|
||||
### Nivel 1 — Normativa real
|
||||
|
||||
- [AIR_ARCHITECTURE.md](./AIR_ARCHITECTURE.md)
|
||||
- [AIR_COLOR_SYSTEM.md](./AIR_COLOR_SYSTEM.md)
|
||||
- [AIR_CONTRACT_ARCHITECTURE.md](./AIR_CONTRACT_ARCHITECTURE.md)
|
||||
- [AIR_VISUAL_LANGUAGE.md](./AIR_VISUAL_LANGUAGE.md)
|
||||
- [AIR_CONTINUITY.md](./AIR_CONTINUITY.md)
|
||||
|
||||
### Nivel 2 — Especificaciones derivadas
|
||||
|
||||
- [AIR_TYPOGRAPHY_SPEC.md](./AIR_TYPOGRAPHY_SPEC.md)
|
||||
- [color-system-guide.md](./color-system-guide.md)
|
||||
|
||||
### Nivel 3 — Históricas / exploratorias
|
||||
|
||||
- [air-arquitectura.md](./air-arquitectura.md)
|
||||
- [AIR_IMPLEMENTATION_GUIDE.md](./AIR_IMPLEMENTATION_GUIDE.md)
|
||||
- [AIR_MOTION_V2.md](./AIR_MOTION_V2.md)
|
||||
- [AIR_SEMANTICS.md](./AIR_SEMANTICS.md)
|
||||
- [AIR_SOUND.md](./AIR_SOUND.md)
|
||||
|
||||
---
|
||||
|
||||
## 6. Recomendaciones concretas
|
||||
|
||||
### Corto plazo
|
||||
|
||||
1. Actualizar [AIR_ARCHITECTURE.md](./AIR_ARCHITECTURE.md)
|
||||
para que deje de referenciar `tokens/contract.css` como contrato principal.
|
||||
|
||||
2. Actualizar [AIR_VISUAL_LANGUAGE.md](./AIR_VISUAL_LANGUAGE.md)
|
||||
para reflejar:
|
||||
- `Lora` en `heading/display`
|
||||
- la ruta `contracts/contract.css`
|
||||
|
||||
3. Actualizar [AIR_CONTINUITY.md](./AIR_CONTINUITY.md)
|
||||
para reflejar el estado real de warnings y cualquier cambio de implementación reciente.
|
||||
|
||||
### Medio plazo
|
||||
|
||||
4. Reescribir o podar [AIR_TYPOGRAPHY_SPEC.md](./AIR_TYPOGRAPHY_SPEC.md)
|
||||
para que no compita con la tipografía real ya implementada.
|
||||
|
||||
5. Marcar explícitamente como históricos o exploratorios:
|
||||
- [air-arquitectura.md](./air-arquitectura.md)
|
||||
- [AIR_IMPLEMENTATION_GUIDE.md](./AIR_IMPLEMENTATION_GUIDE.md)
|
||||
- [AIR_MOTION_V2.md](./AIR_MOTION_V2.md)
|
||||
- [AIR_SEMANTICS.md](./AIR_SEMANTICS.md)
|
||||
- [AIR_SOUND.md](./AIR_SOUND.md)
|
||||
|
||||
### Largo plazo
|
||||
|
||||
6. Si motion/sound vuelven a ser una prioridad real del sistema:
|
||||
- reintroducirlos en la jerarquía documental oficial
|
||||
- pero ya como arquitectura consolidada, no como corpus paralelo
|
||||
|
||||
---
|
||||
|
||||
## 7. Conclusión
|
||||
|
||||
La evolución documental de `air` es buena en arquitectura y regular en edición.
|
||||
|
||||
Lo importante ya está conseguido:
|
||||
|
||||
- la frontera de capas existe
|
||||
- el contrato existe
|
||||
- la continuidad existe
|
||||
|
||||
Lo que falta ahora no es “más documentación”.
|
||||
Lo que falta es **curaduría**:
|
||||
|
||||
- un núcleo normativo corto y claro
|
||||
- especificaciones derivadas realmente alineadas
|
||||
- y un espacio explícito para documentos históricos o exploratorios
|
||||
|
||||
Ese paso editorial es el que hará que `air` deje de sentirse como una arquitectura en conversación y pase a sentirse como un sistema estable.
|
||||
@ -1,113 +0,0 @@
|
||||
/*
|
||||
* air/component/code-block/code-block.css
|
||||
*
|
||||
* Estilos globales del componente CodeBlock.
|
||||
*
|
||||
* CodeBlock es el bloque de código con cabecera estructurada: filename,
|
||||
* badge de lenguaje y botón de copia. Complementa al componente Code,
|
||||
* que es la pieza base inline/block sin estructura propia.
|
||||
*
|
||||
* La cabecera es opcional: solo se renderiza cuando hay filename, language
|
||||
* o copyable=true.
|
||||
*/
|
||||
|
||||
.air-code-block {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: var(--air-code-block-radius);
|
||||
border: 1px solid var(--air-code-block-border);
|
||||
background: var(--air-code-block-bg);
|
||||
overflow: hidden;
|
||||
color: var(--air-code-block-color);
|
||||
}
|
||||
|
||||
/* Cabecera */
|
||||
.air-code-block-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--air-space-2);
|
||||
padding: var(--air-code-block-header-py) var(--air-code-block-header-px);
|
||||
background: var(--air-code-block-header-bg);
|
||||
border-bottom: 1px solid var(--air-code-block-header-border);
|
||||
}
|
||||
|
||||
.air-code-block-filename {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--air-font-code);
|
||||
font-size: var(--air-text-2-size);
|
||||
line-height: var(--air-text-2-lh);
|
||||
font-weight: var(--air-font-weight-medium);
|
||||
color: var(--air-code-block-header-color);
|
||||
}
|
||||
|
||||
.air-code-block-lang {
|
||||
font-family: var(--air-font-code);
|
||||
font-size: var(--air-text-1-size);
|
||||
line-height: var(--air-text-1-lh);
|
||||
font-weight: var(--air-font-weight-medium);
|
||||
color: var(--air-code-block-header-color);
|
||||
text-transform: lowercase;
|
||||
opacity: 0.72;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Botón de copia */
|
||||
.air-code-block-copy {
|
||||
appearance: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--air-space-1);
|
||||
padding: var(--air-space-1) var(--air-space-2);
|
||||
border: 1px solid var(--air-code-block-header-border);
|
||||
border-radius: var(--air-radius-sm);
|
||||
background: transparent;
|
||||
font-family: var(--air-font-code);
|
||||
font-size: var(--air-text-1-size);
|
||||
line-height: var(--air-text-1-lh);
|
||||
color: var(--air-code-block-header-color);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition:
|
||||
background var(--air-duration-fast) var(--air-ease-default),
|
||||
border-color var(--air-duration-fast) var(--air-ease-default),
|
||||
color var(--air-duration-fast) var(--air-ease-default);
|
||||
}
|
||||
|
||||
.air-code-block-copy:hover {
|
||||
background: var(--air-code-block-bg);
|
||||
border-color: var(--air-color-border-default);
|
||||
color: var(--air-color-content-primary);
|
||||
}
|
||||
|
||||
.air-code-block-copy:focus-visible {
|
||||
outline: 2px solid var(--air-color-focus-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.air-code-block-copy[data-copied] {
|
||||
color: var(--air-color-success-text);
|
||||
border-color: var(--air-color-success-track);
|
||||
}
|
||||
|
||||
/* Pre/code */
|
||||
.air-code-block-pre {
|
||||
margin: 0;
|
||||
padding: var(--air-code-block-padding-y) var(--air-code-block-padding-x);
|
||||
overflow-x: auto;
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
.air-code-block-code {
|
||||
display: block;
|
||||
font-family: var(--air-font-code);
|
||||
font-size: var(--_air-code-block-size, var(--air-text-3-size));
|
||||
line-height: var(--_air-code-block-lh, var(--air-text-3-lh));
|
||||
letter-spacing: var(--_air-code-block-ls, var(--air-text-3-ls));
|
||||
font-weight: var(--air-font-weight-regular);
|
||||
color: inherit;
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import './code-block.css';
|
||||
import type { CodeBlockProps } from './types';
|
||||
|
||||
let {
|
||||
language,
|
||||
filename,
|
||||
copyable = true,
|
||||
size = 3,
|
||||
style,
|
||||
children,
|
||||
...restProps
|
||||
}: CodeBlockProps = $props();
|
||||
|
||||
let codeRef = $state<HTMLElement | undefined>();
|
||||
let copied = $state(false);
|
||||
let copyTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const showHeader = $derived(!!filename || !!language || copyable);
|
||||
|
||||
const sizeTokens = $derived(
|
||||
`--_air-code-block-size:var(--air-text-${size}-size);` +
|
||||
`--_air-code-block-lh:var(--air-text-${size}-lh);` +
|
||||
`--_air-code-block-ls:var(--air-text-${size}-ls)`
|
||||
);
|
||||
const mergedStyle = $derived(style ? `${sizeTokens};${style}` : sizeTokens);
|
||||
|
||||
async function handleCopy() {
|
||||
if (!browser || !codeRef) return;
|
||||
|
||||
const text = codeRef.textContent ?? '';
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
copied = true;
|
||||
clearTimeout(copyTimeout);
|
||||
copyTimeout = setTimeout(() => {
|
||||
copied = false;
|
||||
}, 2000);
|
||||
} catch {
|
||||
// Clipboard no disponible — silencioso
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="air-code-block" style={mergedStyle} {...restProps}>
|
||||
{#if showHeader}
|
||||
<div class="air-code-block-header">
|
||||
{#if filename}
|
||||
<span class="air-code-block-filename">{filename}</span>
|
||||
{/if}
|
||||
{#if language && !filename}
|
||||
<span class="air-code-block-lang">{language}</span>
|
||||
{/if}
|
||||
{#if language && filename}
|
||||
<span class="air-code-block-lang">{language}</span>
|
||||
{/if}
|
||||
{#if copyable}
|
||||
<button
|
||||
type="button"
|
||||
class="air-code-block-copy"
|
||||
data-copied={copied ? '' : undefined}
|
||||
onclick={handleCopy}
|
||||
aria-label={copied ? 'Copied' : 'Copy code'}
|
||||
>
|
||||
{copied ? '✓ Copied' : 'Copy'}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<pre class="air-code-block-pre"><code class="air-code-block-code" bind:this={codeRef}>{@render children?.()}</code></pre>
|
||||
</div>
|
||||
@ -1,50 +0,0 @@
|
||||
/*
|
||||
* air/component/code/code.css
|
||||
*
|
||||
* Estilos globales del componente Code.
|
||||
* Familia siempre --air-font-code. Sin prop family ni weight (siempre regular).
|
||||
*
|
||||
* variant='inline': código dentro de texto con fondo sutil.
|
||||
* variant='block': fragmento de código con fondo, borde y padding.
|
||||
*/
|
||||
|
||||
/* Base compartida */
|
||||
.air-code {
|
||||
font-family: var(--air-font-code);
|
||||
font-size: var(--_air-code-size, var(--air-text-3-size));
|
||||
line-height: var(--_air-code-lh, var(--air-text-3-lh));
|
||||
letter-spacing: var(--_air-code-ls, var(--air-text-3-ls));
|
||||
font-weight: var(--air-font-weight-regular);
|
||||
color: var(--air-code-color-primary);
|
||||
}
|
||||
|
||||
.air-code[data-color='secondary'] { color: var(--air-code-color-secondary); }
|
||||
|
||||
/* Inline */
|
||||
.air-code--inline {
|
||||
display: inline;
|
||||
padding: var(--air-code-inline-py) var(--air-code-inline-px);
|
||||
background: var(--air-code-inline-bg);
|
||||
border: 1px solid var(--air-code-inline-border);
|
||||
border-radius: var(--air-code-inline-radius);
|
||||
/* Reducir ligeramente el tamaño dentro de prosa para equilibrio óptico */
|
||||
font-size: calc(var(--_air-code-size, var(--air-text-3-size)) * 0.9);
|
||||
}
|
||||
|
||||
/* Block */
|
||||
.air-code--block {
|
||||
display: block;
|
||||
margin: 0;
|
||||
padding: var(--air-code-padding-y) var(--air-code-padding-x);
|
||||
background: var(--air-code-bg);
|
||||
border: 1px solid var(--air-code-border);
|
||||
border-radius: var(--air-code-radius);
|
||||
overflow-x: auto;
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
.air-code--block code {
|
||||
display: block;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import { getAir } from '$uix/air/config';
|
||||
import * as Icon from '$uix/air/components/icon';
|
||||
import { Dialog as TerraDialog } from '$uix/terra';
|
||||
import type { DialogCloseProps } from './types';
|
||||
|
||||
const air = getAir();
|
||||
|
||||
let {
|
||||
child,
|
||||
children,
|
||||
class: className = '',
|
||||
'aria-label': ariaLabel,
|
||||
...restProps
|
||||
}: DialogCloseProps = $props();
|
||||
|
||||
const closeLabel = $derived(
|
||||
ariaLabel ?? air.translator.current.translate('#?air.dialog.close|Cerrar diálogo')
|
||||
);
|
||||
const iconOnly = $derived(!children && !child);
|
||||
</script>
|
||||
|
||||
<TerraDialog.Close
|
||||
class={['air-dialog-close', className].filter(Boolean).join(' ')}
|
||||
aria-label={closeLabel}
|
||||
data-icon-only={iconOnly ? '' : undefined}
|
||||
{child}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children()}
|
||||
{#if children}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
<Icon.X size="sm" aria-hidden="true" />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</TerraDialog.Close>
|
||||
@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { contextIn, contextOut } from '$uix/air/internal/motion';
|
||||
import { getAir } from '$uix/air/config';
|
||||
import { styleValueToString } from '$uix/air/internal/style';
|
||||
import { Dialog as TerraDialog } from '$uix/terra';
|
||||
import type { DialogContentProps } from './types';
|
||||
|
||||
const air = getAir();
|
||||
|
||||
let {
|
||||
size = 'md',
|
||||
child,
|
||||
children,
|
||||
class: className = '',
|
||||
style,
|
||||
...restProps
|
||||
}: DialogContentProps = $props();
|
||||
|
||||
const resolvedSize = $derived(air.dom.resolve(size) ?? 'md');
|
||||
|
||||
function withClassAndAttrs(props: Record<string, unknown>) {
|
||||
const currentClass = typeof props.class === 'string' ? props.class : '';
|
||||
const currentStyle = styleValueToString(props.style);
|
||||
return {
|
||||
...props,
|
||||
class: ['air-dialog-content', currentClass, className].filter(Boolean).join(' '),
|
||||
style: [currentStyle, style].filter(Boolean).join(';'),
|
||||
'data-size': resolvedSize
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<TerraDialog.Content {...restProps}>
|
||||
{#snippet child(snippetProps)}
|
||||
{@const props = withClassAndAttrs(snippetProps.props)}
|
||||
{#if child}
|
||||
{@render child({ ...snippetProps, props })}
|
||||
{:else}
|
||||
<div
|
||||
{...props}
|
||||
in:contextIn={{ side: resolvedSize === 'full' ? 'bottom' : 'center' }}
|
||||
out:contextOut={{ side: resolvedSize === 'full' ? 'bottom' : 'center' }}
|
||||
>
|
||||
{@render children?.(snippetProps)}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</TerraDialog.Content>
|
||||
@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as TerraDialog } from '$uix/terra';
|
||||
import type { DialogDescriptionProps } from './types';
|
||||
|
||||
let { children, class: className = '', ...restProps }: DialogDescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<TerraDialog.Description
|
||||
class={['air-dialog-description', className].filter(Boolean).join(' ')}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children()}
|
||||
{@render children?.()}
|
||||
{/snippet}
|
||||
</TerraDialog.Description>
|
||||
@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { DialogFooterProps } from './types';
|
||||
|
||||
let { children, class: className = '', ...restProps }: DialogFooterProps = $props();
|
||||
</script>
|
||||
|
||||
<div class={['air-dialog-footer', className].filter(Boolean).join(' ')} data-dialog-footer {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import type { DialogHeaderProps } from './types';
|
||||
|
||||
let { children, class: className = '', ...restProps }: DialogHeaderProps = $props();
|
||||
</script>
|
||||
|
||||
<div class={['air-dialog-header', className].filter(Boolean).join(' ')} data-dialog-header {...restProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import { Dialog as TerraDialog } from '$uix/terra';
|
||||
import type { DialogOverlayProps } from './types';
|
||||
|
||||
let { child, children, class: className = '', ...restProps }: DialogOverlayProps = $props();
|
||||
|
||||
function withClass(props: Record<string, unknown>) {
|
||||
const current = typeof props.class === 'string' ? props.class : '';
|
||||
return {
|
||||
...props,
|
||||
class: ['air-dialog-overlay', current, className].filter(Boolean).join(' ')
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<TerraDialog.Overlay {...restProps}>
|
||||
{#snippet child(snippetProps)}
|
||||
{@const props = withClass(snippetProps.props)}
|
||||
{#if child}
|
||||
{@render child({ ...snippetProps, props })}
|
||||
{:else}
|
||||
<div {...props} in:fade={{ duration: 160 }} out:fade={{ duration: 120 }}>
|
||||
{@render children?.(snippetProps)}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</TerraDialog.Overlay>
|
||||
@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as TerraDialog } from '$uix/terra';
|
||||
import type { DialogTitleProps } from './types';
|
||||
|
||||
let { children, class: className = '', ...restProps }: DialogTitleProps = $props();
|
||||
</script>
|
||||
|
||||
<TerraDialog.Title class={['air-dialog-title', className].filter(Boolean).join(' ')} {...restProps}>
|
||||
{#snippet children()}
|
||||
{@render children?.()}
|
||||
{/snippet}
|
||||
</TerraDialog.Title>
|
||||
@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as TerraDialog } from '$uix/terra';
|
||||
import type { DialogTriggerProps } from './types';
|
||||
|
||||
let { children, class: className = '', ...restProps }: DialogTriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<TerraDialog.Trigger class={className} {...restProps}>
|
||||
{#snippet children()}
|
||||
{@render children?.()}
|
||||
{/snippet}
|
||||
</TerraDialog.Trigger>
|
||||
@ -0,0 +1,146 @@
|
||||
[data-dialog-overlay].air-dialog-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: var(--air-dialog-overlay-bg);
|
||||
backdrop-filter: blur(var(--air-dialog-overlay-blur));
|
||||
z-index: var(--air-dialog-overlay-z);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-dialog-content].air-dialog-content {
|
||||
--_air-dialog-width: var(--air-dialog-content-width-md);
|
||||
--_air-dialog-padding: var(--air-dialog-content-padding-md);
|
||||
|
||||
position: fixed;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
z-index: calc(var(--air-dialog-overlay-z) + 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--air-dialog-stack-gap);
|
||||
width: min(calc(100vw - var(--air-space-6)), var(--_air-dialog-width));
|
||||
max-height: var(--air-dialog-content-max-height);
|
||||
padding: var(--_air-dialog-padding);
|
||||
overflow: auto;
|
||||
border: var(--air-dialog-content-border-width) solid var(--air-dialog-content-border);
|
||||
border-radius: var(--air-dialog-content-radius);
|
||||
background: var(--air-dialog-content-bg);
|
||||
color: var(--air-dialog-content-color);
|
||||
box-shadow: var(--air-dialog-content-shadow);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-dialog-content].air-dialog-content[data-size='sm'] {
|
||||
--_air-dialog-width: var(--air-dialog-content-width-sm);
|
||||
--_air-dialog-padding: var(--air-dialog-content-padding-sm);
|
||||
}
|
||||
|
||||
[data-dialog-content].air-dialog-content[data-size='md'] {
|
||||
--_air-dialog-width: var(--air-dialog-content-width-md);
|
||||
--_air-dialog-padding: var(--air-dialog-content-padding-md);
|
||||
}
|
||||
|
||||
[data-dialog-content].air-dialog-content[data-size='lg'] {
|
||||
--_air-dialog-width: var(--air-dialog-content-width-lg);
|
||||
--_air-dialog-padding: var(--air-dialog-content-padding-lg);
|
||||
}
|
||||
|
||||
[data-dialog-content].air-dialog-content[data-size='full'] {
|
||||
inset: var(--air-space-3);
|
||||
left: auto;
|
||||
top: auto;
|
||||
width: auto;
|
||||
max-height: calc(100dvh - (var(--air-space-3) * 2));
|
||||
border-radius: var(--air-radius-2xl);
|
||||
--_air-dialog-padding: var(--air-dialog-content-padding-full);
|
||||
}
|
||||
|
||||
[data-dialog-header].air-dialog-header {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
gap: var(--air-dialog-header-gap);
|
||||
}
|
||||
|
||||
[data-dialog-footer].air-dialog-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
gap: var(--air-dialog-footer-gap);
|
||||
margin-top: var(--air-space-2);
|
||||
}
|
||||
|
||||
[data-dialog-title].air-dialog-title {
|
||||
margin: 0;
|
||||
font-family: var(--air-dialog-title-font-family);
|
||||
font-size: var(--air-dialog-title-font-size);
|
||||
font-weight: var(--air-dialog-title-font-weight);
|
||||
line-height: var(--air-dialog-title-line-height);
|
||||
color: var(--air-dialog-title-color);
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
[data-dialog-description].air-dialog-description {
|
||||
margin: 0;
|
||||
font-family: var(--air-dialog-description-font-family);
|
||||
font-size: var(--air-dialog-description-font-size);
|
||||
line-height: var(--air-dialog-description-line-height);
|
||||
color: var(--air-dialog-description-color);
|
||||
}
|
||||
|
||||
[data-dialog-close].air-dialog-close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
min-inline-size: var(--air-dialog-close-size);
|
||||
block-size: var(--air-dialog-close-size);
|
||||
padding-inline: var(--air-space-2);
|
||||
border: var(--air-dialog-close-border-width) solid var(--air-dialog-close-border);
|
||||
border-radius: var(--air-dialog-close-radius);
|
||||
background: var(--air-dialog-close-bg);
|
||||
color: var(--air-dialog-close-color);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--air-duration-fast) var(--air-ease-default),
|
||||
color var(--air-duration-fast) var(--air-ease-default),
|
||||
border-color var(--air-duration-fast) var(--air-ease-default),
|
||||
box-shadow var(--air-duration-fast) var(--air-ease-default);
|
||||
}
|
||||
|
||||
[data-dialog-close].air-dialog-close[data-icon-only] {
|
||||
inline-size: var(--air-dialog-close-size);
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
[data-dialog-close].air-dialog-close:hover {
|
||||
background: var(--air-dialog-close-bg-hover);
|
||||
color: var(--air-dialog-close-color-hover);
|
||||
}
|
||||
|
||||
[data-dialog-close].air-dialog-close:focus-visible {
|
||||
outline: none;
|
||||
box-shadow:
|
||||
0 0 0 1px var(--air-dialog-content-bg),
|
||||
0 0 0 calc(1px + var(--air-focus-ring-width)) var(--air-dialog-close-focus-ring-color);
|
||||
}
|
||||
|
||||
[data-dialog-close].air-dialog-close:disabled,
|
||||
[data-dialog-close].air-dialog-close[data-disabled] {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
[data-dialog-content].air-dialog-content {
|
||||
width: calc(100vw - var(--air-space-4));
|
||||
max-height: calc(100dvh - var(--air-space-4));
|
||||
padding: var(--air-space-4);
|
||||
}
|
||||
|
||||
[data-dialog-content].air-dialog-content[data-size='full'] {
|
||||
inset: var(--air-space-2);
|
||||
max-height: calc(100dvh - (var(--air-space-2) * 2));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
import './dialog.css';
|
||||
import { Dialog as TerraDialog } from '$uix/terra';
|
||||
import type { DialogRootProps } from './types';
|
||||
|
||||
let { open = $bindable(false), children, ...restProps }: DialogRootProps = $props();
|
||||
</script>
|
||||
|
||||
<TerraDialog.Root bind:open {...restProps}>
|
||||
{#snippet children()}
|
||||
{@render children?.()}
|
||||
{/snippet}
|
||||
</TerraDialog.Root>
|
||||
@ -0,0 +1,24 @@
|
||||
export { default as Root } from './dialog.svelte';
|
||||
export { default as Trigger } from './dialog-trigger.svelte';
|
||||
export { Portal } from '$uix/terra/dialog';
|
||||
export { default as Overlay } from './dialog-overlay.svelte';
|
||||
export { default as Content } from './dialog-content.svelte';
|
||||
export { default as Header } from './dialog-header.svelte';
|
||||
export { default as Footer } from './dialog-footer.svelte';
|
||||
export { default as Title } from './dialog-title.svelte';
|
||||
export { default as Description } from './dialog-description.svelte';
|
||||
export { default as Close } from './dialog-close.svelte';
|
||||
|
||||
export type {
|
||||
AirDialogSize,
|
||||
DialogRootProps as RootProps,
|
||||
DialogTriggerProps as TriggerProps,
|
||||
DialogPortalProps as PortalProps,
|
||||
DialogOverlayProps as OverlayProps,
|
||||
DialogContentProps as ContentProps,
|
||||
DialogHeaderProps as HeaderProps,
|
||||
DialogFooterProps as FooterProps,
|
||||
DialogTitleProps as TitleProps,
|
||||
DialogDescriptionProps as DescriptionProps,
|
||||
DialogCloseProps as CloseProps,
|
||||
} from './types';
|
||||
@ -0,0 +1,22 @@
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { ResponsiveProp } from '$uix/air/internal/dom';
|
||||
import type { WithChildren } from '$uix/terra/utils';
|
||||
import type { Dialog as TerraDialog } from '$uix/terra';
|
||||
|
||||
export type AirDialogSize = 'sm' | 'md' | 'lg' | 'full';
|
||||
|
||||
export type DialogRootProps = TerraDialog.RootProps;
|
||||
export type DialogTriggerProps = TerraDialog.TriggerProps;
|
||||
export type DialogPortalProps = TerraDialog.PortalProps;
|
||||
export type DialogOverlayProps = TerraDialog.OverlayProps;
|
||||
|
||||
export type DialogContentProps = TerraDialog.ContentProps & {
|
||||
size?: ResponsiveProp<AirDialogSize>;
|
||||
};
|
||||
|
||||
export type DialogTitleProps = TerraDialog.TitleProps;
|
||||
export type DialogDescriptionProps = TerraDialog.DescriptionProps;
|
||||
export type DialogCloseProps = TerraDialog.CloseProps;
|
||||
|
||||
export type DialogHeaderProps = WithChildren<HTMLAttributes<HTMLDivElement>>;
|
||||
export type DialogFooterProps = WithChildren<HTMLAttributes<HTMLDivElement>>;
|
||||
@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import './drawer.css';
|
||||
import { getAir } from '$uix/air/config';
|
||||
import * as Icon from '$uix/air/components/icon';
|
||||
import { Drawer as TerraDrawer } from '$uix/terra';
|
||||
import type { DrawerCloseProps } from './types';
|
||||
|
||||
const air = getAir();
|
||||
|
||||
let {
|
||||
child,
|
||||
children,
|
||||
class: className = '',
|
||||
'aria-label': ariaLabel,
|
||||
...restProps
|
||||
}: DrawerCloseProps = $props();
|
||||
|
||||
const closeLabel = $derived(
|
||||
ariaLabel ?? air.translator.current.translate('#?air.drawer.close|Cerrar panel')
|
||||
);
|
||||
const iconOnly = $derived(!children && !child);
|
||||
</script>
|
||||
|
||||
<TerraDrawer.Close
|
||||
class={['air-drawer-close', className].filter(Boolean).join(' ')}
|
||||
aria-label={closeLabel}
|
||||
data-icon-only={iconOnly ? '' : undefined}
|
||||
{child}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children()}
|
||||
{#if children}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
<Icon.X size="sm" aria-hidden="true" />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</TerraDrawer.Close>
|
||||
@ -0,0 +1,49 @@
|
||||
<script lang="ts">
|
||||
import './drawer.css';
|
||||
import { contextIn, contextOut } from '$uix/air/internal/motion';
|
||||
import { styleValueToString } from '$uix/air/internal/style';
|
||||
import { getAir } from '$uix/air/config';
|
||||
import { Drawer as TerraDrawer } from '$uix/terra';
|
||||
import type { DrawerContentProps } from './types';
|
||||
|
||||
const air = getAir();
|
||||
|
||||
let {
|
||||
size = 'md',
|
||||
child,
|
||||
children,
|
||||
class: className = '',
|
||||
style,
|
||||
...restProps
|
||||
}: DrawerContentProps = $props();
|
||||
|
||||
const resolvedSize = $derived(air.dom.resolve(size) ?? 'md');
|
||||
|
||||
function withClassAndAttrs(props: Record<string, unknown>) {
|
||||
const currentClass = typeof props.class === 'string' ? props.class : '';
|
||||
const currentStyle = styleValueToString(props.style);
|
||||
return {
|
||||
...props,
|
||||
class: ['air-drawer-content', currentClass, className].filter(Boolean).join(' '),
|
||||
style: [currentStyle, style].filter(Boolean).join(';'),
|
||||
'data-size': resolvedSize,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<TerraDrawer.Content {...restProps}>
|
||||
{#snippet child(snippetProps)}
|
||||
{@const props = withClassAndAttrs(snippetProps.props)}
|
||||
{#if child}
|
||||
{@render child({ ...snippetProps, props })}
|
||||
{:else}
|
||||
<div
|
||||
{...props}
|
||||
in:contextIn={{ side: snippetProps.side }}
|
||||
out:contextOut={{ side: snippetProps.side }}
|
||||
>
|
||||
{@render children?.(snippetProps)}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</TerraDrawer.Content>
|
||||
@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import './drawer.css';
|
||||
import { Drawer as TerraDrawer } from '$uix/terra';
|
||||
import type { DrawerDescriptionProps } from './types';
|
||||
|
||||
let {
|
||||
class: className = '',
|
||||
...restProps
|
||||
}: DrawerDescriptionProps = $props();
|
||||
</script>
|
||||
|
||||
<TerraDrawer.Description
|
||||
class={['air-drawer-description', className].filter(Boolean).join(' ')}
|
||||
{...restProps}
|
||||
/>
|
||||
@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import './drawer.css';
|
||||
import type { DrawerFooterProps } from './types';
|
||||
|
||||
let {
|
||||
children,
|
||||
class: className = '',
|
||||
...restProps
|
||||
}: DrawerFooterProps = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
data-drawer-footer
|
||||
class={['air-drawer-footer', className].filter(Boolean).join(' ')}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import './drawer.css';
|
||||
import type { DrawerHeaderProps } from './types';
|
||||
|
||||
let {
|
||||
children,
|
||||
class: className = '',
|
||||
...restProps
|
||||
}: DrawerHeaderProps = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
data-drawer-header
|
||||
class={['air-drawer-header', className].filter(Boolean).join(' ')}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import './drawer.css';
|
||||
import { Drawer as TerraDrawer } from '$uix/terra';
|
||||
import type { DrawerOverlayProps } from './types';
|
||||
|
||||
let {
|
||||
class: className = '',
|
||||
...restProps
|
||||
}: DrawerOverlayProps = $props();
|
||||
</script>
|
||||
|
||||
<TerraDrawer.Overlay class={['air-drawer-overlay', className].filter(Boolean).join(' ')} {...restProps} />
|
||||
@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import './drawer.css';
|
||||
import { Drawer as TerraDrawer } from '$uix/terra';
|
||||
import type { DrawerTitleProps } from './types';
|
||||
|
||||
let {
|
||||
class: className = '',
|
||||
...restProps
|
||||
}: DrawerTitleProps = $props();
|
||||
</script>
|
||||
|
||||
<TerraDrawer.Title class={['air-drawer-title', className].filter(Boolean).join(' ')} {...restProps} />
|
||||
@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as TerraDrawer } from '$uix/terra';
|
||||
import type { DrawerTriggerProps } from './types';
|
||||
|
||||
let props: DrawerTriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<TerraDrawer.Trigger {...props} />
|
||||
@ -0,0 +1,166 @@
|
||||
[data-drawer-overlay].air-drawer-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: var(--air-drawer-overlay-bg);
|
||||
backdrop-filter: blur(var(--air-drawer-overlay-blur));
|
||||
z-index: var(--air-drawer-overlay-z);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[data-drawer-content].air-drawer-content {
|
||||
--_air-drawer-panel-width: var(--air-drawer-content-width-md);
|
||||
--_air-drawer-panel-height: var(--air-drawer-content-height-md);
|
||||
--_air-drawer-padding: var(--air-drawer-content-padding-md);
|
||||
|
||||
position: fixed;
|
||||
z-index: calc(var(--air-drawer-overlay-z) + 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--air-drawer-stack-gap);
|
||||
padding: var(--_air-drawer-padding);
|
||||
overflow: auto;
|
||||
border: var(--air-drawer-content-border-width) solid var(--air-drawer-content-border);
|
||||
border-radius: var(--air-drawer-content-radius);
|
||||
background: var(--air-drawer-content-bg);
|
||||
color: var(--air-drawer-content-color);
|
||||
box-shadow: var(--air-drawer-content-shadow);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
[data-drawer-content].air-drawer-content[data-size='sm'] {
|
||||
--_air-drawer-panel-width: var(--air-drawer-content-width-sm);
|
||||
--_air-drawer-panel-height: var(--air-drawer-content-height-sm);
|
||||
--_air-drawer-padding: var(--air-drawer-content-padding-sm);
|
||||
}
|
||||
|
||||
[data-drawer-content].air-drawer-content[data-size='md'] {
|
||||
--_air-drawer-panel-width: var(--air-drawer-content-width-md);
|
||||
--_air-drawer-panel-height: var(--air-drawer-content-height-md);
|
||||
--_air-drawer-padding: var(--air-drawer-content-padding-md);
|
||||
}
|
||||
|
||||
[data-drawer-content].air-drawer-content[data-size='lg'] {
|
||||
--_air-drawer-panel-width: var(--air-drawer-content-width-lg);
|
||||
--_air-drawer-panel-height: var(--air-drawer-content-height-lg);
|
||||
--_air-drawer-padding: var(--air-drawer-content-padding-lg);
|
||||
}
|
||||
|
||||
[data-drawer-content].air-drawer-content[data-size='full'] {
|
||||
--_air-drawer-padding: var(--air-drawer-content-padding-full);
|
||||
}
|
||||
|
||||
[data-drawer-content].air-drawer-content[data-side='right'] {
|
||||
top: var(--air-drawer-content-inset);
|
||||
right: var(--air-drawer-content-inset);
|
||||
bottom: var(--air-drawer-content-inset);
|
||||
width: min(calc(100vw - (var(--air-drawer-content-inset) * 2)), var(--_air-drawer-panel-width));
|
||||
}
|
||||
|
||||
[data-drawer-content].air-drawer-content[data-side='left'] {
|
||||
top: var(--air-drawer-content-inset);
|
||||
left: var(--air-drawer-content-inset);
|
||||
bottom: var(--air-drawer-content-inset);
|
||||
width: min(calc(100vw - (var(--air-drawer-content-inset) * 2)), var(--_air-drawer-panel-width));
|
||||
}
|
||||
|
||||
[data-drawer-content].air-drawer-content[data-side='top'] {
|
||||
top: var(--air-drawer-content-inset);
|
||||
left: var(--air-drawer-content-inset);
|
||||
right: var(--air-drawer-content-inset);
|
||||
height: min(calc(100dvh - (var(--air-drawer-content-inset) * 2)), var(--_air-drawer-panel-height));
|
||||
}
|
||||
|
||||
[data-drawer-content].air-drawer-content[data-side='bottom'] {
|
||||
right: var(--air-drawer-content-inset);
|
||||
bottom: var(--air-drawer-content-inset);
|
||||
left: var(--air-drawer-content-inset);
|
||||
height: min(calc(100dvh - (var(--air-drawer-content-inset) * 2)), var(--_air-drawer-panel-height));
|
||||
}
|
||||
|
||||
[data-drawer-content].air-drawer-content[data-size='full'][data-side='right'],
|
||||
[data-drawer-content].air-drawer-content[data-size='full'][data-side='left'] {
|
||||
width: calc(100vw - (var(--air-drawer-content-inset) * 2));
|
||||
}
|
||||
|
||||
[data-drawer-content].air-drawer-content[data-size='full'][data-side='top'],
|
||||
[data-drawer-content].air-drawer-content[data-size='full'][data-side='bottom'] {
|
||||
height: calc(100dvh - (var(--air-drawer-content-inset) * 2));
|
||||
}
|
||||
|
||||
[data-drawer-header].air-drawer-header {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
gap: var(--air-drawer-header-gap);
|
||||
}
|
||||
|
||||
[data-drawer-footer].air-drawer-footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
gap: var(--air-drawer-footer-gap);
|
||||
margin-top: var(--air-drawer-footer-margin-top);
|
||||
}
|
||||
|
||||
[data-drawer-title].air-drawer-title {
|
||||
margin: 0;
|
||||
font-family: var(--air-drawer-title-font-family);
|
||||
font-size: var(--air-drawer-title-font-size);
|
||||
font-weight: var(--air-drawer-title-font-weight);
|
||||
line-height: var(--air-drawer-title-line-height);
|
||||
color: var(--air-drawer-title-color);
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
[data-drawer-description].air-drawer-description {
|
||||
margin: 0;
|
||||
font-family: var(--air-drawer-description-font-family);
|
||||
font-size: var(--air-drawer-description-font-size);
|
||||
line-height: var(--air-drawer-description-line-height);
|
||||
color: var(--air-drawer-description-color);
|
||||
}
|
||||
|
||||
[data-drawer-close].air-drawer-close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
min-inline-size: var(--air-drawer-close-size);
|
||||
block-size: var(--air-drawer-close-size);
|
||||
padding-inline: var(--air-drawer-close-px);
|
||||
border: var(--air-drawer-close-border-width) solid var(--air-drawer-close-border);
|
||||
border-radius: var(--air-drawer-close-radius);
|
||||
background: var(--air-drawer-close-bg);
|
||||
color: var(--air-drawer-close-color);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background var(--air-duration-fast) var(--air-ease-default),
|
||||
color var(--air-duration-fast) var(--air-ease-default),
|
||||
border-color var(--air-duration-fast) var(--air-ease-default),
|
||||
box-shadow var(--air-duration-fast) var(--air-ease-default);
|
||||
}
|
||||
|
||||
[data-drawer-close].air-drawer-close[data-icon-only] {
|
||||
inline-size: var(--air-drawer-close-size);
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
[data-drawer-close].air-drawer-close:hover {
|
||||
background: var(--air-drawer-close-bg-hover);
|
||||
color: var(--air-drawer-close-color-hover);
|
||||
}
|
||||
|
||||
[data-drawer-close].air-drawer-close:focus-visible {
|
||||
outline: none;
|
||||
box-shadow:
|
||||
0 0 0 var(--air-drawer-close-focus-ring-offset) var(--air-drawer-content-bg),
|
||||
0 0 0 calc(var(--air-drawer-close-focus-ring-offset) + var(--air-focus-ring-width))
|
||||
var(--air-drawer-close-focus-ring-color);
|
||||
}
|
||||
|
||||
[data-drawer-close].air-drawer-close:disabled,
|
||||
[data-drawer-close].air-drawer-close[data-disabled] {
|
||||
cursor: default;
|
||||
opacity: var(--air-drawer-close-disabled-opacity);
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Drawer as TerraDrawer } from '$uix/terra';
|
||||
import type { DrawerRootProps } from './types';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
...restProps
|
||||
}: DrawerRootProps = $props();
|
||||
</script>
|
||||
|
||||
<TerraDrawer.Root bind:open {...restProps} />
|
||||
@ -0,0 +1,25 @@
|
||||
export { default as Root } from './drawer.svelte';
|
||||
export { default as Trigger } from './drawer-trigger.svelte';
|
||||
export { Portal } from '$uix/terra/drawer';
|
||||
export { default as Overlay } from './drawer-overlay.svelte';
|
||||
export { default as Content } from './drawer-content.svelte';
|
||||
export { default as Header } from './drawer-header.svelte';
|
||||
export { default as Footer } from './drawer-footer.svelte';
|
||||
export { default as Title } from './drawer-title.svelte';
|
||||
export { default as Description } from './drawer-description.svelte';
|
||||
export { default as Close } from './drawer-close.svelte';
|
||||
|
||||
export type {
|
||||
AirDrawerSize,
|
||||
AirDrawerSide,
|
||||
DrawerRootProps as RootProps,
|
||||
DrawerTriggerProps as TriggerProps,
|
||||
DrawerPortalProps as PortalProps,
|
||||
DrawerOverlayProps as OverlayProps,
|
||||
DrawerContentProps as ContentProps,
|
||||
DrawerHeaderProps as HeaderProps,
|
||||
DrawerFooterProps as FooterProps,
|
||||
DrawerTitleProps as TitleProps,
|
||||
DrawerDescriptionProps as DescriptionProps,
|
||||
DrawerCloseProps as CloseProps,
|
||||
} from './types';
|
||||
@ -0,0 +1,23 @@
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { ResponsiveProp } from '$uix/air/internal/dom';
|
||||
import type { WithChildren } from '$uix/terra/utils';
|
||||
import type { Drawer as TerraDrawer } from '$uix/terra';
|
||||
|
||||
export type AirDrawerSize = 'sm' | 'md' | 'lg' | 'full';
|
||||
export type AirDrawerSide = TerraDrawer.DrawerSide;
|
||||
|
||||
export type DrawerRootProps = TerraDrawer.RootProps;
|
||||
export type DrawerTriggerProps = TerraDrawer.TriggerProps;
|
||||
export type DrawerPortalProps = TerraDrawer.PortalProps;
|
||||
export type DrawerOverlayProps = TerraDrawer.OverlayProps;
|
||||
|
||||
export type DrawerContentProps = TerraDrawer.ContentProps & {
|
||||
size?: ResponsiveProp<AirDrawerSize>;
|
||||
};
|
||||
|
||||
export type DrawerTitleProps = TerraDrawer.TitleProps;
|
||||
export type DrawerDescriptionProps = TerraDrawer.DescriptionProps;
|
||||
export type DrawerCloseProps = TerraDrawer.CloseProps;
|
||||
|
||||
export type DrawerHeaderProps = WithChildren<HTMLAttributes<HTMLDivElement>>;
|
||||
export type DrawerFooterProps = WithChildren<HTMLAttributes<HTMLDivElement>>;
|
||||
@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import './field.css';
|
||||
import { Field as TerraField } from '$uix/terra';
|
||||
import type { FieldControlProps } from './types';
|
||||
|
||||
let { class: className = '', children, ...restProps }: FieldControlProps = $props();
|
||||
</script>
|
||||
|
||||
<TerraField.Control class={['air-field-control', className].filter(Boolean).join(' ')} {...restProps}>
|
||||
{@render children?.()}
|
||||
</TerraField.Control>
|
||||
@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import './field.css';
|
||||
import { Field as TerraField } from '$uix/terra';
|
||||
import type { FieldErrorProps } from './types';
|
||||
|
||||
let { class: className = '', children, ...restProps }: FieldErrorProps = $props();
|
||||
</script>
|
||||
|
||||
<TerraField.Error class={['air-field-error', className].filter(Boolean).join(' ')} {...restProps}>
|
||||
{@render children?.()}
|
||||
</TerraField.Error>
|
||||
@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import './field.css';
|
||||
import { Field as TerraField } from '$uix/terra';
|
||||
import type { FieldHelperTextProps } from './types';
|
||||
|
||||
let { class: className = '', children, ...restProps }: FieldHelperTextProps = $props();
|
||||
</script>
|
||||
|
||||
<TerraField.HelperText
|
||||
class={['air-field-helper', className].filter(Boolean).join(' ')}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
</TerraField.HelperText>
|
||||
@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import './field.css';
|
||||
import { Field as TerraField } from '$uix/terra';
|
||||
import type { FieldInputProps } from './types';
|
||||
|
||||
let {
|
||||
value = $bindable(''),
|
||||
class: className = '',
|
||||
...restProps
|
||||
}: FieldInputProps = $props();
|
||||
</script>
|
||||
|
||||
<TerraField.Input
|
||||
bind:value
|
||||
class={['air-field-input', className].filter(Boolean).join(' ')}
|
||||
{...restProps}
|
||||
/>
|
||||
@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import './field.css';
|
||||
import { Field as TerraField } from '$uix/terra';
|
||||
import type { FieldLabelProps } from './types';
|
||||
|
||||
let { class: className = '', children, ...restProps }: FieldLabelProps = $props();
|
||||
</script>
|
||||
|
||||
<TerraField.Label class={['air-field-label', className].filter(Boolean).join(' ')} {...restProps}>
|
||||
{@render children?.()}
|
||||
</TerraField.Label>
|
||||
@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import './field.css';
|
||||
import { Field as TerraField } from '$uix/terra';
|
||||
import type { FieldPrefixProps } from './types';
|
||||
|
||||
let { class: className = '', children, ...restProps }: FieldPrefixProps = $props();
|
||||
</script>
|
||||
|
||||
<TerraField.Prefix class={['air-field-prefix', className].filter(Boolean).join(' ')} {...restProps}>
|
||||
{@render children?.()}
|
||||
</TerraField.Prefix>
|
||||
@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import './field.css';
|
||||
import { Field as TerraField } from '$uix/terra';
|
||||
import type { FieldSuffixProps } from './types';
|
||||
|
||||
let { class: className = '', children, ...restProps }: FieldSuffixProps = $props();
|
||||
</script>
|
||||
|
||||
<TerraField.Suffix class={['air-field-suffix', className].filter(Boolean).join(' ')} {...restProps}>
|
||||
{@render children?.()}
|
||||
</TerraField.Suffix>
|
||||
@ -0,0 +1,200 @@
|
||||
/*
|
||||
* air/component/field/field.css
|
||||
*
|
||||
* Recipe visual de Field sobre la anatomía pública de terra.Field.
|
||||
* Root resuelve densidad y variant; Control dibuja la caja; Input es el
|
||||
* elemento real sin borde propio; Label/Helper/Error completan el bloque.
|
||||
*/
|
||||
|
||||
[data-field].air-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--_air-field-gap, var(--air-field-gap-md));
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-field-label].air-field-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--air-field-label-required-gap);
|
||||
width: fit-content;
|
||||
font-family: var(--air-field-font-family);
|
||||
font-size: var(--_air-field-label-size, var(--air-field-label-size-md));
|
||||
font-weight: var(--air-field-label-weight);
|
||||
line-height: var(--air-field-line-height);
|
||||
color: var(--air-field-label-color);
|
||||
}
|
||||
|
||||
[data-field-label].air-field-label[data-disabled] {
|
||||
color: var(--air-field-label-color-disabled);
|
||||
}
|
||||
|
||||
[data-field-label].air-field-label[data-error] {
|
||||
color: var(--air-field-label-color-error);
|
||||
}
|
||||
|
||||
[data-field-required-indicator] {
|
||||
color: var(--air-field-required-color);
|
||||
}
|
||||
|
||||
[data-field-control].air-field-control {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: var(--_air-input-gap, var(--air-input-adornment-gap-md));
|
||||
height: var(--_air-input-height, var(--air-input-height-md));
|
||||
padding-inline: var(--_air-input-px, var(--air-input-px-md));
|
||||
font-family: var(--air-field-font-family);
|
||||
font-size: var(--_air-input-fsize, var(--air-input-font-size-md));
|
||||
font-weight: var(--air-input-font-weight);
|
||||
line-height: var(--air-field-line-height);
|
||||
color: var(--air-input-color);
|
||||
background: var(--air-input-bg);
|
||||
border: var(--air-input-border-width) solid var(--_air-input-border, var(--air-input-outline-border));
|
||||
border-radius: var(--air-input-radius);
|
||||
box-shadow: var(--air-input-shadow);
|
||||
outline: none;
|
||||
transition:
|
||||
border-color var(--air-duration-fast) var(--air-ease-default),
|
||||
box-shadow var(--air-duration-fast) var(--air-ease-default),
|
||||
background var(--air-duration-fast) var(--air-ease-default);
|
||||
}
|
||||
|
||||
[data-field].air-field[data-variant='ghost'] [data-field-control].air-field-control {
|
||||
background: var(--air-input-ghost-bg);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-field-input].air-field-input {
|
||||
flex: 1 1 0;
|
||||
align-self: stretch;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
caret-color: var(--air-input-caret-color);
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
[data-field-input].air-field-input:focus,
|
||||
[data-field-input].air-field-input:focus-visible,
|
||||
[data-field-input].air-field-input:invalid,
|
||||
[data-field-input].air-field-input:-moz-ui-invalid {
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
[data-field-input].air-field-input::placeholder {
|
||||
color: var(--air-input-placeholder-color);
|
||||
}
|
||||
|
||||
[data-field-input].air-field-input::selection {
|
||||
background: var(--air-input-selection-bg);
|
||||
}
|
||||
|
||||
[data-field-prefix].air-field-prefix,
|
||||
[data-field-suffix].air-field-suffix {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--air-input-adornment-color);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
[data-field-control].air-field-control:hover:not([data-disabled]):not(:has([readonly])) {
|
||||
border-color: var(--_air-input-border-hover, var(--air-input-outline-border-hover));
|
||||
}
|
||||
|
||||
[data-field].air-field[data-variant='ghost']
|
||||
[data-field-control].air-field-control:hover:not([data-disabled]):not(:has([readonly])) {
|
||||
background: var(--air-input-ghost-bg-hover);
|
||||
border-color: var(--air-input-ghost-border-hover);
|
||||
}
|
||||
|
||||
[data-field-control].air-field-control:focus-within:not([data-disabled]),
|
||||
[data-field-control].air-field-control:focus-within:not([data-disabled]):not(:has([readonly])) {
|
||||
border-color: var(--_air-input-border-focus, var(--air-input-outline-border-focus));
|
||||
box-shadow: var(--air-input-focus-ring);
|
||||
}
|
||||
|
||||
[data-field].air-field[data-variant='ghost']
|
||||
[data-field-control].air-field-control:focus-within:not([data-disabled]) {
|
||||
background: var(--air-input-ghost-bg-hover);
|
||||
border-color: var(--air-input-ghost-border-focus);
|
||||
box-shadow: var(--air-input-focus-ring);
|
||||
}
|
||||
|
||||
[data-field].air-field[data-error] [data-field-control].air-field-control,
|
||||
[data-field].air-field[data-state='error'] [data-field-control].air-field-control {
|
||||
border-color: var(--air-input-error-border);
|
||||
box-shadow: var(--air-input-shadow);
|
||||
}
|
||||
|
||||
[data-field].air-field[data-error] [data-field-control].air-field-control:focus-within,
|
||||
[data-field].air-field[data-state='error'] [data-field-control].air-field-control:focus-within {
|
||||
border-color: var(--air-input-error-border-focus);
|
||||
box-shadow: var(--air-input-error-focus-ring);
|
||||
}
|
||||
|
||||
[data-field-control].air-field-control[data-disabled],
|
||||
[data-field-control].air-field-control:has(:disabled) {
|
||||
background: var(--air-input-disabled-bg);
|
||||
color: var(--air-input-disabled-color);
|
||||
border-color: var(--air-input-disabled-border);
|
||||
box-shadow: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
[data-field-control].air-field-control[data-disabled] [data-field-input].air-field-input,
|
||||
[data-field-control].air-field-control:has(:disabled) [data-field-input].air-field-input,
|
||||
[data-field-control].air-field-control[data-disabled] [data-field-prefix].air-field-prefix,
|
||||
[data-field-control].air-field-control[data-disabled] [data-field-suffix].air-field-suffix,
|
||||
[data-field-control].air-field-control:has(:disabled) [data-field-prefix].air-field-prefix,
|
||||
[data-field-control].air-field-control:has(:disabled) [data-field-suffix].air-field-suffix {
|
||||
cursor: not-allowed;
|
||||
color: var(--air-input-disabled-color);
|
||||
}
|
||||
|
||||
[data-field-control].air-field-control:has([readonly]) {
|
||||
background: var(--air-input-readonly-bg);
|
||||
border-color: var(--air-input-readonly-border);
|
||||
color: var(--air-input-readonly-color);
|
||||
box-shadow: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
[data-field-control].air-field-control:has([readonly]) [data-field-prefix].air-field-prefix,
|
||||
[data-field-control].air-field-control:has([readonly]) [data-field-suffix].air-field-suffix {
|
||||
color: var(--air-input-readonly-color);
|
||||
}
|
||||
|
||||
[data-field-helper-text].air-field-helper,
|
||||
[data-field-error].air-field-error {
|
||||
margin: 0;
|
||||
font-family: var(--air-field-font-family);
|
||||
font-size: var(--air-field-helper-size);
|
||||
line-height: var(--air-field-line-height);
|
||||
}
|
||||
|
||||
[data-field-helper-text].air-field-helper {
|
||||
color: var(--air-field-helper-color);
|
||||
}
|
||||
|
||||
[data-field-helper-text].air-field-helper[data-disabled] {
|
||||
color: var(--air-field-helper-color-disabled);
|
||||
}
|
||||
|
||||
[data-field-error].air-field-error {
|
||||
font-size: var(--air-field-error-size);
|
||||
color: var(--air-field-error-color);
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import './field.css';
|
||||
import { Field as TerraField } from '$uix/terra';
|
||||
import { getAir } from '$uix/air/config';
|
||||
import type { FieldRootProps } from './types';
|
||||
|
||||
let {
|
||||
size = 'md',
|
||||
variant = 'outline',
|
||||
style,
|
||||
class: className = '',
|
||||
children,
|
||||
...restProps
|
||||
}: FieldRootProps = $props();
|
||||
|
||||
const air = getAir();
|
||||
const resolvedSize = $derived(air.dom.resolve(size) ?? 'md');
|
||||
|
||||
// Field resuelve solo la capa exterior del control. La caja visual
|
||||
// sigue reutilizando los tokens de Input para mantener densidad y foco
|
||||
// consistentes entre Input standalone y Field.Input.
|
||||
const sizeTokens = $derived(
|
||||
`--_air-field-gap:var(--air-field-gap-${resolvedSize});` +
|
||||
`--_air-field-label-size:var(--air-field-label-size-${resolvedSize});` +
|
||||
`--_air-input-height:var(--air-input-height-${resolvedSize});` +
|
||||
`--_air-input-px:var(--air-input-px-${resolvedSize});` +
|
||||
`--_air-input-py:var(--air-input-py-${resolvedSize});` +
|
||||
`--_air-input-gap:var(--air-input-adornment-gap-${resolvedSize});` +
|
||||
`--_air-input-fsize:var(--air-input-font-size-${resolvedSize})`
|
||||
);
|
||||
|
||||
const variantTokens = $derived(
|
||||
`--_air-input-border:var(--air-input-${variant}-border);` +
|
||||
`--_air-input-border-hover:var(--air-input-${variant}-border-hover);` +
|
||||
`--_air-input-border-focus:var(--air-input-${variant}-border-focus)`
|
||||
);
|
||||
|
||||
const mergedStyle = $derived([sizeTokens, variantTokens, style].filter(Boolean).join(';'));
|
||||
</script>
|
||||
|
||||
<TerraField.Root
|
||||
class={['air-field', className].filter(Boolean).join(' ')}
|
||||
data-size={resolvedSize}
|
||||
data-variant={variant}
|
||||
style={mergedStyle}
|
||||
{...restProps}
|
||||
>
|
||||
{#snippet children(snippetProps)}
|
||||
{@render children?.(snippetProps)}
|
||||
{/snippet}
|
||||
</TerraField.Root>
|
||||
@ -0,0 +1,21 @@
|
||||
export { default as Root } from './field.svelte';
|
||||
export { default as Label } from './field-label.svelte';
|
||||
export { default as Control } from './field-control.svelte';
|
||||
export { default as Input } from './field-input.svelte';
|
||||
export { default as HelperText } from './field-helper.svelte';
|
||||
export { default as Error } from './field-error.svelte';
|
||||
export { default as Prefix } from './field-prefix.svelte';
|
||||
export { default as Suffix } from './field-suffix.svelte';
|
||||
|
||||
export type {
|
||||
AirFieldSize,
|
||||
AirFieldVariant,
|
||||
FieldRootProps as RootProps,
|
||||
FieldLabelProps as LabelProps,
|
||||
FieldControlProps as ControlProps,
|
||||
FieldInputProps as InputProps,
|
||||
FieldHelperTextProps as HelperTextProps,
|
||||
FieldErrorProps as ErrorProps,
|
||||
FieldPrefixProps as PrefixProps,
|
||||
FieldSuffixProps as SuffixProps,
|
||||
} from './types';
|
||||
@ -0,0 +1,18 @@
|
||||
import type { ResponsiveProp } from '$uix/air/internal/dom';
|
||||
import type { Field as TerraField } from '$uix/terra';
|
||||
|
||||
export type AirFieldSize = 'xs' | 'sm' | 'md' | 'lg';
|
||||
export type AirFieldVariant = 'outline' | 'ghost';
|
||||
|
||||
export type FieldRootProps = TerraField.RootProps & {
|
||||
size?: ResponsiveProp<AirFieldSize>;
|
||||
variant?: AirFieldVariant;
|
||||
};
|
||||
|
||||
export type FieldLabelProps = TerraField.LabelProps;
|
||||
export type FieldControlProps = TerraField.ControlProps;
|
||||
export type FieldInputProps = TerraField.InputProps;
|
||||
export type FieldHelperTextProps = TerraField.HelperTextProps;
|
||||
export type FieldErrorProps = TerraField.ErrorProps;
|
||||
export type FieldPrefixProps = TerraField.PrefixProps;
|
||||
export type FieldSuffixProps = TerraField.SuffixProps;
|
||||
@ -0,0 +1,76 @@
|
||||
import type { AirIconSize, AirIconStrokeWidth } from './types';
|
||||
|
||||
const ICON_SIZE_PX_MAP = {
|
||||
xxs: 12,
|
||||
xs : 14,
|
||||
sm : 16,
|
||||
md : 18,
|
||||
lg : 20,
|
||||
xl : 24,
|
||||
xxl: 48,
|
||||
} as const;
|
||||
|
||||
const ICON_SIZE_MAP = {
|
||||
xxs: 'var(--air-icon-size-xxs)',
|
||||
xs : 'var(--air-icon-size-xs)',
|
||||
sm : 'var(--air-icon-size-sm)',
|
||||
md : 'var(--air-icon-size-md)',
|
||||
lg : 'var(--air-icon-size-lg)',
|
||||
xl : 'var(--air-icon-size-xl)',
|
||||
xxl: 'var(--air-icon-size-xxl)',
|
||||
} as const;
|
||||
|
||||
const ICON_STROKE_WIDTH_MAP = {
|
||||
sm: 'var(--air-icon-stroke-width-sm)',
|
||||
md: 'var(--air-icon-stroke-width-md)',
|
||||
lg: 'var(--air-icon-stroke-width-lg)',
|
||||
} as const;
|
||||
|
||||
export const AIR_ICON_AUTO_STROKE_BY_SIZE = {
|
||||
xxs: 1.125,
|
||||
xs : 1.25,
|
||||
sm : 1.375,
|
||||
md : 1.5,
|
||||
lg : 1.625,
|
||||
xl : 1.75,
|
||||
xxl: 2.0,
|
||||
} as const;
|
||||
|
||||
function resolveAirIconSizePx(size: AirIconSize = 'md'): number | null {
|
||||
if (typeof size === 'number') return size;
|
||||
if (typeof size === 'string' && /^\d+(\.\d+)?$/.test(size)) return Number(size);
|
||||
if (typeof size === 'string' && /^\d+(\.\d+)?px$/.test(size)) return Number.parseFloat(size);
|
||||
return ICON_SIZE_PX_MAP[size as keyof typeof ICON_SIZE_PX_MAP] ?? null;
|
||||
}
|
||||
|
||||
function resolveAutoAirIconStrokeWidth(size: AirIconSize = 'md'): number {
|
||||
const sizePx = resolveAirIconSizePx(size);
|
||||
|
||||
if (sizePx === null) return 1.5;
|
||||
if (sizePx <= 12) return AIR_ICON_AUTO_STROKE_BY_SIZE.xxs;
|
||||
if (sizePx <= 14) return AIR_ICON_AUTO_STROKE_BY_SIZE.xs;
|
||||
if (sizePx <= 16) return AIR_ICON_AUTO_STROKE_BY_SIZE.sm;
|
||||
if (sizePx <= 18) return AIR_ICON_AUTO_STROKE_BY_SIZE.md;
|
||||
if (sizePx <= 20) return AIR_ICON_AUTO_STROKE_BY_SIZE.lg;
|
||||
if (sizePx <= 24) return AIR_ICON_AUTO_STROKE_BY_SIZE.xl;
|
||||
if (sizePx <= 48) return AIR_ICON_AUTO_STROKE_BY_SIZE.xxl;
|
||||
return AIR_ICON_AUTO_STROKE_BY_SIZE.xxl;
|
||||
}
|
||||
|
||||
export function resolveAirIconSize(size: AirIconSize = 'md'): string {
|
||||
if (typeof size === 'number') return `${size}px`;
|
||||
if (typeof size === 'string' && /^\d+(\.\d+)?$/.test(size)) return `${size}px`;
|
||||
return ICON_SIZE_MAP[size as keyof typeof ICON_SIZE_MAP] ?? size;
|
||||
}
|
||||
|
||||
export function resolveAirIconStrokeWidth(
|
||||
strokeWidth: AirIconStrokeWidth = 'auto',
|
||||
size: AirIconSize = 'md'
|
||||
): string | number {
|
||||
if (strokeWidth === 'auto') return resolveAutoAirIconStrokeWidth(size);
|
||||
if (typeof strokeWidth === 'number') return strokeWidth;
|
||||
if (typeof strokeWidth === 'string' && /^\d+(\.\d+)?$/.test(strokeWidth)) {
|
||||
return Number(strokeWidth);
|
||||
}
|
||||
return ICON_STROKE_WIDTH_MAP[strokeWidth as keyof typeof ICON_STROKE_WIDTH_MAP] ?? strokeWidth;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue