blocks(app-shell): la demo no usaba los componentes que SIGNIFICAN lo que enseñaba

Rehecha entera. Cada pieza es ahora el componente del canon que corresponde al
acto, no una caja con el aspecto adecuado:

- las tres cifras son `Metrics` (label · value · delta), con la dirección
  desacoplada de la valencia: en «sin asignar», BAJAR es bueno (`goodTrend`),
  que es justo lo que las referencias hacen mal;
- la bandeja y la actividad son `Feed` — `role="feed"`, cada conversación un
  `role="article"` con `aria-posinset/setsize` y su título con `aria-level`,
  teclado de feed incluido. Medido: 2 feeds, 11 artículos, 11 encabezados;
- la barra superior es `Toolbar` (`role="toolbar"`, roving tabindex: cinco
  controles cuestan UNA parada de tabulación) más `Breadcrumb` con `current`;
- la tipografía sale de los estilos SEMÁNTICOS del tema — `Text
  style="label|caption|body"`, `Heading level` + `style` — en vez de tamaños
  elegidos a ojo, que es lo que había.

**Y la avería que esto destapó, que era del arnés y afectaba a los 19
previews**: la foundation declara los `@font-face` y emite los estilos
(`--style-body-*`…) pero no los aplica a ningún elemento — un `Text` ancla su
familia en su recipe y todo lo demás HEREDA del documento. El arnés de blocks no
anclaba nada, así que `html`, `body`, `[data-sidebar]` y cada fila del raíl
computaban **Times New Roman** al lado de los `Text` en Instrument Sans. El
shell de docs sí lo ancla (`uix.css`); éste no. Anclado en `_lib/reset.css` con
los tokens del tema, no con una familia escrita. De paso, el sangrado de 40px
que el navegador pone a `dd`.

Tres huecos del canon que salieron al componer, registrados en los Gaps:

1. **`Sidebar.MenuButton` es mudo y `NavigationMenu.Link` no** — el mismo acto
   con dos respuestas. El README del `Sidebar` firma que «navegar una fila es
   nativo y no suena», pero entonces el `NavigationMenu` contradice la firma; y
   en un shell la fila casi nunca navega: SELECCIONA la sección. Es de sema, no
   del tier.
2. **`Toolbar.Button` no acepta `variant`/`color`** — tipa contra el
   `ButtonProps` de soma (`{id, disabled}`), así que un cluster no puede decir
   cuál es su acción principal. Aquí va al lado, como en las referencias.
3. **No existe `description-list`** y el panel de detalle es exactamente el
   «primer detail-view real» que su ficha F5 pone como disparador.

Guards: `blocks:check` verde · `docs:check` 0/0 · `svelte-check` sin errores
propios (dos props inexistentes corregidas de paso: `Icon.Filter` y el
`variant` del `Toolbar.Button`).

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

