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/_lib/BlockDemo.svelte

216 lines
7.9 KiB

<script lang="ts">
/**
* The shared shell for every block demo — written once, used by the 14.
*
* The block is presented FULL-BLEED at the top of this page by default: no
* frame, no scroll box. A block like `site-header` pins against the viewport,
* and showing it inside a scrolling div would demo a `position: sticky`
* nobody will ever experience.
*
* Picking a device width swaps that for an iframe of the block's `preview`
* route — a real document whose viewport IS the chosen width, which is the
* only honest way to see the narrow branch of a responsive block. It is
* opt-in because in dev two unbundled documents at once can exhaust the
* browser's connections.
*
* Everything around it is composed from canon components.
*/
import { getActiveUix, readActiveUixPrefsSlot } from '$active-uix';
import { ActiveEidos } from '$uix/eidos';
import { Container } from '$uix/eidos/components/container';
import { Section } from '$uix/eidos/components/section';
import { Stack } from '$uix/eidos/components/stack';
import { Group } from '$uix/eidos/components/group';
import { Wrap } from '$uix/eidos/components/wrap';
import { Box } from '$uix/eidos/components/box';
import { Card } from '$uix/eidos/components/card';
import { Heading } from '$uix/eidos/components/heading';
import { Text } from '$uix/eidos/components/text';
import { Badge } from '$uix/eidos/components/badge';
import { Link } from '$uix/eidos/components/link';
import { Separator } from '$uix/eidos/components/separator';
import { Tabs } from '$uix/eidos/components/tabs';
import { ToggleGroup } from '$uix/eidos/components/toggle-group';
import { Button } from '$uix/eidos/components/button';
import * as Icon from '$uix/eidos/components/icon';
import type { BlockDemoProps } from './types';
let {
name,
slug,
lede,
meta = [],
preview,
previewSrc,
previewHeight = '620px',
controls,
composition,
api,
a11y,
gaps,
notes
}: BlockDemoProps = $props();
type Width = 'phone' | 'tablet' | 'full';
const WIDTHS: Record<Width, string> = { phone: '375px', tablet: '768px', full: '100%' };
let width = $state<Width>('full');
let reloadKey = $state(0);
// ── The section's axes, carried INTO the preview (A-29) ───────────────
// The preview is another document with its own runtime (`BootUix` runs its
// own `createActiveUix`), so the section's live state does not cross the
// frame: the URL is the serialization the second runtime boots from. The
// axes are read from the FRAMEWORK's live state — the prefs dimension reads
// are rune-backed (per-dimension change counters) and `getThemeContext()`
// reads the shell's `modeSource` — so flipping a topbar toggle re-keys the
// iframe with the new params. The shell is the single owner of these axes
// (the demos' own "dir (preview only)" controls were retired with this):
// a demo's query string carries only its BLOCK props.
const uix = getActiveUix();
const eidos = ActiveEidos.require();
const resolvedSrc = $derived.by(() => {
const [path, query = ''] = previewSrc.split('?');
const params = new URLSearchParams(query);
params.set('mode', eidos.getThemeContext().mode);
const dir = readActiveUixPrefsSlot<string>(uix.prefs, 'direction')?.get();
if (dir) params.set('dir', dir);
const lang = readActiveUixPrefsSlot<string>(uix.prefs, 'language')?.get();
if (lang) params.set('lang', lang);
return `${path}?${params}`;
});
let tab = $state('composition');
const tabs = $derived(
[
{ value: 'composition', label: 'Composición', snippet: composition },
{ value: 'api', label: 'API', snippet: api },
{ value: 'a11y', label: 'A11y', snippet: a11y },
{ value: 'gaps', label: 'Gaps', snippet: gaps },
{ value: 'notes', label: 'Notas', snippet: notes }
].filter((t) => !!t.snippet)
);
</script>
{#if width === 'full'}
<!-- Full-bleed: this IS the block in a page, not a picture of it. -->
{@render preview()}
<Separator />
{/if}
<Section>
<Container size="lg">
<Stack gap={6}>
<!-- ── Identity ────────────────────────────────────────────────── -->
<Stack gap={3}>
<Group gap={2} align="center" justify="start">
<Badge size="xs" variant="soft">block</Badge>
<Text size="sm" color="muted">{slug}</Text>
<Link href="/blocks" variant="subtle" size="sm">← galería</Link>
</Group>
<Heading level={1}>{name}</Heading>
{#if lede}
<Box maxWidth="72ch">
<Text color="muted">{@render lede()}</Text>
</Box>
{/if}
{#if meta.length}
<Wrap gap={2}>
{#each meta as fact (fact.key)}
<Badge size="sm" variant="outline" color="neutral">
{fact.key}: {fact.value}
</Badge>
{/each}
</Wrap>
{/if}
</Stack>
<!-- ── The block, live ─────────────────────────────────────────── -->
<Stack gap={2}>
<Group justify="space-between" align="center" gap={3}>
<Text size="sm" color="muted">
{#if width === 'full'}
Abajo está el block en esta misma página: lo que se pega, se pega al viewport de
verdad. Elige un ancho de dispositivo para verlo en un documento propio.
{:else}
Documento propio a {WIDTHS[width]} de viewport — la rama estrecha del block, real.
{/if}
</Text>
<Group gap={2} align="center" justify="end">
<ToggleGroup
selectionMode="single"
size="sm"
attached
value={[width]}
onValueChange={(v) => (width = (v[0] ?? width) as Width)}
aria-label="Ancho de la vista previa"
>
<ToggleGroup.Item value="phone">375</ToggleGroup.Item>
<ToggleGroup.Item value="tablet">768</ToggleGroup.Item>
<ToggleGroup.Item value="full">esta página</ToggleGroup.Item>
</ToggleGroup>
{#if width !== 'full'}
<Button
size="sm"
variant="ghost"
aria-label="Recargar la vista previa"
onclick={() => (reloadKey += 1)}
>
<Icon.RotateCw size="sm" />
</Button>
{/if}
<Link href={resolvedSrc} target="_blank" variant="subtle" size="sm">abrir ↗</Link>
</Group>
</Group>
</Stack>
{#if width !== 'full'}
<div data-blocks-stage style={`--blocks-stage-height: ${previewHeight};`}>
{#key `${resolvedSrc}-${reloadKey}`}
<iframe
data-blocks-preview
src={resolvedSrc}
title={`Vista previa de ${name}`}
loading="lazy"
style={`inline-size: ${WIDTHS[width]};`}
></iframe>
{/key}
</div>
{/if}
<!-- ── Controls ────────────────────────────────────────────────── -->
{#if controls}
<Card variant="soft" size="sm">
<Stack gap={3}>
<Text size="xs" weight="medium" color="muted">Controles</Text>
{@render controls()}
</Stack>
</Card>
{/if}
<!-- ── Documentation ───────────────────────────────────────────── -->
{#if tabs.length}
<Stack gap={3}>
<Separator />
<Tabs bind:value={tab} variant="line">
<!-- Un tablist necesita nombre: sin él, un lector de pantalla anuncia
«lista de pestañas» y ya. Lo avisa el propio soma por consola. -->
<Tabs.List aria-label="Documentación del block">
{#each tabs as t (t.value)}
<Tabs.Trigger value={t.value}>{t.label}</Tabs.Trigger>
{/each}
<Tabs.Indicator />
</Tabs.List>
{#each tabs as t (t.value)}
<Tabs.Content value={t.value}>
<Box paddingY={4}>
{@render t.snippet?.()}
</Box>
</Tabs.Content>
{/each}
</Tabs>
</Stack>
{/if}
</Stack>
</Container>
</Section>

Powered by TurnKey Linux.