Living documentation

Design system

Dodrawing tokens, primitives, and patterns. Everything on this page is interactive: these are the same components used by the editor, and their values come from the code that builds them.

17 color tokens17 primitives69 shortcuts

Principles

Six rules behind most decisions in the system.

Code is the source of truth

Every value on this page comes from code rather than a separate style guide. A smoke test (`scripts/dls-dom.tsx`) compares this documentation with `index.css`, `tailwind.config.ts`, and the real components, so changes to the DLS cannot leave the docs behind.

One token per decision, not per color

There is no “Dodrawing blue.” Use `primary` for actions, `destructive` for destructive actions, `accent` for hover, and `muted` for subdued fills. A theme changes token values, never a list of conditionals.

Height before padding

Controls use fixed heights (`h-9`, `h-8`, `h-7`...). This keeps chips, badges, and panels in the same visual family without tuning each control’s padding.

One pattern for each intent

One primary action per screen, bordered cards, grouped menus, and shortcuts on the right. When a new pattern duplicates an existing one, reuse the existing pattern.

Accessible by default

Focus rings, state retention, and focus restoration are built into base components. A `Button` works with a keyboard without special accessibility props.

Empty states are part of the pattern

Loading, empty, and error states have dedicated components (`Skeleton`, `EmptyState`). No screen stays blank while waiting for data.

Color

HSL variables work consistently in light and dark themes. Components never hard-code colors.

Values are shown exactly as defined in src/index.css. The theme changes by toggling the dark class on documentElement and storing dodrawing.theme in localStorage.

--background

Page background. `body` always uses this token, so changing it affects the whole app.

light 0 0% 100%

dark 240 10% 4%

--foreground

Default text color. It also colors selected text through `::selection` at 15%.

light 240 10% 4%

dark 0 0% 98%

--surface

Cards, panels, and dialogs: the base content surface.

light 0 0% 100%

dark 240 8% 6%

--surface-raised

One level above: tooltips, menus, and card hover states.

light 240 5% 98%

dark 240 7% 9%

--border

Lines and dividers. `*` applies this to every element, so it does not need to be repeated.

light 240 6% 90%

dark 240 5% 17%

--input

Field and checkbox borders. In dark mode this is lighter than `border` to distinguish controls.

light 240 6% 90%

dark 240 5% 20%

--ring

Focus ring. `:focus-visible` applies it with `ring-2` and a 2px offset.

light 240 5% 10%

dark 240 5% 84%

--primary

Primary action. It is nearly black in light mode and nearly white in dark mode, keeping the button clear in both.

light 240 6% 10%

dark 0 0% 98%

--primary-foreground

Text and icons on `primary`. Always the inverse of the token.

light 0 0% 98%

dark 240 6% 10%

--secondary

Secondary actions, chips, and avatar fills.

light 240 5% 96%

dark 240 5% 14%

--secondary-foreground

Text on `secondary`.

light 240 6% 10%

dark 0 0% 98%

--muted

Subdued fill for tabs, segmented controls, and menu hover states.

light 240 5% 96%

dark 240 5% 13%

--muted-foreground

Secondary text and placeholders. In dark mode it rises from 46% to 62% to maintain contrast.

light 240 4% 46%

dark 240 5% 62%

--accent

Button and row hover state. This is not a brand color; it signals “you are here.”

light 240 5% 94%

dark 240 5% 16%

--accent-foreground

Text on `accent`.

light 240 6% 10%

dark 0 0% 98%

--destructive

Delete, undo, and destructive actions. The only token with high chroma.

light 0 72% 51%

dark 0 63% 50%

--destructive-foreground

Text on `destructive`.

light 0 0% 98%

dark 0 0% 98%

States outside the token system

success and warning are not tokens. They only appear in a badge or a canvas marker, never on a large surface, so they use Tailwind colors with 10% fill.

successwarningdangermutedoutlinedefault
  • successemerald`bg-emerald-500/10` + `text-emerald-600` (light) / `text-emerald-400` (dark).
  • warningamber`bg-amber-500/10` + `text-amber-600` (light) / `text-amber-400` (dark).
  • destructivedestructiveWhen using the token: `bg-destructive/10` + `text-destructive`.
  • neutralmuted / accent`bg-muted text-muted-foreground` or `bg-accent text-accent-foreground`.

