@lagabote/design-system
v0.7.1
Published
Lagabøte design system: tokens, fonts, CSS and React components.
Downloads
1,109
Readme
Lagabøte design system
One system for every Lagabøte surface (the app, the website, Lagabøte-konto, the trust center and the operator tools): tokens, fonts and React components. It is the coded form of the "Lagabøte" Claude Design system (https://claude.ai/artifact/81ZvnrKje36bAMXTueXVNY), whose principles apply here: verifiable, calm, source text in serif and our own words in sans, status always visible, Norwegian first.
The Magnus repository owns this package (packages/design-system). The app
depends on it with a file: dependency; every other repository installs
@lagabote/design-system from npm and never keeps a copy. Release Please
releases this path as the component design-system (tag
design-system-vX.Y.Z), and each consumer takes a new version by bumping its
dependency. The package has no build step and no app-specific imports.
Publishing: the publish-design-system job in release-please.yml is meant
to publish over npm trusted publishing, but npm refuses the OIDC exchange for
this repository (npm/cli#9969). Until npm fixes that, an owner of the npm org
lagabote publishes each release by hand, with 2FA:
git checkout design-system-vX.Y.Z
cd packages/design-system && npm publishUse
/* Any bundler: tokens, base and components as plain CSS. */
@import "@lagabote/design-system/styles.css";
/* With Tailwind CSS 4, after `@import "tailwindcss"`: token utilities. */
@import "@lagabote/design-system/tailwind.css";import { Button, Citation, SourceCard } from "@lagabote/design-system";- Fonts. The tokens name Newsreader, Hanken Grotesk and IBM Plex Mono.
@import "@lagabote/design-system/fonts.css"self-hosts them (latin subset, OFL); the bundler copies the files, so nothing loads from a third party and the CSP needs onlyfont-src 'self'. Next.js apps may instead override--font-serif,--font-sansand--font-monoon:rootwithnext/font. - Themes. Light is the default. Set
data-theme="dark"on<html>, or on any subtree. - Root size. Sizes are in rem and assume a 16 px root.
- Navigation. Components that take
hrefrender<a>. Pass the router's link aslinkAsto navigate without a page load. - Icons.
icontakes a name from the built-in set or any element, for example a Lucide icon withstrokeWidth={1.5}. - Classes. The
lb-*classes are public. Put them on your own markup when a component does not fit:lb-prosefor an answer rendered from Markdown,lb-readingfor source text that arrives as HTML,lb-menuon a context menu,lb-toast-viewportaround toasts,lb-playfor an illustration that acts out a task (the website). - Strict CSP. Everything works under
style-src 'self': no inline styles and no injected style elements.Dialogand its parts (DialogRoot,DialogContent,DialogHeader,DialogTitle,DialogDescription,DialogFooter,DialogClose,DialogTrigger) are Radix's: focus stays in the dialog and returns to what opened it, a menu's button included (focusOrigin,returnFocus). - Next.js. Add the package to
transpilePackages.
Layout
| Path | What |
| ---------------------------- | ------------------------------------------------------- |
| tokens.json | The source of truth, in the Claude Design token format |
| scripts/build-tokens.mjs | Generates tokens.css and tailwind.css |
| scripts/check-contrast.mjs | WCAG contrast for every text and control pairing |
| src/styles/tokens.css | Generated: custom properties for both themes |
| src/styles/tailwind.css | Generated: bg-surface, text-ink-muted, type-body… |
| src/styles/base.css | Body, focus ring, selection, reduced motion |
| src/styles/components.css | The lb-* component classes |
| src/components | The React components, grouped as in Claude Design |
| src/fonts | The self-hosted faces, fonts.css and their licences |
Change a token in tokens.json, run pnpm tokens, and bring the Claude
Design system in step. pnpm check fails when the generated files are stale
or a pairing loses contrast.
Across Lagabøte
The products are separate repositories on separate hosts, but to a customer and to an operator they are one Lagabøte. Notion is the reference: one quiet workspace, one accent, the same words everywhere. Every surface uses one of three shells:
| Shell | Where | Built from |
| --- | --- | --- |
| Page | website, trust center | TopBar, content on bg, the shared footer |
| Card | sign-in and sign-up (Lagabøte-konto, the app's sign-in) | Logo above one Card on bg |
| Workspace | the app, konto's customer and staff screens, Corpus, Acquisition | SideNav on bg-sunken with Logo and the surface's name muted (Lagabøte · Corpus), TopBar with Breadcrumbs |
- Colours only from tokens; no palette of your own and no raw values.
- Fonts from
fonts.css(ornext/fontwith the same faces). - Bokmål in the interface, also for operators.
- Status in the app's words with
StatusTag: «Pågår», «Venter på svar», «Til gjennomgang», «Satt på pause», «Avbrutt». - Icons from
Icon, or Lucide atstrokeWidth={1.5}; no glyphs. - The product is Lagabøte; only the assistant is Magnus.
- Footers of the page and card shells link the same set: Lagabøte (www), Tillitssenter (trust), Personvern, Vilkår and Logg inn.
Tokens
- Surfaces:
surface(white) is cards and the app's work surface.bgis the ground of a page that holds surfaces: the website and the sign-in pages.bg-sunkenis alternating website sections and the app's sidebar,surface-mutedthe source panel,surface-raisedmenus and dialogs, andsurface-inversethe dark band and the toast. - Text:
ink,ink-secondary,ink-muted,ink-subtle,ink-disabled,ink-inverse,ink-inverse-muted. - Lines:
line,line-subtle,line-strong(controls),line-inverse. - Accent:
accentwith-hover,-pressed,-soft,-soft-hover;on-accent,link,focus-ring. One accent; one primary action per view. - Signals:
success,warning,danger,info, each with-soft. - Sources:
src-lov,src-forarbeid,src-praksis,src-forvaltning,src-eos,src-litteratur. Always next to a text label. - Other:
highlight,highlight-strong,hover,scrim,viz-1–4, and thestein-*andfjord-*primitives for special cases. - Type:
--type-<style>font shorthands andtype-<style>utilities:display-xl,display-l,display-m,heading-l,heading-m,reading,reading-s,lead,title,body,body-s,label,caption,overline,ref. - Scale:
space-1–32(4 px rhythm),radius-xs–xlandfull,shadow-sm,-md,-lg,-frame,control-sm,-md,-lg,sidebar-w,source-panel-w,content-max,reading-max,z-*.
Components
- Brand:
Logo,Icon,Avatar,Kbd - Actions:
Button,IconButton,Menu,DropdownMenu* - Forms:
TextField,TextArea,Select,Checkbox,RadioGroup,Switch,SegmentedControl,SearchField,Field,Input,Textarea - Status and feedback:
Badge,StatusTag,Chip,Alert,Toast,Tooltip,Spinner,Skeleton - Legal sources:
SourceTypeChip,Citation,SourceCard,LegalText - Assistant:
UserMessage,AnswerBlock,Composer - Navigation:
TopBar,SideNav,Tabs,TabPanel,Breadcrumbs - Structure:
Card,ListRow,Table,Dialog,Popover,EmptyState - Website:
SectionHeader,PricingCard
The website's /design-system page shows them all in both themes.
Beyond the Claude Design system
Added because the app needs them: Popover, Chip, ListRow,
SegmentedControl, Field, Input, Textarea, the DropdownMenu* parts
(groups, checkable items, switch items for on/off settings, submenus),
TabPanel, SideNav collapsed,
Skeleton shape="block", Card variant, StatusTag tone, Citation href,
the annet source type, lb-prose and lb-reading, and the tokens
line-inverse, highlight-strong and hover.
Changed on purpose:
AnswerBlockshowscertaintyonly when it is given. The app does not grade legal certainty today, and a default of "Klar rett" would invent one.Dialog,Menu,TooltipandTabshave real behaviour (focus, keyboard, placement); in Claude Design they are drawings.Switch,RadioGroupandSegmentedControlare native inputs and need no JavaScript.- Sizes are rem instead of px, and the fluid display sizes and the overline's casing are token data instead of prose.
- Menu rows are 32 px instead of 36 and grow with their text, and a checked item's check trails its label, so checkable and plain items line up.
DropdownMenuis modal, as in Radix: a click outside only closes it. Radix locks scrolling with an injected<style>, so a page whose CSP forbids inline styles (the website) passesmodal={false}.lb-currentis the current item of a list or toggle the app builds itself: a surface with a hairline, never a tint.- Small fields use 14 px text, like small buttons, so a dense toolbar reads as one size.
- A disabled quiet button has no ground, native checkboxes and radios take the accent, a tooltip can sit on any side, and a popover that takes focus shows no ring of its own.
- Escape closes an open tooltip and is then passed on, so the panel or dialog behind it closes on the same press instead of needing a second one. A press that a menu or dialog has already taken is not passed on.
In the Lagabøte app
The app (apps/frontend) is written on this system; there is no alias layer.
Where the old app and this system disagreed, this system decided:
- Root size: 16 px (
html { font-size: 93.75% }is gone). - Weights: 400, 500 and 600, with no remapping.
- Corners:
rounded-mdis 4 px (was 8). - Fonts: Newsreader, Hanken Grotesk and IBM Plex Mono replaced Fraunces,
Inter and JetBrains Mono in
public/fontsandfonts.css. - Accent: primary actions, links and citations are
accent; they were ink. - Density: controls are 32, 40 and 48 px; the app uses
size="sm".
src/app/styles/global.css imports this package and keeps only what is the
app's own (shell widths, icon sizes, easings). The architecture test fails on
any colour utility that is not a token of this package.
These modules stay in the app on Radix, because their call sites depend on
behaviour this package's simpler components do not have, and are drawn with
this package's classes: context-menu.tsx (lb-menu),
toast.tsx (lb-toast), side-panel.tsx, panel.tsx and theme.tsx.
The tables below map the old names to this system; the modules above are the
exceptions. Not yet moved: the source rows in EvidencePanel, the composer
and .prose-answer, which use the tokens but their own markup.
Tokens and utilities
| Today in the app | In this system |
| ----------------------------------------- | ----------------------------------------------- |
| background | surface (bg on a page that holds a card) |
| card, input | surface |
| panel-strong, popover surfaces | surface-raised |
| secondary, muted, rail-bg | bg-sunken |
| foreground, ink-1, nav-fg-strong | ink |
| ink-2, nav-fg, akn-accent | ink-secondary |
| muted-foreground, ink-3 | ink-muted |
| ink-4, nav-fg-faint | ink-subtle |
| ink-5 | ink-disabled |
| primary, primary-foreground | accent, on-accent |
| border, rule | line |
| rule-2, rail-border | line-subtle |
| border-strong | line-strong (controls only) |
| ring | focus-ring (global; drop ring-ring classes) |
| destructive, destructive-foreground | danger, on-danger |
| row-hover, accent-quiet, nav-tint | hover |
| row-selected | accent-soft |
| flag-bg, flag-ink, flag-dot | warning-soft, warning |
| verify-bg, verify-ink, verify-dot | success-soft, success |
| hl, hl-strong | highlight, highlight-strong |
| state-{info,warning,success}-bg | {info,warning,success}-soft |
| state-{info,warning,success}-fg | info, warning, success |
| state-*-border | none; tinted surfaces have no border |
| shadow, shadow-lg | shadow-sm, shadow-lg |
| text-label, text-meta, text-caption | type-caption |
| text-body-sm, text-sm on controls | type-body-s, type-label |
| doc-font-*, doc-measure | type-reading, reading-max |
| sidebar-w (232 px) | sidebar-w (264 px) |
| rounded-2xl, rounded-pill | rounded-xl, rounded-full |
The utility follows the token: bg-background becomes bg-surface, bg-muted
becomes bg-bg-sunken, text-muted-foreground becomes text-ink-muted,
border-border becomes border-line.
Components
| Today in the app | In this system |
| --------------------------------------- | ------------------------------------------------------------- |
| Button default, destructive | Button variant="primary", "danger" |
| Button outline, secondary | Button (secondary is the default) |
| Button ghost, link | Button variant="quiet", "link" |
| Button size sm and default, lg | size="sm", "md" |
| Button size icon, icon-sm, icon-touch | IconButton with size and a required label |
| Input, Textarea, NativeSelect | Input, Textarea, Select |
| Label + control + FieldError | TextField, TextArea, Select, or Field |
| ErrorSummary, Alert | Alert tone="danger" |
| Badge default, secondary, muted | Badge tone="accent", neutral |
| Badge flag, verify, destructive | Badge tone="warning", "success", "danger" |
| Panel default, muted, dashed | Card compact with variant |
| Panel info, warning, success | Alert |
| Skeleton | Skeleton shape="block" |
| Tabs, SegmentedFilter | SegmentedControl, Tabs, or TabsRoot and its parts |
| Dialog parts, ConfirmDialog | Dialog; keep useConfirm and render Dialog in it |
| DropdownMenu* | DropdownMenu* (same names), or Menu with items |
| ContextMenu* | Radix context menu with the lb-menu classes |
| Popover* | Popover* |
| ToastProvider, useToast | Keep; render Toast. Errors that need action: Alert |
| ListRow, EmptyState | ListRow, EmptyState |
| CitationChip, CitationPopover | Citation, Popover with lb-source__excerpt |
| Source rows in EvidencePanel | SourceCard |
| SourceFamilyChip, context chips | Chip with selected |
| docTypeLabel text | SourceTypeChip type (the family) with the label as children |
| AsOfChip, in-force and repealed text | StatusTag |
| WorkStatus, EntitlementBadge | Badge with icon, or StatusTag tone |
| MagnusComposer | Composer, or its lb-composer classes |
| .product-nav-*, .product-topbar | SideNav (with collapsed), TopBar |
| .prose-answer | lb-prose (inside AnswerBlock) |
| .akn-* reading styles | lb-reading, type-reading, highlight |
| Loader2 with animate-spin, <kbd> | Spinner, Kbd |
| The "M" tile and the teal favicon | Logo, assets/lagabote-symbol.svg |
| SidePanel, ThemeProvider, data grid | Stay in the app; restyle with tokens |
