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

261 lines
8.5 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<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>

Powered by TurnKey Linux.