|
|
|
|
<script lang="ts">
|
|
|
|
|
import { getContext } from 'svelte';
|
|
|
|
|
import { Icon } from '$uix/air';
|
|
|
|
|
import { AIR_ICON_AUTO_STROKE_BY_SIZE } from '$uix/air/components/icon/create-icon';
|
|
|
|
|
import { iconCategories } from '$uix/air/components/icon/categories';
|
|
|
|
|
import type { AirDemoContext } from '../demo-context';
|
|
|
|
|
import { AIR_DEMO_CONTEXT_KEY } from '../demo-context';
|
|
|
|
|
|
|
|
|
|
let search = $state('');
|
|
|
|
|
let selectedCategory = $state<string | null>(null);
|
|
|
|
|
let sizeMode = $state<'token' | 'custom'>('token');
|
|
|
|
|
let size = $state<'xxs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl'>('sm');
|
|
|
|
|
let customSize = $state(18);
|
|
|
|
|
let strokeWidth = $state<'auto' | 'sm' | 'md' | 'lg'>('auto');
|
|
|
|
|
let tone = $state<'default' | 'accent' | 'neutral' | 'success' | 'warning' | 'danger' | 'info'>(
|
|
|
|
|
'default'
|
|
|
|
|
);
|
|
|
|
|
let copiedIcon = $state<string | null>(null);
|
|
|
|
|
|
|
|
|
|
const demoContext = getContext<AirDemoContext>(AIR_DEMO_CONTEXT_KEY);
|
|
|
|
|
const t = (path: string, ...args: unknown[]) => demoContext?.t(path, ...args) ?? path;
|
|
|
|
|
|
|
|
|
|
const sizes = ['xxs', 'xs', 'sm', 'md', 'lg', 'xl', 'xxl'] as const;
|
|
|
|
|
const strokeWidths = ['auto', 'sm', 'md', 'lg'] as const;
|
|
|
|
|
const tones = ['default', 'accent', 'neutral', 'success', 'warning', 'danger', 'info'] as const;
|
|
|
|
|
const sizeTokens = {
|
|
|
|
|
xxs: '12px',
|
|
|
|
|
xs: '14px',
|
|
|
|
|
sm: '16px',
|
|
|
|
|
md: '18px',
|
|
|
|
|
lg: '20px',
|
|
|
|
|
xl: '24px',
|
|
|
|
|
xxl: '48px'
|
|
|
|
|
} as const;
|
|
|
|
|
const resolvedPreviewSize = $derived(sizeMode === 'custom' ? customSize : size);
|
|
|
|
|
const previewSizeLabel = $derived(sizeMode === 'custom' ? `${customSize}px` : size);
|
|
|
|
|
const autoStrokeValue = $derived.by(() => {
|
|
|
|
|
if (sizeMode === 'token') return AIR_ICON_AUTO_STROKE_BY_SIZE[size];
|
|
|
|
|
if (customSize <= 12) return AIR_ICON_AUTO_STROKE_BY_SIZE.xxs;
|
|
|
|
|
if (customSize <= 14) return AIR_ICON_AUTO_STROKE_BY_SIZE.xs;
|
|
|
|
|
if (customSize <= 16) return AIR_ICON_AUTO_STROKE_BY_SIZE.sm;
|
|
|
|
|
if (customSize <= 18) return AIR_ICON_AUTO_STROKE_BY_SIZE.md;
|
|
|
|
|
if (customSize <= 20) return AIR_ICON_AUTO_STROKE_BY_SIZE.lg;
|
|
|
|
|
return AIR_ICON_AUTO_STROKE_BY_SIZE.xl;
|
|
|
|
|
});
|
|
|
|
|
const previewStrokeLabel = $derived(
|
|
|
|
|
strokeWidth === 'auto' ? `auto (${autoStrokeValue})` : strokeWidth
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const filteredCategories = $derived(
|
|
|
|
|
iconCategories
|
|
|
|
|
.map((category) => ({
|
|
|
|
|
...category,
|
|
|
|
|
icons: category.icons.filter(
|
|
|
|
|
(icon) =>
|
|
|
|
|
!search ||
|
|
|
|
|
icon.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
|
|
|
icon.pascal.toLowerCase().includes(search.toLowerCase())
|
|
|
|
|
)
|
|
|
|
|
}))
|
|
|
|
|
.filter((category) => category.icons.length > 0)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const totalIconCount = $derived(
|
|
|
|
|
filteredCategories.reduce((sum, category) => sum + category.icons.length, 0)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
function getIconComponent(pascalName: string) {
|
|
|
|
|
// @ts-expect-error dynamic namespace access over generated icon exports
|
|
|
|
|
return Icon[pascalName];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function copyCode(iconName: string) {
|
|
|
|
|
const sizePart = `size="${previewSizeLabel}"`;
|
|
|
|
|
const strokePart = strokeWidth === 'auto' ? '' : ` strokeWidth="${strokeWidth}"`;
|
|
|
|
|
const snippet = `<Icon.${iconName} ${sizePart}${strokePart} />`;
|
|
|
|
|
await navigator.clipboard.writeText(snippet);
|
|
|
|
|
copiedIcon = iconName;
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (copiedIcon === iconName) copiedIcon = null;
|
|
|
|
|
}, 1400);
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<svelte:head>
|
|
|
|
|
<title>Air Icons</title>
|
|
|
|
|
</svelte:head>
|
|
|
|
|
|
|
|
|
|
<div class="icons-demo-page">
|
|
|
|
|
<header class="hero">
|
|
|
|
|
<div class="hero-copy">
|
|
|
|
|
<p class="eyebrow">Air / Icons</p>
|
|
|
|
|
<h1>Specimen de iconografia</h1>
|
|
|
|
|
<p class="lead">
|
|
|
|
|
Galeria real dentro de `air`, montada sobre su layout y sus tokens. Aqui validamos escala,
|
|
|
|
|
grosor, tono y legibilidad del set completo sin salir del sistema.
|
|
|
|
|
</p>
|
|
|
|
|
<div class="hero-signals">
|
|
|
|
|
<span>{totalIconCount} glyphs</span>
|
|
|
|
|
<span>{previewSizeLabel} size</span>
|
|
|
|
|
<span>{previewStrokeLabel} stroke</span>
|
|
|
|
|
<span>{tone} tone</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="hero-facts" aria-label="Icon demo metadata">
|
|
|
|
|
<div class="hero-fact">
|
|
|
|
|
<span>API</span>
|
|
|
|
|
<strong>Icon.Name size='xxs|xs|sm|md|lg|xl|xxl|number|string' · strokeWidth='auto|sm|md|lg|number'</strong>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="hero-fact">
|
|
|
|
|
<span>DOM</span>
|
|
|
|
|
<strong>button.icon-card > span.icon-stage > svg</strong>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="hero-side">
|
|
|
|
|
<section class="control-panel control-panel--search">
|
|
|
|
|
<header class="control-panel__header">
|
|
|
|
|
<span>Search</span>
|
|
|
|
|
<strong>{totalIconCount} results</strong>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<div class="search">
|
|
|
|
|
<span class="search-icon">
|
|
|
|
|
<Icon.Search size="sm" />
|
|
|
|
|
</span>
|
|
|
|
|
<input type="text" placeholder="Search icons..." bind:value={search} />
|
|
|
|
|
{#if search}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class="clear"
|
|
|
|
|
onclick={() => (search = '')}
|
|
|
|
|
aria-label="Clear search"
|
|
|
|
|
>
|
|
|
|
|
<Icon.X size="xs" />
|
|
|
|
|
</button>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<div class="controls-grid">
|
|
|
|
|
<section class="control-panel">
|
|
|
|
|
<header class="control-panel__header">
|
|
|
|
|
<span>Scale</span>
|
|
|
|
|
<strong>{previewSizeLabel}</strong>
|
|
|
|
|
</header>
|
|
|
|
|
<div class="size-mode">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class:chip={true}
|
|
|
|
|
class:chip--active={sizeMode === 'token'}
|
|
|
|
|
onclick={() => (sizeMode = 'token')}>Tokens</button
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class:chip={true}
|
|
|
|
|
class:chip--active={sizeMode === 'custom'}
|
|
|
|
|
onclick={() => (sizeMode = 'custom')}>Custom px</button
|
|
|
|
|
>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="chips">
|
|
|
|
|
{#each sizes as item}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class:chip={true}
|
|
|
|
|
class:chip--active={size === item}
|
|
|
|
|
disabled={sizeMode !== 'token'}
|
|
|
|
|
onclick={() => {
|
|
|
|
|
sizeMode = 'token';
|
|
|
|
|
size = item;
|
|
|
|
|
}}>{item}</button
|
|
|
|
|
>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="slider-control">
|
|
|
|
|
<label for="icon-size-slider">Custom size</label>
|
|
|
|
|
<div class="slider-row">
|
|
|
|
|
<input
|
|
|
|
|
id="icon-size-slider"
|
|
|
|
|
type="range"
|
|
|
|
|
min="12"
|
|
|
|
|
max="40"
|
|
|
|
|
step="1"
|
|
|
|
|
bind:value={customSize}
|
|
|
|
|
oninput={() => (sizeMode = 'custom')}
|
|
|
|
|
/>
|
|
|
|
|
<strong>{customSize}px</strong>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section class="control-panel">
|
|
|
|
|
<header class="control-panel__header">
|
|
|
|
|
<span>Stroke</span>
|
|
|
|
|
<strong>{previewStrokeLabel}</strong>
|
|
|
|
|
</header>
|
|
|
|
|
<div class="chips">
|
|
|
|
|
{#each strokeWidths as item}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class:chip={true}
|
|
|
|
|
class:chip--active={strokeWidth === item}
|
|
|
|
|
onclick={() => (strokeWidth = item)}>{item}</button
|
|
|
|
|
>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
<p class="control-note">
|
|
|
|
|
`auto` adapta el grosor al tamano del icono. `sm`, `md` y `lg` fuerzan un
|
|
|
|
|
override fijo.
|
|
|
|
|
</p>
|
|
|
|
|
<div class="stroke-map" aria-label="Auto stroke map">
|
|
|
|
|
{#each sizes as item}
|
|
|
|
|
<div class="stroke-map-row">
|
|
|
|
|
<span>{item}</span>
|
|
|
|
|
<strong>{AIR_ICON_AUTO_STROKE_BY_SIZE[item]}</strong>
|
|
|
|
|
</div>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<section class="control-panel">
|
|
|
|
|
<header class="control-panel__header">
|
|
|
|
|
<span>Tone</span>
|
|
|
|
|
<strong>{tone}</strong>
|
|
|
|
|
</header>
|
|
|
|
|
<div class="chips">
|
|
|
|
|
{#each tones as item}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class:chip={true}
|
|
|
|
|
class:chip--active={tone === item}
|
|
|
|
|
onclick={() => (tone = item)}>{item}</button
|
|
|
|
|
>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<section class="scale-reference">
|
|
|
|
|
<header class="section-header">
|
|
|
|
|
<h2>Token scale reference</h2>
|
|
|
|
|
<p>
|
|
|
|
|
La escala real del sistema de iconos. Aqui se ven a la vez los seis tamanos canonicos,
|
|
|
|
|
el valor en px y el stroke automatico asociado.
|
|
|
|
|
</p>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<div class="scale-reference-grid">
|
|
|
|
|
{#each sizes as item}
|
|
|
|
|
<div class={`scale-reference-item icon-card--tone-${tone}`}>
|
|
|
|
|
<Icon.Search
|
|
|
|
|
size={item}
|
|
|
|
|
strokeWidth={strokeWidth === 'auto' ? undefined : strokeWidth}
|
|
|
|
|
/>
|
|
|
|
|
<strong>{item}</strong>
|
|
|
|
|
<span>{sizeTokens[item]} · auto {AIR_ICON_AUTO_STROKE_BY_SIZE[item]}</span>
|
|
|
|
|
</div>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
<div class="content">
|
|
|
|
|
<nav class="categories-nav" aria-label="Icon categories">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class:category-btn={true}
|
|
|
|
|
class:category-btn--active={selectedCategory === null}
|
|
|
|
|
onclick={() => (selectedCategory = null)}
|
|
|
|
|
>
|
|
|
|
|
<span>{t('air.icon.all') || 'All'}</span>
|
|
|
|
|
<strong>{totalIconCount}</strong>
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
{#each filteredCategories as category}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class:category-btn={true}
|
|
|
|
|
class:category-btn--active={selectedCategory === category.name}
|
|
|
|
|
onclick={() => (selectedCategory = category.name)}
|
|
|
|
|
>
|
|
|
|
|
<span>{category.label}</span>
|
|
|
|
|
<strong>{category.icons.length}</strong>
|
|
|
|
|
</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</nav>
|
|
|
|
|
|
|
|
|
|
<div class="categories">
|
|
|
|
|
{#each filteredCategories as category}
|
|
|
|
|
{#if selectedCategory === null || selectedCategory === category.name}
|
|
|
|
|
<section class="category">
|
|
|
|
|
<header class="category-header">
|
|
|
|
|
<div>
|
|
|
|
|
<p>{category.name}</p>
|
|
|
|
|
<h2>{category.label}</h2>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="count">{category.icons.length} icons</span>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<div class="icons-grid">
|
|
|
|
|
{#each category.icons as icon}
|
|
|
|
|
{@const IconComponent = getIconComponent(icon.pascal)}
|
|
|
|
|
{#if IconComponent}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class={`icon-card icon-card--tone-${tone}`}
|
|
|
|
|
onclick={() => copyCode(icon.pascal)}
|
|
|
|
|
aria-label={`Copy ${icon.pascal} icon snippet`}
|
|
|
|
|
title={`Copy <Icon.${icon.pascal} />`}
|
|
|
|
|
>
|
|
|
|
|
<span class="icon-stage">
|
|
|
|
|
<IconComponent
|
|
|
|
|
size={resolvedPreviewSize}
|
|
|
|
|
strokeWidth={strokeWidth === 'auto' ? undefined : strokeWidth}
|
|
|
|
|
/>
|
|
|
|
|
</span>
|
|
|
|
|
<span class="icon-name">{icon.pascal}</span>
|
|
|
|
|
{#if copiedIcon === icon.pascal}
|
|
|
|
|
<span class="icon-feedback">Copied</span>
|
|
|
|
|
{/if}
|
|
|
|
|
</button>
|
|
|
|
|
{/if}
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
</section>
|
|
|
|
|
{/if}
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
.icons-demo-page {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: var(--air-space-5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hero,
|
|
|
|
|
.category,
|
|
|
|
|
.categories-nav,
|
|
|
|
|
.control-panel {
|
|
|
|
|
border-radius: var(--air-radius-xl);
|
|
|
|
|
border: 1px solid color-mix(in srgb, var(--air-color-border) 84%, transparent);
|
|
|
|
|
box-shadow: var(--air-shadow-1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hero {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr);
|
|
|
|
|
gap: var(--air-space-5);
|
|
|
|
|
padding: var(--air-space-5);
|
|
|
|
|
background:
|
|
|
|
|
radial-gradient(circle at top left, color-mix(in srgb, var(--air-color-accent-muted) 18%, transparent), transparent 42%),
|
|
|
|
|
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-side {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: var(--air-space-3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.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,
|
|
|
|
|
.category-header h2 {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-weight: var(--air-font-weight-semibold);
|
|
|
|
|
line-height: var(--air-leading-heading);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hero h1 {
|
|
|
|
|
font-size: clamp(var(--air-font-size-2xl), 2.5vw, 34px);
|
|
|
|
|
letter-spacing: var(--air-tracking-tight);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.lead {
|
|
|
|
|
margin: 0;
|
|
|
|
|
max-width: 60ch;
|
|
|
|
|
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);
|
|
|
|
|
font-family: var(--air-font-mono);
|
|
|
|
|
font-size: var(--air-font-size-xs);
|
|
|
|
|
line-height: var(--air-leading-ui);
|
|
|
|
|
color: var(--air-color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hero-facts {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
gap: var(--air-space-3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hero-fact {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: var(--air-space-1);
|
|
|
|
|
padding: var(--air-space-3);
|
|
|
|
|
border-radius: var(--air-radius-lg);
|
|
|
|
|
border: 1px solid color-mix(in srgb, var(--air-color-border) 82%, transparent);
|
|
|
|
|
background: color-mix(in srgb, var(--air-color-surface) 84%, transparent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hero-fact span {
|
|
|
|
|
font-family: var(--air-font-mono);
|
|
|
|
|
font-size: var(--air-font-size-xs);
|
|
|
|
|
line-height: var(--air-leading-ui);
|
|
|
|
|
letter-spacing: var(--air-tracking-wide);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
color: var(--air-color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hero-fact strong {
|
|
|
|
|
font-size: var(--air-font-size-sm);
|
|
|
|
|
line-height: var(--air-leading-text);
|
|
|
|
|
font-weight: var(--air-font-weight-medium);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.controls-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
gap: var(--air-space-3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-panel {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: var(--air-space-3);
|
|
|
|
|
padding: var(--air-space-3);
|
|
|
|
|
background: color-mix(in srgb, var(--air-color-surface-overlay) 72%, var(--air-color-surface-raised));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-panel--search {
|
|
|
|
|
background:
|
|
|
|
|
linear-gradient(
|
|
|
|
|
180deg,
|
|
|
|
|
color-mix(in srgb, var(--air-color-accent-muted) 10%, var(--air-color-surface-overlay)),
|
|
|
|
|
transparent
|
|
|
|
|
),
|
|
|
|
|
color-mix(in srgb, var(--air-color-surface-overlay) 72%, var(--air-color-surface-raised));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-panel__header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: var(--air-space-2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-panel__header span,
|
|
|
|
|
.category-header p {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-family: var(--air-font-mono);
|
|
|
|
|
font-size: var(--air-font-size-xs);
|
|
|
|
|
line-height: var(--air-leading-ui);
|
|
|
|
|
letter-spacing: var(--air-tracking-wide);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
color: var(--air-color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-panel__header strong,
|
|
|
|
|
.count {
|
|
|
|
|
font-family: var(--air-font-mono);
|
|
|
|
|
font-size: var(--air-font-size-xs);
|
|
|
|
|
line-height: var(--air-leading-ui);
|
|
|
|
|
color: var(--air-color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-note {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-size: var(--air-font-size-xs);
|
|
|
|
|
line-height: var(--air-leading-text);
|
|
|
|
|
color: var(--air-color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stroke-map {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
gap: var(--air-space-2);
|
|
|
|
|
padding-top: var(--air-space-1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stroke-map-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: var(--air-space-2);
|
|
|
|
|
padding: var(--air-space-2);
|
|
|
|
|
border-radius: var(--air-radius-md);
|
|
|
|
|
border: 1px solid color-mix(in srgb, var(--air-color-border-subtle) 72%, transparent);
|
|
|
|
|
background: color-mix(in srgb, var(--air-color-surface) 78%, transparent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stroke-map-row span,
|
|
|
|
|
.stroke-map-row strong {
|
|
|
|
|
font-family: var(--air-font-mono);
|
|
|
|
|
font-size: var(--air-font-size-xs);
|
|
|
|
|
line-height: var(--air-leading-ui);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stroke-map-row span {
|
|
|
|
|
color: var(--air-color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stroke-map-row strong {
|
|
|
|
|
color: var(--air-color-text-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: var(--air-space-3);
|
|
|
|
|
padding: var(--air-space-3) var(--air-space-4);
|
|
|
|
|
border-radius: var(--air-radius-lg);
|
|
|
|
|
border: 1px solid var(--air-color-border);
|
|
|
|
|
background: color-mix(in srgb, var(--air-color-surface) 84%, transparent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-icon,
|
|
|
|
|
.clear {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
width: 18px;
|
|
|
|
|
height: 18px;
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search :global(svg) {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search input {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
border: none;
|
|
|
|
|
background: transparent;
|
|
|
|
|
font: inherit;
|
|
|
|
|
font-size: var(--air-font-size-sm);
|
|
|
|
|
color: var(--air-color-text-primary);
|
|
|
|
|
outline: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search input::placeholder {
|
|
|
|
|
color: var(--air-color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.clear {
|
|
|
|
|
padding: 0;
|
|
|
|
|
border: none;
|
|
|
|
|
background: transparent;
|
|
|
|
|
color: var(--air-color-text-secondary);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
border-radius: var(--air-radius-full);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chips {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
gap: var(--air-space-2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.size-mode {
|
|
|
|
|
display: flex;
|
|
|
|
|
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:disabled {
|
|
|
|
|
opacity: 0.45;
|
|
|
|
|
cursor: default;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chip--active {
|
|
|
|
|
background: var(--air-color-accent);
|
|
|
|
|
border-color: var(--air-color-accent);
|
|
|
|
|
color: var(--air-color-surface-raised);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slider-control {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: var(--air-space-2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slider-control label {
|
|
|
|
|
font-family: var(--air-font-mono);
|
|
|
|
|
font-size: var(--air-font-size-xs);
|
|
|
|
|
line-height: var(--air-leading-ui);
|
|
|
|
|
letter-spacing: var(--air-tracking-wide);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
color: var(--air-color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slider-row {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr auto;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: var(--air-space-3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slider-row input {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.slider-row strong {
|
|
|
|
|
font-family: var(--air-font-mono);
|
|
|
|
|
font-size: var(--air-font-size-xs);
|
|
|
|
|
line-height: var(--air-leading-ui);
|
|
|
|
|
color: var(--air-color-text-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.content {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 240px 1fr;
|
|
|
|
|
gap: var(--air-space-5);
|
|
|
|
|
align-items: start;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scale-reference {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: var(--air-space-3);
|
|
|
|
|
padding: var(--air-space-4);
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scale-reference-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
|
|
|
gap: var(--air-space-3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scale-reference-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
gap: var(--air-space-2);
|
|
|
|
|
padding: var(--air-space-4) var(--air-space-3);
|
|
|
|
|
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);
|
|
|
|
|
color: var(--icon-tone-color, var(--air-color-text-primary));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scale-reference-item strong,
|
|
|
|
|
.scale-reference-item span {
|
|
|
|
|
font-family: var(--air-font-mono);
|
|
|
|
|
line-height: var(--air-leading-ui);
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scale-reference-item strong {
|
|
|
|
|
font-size: var(--air-font-size-xs);
|
|
|
|
|
color: var(--air-color-text-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scale-reference-item span {
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
color: var(--air-color-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.categories-nav {
|
|
|
|
|
position: sticky;
|
|
|
|
|
top: var(--air-space-4);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: var(--air-space-1);
|
|
|
|
|
padding: var(--air-space-3);
|
|
|
|
|
background: color-mix(in srgb, var(--air-color-surface-overlay) 72%, var(--air-color-surface-raised));
|
|
|
|
|
max-height: calc(100vh - var(--air-space-8));
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.category-btn {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: var(--air-space-3);
|
|
|
|
|
padding: var(--air-space-2) var(--air-space-3);
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: var(--air-radius-md);
|
|
|
|
|
background: transparent;
|
|
|
|
|
color: var(--air-color-text-secondary);
|
|
|
|
|
font: inherit;
|
|
|
|
|
text-align: left;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.category-btn strong {
|
|
|
|
|
font-family: var(--air-font-mono);
|
|
|
|
|
font-size: var(--air-font-size-xs);
|
|
|
|
|
font-weight: var(--air-font-weight-medium);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.category-btn--active {
|
|
|
|
|
background: var(--air-color-accent);
|
|
|
|
|
color: var(--air-color-surface-raised);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.categories {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: var(--air-space-4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.category {
|
|
|
|
|
padding: var(--air-space-5);
|
|
|
|
|
background: color-mix(in srgb, var(--air-color-surface-overlay) 70%, var(--air-color-surface-raised));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.category-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: var(--air-space-4);
|
|
|
|
|
margin-bottom: var(--air-space-4);
|
|
|
|
|
padding-bottom: var(--air-space-3);
|
|
|
|
|
border-bottom: 1px solid color-mix(in srgb, var(--air-color-border) 80%, transparent);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.icons-grid {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(108px, 1fr));
|
|
|
|
|
gap: var(--air-space-3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.icon-card {
|
|
|
|
|
--icon-tone-color: var(--air-color-text-primary);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
padding: var(--air-space-3) var(--air-space-2);
|
|
|
|
|
min-height: 92px;
|
|
|
|
|
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) 86%, transparent);
|
|
|
|
|
color: var(--icon-tone-color);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.icon-card--tone-default { --icon-tone-color: var(--air-color-text-primary); }
|
|
|
|
|
.icon-card--tone-accent { --icon-tone-color: var(--air-color-accent); }
|
|
|
|
|
.icon-card--tone-neutral { --icon-tone-color: var(--air-color-neutral-solid); }
|
|
|
|
|
.icon-card--tone-success { --icon-tone-color: var(--air-color-success); }
|
|
|
|
|
.icon-card--tone-warning { --icon-tone-color: var(--air-color-warning); }
|
|
|
|
|
.icon-card--tone-danger { --icon-tone-color: var(--air-color-error); }
|
|
|
|
|
.icon-card--tone-info { --icon-tone-color: var(--air-color-info-solid); }
|
|
|
|
|
|
|
|
|
|
.icon-stage {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
min-height: 40px;
|
|
|
|
|
border-radius: 0;
|
|
|
|
|
background: transparent;
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
line-height: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.icon-stage :global(svg) {
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.icon-name,
|
|
|
|
|
.icon-feedback {
|
|
|
|
|
font-family: var(--air-font-mono);
|
|
|
|
|
line-height: var(--air-leading-ui);
|
|
|
|
|
text-align: center;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.icon-name {
|
|
|
|
|
font-size: 9px;
|
|
|
|
|
font-weight: var(--air-font-weight-medium);
|
|
|
|
|
color: var(--air-color-text-primary);
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.icon-feedback {
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
color: var(--air-color-accent-text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 960px) {
|
|
|
|
|
.hero,
|
|
|
|
|
.content,
|
|
|
|
|
.controls-grid {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.hero-facts {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.categories-nav {
|
|
|
|
|
position: static;
|
|
|
|
|
max-height: none;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|