Typography

Outfit for the interface and Poppins for headings, with system fallbacks.

font-sans

Map the flow of an idea

Outfit, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, sans-serif

The interface’s geometric typeface. Poppins is reserved for headings.

font-display

Map the flow of an idea

Poppins, ui-sans-serif, system-ui, sans-serif

Typeface for page and section headings and h1–h6 text styles.

font-mono

Map the flow of an idea

ui-monospace, SFMono-Regular, Menlo, Consolas, monospace

Values, IDs, menu shortcuts, and any text that must not wrap.

Scale

text-2xs0.6875rem / 1rem
text-xs0.75rem
text-sm0.875rem
text-xl1.25rem
text-2xl1.5rem
text-3xl1.875rem
text-4xl2.25rem
text-5xl3rem

Below text-2xs the design system uses arbitrary sizes because the canvas and layer tree need to show a lot of information in a small space:

text-[11px]text-[10px]text-[9px]text-[8px]text-[7px]

text-[11px] (Inspector fine print.) · text-[10px] (`<Kbd>`, status badges, and `sm` avatars.) · text-[9px] (`xs` avatars and canvas microcopy.) · text-[8px] (Canvas minimum-scale markers.) · text-[7px] (Ruler numbers.)

Weights

font-normal Body text.font-medium Default interface weight for buttons, labels, and menu items.font-semibold Section and dialog headings.

Shape and sizing

A single base radius and a height scale set the interface rhythm.

--radius

0.625rem

Base value. Defined only in `:root`; corner radii do not change with the theme.

rounded-lg

var(--radius)

Cards, dialogs, tooltips, menus, and empty states.

rounded-md

calc(var(--radius) - 2px)

Buttons, inputs, badges, tabs, and menu items. The everyday radius.

rounded-sm

calc(var(--radius) - 4px)

Small elements that should not compete with the text.

rounded-[4px]

4px

Checkbox. Nearly square so the check mark stays legible.

rounded-[3px]

3px

Canvas selection outline, inset so it does not cover the node border.

Control heights

h-11
`lg` buttons and avatars.
h-9
`md` and icon buttons, inputs, and panel headers. The default control height.
h-8
`sm` buttons, public navigation, and workspace menu items.
h-7
Chips, `md` tabs, and tree items.
h-6
`sm` tabs, segmented controls, `sm` avatars, and dialog close buttons.
h-5
`sm` badges, `<Kbd>`, and `xs` avatars.
h-4
Checkbox.

Components

The primitives in src/components/ui, all imported from one index.

The primary action. An HTML button with variants, four heights, and a loading state.

Variants

NameClassesWhen
primarybg-primary text-primary-foregroundhover `/90`, active `/80`, with `shadow-sm`. Use once per screen.
secondarybg-secondary text-secondary-foregroundhover `/70`.
ghostno fillOnly `hover:bg-accent hover:text-accent-foreground`.
outlineborder border-border bg-transparentExplicit border; for toolbar actions.
dangerbg-destructive text-destructive-foregroundhover `/90`.
linktext-foreground underline-offset-4No background or fixed height; underlined on hover.

Sizes

NameClasses
smh-8 px-3 text-xs gap-1.5
mdh-9 px-4 text-sm gap-2
lgh-11 px-6 text-sm gap-2
iconh-9 w-9 p-0

Props

PropTypeNote
variant'primary' | 'secondary' | 'ghost' | 'outline' | 'danger' | 'link'Defaults to `primary`.
size'sm' | 'md' | 'lg' | 'icon'Defaults to `md`.
loadingbooleanReplaces the content with a spinner in the text color while preserving the width.

Details

  • Base classes are `inline-flex shrink-0 items-center justify-center whitespace-nowrap rounded-md font-medium transition-colors duration-150`; disabled state uses `disabled:pointer-events-none disabled:opacity-50`.
  • `buttonStyles({ variant, size, className })` returns the same classes for use on a `<Link>`, keeping styling consistent. The public navigation uses this for its CTAs.
  • The spinner uses `h-3.5 w-3.5 animate-spin border-[1.5px] border-current border-t-transparent`. It inherits the text color, so it works with every variant.