@ -203,11 +203,14 @@ lo logra. Una receta que no se ha visto en el navegador NO entra en esta tabla._
## Gaps
| Gap | Disposición |
| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Los primitivos de layout no pueden cambiar de elemento** (F21) | **canon, registrado**: `Box` —y con él `Grid`/`Flex`/`Stack`— renderiza un `<div>` fijo, así que `<main>` / `<aside>` no pueden SER una caja del sistema. Por eso esos dos elementos llevan un `style` en línea con dos declaraciones (`flex`, `min-inline-size`, `overflow`). No es re-estilizar internals de un compuesto (B-3): es el elemento propio del block. Precedente en el tier: `article-grid-article.svelte` |
| **Topbar a todo lo ancho** (eje `layout` del `Sidebar`) | **canon, v2** — el provider del `Sidebar` es la fila flex. Lo pedirá `docs-shell` (F4.1) |
| **El `Sidebar` no publica su ancho actual** | no hace falta aquí (la reserva es flexbox), pero un app que quiera anclar algo al borde del rail no tiene a qué. Mantine expone 8 vars `--app-shell-*`; anotado por si aparece un consumidor real |
| **Superficie online/offline** | no existe en el ecosistema (ni art ni dimensión de prefs). Un ribbon de «sin conexión» en `banner` lo tiene que alimentar el app desde `Connections.anyReconnecting` |
| **Dos landmarks `banner`** cuando el `banner` que entra es el canon `Banner` | **ledger A-109**, del canon: `Banner` estampa `role="banner"` DESPUÉS de los rest props, así que no se puede quitar. Nombrar los dos es todo lo que puede un app |
| `scroll-margin-block-start` en los destinos bajo `scroll="body"` | del consumidor: la altura de la cabecera la sabe él. Lo cerrará `docs-shell`, que es quien tiene anclas |
| Gap | Disposición |
| ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Los primitivos de layout no pueden cambiar de elemento** (F21) | **canon, registrado**: `Box` —y con él `Grid`/`Flex`/`Stack`— renderiza un `<div>` fijo, así que `<main>` / `<aside>` no pueden SER una caja del sistema. Por eso esos dos elementos llevan un `style` en línea con dos declaraciones (`flex`, `min-inline-size`, `overflow`). No es re-estilizar internals de un compuesto (B-3): es el elemento propio del block. Precedente en el tier: `article-grid-article.svelte` |
| **Topbar a todo lo ancho** (eje `layout` del `Sidebar`) | **canon, v2** — el provider del `Sidebar` es la fila flex. Lo pedirá `docs-shell` (F4.1) |
| **El `Sidebar` no publica su ancho actual** | no hace falta aquí (la reserva es flexbox), pero un app que quiera anclar algo al borde del rail no tiene a qué. Mantine expone 8 vars `--app-shell-*`; anotado por si aparece un consumidor real |
| **Superficie online/offline** | no existe en el ecosistema (ni art ni dimensión de prefs). Un ribbon de «sin conexión» en `banner` lo tiene que alimentar el app desde `Connections.anyReconnecting` |
| **Dos landmarks `banner`** cuando el `banner` que entra es el canon `Banner` | **ledger A-109**, del canon: `Banner` estampa `role="banner"` DESPUÉS de los rest props, así que no se puede quitar. Nombrar los dos es todo lo que puede un app |
| `scroll-margin-block-start` en los destinos bajo `scroll="body"` | del consumidor: la altura de la cabecera la sabe él. Lo cerrará `docs-shell`, que es quien tiene anclas |
| **`Sidebar.MenuButton` es MUDO y `NavigationMenu.Link` no** — el mismo acto, dos respuestas | **canon, para una sesión de sema**: elegir un destino en una superficie de navegación emite `commit-select` en el `NavigationMenu` y NADA en el `Sidebar` ni en el `NavTree` (sus morfos sólo declaran `emerge-expand`/`emerge-collapse` sobre el panel). El README del `Sidebar` lo firma —«navegar una fila es nativo y no suena»— pero entonces el `NavigationMenu` contradice la firma. Además, en un shell la fila del raíl casi nunca navega: SELECCIONA la sección (`aria-current="page"`), que es exactamente el caso de `commit.select`. No se decide desde el tier |
| **`Toolbar.Button` no acepta `variant`/`color`** | **canon, anotado**: `ToolbarButtonProps` tipa contra el `ButtonProps` de SOMA (`{ id, disabled }`), así que el tratamiento es del root y todos los ítems lo llevan igual. Un grupo donde una acción es la principal no tiene cómo decirlo desde dentro; aquí la principal va al lado del cluster, como en las referencias |
| **No existe `description-list`** | **canon, F5**: el backlog lo condiciona a «el primer detail-view real» — y el panel de detalle de este shell lo es. Mientras no exista, el significado lo llevan `dl`/`dt`/`dd` con la tipografía del tema, y se declara aquí en vez de fingirlo con `div`s |

