blocks(app-shell): la demo estaba escrita a mano, no con la capa visual

La escribí sin haber leído la doctrina de eidos y salió lo que sale así: `<div
style>` donde hay primitivos, `inline-size: 16rem` clavado, `font-size` en un
`<strong>`, bordes y fondos a pelo, y contenido de relleno («Bloque de contenido
1»). Rehecha contra la tabla «Build contract» de `guides/component-guide.md`.

Ahora **cero literales y cero `style=`** en el fichero: color por token de rol,
espaciado por `--space-*` a través de los props de layout, tipografía por la
escala de `Text`/`Heading`, paneles por `Card`, separaciones por `Separator`,
anchura del aside por `--sidebar-width` (la medida del propio sistema, que
respeta densidad y `--scaling`).

Dos props que no hacían NADA, encontradas al reescribir:

- `Badge intent="affirm"` — `Badge` no tiene `intent`, tiene `color`.
- `AutoGrid minChildWidth="var(--container-width-sm)"` — le pasé un breakpoint
  como suelo de pista, así que las tres tarjetas caían en una columna. Ahora
  `Grid columns={{ base: 1, sm: 3 }}`, sin literal ninguno.

Y una tercera cosa medida que NO se arregla desde aquí: el aside llevaba
`Surface variant="soft"` y es **inerte** — `oklch(0.9911 0 0)` contra un fondo
de página de la misma claridad, o sea invisible en claro. Es **F15** de
`PLAN-blocks-quality.md` §6, gap de canon ya registrado («soft no acota un
panel, y Surface no tiene borde»). La frontera la pone un `Separator`, que es lo
que sí acota, y el porqué queda escrito en el fichero para que nadie lo
«arregle» con un fondo a mano.

Verificado mirando las capturas, claro y oscuro, 1280 y 375.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent fd02780002
commit 026e07beaa