Accessibility

  • It is a native `<button>`, so it inherits disabled behavior, focus, and activation with Enter or Space.

Live example

Variants

Sizes

Loading and disabled

buttonStyles on a Link

Without the component

src/components/ui/Button.tsx

Layers

Stacking order, from highest to lowest.

z-[100]TooltipAbove everything else; it lives in a portal and must never be trapped.
z-50DialogModal surfaces.
z-40Public headerThe header sits above page content.
z-[76]Selector de colorAbove floating menus. It opens from the inspector or context menu and must stay in front of its trigger.
z-[74]Canvas context menu, DropdownMenuAbove the inspector and below the command palette. Menus float in a portal.
z-[70]Paleta de comandosFull overlay, above all editable content.
z-[60]ToasterTemporary notifications above the palette so they are never covered.

Motion

Five animations, two durations, and a global prefers-reduced-motion override.

animate-fade-infade-in 160ms ease-outDialog and overlay entrance.
animate-scale-inscale-in 140ms ease-outTooltips, menus, and command palette. Enters from `translateY(4px) scale(0.98)`.
animate-slide-downslide-down 160ms ease-outMobile public navigation menu.
animate-slide-upslide-up 400ms cubic-bezier(0.16, 1, 0.3, 1) bothEntrance for blocks with a longer travel distance.
animate-marqueemarquee 32s linear infiniteLogo strips.
  • Two durations only: `duration-150` for hover and focus, and `duration-200` for the header on scroll. Controls use no long transitions.
  • Color changes use `transition-colors`; position changes use `transition-transform`. `transition-all` is reserved for pressing a canvas tile.
  • Everything is disabled by `prefers-reduced-motion: reduce`: `index.css` sets animation and transition to 0.01ms and removes smooth scrolling.
  • Tooltips and menus use `animate-scale-in` instead of a fade, so they feel elevated rather than like a flat state change.

Shortcuts

Generated from src/lib/shortcuts.ts, using the current platform symbol.

Global

Open the command palette⌘ / Ctrl + K
View all shortcuts⇧ / Shift + ?
Toggle light / dark theme⌘ / Ctrl + ⇧ / Shift + L
Show or hide navigation⌘ / Ctrl + B
Search the current view/
Save changes⌘ / Ctrl + S
Close dialogs and menusEsc

Navigation

Go to dashboardG then D
Go to projectsG then P
Create a new projectG then N

Editor

Selection toolV
Pan toolH
Create screen (frame)F
Insert textT
Insert rectangleR
Insert lineL
Insert imageI
Insert componentC
Edit selected text↵ / Enter
Edit selected text (alternative)F2
Undo⌘ / Ctrl + Z
Redo⌘ / Ctrl + ⇧ / Shift + Z
Duplicate selection⌘ / Ctrl + D
Copy selection⌘ / Ctrl + C
Cut selection⌘ / Ctrl + X
Paste⌘ / Ctrl + V
Group selection⌘ / Ctrl + G
Ungroup selection⌘ / Ctrl + ⇧ / Shift + G
Save selection as component⌘ / Ctrl + ⌥ / Alt + K
Delete selection⌫ / Del
Delete selection (alternative)⌫ / Del
Select all⌘ / Ctrl + A
Clear selection or exit prototype modeEsc
Armar componente reciente 11
Insertar componente reciente 1⇧ / Shift + 1
Armar componente reciente 22
Insertar componente reciente 2⇧ / Shift + 2
Armar componente reciente 33
Insertar componente reciente 3⇧ / Shift + 3
Armar componente reciente 44
Insertar componente reciente 4⇧ / Shift + 4
Armar componente reciente 55
Insertar componente reciente 5⇧ / Shift + 5
Armar componente reciente 66
Insertar componente reciente 6⇧ / Shift + 6
Armar componente reciente 77
Insertar componente reciente 7⇧ / Shift + 7
Armar componente reciente 88
Insertar componente reciente 8⇧ / Shift + 8
Armar componente reciente 99
Insertar componente reciente 9⇧ / Shift + 9

Vista

