|
|
<script lang="ts">
|
|
|
/**
|
|
|
* Blocks section shell. Boots UIX (createActiveUix → Soma → prefs projection
|
|
|
* → ActiveEidos) and puts REAL chrome around it: a rail with the catalog and
|
|
|
* a topbar whose axes actually drive the framework — colour mode, language,
|
|
|
* direction and density. Without them a demo can only be looked at, never
|
|
|
* exercised, which is the whole point of a demo.
|
|
|
*
|
|
|
* The three regions are placed by `blocks.css` (app-land chrome, the role
|
|
|
* `uix.css` plays for the component docs); everything inside them is a canon
|
|
|
* component, so the section dogfoods the framework like the rest of the site.
|
|
|
*/
|
|
|
import { onDestroy } from 'svelte';
|
|
|
import { page } from '$app/state';
|
|
|
import { createActiveUix, setActiveUix } from '$active-uix';
|
|
|
import { standardPrefsDimensions } from '$prefs';
|
|
|
import { Soma } from '$soma/core/soma.svelte';
|
|
|
import { siumLangs } from '$sium/langs/langs';
|
|
|
import { secsLangs } from '$libs/secs';
|
|
|
import { blocksLangs } from './_lib/blocks-langs';
|
|
|
import { ActiveEidos } from '$uix/eidos';
|
|
|
import { semaPacks } from './_lib/sema-packs';
|
|
|
import { Stack } from '$uix/eidos/components/stack';
|
|
|
import { Group } from '$uix/eidos/components/group';
|
|
|
import { Text } from '$uix/eidos/components/text';
|
|
|
import { Link } from '$uix/eidos/components/link';
|
|
|
import { Badge } from '$uix/eidos/components/badge';
|
|
|
import { Separator } from '$uix/eidos/components/separator';
|
|
|
import { ToggleGroup } from '$uix/eidos/components/toggle-group';
|
|
|
import { Button } from '$uix/eidos/components/button';
|
|
|
import * as Icon from '$uix/eidos/components/icon';
|
|
|
import { BLOCK_CATALOG } from './_lib/catalog';
|
|
|
import '@/uix/eidos/index.css';
|
|
|
// The box-model reset the section needs. Imported HERE as well as in `BootUix`
|
|
|
// because this shell boots UIX inline instead of going through it — the
|
|
|
// previews get it from `BootUix`, the gallery from this line.
|
|
|
import './_lib/reset.css';
|
|
|
import './blocks.css';
|
|
|
|
|
|
let { children } = $props();
|
|
|
|
|
|
type Language = 'es' | 'en' | 'fr' | 'de';
|
|
|
type Density = 'compact' | 'comfortable' | 'spacious';
|
|
|
|
|
|
const blocksPrefsSchema = standardPrefsDimensions({
|
|
|
languages: ['es', 'en', 'fr', 'de'] satisfies readonly Language[],
|
|
|
locales: ['es-ES', 'en-US', 'en-GB', 'de-DE'] as const,
|
|
|
currencies: ['EUR', 'USD', 'GBP', 'JPY'] as const,
|
|
|
defaults: { language: 'es', locale: 'es-ES', currency: 'EUR' }
|
|
|
});
|
|
|
|
|
|
const uix = createActiveUix({
|
|
|
langs: {
|
|
|
schema: { sium: siumLangs, secs: secsLangs, blocks: blocksLangs },
|
|
|
defaultLocale: 'es' as const
|
|
|
},
|
|
|
// The blocks tier is shown, not shipped: this is the gallery of a SEMANTIC
|
|
|
// framework, so the perceptual channels are on. Without this the engine
|
|
|
// still stamps `data-event-*` correctly and nothing is ever audible —
|
|
|
// which is how the section shipped from F0.
|
|
|
events: { sound: true, haptic: true, components: semaPacks },
|
|
|
prefs: { schema: blocksPrefsSchema }
|
|
|
});
|
|
|
setActiveUix(uix);
|
|
|
Soma.create();
|
|
|
|
|
|
// ── Axes ──────────────────────────────────────────────────────────────
|
|
|
let mode = $state<'light' | 'dark'>('light');
|
|
|
let language = $state<Language>('es');
|
|
|
let dir = $state<'ltr' | 'rtl'>('ltr');
|
|
|
let density = $state<Density>('comfortable');
|
|
|
|
|
|
const DENSITY_SCALES: Record<Density, { space: number; control: number }> = {
|
|
|
compact: { space: 0.85, control: 0.9 },
|
|
|
comfortable: { space: 1, control: 1 },
|
|
|
spacious: { space: 1.15, control: 1.1 }
|
|
|
};
|
|
|
|
|
|
const modeListeners = new Set<(value: 'light' | 'dark') => void>();
|
|
|
const activeEidos = ActiveEidos.create({
|
|
|
applyDom: true,
|
|
|
theme: 'base',
|
|
|
modeSource: {
|
|
|
get: () => mode,
|
|
|
onChange(handler) {
|
|
|
modeListeners.add(handler);
|
|
|
return () => modeListeners.delete(handler);
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// Restore once: a stored choice wins, otherwise the OS scheme decides.
|
|
|
$effect(() => {
|
|
|
if (typeof localStorage === 'undefined') return;
|
|
|
const stored = localStorage.getItem('uix-blocks-prefs');
|
|
|
if (stored) {
|
|
|
try {
|
|
|
const blob = JSON.parse(stored);
|
|
|
if (blob.mode === 'dark' || blob.mode === 'light') mode = blob.mode;
|
|
|
if (blob.language) language = blob.language;
|
|
|
if (blob.dir) dir = blob.dir;
|
|
|
if (blob.density) density = blob.density;
|
|
|
return;
|
|
|
} catch {
|
|
|
/* ignore a corrupt blob */
|
|
|
}
|
|
|
}
|
|
|
mode = window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
|
});
|
|
|
|
|
|
$effect(() => {
|
|
|
if (typeof localStorage === 'undefined') return;
|
|
|
localStorage.setItem('uix-blocks-prefs', JSON.stringify({ mode, language, dir, density }));
|
|
|
});
|
|
|
|
|
|
$effect(() => {
|
|
|
const current = mode;
|
|
|
for (const listener of modeListeners) listener(current);
|
|
|
});
|
|
|
|
|
|
// Language and direction are PREFS intents: they reach langs, formats and the
|
|
|
// document direction through the framework, not through local wiring.
|
|
|
$effect(() => {
|
|
|
uix.prefs.setIntent('language', language);
|
|
|
uix.prefs.setIntent('direction', dir);
|
|
|
});
|
|
|
|
|
|
// Density on :root, so portaled overlays get it too.
|
|
|
$effect(() => {
|
|
|
const d = DENSITY_SCALES[density];
|
|
|
uix.dom.writeStyle(
|
|
|
'uix-blocks-display',
|
|
|
`:root{--density-space-scale:${d.space};--density-control-scale:${d.control};}`
|
|
|
);
|
|
|
});
|
|
|
|
|
|
const currentSlug = $derived(page.url.pathname.replace(/^\/blocks\/?/, ''));
|
|
|
|
|
|
onDestroy(() => {
|
|
|
activeEidos.dispose();
|
|
|
uix.dispose();
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
<svelte:head>
|
|
|
<title>Blocks — UIX</title>
|
|
|
<meta name="color-scheme" content="light dark" />
|
|
|
</svelte:head>
|
|
|
|
|
|
<div data-blocks-shell data-theme={mode} data-mode={mode}>
|
|
|
<header data-blocks-topbar>
|
|
|
<Group align="center" gap={3} justify="start">
|
|
|
<Link href="/blocks" variant="plain" underline="none">
|
|
|
<Group align="center" gap={2} justify="start">
|
|
|
<Icon.Boxes size="sm" />
|
|
|
<Text weight="semibold">Blocks</Text>
|
|
|
</Group>
|
|
|
</Link>
|
|
|
<Badge size="xs" variant="soft">tier</Badge>
|
|
|
</Group>
|
|
|
|
|
|
<Group align="center" gap={4} justify="end">
|
|
|
<Group align="center" gap={2} justify="end">
|
|
|
<Text size="xs" color="muted">idioma</Text>
|
|
|
<ToggleGroup
|
|
|
selectionMode="single"
|
|
|
size="sm"
|
|
|
attached
|
|
|
value={[language]}
|
|
|
onValueChange={(v) => (language = (v[0] ?? language) as Language)}
|
|
|
aria-label="Idioma"
|
|
|
>
|
|
|
{#each ['es', 'en', 'fr', 'de'] as value (value)}
|
|
|
<ToggleGroup.Item {value}>{value.toUpperCase()}</ToggleGroup.Item>
|
|
|
{/each}
|
|
|
</ToggleGroup>
|
|
|
</Group>
|
|
|
|
|
|
<Group align="center" gap={2} justify="end">
|
|
|
<Text size="xs" color="muted">densidad</Text>
|
|
|
<ToggleGroup
|
|
|
selectionMode="single"
|
|
|
size="sm"
|
|
|
attached
|
|
|
value={[density]}
|
|
|
onValueChange={(v) => (density = (v[0] ?? density) as Density)}
|
|
|
aria-label="Densidad"
|
|
|
>
|
|
|
<ToggleGroup.Item value="compact">−</ToggleGroup.Item>
|
|
|
<ToggleGroup.Item value="comfortable">·</ToggleGroup.Item>
|
|
|
<ToggleGroup.Item value="spacious">+</ToggleGroup.Item>
|
|
|
</ToggleGroup>
|
|
|
</Group>
|
|
|
|
|
|
<Group align="center" gap={2} justify="end">
|
|
|
<Text size="xs" color="muted">dir</Text>
|
|
|
<ToggleGroup
|
|
|
selectionMode="single"
|
|
|
size="sm"
|
|
|
attached
|
|
|
value={[dir]}
|
|
|
onValueChange={(v) => (dir = (v[0] ?? dir) as 'ltr' | 'rtl')}
|
|
|
aria-label="Dirección"
|
|
|
>
|
|
|
<ToggleGroup.Item value="ltr">LTR</ToggleGroup.Item>
|
|
|
<ToggleGroup.Item value="rtl">RTL</ToggleGroup.Item>
|
|
|
</ToggleGroup>
|
|
|
</Group>
|
|
|
|
|
|
<Button
|
|
|
size="sm"
|
|
|
variant="ghost"
|
|
|
aria-label={mode === 'light' ? 'Cambiar a modo oscuro' : 'Cambiar a modo claro'}
|
|
|
onclick={() => (mode = mode === 'light' ? 'dark' : 'light')}
|
|
|
>
|
|
|
{#if mode === 'light'}
|
|
|
<Icon.Moon size="sm" />
|
|
|
{:else}
|
|
|
<Icon.Sun size="sm" />
|
|
|
{/if}
|
|
|
</Button>
|
|
|
</Group>
|
|
|
</header>
|
|
|
|
|
|
<div data-blocks-body>
|
|
|
<nav data-blocks-rail aria-label="Catálogo de blocks">
|
|
|
<Stack gap={4}>
|
|
|
{#each BLOCK_CATALOG as group, i (group.title)}
|
|
|
<Stack gap={2}>
|
|
|
<Text size="xs" weight="medium" color="muted">{group.title}</Text>
|
|
|
<Stack gap={1}>
|
|
|
{#each group.items as item (item.slug)}
|
|
|
{#if item.shipped}
|
|
|
<Link href={`/blocks/${item.slug}`} variant="plain" underline="none">
|
|
|
<Text
|
|
|
size="sm"
|
|
|
weight={currentSlug === item.slug ? 'semibold' : 'regular'}
|
|
|
color={currentSlug === item.slug ? 'primary' : 'secondary'}
|
|
|
>
|
|
|
{item.label}
|
|
|
</Text>
|
|
|
</Link>
|
|
|
{:else}
|
|
|
<Text size="sm" color="disabled">{item.label}</Text>
|
|
|
{/if}
|
|
|
{/each}
|
|
|
</Stack>
|
|
|
</Stack>
|
|
|
{#if i < BLOCK_CATALOG.length - 1}
|
|
|
<Separator />
|
|
|
{/if}
|
|
|
{/each}
|
|
|
</Stack>
|
|
|
</nav>
|
|
|
|
|
|
<div data-blocks-content {dir}>
|
|
|
{@render children?.()}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|