@ -5,18 +5,27 @@
* the rail that collapses, the bar that stays put while the page scrolls,
* the aside that folds when the width runs out, and the bypass links that
* appear on the first Tab.
*
* ⚠️ Every surface here is a canon component consumed through its PROPS,
* and there is not one literal length, colour or font size in the file.
* That is the build contract (`guides/component-guide.md` §Build contract):
* colour by role token, spacing by `--space-*` through the layout props,
* type by the `Text`/`Heading` scale, panels by `Card`/`Surface` — never a
* hand-written `px`/`rem` (it bypasses density and `--scaling`) and never a
* `<div style>` where a primitive exists.
*/
import { AppShell, type AppShellScroll } from '$blocks/app-shell';
import { Sidebar } from '$uix/eidos/components/sidebar';
import { Banner } from '$uix/eidos/components/banner';
import { Breadcrumb } from '$uix/eidos/components/breadcrumb';
import { Avatar } from '$uix/eidos/components/avatar';
import { Badge } from '$uix/eidos/components/badge';
import { Banner } from '$uix/eidos/components/banner';
import { Box } from '$uix/eidos/components/box';
import { Breadcrumb } from '$uix/eidos/components/breadcrumb';
import { Button } from '$uix/eidos/components/button';
import { Card } from '$uix/eidos/components/card';
import { Box } from '$uix/eidos/components/box';
import { Container } from '$uix/eidos/components/container';
import { Grid } from '$uix/eidos/components/grid';
import { Flex } from '$uix/eidos/components/flex';
import { Group } from '$uix/eidos/components/group';
import { Heading } from '$uix/eidos/components/heading';
import * as Icon from '$uix/eidos/components/icon';
@ -45,12 +54,71 @@
let active = $state('/app/inbox');
let noticeUp = $state(true);
const stats = [
{ label: 'Abiertos', value: '128', note: '+12 esta semana' },
{ label: 'Sin asignar', value: '17', note: '−4 desde ayer' },
{ label: 'Cerrados hoy', value: '42', note: 'objetivo 40' }
];
const rows = [
{ from: 'Ada Lovelace', subject: 'Analytical engine — v2 notes', when: '09:41' },
{ from: 'Grace Hopper', subject: 'Compiler pass is green', when: '08:12' },
{ from: 'Karen Spärck Jones', subject: 'Ranking weights for the index', when: 'Yesterday' },
{ from: 'Radia Perlman', subject: 'Spanning tree, revisited', when: 'Yesterday' },
{ from: 'Barbara Liskov', subject: 'Substitution in the new API', when: 'Monday' }
{
id: 'ada',
from: 'Ada Lovelace',
initials: 'AL',
subject: 'Notas de la máquina analítica, v2',
when: '09:41',
unread: true
},
{
id: 'grace',
from: 'Grace Hopper',
initials: 'GH',
subject: 'El compilador pasa en verde',
when: '08:12',
unread: true
},
{
id: 'karen',
from: 'Karen Spärck Jones',
initials: 'KS',
subject: 'Pesos de ranking para el índice',
when: 'Ayer',
unread: false
},
{
id: 'radia',
from: 'Radia Perlman',
initials: 'RP',
subject: 'El árbol de expansión, revisitado',
when: 'Ayer',
unread: false
},
{
id: 'barbara',
from: 'Barbara Liskov',
initials: 'BL',
subject: 'Sustitución en la API nueva',
when: 'Lunes',
unread: false
}
];
const nav = [
{
group: 'Bandeja',
items: [
{ href: '/app/inbox', label: 'Entrada', icon: Icon.Inbox, badge: '5' },
{ href: '/app/drafts', label: 'Borradores', icon: Icon.File },
{ href: '/app/sent', label: 'Enviados', icon: Icon.Send }
]
},
{
group: 'Trabajo',
items: [
{ href: '/app/projects', label: 'Proyectos', icon: Icon.Folder },
{ href: '/app/reports', label: 'Informes', icon: Icon.ChartBar }
]
}
];
</script>
@ -73,77 +141,36 @@
<Sidebar.Header>
<Group gap={2} align="center">
<Icon.Sparkles size="sm" />
<strong style="font-size: var(--font-size-sm);">Acme</strong>
<Text size="sm" weight="semibold">Acme</Text>
</Group>
</Sidebar.Header>
<Sidebar.Content>
<Sidebar.Group>
<Sidebar.GroupLabel>Bandeja</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton
href="#/app/inbox"
active={active === '/app/inbox'}
tooltip="Entrada"
onclick={(e: MouseEvent) => {
e.preventDefault();
active = '/app/inbox';
}}
>
<Icon.Inbox size="sm" />
<span>Entrada</span>
<Sidebar.MenuBadge>5</Sidebar.MenuBadge>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
<Sidebar.MenuItem>
<Sidebar.MenuButton
href="#/app/drafts"
active={active === '/app/drafts'}
tooltip="Borradores"
onclick={(e: MouseEvent) => {
e.preventDefault();
active = '/app/drafts';
}}
>
<Icon.File size="sm" />
<span>Borradores</span>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
<Sidebar.Group>
<Sidebar.GroupLabel>Trabajo</Sidebar.GroupLabel>
<Sidebar.Menu>
<Sidebar.MenuItem>
<Sidebar.MenuButton
href="#/app/projects"
active={active === '/app/projects'}
tooltip="Proyectos"
onclick={(e: MouseEvent) => {
e.preventDefault();
active = '/app/projects';
}}
>
<Icon.Folder size="sm" />
<span>Proyectos</span>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
<Sidebar.MenuItem>
<Sidebar.MenuButton
href="#/app/reports"
active={active === '/app/reports'}
tooltip="Informes"
onclick={(e: MouseEvent) => {
e.preventDefault();
active = '/app/reports';
}}
>
<Icon.ChartBar size="sm" />
<span>Informes</span>
</Sidebar.MenuButton>
</Sidebar.MenuItem>
</Sidebar.Menu>
</Sidebar.Group>
{#each nav as section (section.group)}
<Sidebar.Group>
<Sidebar.GroupLabel>{section.group}</Sidebar.GroupLabel>
<Sidebar.Menu>
{#each section.items as item (item.href)}
<Sidebar.MenuItem>
<Sidebar.MenuButton
href={`#${item.href}`}
active={active === item.href}
tooltip={item.label}
onclick={(e: MouseEvent) => {
e.preventDefault();
active = item.href;
}}
>
<item.icon size="sm" />
<span>{item.label}</span>
{#if item.badge}
<Sidebar.MenuBadge>{item.badge}</Sidebar.MenuBadge>
{/if}
</Sidebar.MenuButton>
</Sidebar.MenuItem>
{/each}
</Sidebar.Menu>
</Sidebar.Group>
{/each}
</Sidebar.Content>
<Sidebar.Footer>
<Group gap={2} align="center">
@ -154,66 +181,101 @@
{/snippet}
{#snippet header()}
<Box
paddingX={4}
paddingY={3}
style="border-block-end: var(--border-width) solid var(--color-border-subtle); background: var(--color-surface-default);"
>
<Group gap={3} align="center" justify="space-between">
<Group gap={3} align="center">
{#if rail}
<Sidebar.Trigger><Icon.PanelLeft size="sm" /></Sidebar.Trigger>
{/if}
<Breadcrumb>
<Breadcrumb.List>
<Breadcrumb.Item><Breadcrumb.Link href="#/app">Acme</Breadcrumb.Link></Breadcrumb.Item>
<Breadcrumb.Separator />
<Breadcrumb.Item
><Breadcrumb.Link href="#/app/inbox">Entrada</Breadcrumb.Link></Breadcrumb.Item
>
</Breadcrumb.List>
</Breadcrumb>
</Group>
<Group gap={2} align="center">
<Badge size="sm">Beta</Badge>
<Button variant="ghost" size="sm">Nuevo</Button>
<Avatar size="xs"><Avatar.Fallback>JV</Avatar.Fallback></Avatar>
<Stack gap={0}>
<Box paddingX={4} paddingY={3}>
<Group gap={3} align="center" justify="space-between">
<Group gap={2} align="center">
{#if rail}
<Sidebar.Trigger><Icon.PanelLeft size="sm" /></Sidebar.Trigger>
{/if}
<Breadcrumb size="sm">
<Breadcrumb.List>
<Breadcrumb.Item>
<Breadcrumb.Link href="#/app">Acme</Breadcrumb.Link>
</Breadcrumb.Item>
<Breadcrumb.Separator />
<Breadcrumb.Item>
<Breadcrumb.Link href="#/app/inbox">Entrada</Breadcrumb.Link>
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb>
</Group>
<Group gap={2} align="center">
<Badge size="sm" variant="soft">Beta</Badge>
<Button variant="ghost" size="sm">Buscar</Button>
<Button size="sm">Nuevo</Button>
<Avatar size="xs"><Avatar.Fallback>JV</Avatar.Fallback></Avatar>
</Group>
</Group>
</Group>
</Box>
</Box>
<Separator decorative />
</Stack>
{/snippet}
{#snippet asideContent()}
<Box
padding={4}
height="100%"
style="inline-size: 16rem; border-inline-start: var(--border-width) solid var(--color-border-subtle); background: var(--color-surface-muted);"
>
<Stack gap={3}>
<Heading level={2} size="sm">Detalle</Heading>
<Text size="sm" color="muted">
El panel que se pliega cuando la anchura no da. A 1280 está; a 375 no.
</Text>
<Separator />
<Stack gap={2}>
<Text size="sm">Estado: <strong>abierto</strong></Text>
<Text size="sm">Asignado: Grace H.</Text>
</Stack>
</Stack>
</Box>
<!--
Two measured decisions here, both of them the system's and neither mine:
1. The width is `--sidebar-width`, the system's own rail measure, so both
flanks of the page agree — instead of a hand-picked `16rem`, which
would ignore density and `--scaling`. A right-panel measure of its own
does not exist yet; registered in the block's Gaps.
2. The boundary is a `Separator`, NOT a tinted canvas. `Surface
variant="soft"` was the obvious choice and it is inert here: measured
`oklch(0.9911 0 0)` against a page ground of the same lightness — the
panel simply did not appear. That is F15 in
`PLAN-blocks-quality.md` §6, a registered canon gap («soft no acota un
panel, y Surface no tiene borde»), not something to work around with a
hand-written background.
-->
<Flex align="stretch" height="100%">
<Separator orientation="vertical" decorative />
<Box width="var(--sidebar-width)">
<Box padding={4}>
<Stack gap={4}>
<Stack gap={1}>
<Text size="xs" color="muted">Conversación</Text>
<Heading level={2} size="sm">Detalle</Heading>
</Stack>
<Separator decorative />
<Stack gap={4}>
<Stack gap={1}>
<Text size="xs" color="muted">Estado</Text>
<Group gap={2} align="center">
<Badge size="sm" color="affirm" variant="soft">Abierto</Badge>
</Group>
</Stack>
<Stack gap={1}>
<Text size="xs" color="muted">Asignado</Text>
<Group gap={2} align="center">
<Avatar size="xs"><Avatar.Fallback>GH</Avatar.Fallback></Avatar>
<Text size="sm">Grace H.</Text>
</Group>
</Stack>
<Stack gap={1}>
<Text size="xs" color="muted">Etiquetas</Text>
<Group gap={2} align="center">
<Badge size="sm" variant="soft">infra</Badge>
<Badge size="sm" variant="soft">v2</Badge>
</Group>
</Stack>
</Stack>
</Stack>
</Box>
</Box>
</Flex>
{/snippet}
{#snippet pageFooter()}
<Box
paddingX={4}
paddingY={3}
style="border-block-start: var(--border-width) solid var(--color-border-subtle);"
>
<Group gap={3} align="center" justify="space-between">
<Text size="sm" color="muted">© 2026 Acme</Text>
<Text size="sm" color="muted">v0.1</Text>
</Group>
</Box>
<Stack gap={0}>
<Separator decorative />
<Box paddingX={4} paddingY={3}>
<Group gap={3} align="center" justify="space-between">
<Text size="sm" color="muted">© 2026 Acme</Text>
<Text size="sm" color="muted">v0.1</Text>
</Group>
</Box>
</Stack>
{/snippet}
<AppShell
@ -222,7 +284,7 @@
nav={{ collapsible, mobileBreakpoint: 'md' }}
aside={{ breakpoint: 'lg' }}
navLabel="Producto"
asideLabel="Detalle del elemento"
asideLabel="Detalle de la conversación"
banner={banner ? notice : undefined}
navContent={rail ? navContent : undefined}
{header}
@ -230,59 +292,83 @@
footer={footer ? pageFooter : undefined}
>
<Box padding={5}>
<Stack gap={5}>
<Group gap={3} align="center" justify="space-between">
<Heading level={1} size="lg">Entrada</Heading>
<Text size="sm" color="muted">5 sin leer</Text>
<Stack gap={6}>
<Group gap={3} align="end" justify="space-between">
<Stack gap={1}>
<Text size="sm" color="muted">Bandeja compartida</Text>
<Heading level={1} size="xl">Entrada</Heading>
</Stack>
<Group gap={2} align="center">
<Button variant="outline" size="sm">Filtrar</Button>
<Button variant="outline" size="sm">Ordenar</Button>
</Group>
</Group>
<Grid columns={{ base: 1, md: 3 }} gap={4}>
<Card
><Stack gap={1}
><Text size="sm" color="muted">Abiertos</Text><Heading level={2} size="md">128</Heading
></Stack
></Card
>
<Card
><Stack gap={1}
><Text size="sm" color="muted">Sin asignar</Text><Heading level={2} size="md"
>17</Heading
></Stack
></Card
>
<Card
><Stack gap={1}
><Text size="sm" color="muted">Cerrados hoy</Text><Heading level={2} size="md"
>42</Heading
></Stack
></Card
>
<Grid columns={{ base: 1, sm: 3 }} gap={4}>
{#each stats as stat (stat.label)}
<Card variant="outline">
<Stack gap={1}>
<Text size="sm" color="muted">{stat.label}</Text>
<Heading level={2} size="lg">{stat.value}</Heading>
<Text size="xs" color="muted">{stat.note}</Text>
</Stack>
</Card>
{/each}
</Grid>
<Stack gap={0}>
{#each rows as row (row.subject)}
<div
style="padding-block: var(--space-3); border-block-end: var(--border-width) solid var(--color-border-subtle);"
>
<Group gap={3} align="center" justify="space-between">
<Stack gap={1}>
<Text size="sm"><strong>{row.from}</strong></Text>
<Text size="sm" color="muted">{row.subject}</Text>
</Stack>
<Text size="sm" color="muted">{row.when}</Text>
</Group>
</div>
{/each}
<Stack gap={3}>
<Heading level={2} size="sm">Conversaciones</Heading>
<Card variant="outline">
<Stack gap={0}>
{#each rows as row, i (row.id)}
{#if i > 0}
<Separator decorative />
{/if}
<Box paddingY={3}>
<Group gap={4} align="center" justify="space-between">
<Group gap={3} align="center">
<Avatar size="sm">
<Avatar.Fallback>{row.initials}</Avatar.Fallback>
</Avatar>
<Stack gap={1}>
<Text size="sm" weight={row.unread ? 'semibold' : 'regular'}>
{row.from}
</Text>
<Text size="sm" color="muted">{row.subject}</Text>
</Stack>
</Group>
<Group gap={3} align="center">
{#if row.unread}
<Badge size="xs" color="primary" variant="soft">nuevo</Badge>
{/if}
<Text size="xs" color="muted">{row.when}</Text>
</Group>
</Group>
</Box>
{/each}
</Stack>
</Card>
</Stack>
<!-- Enough page to make the scroll model visible: with `scroll="main"`
the bar above stays while this moves; with `body` the whole document
goes and the bar pins. -->
{#each Array(8) as _, i (i)}
<Text color="muted">
Bloque de contenido {i + 1} — desplaza para ver quién scrollea.
</Text>
{/each}
<!--
Enough page to make the scroll model visible: with `scroll="main"`
the bar above stays while this moves; with `body` the whole document
goes and the bar pins.
-->
<Stack gap={3}>
<Heading level={2} size="sm">Actividad</Heading>
{#each Array(6) as _, i (i)}
<Card variant="soft">
<Stack gap={1}>
<Text size="sm" weight="semibold">Registro {i + 1}</Text>
<Text size="sm" color="muted">
Desplaza para ver quién scrollea: bajo scroll="main" la cabecera se queda quieta
porque nunca se desplaza.
</Text>
</Stack>
</Card>
{/each}
</Stack>
</Stack>
</Box>
</AppShell>

Loading…
Cancel
Save

Powered by TurnKey Linux.