Prototype modeP
Preview prototype⇧ / Shift + P
Zoom in⌘ / Ctrl + +
Zoom out⌘ / Ctrl + -
Zoom to 100%⌘ / Ctrl + 0
Fit all content⌘ / Ctrl + ⇧ / Shift + F
Show or hide layers⇧ / Shift + L
Show or hide inspector⇧ / Shift + I
Export to PDF⌘ / Ctrl + ⇧ / Shift + E

Canvas navigation

Pan left←
Pan right→
Pan up↑
Pan down↓
Pan faster with Shift + arrow keys⇧ / Shift + ←
Pan quickly to the right⇧ / Shift + →
Pan quickly up⇧ / Shift + ↑
Pan quickly down⇧ / Shift + ↓
Center on selection⌘ / Ctrl + ⇧ / Shift + 0

Inside a text field, only Escape, Enter and shortcuts using mod or alt. The editor icons V H F select, pan, and frame content.

Editor toolbar:

Accessibility

Built-in accessibility, without extra props.

Always-visible focus

`:focus-visible` applies `outline-none ring-2 ring-ring ring-offset-2 ring-offset-background`. The browser outline is removed while the ring remains visible, including on `primary`.

The real element holds the state

`Checkbox` visually hides its input with `sr-only`, and `Dialog` renders its overlay as a `div`. In both cases the DOM keeps the semantics separate from the visuals.

Real labels

`Input` links its `label` by `id` instead of relying on a placeholder. `Avatar` uses `name` as `alt`, and `Tooltip` uses `aria-describedby` instead of `title`.

Focus restoration

`Dialog` stores the active element when opened and restores it when closed, so `Escape` returns the user to where they were.

Scrolling is locked while a modal is open

`Dialog` sets `overflow: hidden` on `body` while open and restores it when closed, including if it unmounts while open.

Motion follows system settings

`@media (prefers-reduced-motion: reduce)` sets animations and transitions to 0.01ms and disables smooth scrolling. Components do not need to opt in individually.

Shortcuts adapt to each platform keyboard

`formatChord` maps `mod` to `⌘` on Mac and `Ctrl` elsewhere. `eventMatchesChord` ignores shortcuts inside text fields except `Escape`, `Enter`, and combinations using `mod` or `alt`.

Tabular numbers where values change

Tab counters, inspector measurements, and prices use `tabular-nums` so columns stay aligned as values change.

Focus any control with Tab:

Editor

Canvas gestures and the rules behind them.

Single clickSelects a node. A second click on the same node deselects it.
Double-clickOpens inline text editing. Double-clicking another node selects it and opens its text editor.
Enter or F2Opens text editing for the current selection without clicking directly on the text or counting clicks.
Click and dragMoves the node. Dragging stops at the edge of the active scope.
Triple-clickIsolates the node and its children. Triple-clicking a descendant moves the scope into it without stacking isolation levels.
Gray area, Escape, or click on the backgroundExits isolation and returns to the overview without restoring the previous scope.
ResizeOnly within the active scope. Ancestor containers remain visible above the dimmed overlay so they can still be adjusted.
  • Isolation uses a single scope, not a stack. Returning to the overview does not restore the previous scope.
  • Outside the active scope, nodes cannot be moved, resized, edited, or opened in a context menu; pointer events pass through.
  • While editing, text is tinted and outlined above the node frame so the selection remains visible.
  • Any element can contain editable text, whether or not it has children. In a container, text sits at the top left like its visible title, with pointer events disabled so it does not cover children.
  • A container only renders its text layer while it is being edited or when its text differs from its name; otherwise, the name would appear twice.
  • Double-click opens only the actual text editor (`data-text-hit`). Press `Enter` to edit a container title.

Panels

The workspace and its dockable panels.

  • Side docks are horizontal; top and bottom docks are vertical.
  • The toolbar stays against the edge and does not shrink with the other panels.
  • A dock must be at least 120 px wide; below that, its contents are not usable.
  • The bottom bar is permanent and sits outside the docks, so it does not depend on available panel space.
  • Sizes are stored in `dodrawing.panel.sizes.v2`; visible docks are stored in `dodrawing.panel.docks.v1`.