Skip to content
LyEve UI v0.60.1

Foundations

Sizing

Two ladders, both declared as tokens beside the palette. A page names a role and the shell resolves it. A surface lifted off the page takes a rung, and if it holds a form it takes the rung its content earns.

The page cap

PageShell's width, and nothing else. A page never writes max-w-*, mx-auto or a gutter of its own.

WidthCapFor
narrow896pxoutside an app shell only: a first-run setup step that sits in no shell
default1152pxkept for pages outside an app shell. No console page uses it
wide1536pxevery page inside an app shell: lists, settings, forms and detail
fullno capa canvas, a split pane or a page that scrolls itself. Takes fill

full is not wider than wide. It is the absence of a cap, and a table stretched across a 2560px window is unreadable in the same way a 768px column in that window is wasteful.

Every page inside an app's shell is wide, whatever it holds: a list, a settings form or a detail page. One cap keeps the content's left edge in one place from screen to screen. A form inside a wide page stays readable because its card and its fields carry their own width, not the page. narrow and default are for pages outside the shell. Name a width on every page: left unset, it falls back to default.

The overlay ladder

One ladder for Modal, Drawer and the dialog stack. Each component takes the slice its role allows: a docked panel and a modal stop at xl. Modal and Drawer size themselves to their fields. A form goes in a Drawer, and a Modal holds a one-field prompt at most. The dialog stack has no auto size: openDialog() takes md unless told, so a form of more than four fields is wider in a Drawer than in a dialog.

RungWidthFor
sm448pxa confirmation
md576pxwhere auto puts a form of up to four fields
lg704pxwhere auto puts a form past four fields
xl896pxwhere auto puts a form past eight fields, and a preview or a log, named by hand
full1088pxa dialog holding a table. Dialog only

A panel sizes itself to its form

Modal and Drawer default to size="auto", which takes the rung the body needs: md up to four fields, lg past four and xl past eight. The count is measured from what the panel rendered, and re-measured while it is open, so a form that reveals two more fields when a period is set to custom grows with them.

So never pass size to a Drawer whose body is a form, or to a Modal holding a prompt. Name a rung only when the body holds no field and the measurement has nothing to read: a preview, a log, a picture.

Fields are counted by the marker every labeled control carries, and only the outermost one counts, so a radio or checkbox group is the one question it asks rather than one field per option. Hand-rolled markup carries no marker and is invisible to the fit, which is one more reason to build a form from the kit's own fields.

<Drawer bind:open title="New budget">   <!-- eight fields: 704px -->
<Drawer bind:open title="Rename">        <!-- one field:    576px -->
<Drawer bind:open title="Preview" size="xl">  <!-- no fields: name the rung -->

Touch targets

Every single-line control takes its height from one token, --spacing-control: 38px under a mouse and 44px under a touch screen. A field and the button beside it grow together, so a row of them still lines up. A control drawn small on purpose, such as a switch, a checkbox or a close button, keeps its drawing and takes an invisible hit box of at least 44px instead.

A control made of several targets gives each one that size. Every segment of a SegmentedControl and both steppers of a NumberInput take a 44px hit box, and the collapsed SidebarNav rail stacks a group's disclosure under its link so the two still fit the rail.

Where targets wrap or sit in a row, the kit spaces them so the boxes do not overlap: FilterChips rows 18px apart, Pagination pages 16px apart, and TimePicker segments spread apart, CheckboxGroup and RadioGroup options far enough apart for each, and DatePicker weeks 12px apart in a 320px calendar, each target 44px. Spacing you write yourself follows the same rule: stacked Toggle rows need 24px between them under a touch screen.

Icons

An icon on a line of text matches that text. Everything else is named for the shape of the space it fills. An app keeps the five rungs in one module and every icon takes its size from there, never from a class such as h-4 w-4, which reads as styling and lets two pages disagree unseen.

RungSizeForDrawn
xs12pxon the line with text-xs: a badge, a chip, a meta line
sm14pxon the line with text-sm: a button, a row action, a table cell
md16pxa glyph in its own column, not on the text's baseline
lg20pxalone in its box: an empty state, a section medallion
placeholder40pxstanding in for a file that cannot be previewed
<script lang="ts">
  import { Plus } from '@lucide/svelte';
  import { ICON } from '$lib/icon'; // { xs: 12, sm: 14, md: 16, lg: 20, placeholder: 40 }
</script>

<Button size="sm"><Plus size={ICON.sm} /> New entry</Button>

Where a measurement is still yours

Inside a panel, sm: is the viewport's breakpoint and not the panel's, so sm:grid-cols-2 splits a row in two however narrow the panel is. Pair fields only where the ladder guarantees the room: at the md rung and above, two columns are 255px each or wider.