DESIGN.md@workspace/.claude/skills/moi-workspace/references · git:20260811.b27e90e · 2026-08-11 · sha256 6706b07e5d17bdd2

DESIGN.md@workspace/.claude/skills/moi-workspace/references git:20260811.b27e90eA

Immutable. This exact content is served forever at /api/v1/blob/6706b07e5d17bdd2.

# moi applet design

This is the visual contract for widgets and views inside a moi workspace. The host app stays calm,
precise, and quiet. Applets may be more expressive and information-rich while still feeling related
to the workspace around them.

This file owns visual direction, layout, states, and interaction design. `SKILL.md` owns file layout,
imports, styling syntax, and build instructions. The host owns fonts and semantic color values.
Widgets and views resolve color tokens in different scopes, described below.

## Visual direction

Design around the content and the task. Give every applet a clear first reading, then add detail in
proportion to its size and purpose. Dense information is welcome when hierarchy and alignment keep
it easy to scan.

An applet may have one expressive focal point: a visualization, strong surface, image, or satisfying
state change. It is optional. Use it when it helps the content and keep the rest restrained so it
remains meaningful. Avoid decorative clutter, competing effects, and a different visual system for
every section.

Every visible element must help the user understand useful content, see a real state, or take an
action. Do not add category eyebrows that repeat the title, readiness badges for an obvious default
state, helper copy that repeats nearby text, decorative status pills, or icons with no useful
meaning. Clear and simple applets contain fewer elements, and each one earns its place.

Widgets and views in one workspace should feel like a family through shared typography, palette,
spacing, or interaction patterns. They do not need to look identical.

## Color

Widgets and views use the same semantic token vocabulary. Use
`h-full w-full bg-background text-foreground` for most applet roots. Each token keeps the same
purpose in both applet types; the host changes its value to suit the surrounding surface.

### How tokens resolve

- **Widgets:** The host applies a widget-local semantic theme derived from the selected workspace
  color. `background` is the workspace `primary`, `foreground` is its paired readable color, and
  the local `primary` pair becomes the light action surface derived from that color. The host frame
  stays transparent, so every widget root must cover the full frame with an opaque background. An
  image or visualization may own the surface when it has an opaque fallback.
- **Views:** Semantic tokens inherit the workspace page theme. The default root uses the normal
  workspace page surface and text.
- **Portalled UI:** Menus, tooltips, and popovers render in workspace scope and keep
  `bg-popover text-popover-foreground`.

### Semantic token reference

| Context or intent | Tailwind classes | Use |
| --- | --- | --- |
| Applet root | `bg-background text-foreground` | The full widget surface or view page |
| Main content | `text-foreground` | Titles, labels, values, and important icons |
| Supporting content | `text-muted-foreground` | Descriptions and secondary information |
| Inset surface | `bg-card text-card-foreground` | A functionally distinct contained region, used sparingly |
| Floating surface | `bg-popover text-popover-foreground` | Menus, tooltips, and other floating content |
| Quiet fill | `bg-muted` | Inset regions, skeletons, and disabled structure |
| Control state | `bg-accent text-accent-foreground` | Hover, active, and selected controls |
| Main action | `bg-primary text-primary-foreground hover:bg-primary/90` | The most important action in a local region |
| Positive or active state | `text-success` / `bg-success/10` | Positive outcomes, healthy states, presence, and progress indicators |
| Error or danger | `text-destructive` / `bg-destructive/10` | Errors, destructive actions, and invalid states |
| Object edge | `ring-1 ring-border` | Complete outlines around containers and controls |
| Focus | `focus-visible:ring-2 focus-visible:ring-ring` | Visible keyboard focus on interactive elements |
| Separator | `border-b border-border` | A one-sided division between adjacent regions |

Use lower opacity from the surface's foreground only for tertiary metadata that remains readable.
Avoid weak alpha text for important labels or values.

Prefer a subtle ring for the complete outline of a container or control. Use `ring-1 ring-border` on
semantic surfaces. Reserve one-sided borders such as `border-b` for separators between adjacent
regions. Never use a border and a ring on the same element. Avoid raw high-contrast colors for
either treatment.

Derive extra neutral or tonal colors and custom gradients from the local semantic tokens with
`color-mix()`. Use Tailwind palette colors only when they have clear content meaning, distinguish
infographic data, or follow an explicit user request. Keep each mapping consistent and explain it
with labels, legends, shapes, or direct values. Do not use palette colors for generic decoration or
unrelated widget backgrounds. Purple, violet, and fuchsia need a content, brand, or user reason;
never use a purple gradient as a default creativity or technology cue. On a saturated surface,
choose an explicit light or dark foreground with strong contrast.

### Surfaces and grouping

Use an open layout by default. Group content with spacing, alignment, type hierarchy, and, when
needed, a one-sided separator. Add one surface around content that shares a functional boundary: an
interactive object, an independently scrolling region, a distinct state, or a self-contained work
region. Page headers, tab rows, summaries, metrics, and ordinary sections stay unboxed by default.
Standard controls keep their normal component surfaces.

