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.
216 lines
7.9 KiB
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>
|