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