Never use a large rounded surface as a second page frame around the header, navigation, summary,
primary content, and supporting sections. Avoid adjacent or nested cards and wrappers that only
repeat the page structure.

Align page-level content to shared edges or grid columns. Apply outer padding once on the root or
layout grid, then use gaps to group unboxed content. When removing a surface, remove its card-like
padding and radius and realign its children; do not leave spacing that implies an invisible card.

### Textures

Texture is optional. Choose the root treatment after the content layout is clear, and use no more
than one texture per applet. Pair a texture with an opaque semantic base, such as
`h-full w-full bg-background text-foreground texture-checker`. The same classes resolve through the
widget-local theme or view page theme.

| Texture | Tailwind class | Character |
| --- | --- | --- |
| Checker | `texture-checker` | Two softly tinted semantic squares with no empty cells |
| Grid | `texture-grid` | Fine semantic graph-paper lines over the base color |
| Noise | `texture-noise` | Foreground-tinted fractal grain that lets the base tint show through |
| Linear gradient | `texture-gradient-linear` | A restrained top-to-bottom wash derived from `primary` |
| Inset shadow | `texture-inset-shadow` | A soft edge glow derived from `primary` |

Textures add an effect without setting `background-color`; the base color remains visible through
them. Match the frequency of the background to the density of the content:

| Content and layout | Root treatment |
| --- | --- |
| Dense content placed directly on the root, such as tables, long lists, forms, detailed charts, or multi-column dashboards | Plain `bg-background`, `texture-gradient-linear`, or `texture-inset-shadow` |
| Sparse content with large quiet areas | Any texture that preserves contrast |
| Dense content inside a functionally justified opaque or tonal work surface | A checker, grid, or noise texture may appear on the exposed background |
| Opaque full-bleed image, map, canvas, or visualization | No texture |

Never place `texture-checker`, `texture-grid`, or `texture-noise` directly behind dense open content.
Do not add cards or solid wrappers only to make a busy texture readable; choose a quieter root
treatment instead. A texture should become easy to ignore while reading.

Do not apply a texture to a card, panel, control, or text region, and do not stack textures or add
tuning variables.

Check nearby widgets and views before choosing one. Give unrelated applets different textures so
they remain distinct at a glance. A widget that opens, summarizes, or represents a view shares that
view's texture as part of the same visual language.

## Typography and hierarchy

Inherit the workspace font. Use sentence case for headings, labels, actions, and navigation. Use
regular weight for most text and medium for emphasis. Other weights are reserved for owner
hand-tuning.

Use a small, consistent type scale. `text-sm` (14 px) is the default for UI text. Use smaller text
only for genuinely compact metadata and larger text only for clear headings or one meaningful
display value. Do not create many label styles with slightly different sizes.

Keep text contrast clear and follow the foreground pairing for the chosen surface. Avoid several
near-identical text styles that make hierarchy hard to read.

Reserve `font-mono` for code, command text, and truly code-like identifiers. Do not use monospace for
numeric UI values, measurements, percentages, timers, timestamps, prices, counts, or labels. Numbers
inherit the workspace's default font; add `tabular-nums` when stable digit widths or alignment help.
A stronger display treatment may lead a widget or view when it serves the content, but it should not
become a separate type system. Keep no more than two clear type roles in one applet.

Build hierarchy with a visible size gap. Give the main value, message, or task the strongest
emphasis, keep supporting content quieter, and make tertiary metadata easy to ignore. A widget may
use a large hero value when its footprint supports it. If two elements compete for attention, reduce
one.

Use a 4 px spacing rhythm. As a guide:

- 4–8 px keeps tightly related items together.
- 12–16 px separates groups within a region.
- 24 px or more marks a section change in larger widgets and views.

Prefer spacing over dividers. Keep repeated rows, chart labels, status markers, counters, and
trailing actions in stable lanes so changing values do not shift the layout.

## Shape and radius

Use the shared shadcn radius scale and keep its default radius on standard controls. Ordinary cards,
fields, list rows, and repeated sections should use `rounded-lg` or `rounded-xl`. Reserve
`rounded-2xl` and `rounded-3xl` for one expressive focal surface, and use `rounded-full` only for
real pills, circular buttons, avatars, and status dots. Controls and rectangular content regions
must not look capsule-shaped.

## Interaction and motion

Widgets may include compact controls such as filters, toggles, refresh actions, and links. Keep
interaction feedback on the control that performs the action. The widget surface stays unchanged on
hover. Multi-step workflows and deep navigation belong in a view.

Controls must be obvious, keyboard-operable, and large enough to use comfortably. Give them a
visible focus state and an accessible name. Use Tabler icons with consistent visual weight. Do not
rely on color alone, and keep charts understandable with labels, legends, patterns, or direct values
where needed.