@ -44,3 +44,55 @@
body {
margin: 0;
}
/*
* The document's typographic anchor — the theme's own `body` style, and the
* second thing the foundation leaves to the app (the box model was the first).
*
* The foundation declares the `@font-face`s and emits the semantic styles
* (`--style-body-*`, `--style-label-*`, `--style-h1-*` …) but applies none of
* them to any element: a `Text` or a `Heading` anchors its own family in its
* recipe, and everything else INHERITS from the document. So any text the canon
* renders as plain text — the label `<span>` inside a `Sidebar.MenuButton`, a
* `Sidebar.GroupLabel` — falls to the browser default when the document says
* nothing. Measured 2026-08-19 on the app-shell preview: `html`, `body`,
* `[data-sidebar]` and every rail item computed `"Times New Roman"`, next to
* `Text` nodes in Instrument Sans. The component docs shell anchors its root
* (`uix.css`, `[data-uix-docs]`); this harness did not, on any of its 19
* previews.
*
* The values are the theme's `body` style, by token — never a family or a
* size written here — so a theme that redefines its body redefines this too.
*/
body {
font-family: var(--style-body-font-family);
font-size: var(--style-body-font-size);
line-height: var(--style-body-line-height);
letter-spacing: var(--style-body-letter-spacing);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/*
* The description-list defaults, removed for the same reason as the body
* margin: the browser indents `dd` by 40px, which reads as an alignment choice
* nobody made. Measured on the app-shell preview's detail panel, where every
* value sat a step to the right of its term. Layout in the tier comes from the
* layout components' props, so a UA margin is noise, not a decision.
*/
dl,
dd {
margin: 0;
}
/*
* The description-list defaults, removed for the same reason as the body
* margin: the browser indents `dd` by 40px, which reads as an alignment choice
* nobody made. Measured on the app-shell preview's detail panel, where every
* value sat a step to the right of its term. Layout in the tier comes from the
* layout components' props, so a UA margin is noise, not a decision.
*/
dl,
dd {
margin: 0;
}

@ -6,13 +6,21 @@
* 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.
* Two rules this file obeys, both learnt the expensive way on 2026-08-19:
*
* 1. Every piece is the canon component that MEANS what it shows, consumed
* through its props — `Metrics` for a KPI, `Feed` for a stream of
* articles, `Toolbar` for the action cluster, `Breadcrumb` for the trail,
* `Sidebar` for the rail. Not one `<div style>`, not one literal length,
* colour or font size: colour by role token, spacing by `--space-*`
* through the layout props, elevation by `Card`. A hand-rolled panel
* where a primitive exists is the drift the build contract names
* (`guides/component-guide.md` §Build contract).
* 2. Type comes from the theme's SEMANTIC styles, never from a size picked
* by eye: `Heading` takes `level` (the outline) and `style` (which of the
* theme's `--style-h*` treatments), `Text` takes `style="label"` /
* `"caption"` / `"body"`. The theme decides what a label looks like; the
* page decides which things are labels.
*/
import { AppShell, type AppShellScroll } from '$blocks/app-shell';
import { Sidebar } from '$uix/eidos/components/sidebar';
@ -24,14 +32,17 @@
import { Button } from '$uix/eidos/components/button';
import { Card } from '$uix/eidos/components/card';
import { Container } from '$uix/eidos/components/container';
import { Grid } from '$uix/eidos/components/grid';
import { Feed } from '$uix/eidos/components/feed';
import { Flex } from '$uix/eidos/components/flex';
import { Grid } from '$uix/eidos/components/grid';
import { Group } from '$uix/eidos/components/group';
import { Heading } from '$uix/eidos/components/heading';
import * as Icon from '$uix/eidos/components/icon';
import { Metrics } from '$uix/eidos/components/metrics';
import { Separator } from '$uix/eidos/components/separator';
import { Stack } from '$uix/eidos/components/stack';
import { Text } from '$uix/eidos/components/text';
import { Toolbar } from '$uix/eidos/components/toolbar';
import type { SidebarCollapsible } from '$uix/eidos/components/sidebar';
let {
@ -54,18 +65,32 @@
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 kpis = [
{ label: 'Abiertos', value: 128, delta: '12%', trend: 'up' as const, note: 'esta semana' },
{
label: 'Sin asignar',
value: 17,
delta: '4',
trend: 'down' as const,
goodTrend: 'down' as const,
note: 'desde ayer'
},
{
label: 'Cerrados hoy',
value: 42,
delta: '2',
trend: 'up' as const,
note: 'sobre el objetivo'
}
];
const rows = [
const conversations = [
{
id: 'ada',
from: 'Ada Lovelace',
initials: 'AL',
subject: 'Notas de la máquina analítica, v2',
summary: 'Adjunto la segunda pasada. Los diagramas de las tarjetas siguen sin cuadrar.',
when: '09:41',
unread: true
},
@ -74,6 +99,7 @@
from: 'Grace Hopper',
initials: 'GH',
subject: 'El compilador pasa en verde',
summary: 'Toda la batería en verde por primera vez. Falta el caso de la coma decimal.',
when: '08:12',
unread: true
},
@ -82,6 +108,7 @@
from: 'Karen Spärck Jones',
initials: 'KS',
subject: 'Pesos de ranking para el índice',
summary: 'Propongo IDF sobre el corpus entero, no por colección.',
when: 'Ayer',
unread: false
},
@ -90,6 +117,7 @@
from: 'Radia Perlman',
initials: 'RP',
subject: 'El árbol de expansión, revisitado',
summary: 'Un poema, y una prueba de que converge.',
when: 'Ayer',
unread: false
},
@ -98,11 +126,31 @@
from: 'Barbara Liskov',
initials: 'BL',
subject: 'Sustitución en la API nueva',
summary: 'Si un subtipo no puede sustituir al tipo, no es un subtipo.',
when: 'Lunes',
unread: false
}
];
const activity = [
{
id: 1,
who: 'Grace H.',
what: 'cerró la conversación «Compilador» como resuelta',
when: 'hace 12 min'
},
{ id: 2, who: 'Ada L.', what: 'reasignó «Máquina analítica» a Radia P.', when: 'hace 40 min' },
{ id: 3, who: 'Karen S.', what: 'añadió la etiqueta «índice»', when: 'hace 2 h' },
{
id: 4,
who: 'Barbara L.',
what: 'comentó en «Sustitución en la API nueva»',
when: 'hace 3 h'
},
{ id: 5, who: 'Radia P.', what: 'abrió una conversación nueva', when: 'ayer' },
{ id: 6, who: 'Sistema', what: 'importó 14 conversaciones del buzón compartido', when: 'ayer' }
];
const nav = [
{
group: 'Bandeja',
@ -127,7 +175,7 @@
<Banner intent="primary" variant="solid" aria-label="Aviso de producto">
<Container width="100%" paddingX={0}>
<Group gap={3} align="center" justify="space-between">
<Text size="sm">La facturación cambia el 1 de septiembre.</Text>
<Text style="body" size="sm">La facturación cambia el 1 de septiembre.</Text>
<Banner.Close onclick={() => (noticeUp = false)} />
</Group>
</Container>
@ -141,7 +189,7 @@
<Sidebar.Header>
<Group gap={2} align="center">
<Icon.Sparkles size="sm" />
<Text size="sm" weight="semibold">Acme</Text>
<Text style="label">Acme</Text>
</Group>
</Sidebar.Header>
<Sidebar.Content>
@ -175,14 +223,21 @@
<Sidebar.Footer>
<Group gap={2} align="center">
<Avatar size="xs"><Avatar.Fallback>JV</Avatar.Fallback></Avatar>
<Text size="sm" color="muted">Juan V.</Text>
<Text style="caption">Juan V.</Text>
</Group>
</Sidebar.Footer>
{/snippet}
<!--
The top bar. Two semantic regions, not one row of buttons: the TRAIL on the
start side (`Breadcrumb` — a `<nav>` naming where the reader is) and the
ACTION CLUSTER on the end side (`Toolbar` — `role="toolbar"`, roving tabindex,
arrow keys, so five controls cost one tab stop). The rail trigger sits before
the trail because it is chrome, not an action on this page.
-->
{#snippet header()}
<Stack gap={0}>
<Box paddingX={4} paddingY={3}>
<Box paddingX={4} paddingY={2}>
<Group gap={3} align="center" justify="space-between">
<Group gap={2} align="center">
{#if rail}
@ -195,16 +250,40 @@
</Breadcrumb.Item>
<Breadcrumb.Separator />
<Breadcrumb.Item>
<Breadcrumb.Link href="#/app/inbox">Entrada</Breadcrumb.Link>
<Breadcrumb.Link current>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>
<Toolbar aria-label="Acciones de la página" size="sm" variant="ghost">
<Toolbar.Button>
<Icon.Search size="sm" />
Buscar
</Toolbar.Button>
<Toolbar.Button>
<Icon.Bell size="sm" />
Avisos
</Toolbar.Button>
<Toolbar.Separator />
<Toolbar.Button aria-label="Cuenta de Juan V.">
<Avatar size="xs"><Avatar.Fallback>JV</Avatar.Fallback></Avatar>
</Toolbar.Button>
</Toolbar>
<!--
The primary action stays OUTSIDE the toolbar, and that is a
finding rather than a preference: `ToolbarButtonProps` types to
SOMA's `ButtonProps` (`{ id, disabled }`), so a toolbar button
takes no `variant`/`color` — the treatment belongs to the
Toolbar root and every item wears it. A cluster where one action
is the principal one has no way to say so from inside. Registered
in the block's Gaps; here the canon `Button` sits beside the
cluster, which is also what the references do.
-->
<Button size="sm">
<Icon.Plus size="sm" />
Nueva
</Button>
</Group>
</Group>
</Box>
@ -212,56 +291,62 @@
</Stack>
{/snippet}
{#snippet asideContent()}
<!--
Two measured decisions here, both of them the system's and neither mine:
<!--
The detail panel. Its content is a DESCRIPTION LIST — term / detail pairs —
and the canon has no component for that yet (`description-list` sits in the
tier's F5 backlog, waiting for «the first real detail view»; this is it, and
it is registered in the block's Gaps). So the semantics come from the
elements that carry them, `dl` / `dt` / `dd`, and the type from the theme's
`label` and `body` styles — the honest form of a gap: name it, and use the
platform's own meaning while it waits, not a stack of divs pretending.
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.
-->
Two more measured decisions, both the system's: the width is
`--sidebar-width` (the system's own rail measure, so the two flanks agree),
and the boundary is a `Separator` — `Surface variant="soft"` was inert here,
`oklch(0.9911 0 0)` against a page ground of the same lightness (F15, a
registered canon gap: soft does not bound a panel).
-->
{#snippet asideContent()}
<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 />
<Box width="var(--sidebar-width)" padding={4}>
<Stack gap={4}>
<Stack gap={1}>
<Text style="caption">Conversación</Text>
<Heading level={2} style="h4">Detalle</Heading>
</Stack>
<Separator decorative />
<dl>
<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>
<dt><Text style="label" color="muted">Estado</Text></dt>
<dd><Badge size="sm" color="affirm" variant="soft">Abierta</Badge></dd>
</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>
<dt><Text style="label" color="muted">Asignada a</Text></dt>
<dd>
<Group gap={2} align="center">
<Avatar size="xs"><Avatar.Fallback>GH</Avatar.Fallback></Avatar>
<Text style="body" size="sm">Grace H.</Text>
</Group>
</dd>
</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>
<dt><Text style="label" color="muted">Etiquetas</Text></dt>
<dd>
<Group gap={2} align="center">
<Badge size="sm" variant="soft">infra</Badge>
<Badge size="sm" variant="soft">v2</Badge>
</Group>
</dd>
</Stack>
<Stack gap={1}>
<dt><Text style="label" color="muted">Última actividad</Text></dt>
<dd><Text style="body" size="sm">hace 12 min</Text></dd>
</Stack>
</Stack>
</Stack>
</Box>
</dl>
</Stack>
</Box>
</Flex>
{/snippet}
@ -271,8 +356,8 @@
<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>
<Text style="caption">© 2026 Acme</Text>
<Text style="caption">v0.1</Text>
</Group>
</Box>
</Stack>
@ -293,81 +378,108 @@
>
<Box padding={5}>
<Stack gap={6}>
<!--
The page's own header: an `h1` at the theme's `h3` treatment. `level`
is the outline (this is the page's one h1); `style` is the theme's
register for chrome-sized titles — the display serif of `h1`/`h2`
belongs to a hero, not to a bandeja.
-->
<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>
<Text style="caption">Bandeja compartida</Text>
<Heading level={1} style="h3">Entrada</Heading>
</Stack>
<Group gap={2} align="center">
<Button variant="outline" size="sm">Filtrar</Button>
<Button variant="outline" size="sm">Ordenar</Button>
</Group>
<Toolbar aria-label="Vista" size="sm" variant="outline">
<Toolbar.Button>
<Icon.ListFilter size="sm" />
Filtrar
</Toolbar.Button>
<Toolbar.Button>
<Icon.ArrowUpDown size="sm" />
Ordenar
</Toolbar.Button>
</Toolbar>
</Group>
<!-- KPIs are `Metrics` — label / value / delta with the direction
decoupled from the valence: «sin asignar» FALLING is good. -->
<Grid columns={{ base: 1, sm: 3 }} gap={4}>
{#each stats as stat (stat.label)}
{#each kpis as kpi (kpi.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>
<Metrics size="md">
<Metrics.Label>{kpi.label}</Metrics.Label>
<Metrics.Value value={kpi.value}>{kpi.value}</Metrics.Value>
<Metrics.Trend>
<Metrics.Delta trend={kpi.trend} goodTrend={kpi.goodTrend} value={kpi.delta} />
<Metrics.Description>{kpi.note}</Metrics.Description>
</Metrics.Trend>
</Metrics>
</Card>
{/each}
</Grid>
<!-- The inbox is a `Feed`: `role="feed"`, each conversation an
`article` with its position in the set, PageUp/PageDown between
them — the APG pattern, not a stack of rows. -->
<Stack gap={3}>
<Heading level={2} size="sm">Conversaciones</Heading>
<Heading level={2} style="h4">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>
<Feed
aria-label="Conversaciones de la bandeja"
totalItems={conversations.length}
size="md"
>
{#each conversations as c, i (c.id)}
<Feed.Article posinset={i + 1}>
<Group gap={3} align="start" justify="space-between">
<Group gap={3} align="start">
<Avatar size="sm"><Avatar.Fallback>{c.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>
<Feed.ArticleTitle level={3}>
<Text style="label">{c.from}</Text>
{#if c.unread}
<Badge size="xs" color="primary" variant="soft">nuevo</Badge>
{/if}
</Feed.ArticleTitle>
<Text style="body" size="sm">{c.subject}</Text>
<Feed.ArticleDescription>
<Text style="caption">{c.summary}</Text>
</Feed.ArticleDescription>
</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>
<Text style="caption" numeric="tabular">{c.when}</Text>
</Group>
</Box>
</Feed.Article>
{/each}
</Stack>
</Feed>
</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.
-->
<!-- 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. Also a `Feed`, because that is what an
activity stream is. -->
<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}
<Heading level={2} style="h4">Actividad</Heading>
<Feed aria-label="Actividad reciente" totalItems={activity.length} size="sm">
{#each activity as a, i (a.id)}
<Feed.Article posinset={i + 1}>
<Card variant="soft">
<Group gap={3} align="start" justify="space-between">
<Stack gap={1}>
<Feed.ArticleTitle level={3}>
<Text style="label">{a.who}</Text>
</Feed.ArticleTitle>
<Feed.ArticleDescription>
<Text style="body" size="sm">{a.what}</Text>
</Feed.ArticleDescription>
</Stack>
<Text style="caption" numeric="tabular">{a.when}</Text>
</Group>
</Card>
</Feed.Article>
{/each}
</Feed>
</Stack>
</Stack>
</Box>

Loading…
Cancel
Save

Powered by TurnKey Linux.