Use short, interruptible motion for meaningful state changes and control feedback. Respect
reduced-motion preferences and animate transform and opacity where possible. Do not loop decorative
motion or animate frequently updating numbers. Audio and video must be user-initiated.

## States

Design every state the applet can actually reach. Static applets do not need invented async states.
Data-driven applets need the following when applicable:

- **Loading:** show a skeleton that mirrors the final layout and preserves its surface. Use a spinner
  only for a compact inline action or work with no meaningful content shape.
- **Error:** explain the problem in one human sentence and provide a retry or clear recovery action.
- **Empty:** show a short explanation and a useful next step. Never leave a data region blank.
- **Refreshing:** keep current data visible and show progress on the refresh action. Do not replace
  useful content with the initial loading state.
- **Stale:** show a quiet timestamp or status near the affected data and keep it current.
- **Disabled:** preserve the label and structure while making the unavailable state clear without
  relying on color alone.

Keep state layouts stable so loading, success, empty, and error do not cause avoidable jumps. Errors
and empty states should still fit the configured widget height or the view's normal content frame.
When a primary region uses a surface, loading, populated, empty, success, and error states keep that
surface in the same position and size.

## Widgets

Widgets are compact dashboard surfaces seen alongside other widgets. They should reveal their main
meaning within a quick scan. Larger widgets may add supporting charts, lists, metadata, filters, and
direct actions while keeping one clear first reading.

### Frame

The host card owns the outer radius, border, shadow, clipping, and elevation. It leaves the inside
transparent. The widget root fills that rectangle and adds no outer card chrome. Its surface follows
the color guidance above.

Use a column layout when content needs a footer or timestamp, and keep lower-priority information at
the bottom. Important content needs safe padding from every edge. Use `p-4` as the compact default;
`p-5` suits larger footprints when the content still fits.

### Grid and sizing

The dashboard grid has four columns, 160 px rows, 8 px gaps, and a maximum width of 640 px. A widget
may span one to four rows and columns. Width shrinks with the workspace; height stays fixed by the
selected row span.

The exact height is:

`rowSpan × 160 + (rowSpan − 1) × 8`

Common maximum sizes are:

| Footprint | Maximum outer size | Approximate content after `p-4` |
| --- | --- | --- |
| 1 × 1 | 154 × 160 px | 122 × 128 px |
| 2 × 1 | 316 × 160 px | 284 × 128 px |
| 2 × 2 | 316 × 328 px | 284 × 296 px |
| 4 × 2 | 640 × 328 px | 608 × 296 px |
| 4 × 3 | 640 × 496 px | 608 × 464 px |
| 4 × 4 | 640 × 664 px | 608 × 632 px |

Treat width as flexible and height as the hard constraint. Start with the smallest footprint that
presents the content clearly. Test the narrowest realistic width and the longest realistic values.
Clip decorative overflow deliberately, clamp supporting prose, and keep the widget itself from
creating horizontal page scroll or an accidental inner page scroller.

### Composition

Give the eye one primary element and no more than two supporting groups in a small widget. Larger
footprints may carry more information when spacing and alignment preserve the first reading. Prefer
live information over labels that repeat obvious context.

Keep controls limited to the immediate task. A compact mode switch can work; tabs, wizards, and
multi-step flows usually indicate that the content should become a view.

## Views

Views are full app screens for denser information and sustained work. They own their page hierarchy,
content layout, spacing, chrome, and scrolling. A view should feel complete at the available size and
remain usable when the chat shares the workspace.

One view represents one screen. Cross-screen navigation belongs to the workspace. Internal tabs,
filters, or a master-detail split may organize the current task without introducing a separate
client-side routing system.

Let the content choose the frame. Most views need clear page context, a primary work area, and an
obvious place for the main action. A header with a title and optional subtitle or action is a useful
default. A canvas, map, or immersive data surface may establish context another way.

Use generous, consistent page padding such as 24 or 32 px for normal views. Constrain prose, forms,
and narrow task flows to a readable maximum width. Let tables, boards, maps, and visual workspaces use
more of the frame when that improves the task.

Prefer one main page scroller. Use bounded internal scrolling only for regions that benefit from
independent position, such as a sticky table body, timeline, or board. The page itself must not
create horizontal scroll.

Views and widgets share the same color, type, motion, and state vocabulary. Views may use more
sections, controls, and information layers while preserving a clear primary work area. Avoid
stretching a widget-like card to fill the page.

## Final review

After implementation, do a removal pass from top to bottom. For every visible element and wrapper,
ask what it helps the user understand, do, or recognize as a state. Remove it when the screen stays
clear without it, then realign the remaining content.

Before finishing an applet, confirm:

- The first reading and every visible element have a clear purpose.
- Color, texture, type, spacing, and expression support the content and remain readable.
- Surfaces mark real boundaries, and unboxed page content shares clear alignment axes.
- States remain stable, and the layout handles realistic content and deliberate overflow.
- Controls are necessary, complete, and accessible.
- The widget or view follows its frame, sizing, and scrolling rules and feels related to the
  workspace.