ui-styling
This guide covers the rules and patterns for authoring styles in `packages/ui`. The system uses [vanilla-extract](https://vanilla-extract.style/) (VE) with CSS `@layer` ordering, shared recipes, and composition over inheritance.
How it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
This guide covers the rules and patterns for authoring styles in `packages/ui`. The system uses [vanilla-extract](https://vanilla-extract.style/) (VE) with CSS `@layer` ordering, shared recipes, and composition over inheritance.
Agent definition
ui-styling.mdUI Styling Conventions
This guide covers the rules and patterns for authoring styles in `packages/ui`. The system uses [vanilla-extract](https://vanilla-extract.style/) (VE) with CSS `@layer` ordering, shared recipes, and composition over inheritance.
Core Principle: One Owner Per Property
Every CSS property on an element should have exactly **one authoritative source** of truth. When two rules set the same property at the same specificity, source order decides — which is fragile and hard to reason about.
**Anti-pattern (two owners fighting):**
// primitive: always sets padding 0.25rem
export const comboboxList = style({ padding: '0.25rem' });
// consumer: also sets padding → source-order roulette
export const list = style({ padding: '0.25rem' });**Correct (single owner):**
// primitive owns padding; consumer just composes it
// consumer delegates to primitive, no override
<ComboboxList> {/* no extra className needed */}State = Variant, Not Higher-Specificity Selector
Express component state through `data-*` attributes handled inside the owning `style()` or `recipe()`, not by adding specificity from the outside.
**Anti-pattern (state via higher specificity):**
// parent component pushing state into child via specificity escalation
globalStyle(`${someParent} ${childClass}`, { padding: 0 });**Correct (state-owned selector inside the primitive):**
export const comboboxList = style({
padding: '0.25rem',
selectors: {
'&[data-empty]': { padding: 0 }, // library-owned state drives own style
},
});No globalStyle Across Component Boundaries
`globalStyle` with a multi-segment selector that crosses a component boundary (e.g. `${parentClass} [data-slot="child"]`) creates invisible coupling. If the child's class changes, the parent silently breaks.
**Anti-pattern (cross-boundary globalStyle):**
// combobox.css.ts reaching into input-group internals
globalStyle(`${comboboxContent} [data-slot="input-group"]`, {
borderRadius: 0,
boxShadow: 'none',
});**Correct (variant prop on the child):**
// input-group.css.ts adds an "embedded" variant
export const inputGroup = recipe({
variants: {
variant: {
embedded: { borderRadius: 0, boxShadow: 'none' },
},
},
});
// consumer passes the variant explicitly
<InputGroup variant="embedded" />Prefer Composition Over Inheritance
Reuse styles by **composing** `style()` arrays rather than inheriting through class hierarchies or overrides.
// compose shared base into component-specific style
export const menuItem = style([
menuItemBase(), // shared structural recipe
{
selectors: { '&:focus': { backgroundColor: vars.surfaceHover } },
},
]);VE's `style([...])` merges multiple style objects/classes into one atomic class at build time. The `recipe()` base also accepts an array:
recipe({ base: [sharedBase, { componentSpecific: '...' }] })SVG Sizing: Use svg-helpers
Use the shared helpers from `@styles/effects/svg-helpers.css` instead of hand-rolling `globalStyle` for SVG sizing.
| Helper | Effect | |--------|--------| | `svgContainer` | `svg { pointer-events: none; flex-shrink: 0 }` | | `svgDefaultSize` | `svg:not([class*='size-']) { width: 1rem; height: 1rem }` | | `svgSmSize` | `svg:not([class*='size-']) { width: 0.75rem; height: 0.75rem }` | | `svgTextSize` | same as default (alias for inline text contexts) |
export const menuItem = style([svgContainer, svgDefaultSize, { /* ... */ }]);Shared Recipes for Common Patterns
menuItemBase
`@styles/recipes/menu-item.css` — structural recipe for all list item rows (DropdownMenu, Select, Combobox, ComboboxPopup).
import { menuItemBase } from '@styles/recipes/menu-item.css';
export const myItem = style([
menuItemBase({ trailingIndicator: true, fullWidth: true }),
{ selectors: { '&:focus': { backgroundColor: vars.surfaceHover } } },
]);Variants: `trailingIndicator`, `fullWidth`, `inset`, `muted`.
popupSurface + popupShadow*
`@styles/recipes/popup-surface.css` — base style for floating popup containers with the animation keyframe selectors and visual properties already wired up.
import { popupSurface, popupShadowMd } from '@styles/recipes/popup-surface.css';
export const myMenu = style([
popupSurface,
popupShadowMd,
{ minWidth: '12rem', padding: '0.25rem' },
]);Shadow variants: `popupShadowSm` (tooltips, comboboxes), `popupShadowMd` (menus, selects).
InputGroup variant prop
`InputGroup` accepts a `variant` prop:
- `default` — standalone field with border, shadow, and focus ring
- `embedded` — bottom-border-only divider for use inside popup containers
// ComboboxInput uses "embedded" automatically
<InputGroup variant="embedded" />
Input bare prop
`Input` accepts a `bare` boolean that strips its standalone border/shadow/focus ring. `InputGroupInput` passes `bare` automatically; consumers should not need it directly.
CSS @layer Discipline
The layer order is: `reset < tokens < base < recipes < utilities`.
- `reset` / `base` — `globalStyle` rules in `reset.css.ts` and `base.css.ts`
- `recipes` — component `style()` and `recipe()` output (target destination)
- `utilities` — `sx()` sprinkles; always overrides component styles
**Migration path:** To place a style in the `recipes` layer, wrap properties inside `'@layer': { recipes: { ... } }`:
export const foo = style({
'@layer': {
recipes: {
color: vars.foreground,
selectors: { '&:hover': { backgroundColor: vars.surfaceHover } },
},
},
});> **Important:** Unlayered styles always beat ALL layered styles. The migration must > be coordinated — mixing layered and unlayered styles for the same property on the > same element will make the unlayered one always win regardless of intent. > Migrate an entire property-ownership group at once.
Overrides Go Through utilities Layer
If a consumer g
Read more
UI Styling Conventions
This guide covers the rules and patterns for authoring styles in `packages/ui`. The system uses [vanilla-extract](https://vanilla-extract.style/) (VE) with CSS `@layer` ordering, shared recipes, and composition over inheritance.
Core Principle: One Owner Per Property
Every CSS property on an element should have exactly **one authoritative source** of truth. When two rules set the same property at the same specificity, source order decides — which is fragile and hard to reason about.
**Anti-pattern (two owners fighting):**
// primitive: always sets padding 0.25rem
export const comboboxList = style({ padding: '0.25rem' });
// consumer: also sets padding → source-order roulette
export const list = style({ padding: '0.25rem' });**Correct (single owner):**
// primitive owns padding; consumer just composes it
// consumer delegates to primitive, no override
<ComboboxList> {/* no extra className needed */}State = Variant, Not Higher-Specificity Selector
Express component state through `data-*` attributes handled inside the owning `style()` or `recipe()`, not by adding specificity from the outside.
**Anti-pattern (state via higher specificity):**
// parent component pushing state into child via specificity escalation
globalStyle(`${someParent} ${childClass}`, { padding: 0 });**Correct (state-owned selector inside the primitive):**
export const comboboxList = style({
padding: '0.25rem',
selectors: {
'&[data-empty]': { padding: 0 }, // library-owned state drives own style
},
});No globalStyle Across Component Boundaries
`globalStyle` with a multi-segment selector that crosses a component boundary (e.g. `${parentClass} [data-slot="child"]`) creates invisible coupling. If the child's class changes, the parent silently breaks.
**Anti-pattern (cross-boundary globalStyle):**
// combobox.css.ts reaching into input-group internals
globalStyle(`${comboboxContent} [data-slot="input-group"]`, {
borderRadius: 0,
boxShadow: 'none',
});**Correct (variant prop on the child):**
// input-group.css.ts adds an "embedded" variant
export const inputGroup = recipe({
variants: {
variant: {
embedded: { borderRadius: 0, boxShadow: 'none' },
},
},
});
// consumer passes the variant explicitly
<InputGroup variant="embedded" />Prefer Composition Over Inheritance
Reuse styles by **composing** `style()` arrays rather than inheriting through class hierarchies or overrides.
// compose shared base into component-specific style
export const menuItem = style([
menuItemBase(), // shared structural recipe
{
selectors: { '&:focus': { backgroundColor: vars.surfaceHover } },
},
]);VE's `style([...])` merges multiple style objects/classes into one atomic class at build time. The `recipe()` base also accepts an array:
recipe({ base: [sharedBase, { componentSpecific: '...' }] })SVG Sizing: Use svg-helpers
Use the shared helpers from `@styles/effects/svg-helpers.css` instead of hand-rolling `globalStyle` for SVG sizing.
| Helper | Effect | |--------|--------| | `svgContainer` | `svg { pointer-events: none; flex-shrink: 0 }` | | `svgDefaultSize` | `svg:not([class*='size-']) { width: 1rem; height: 1rem }` | | `svgSmSize` | `svg:not([class*='size-']) { width: 0.75rem; height: 0.75rem }` | | `svgTextSize` | same as default (alias for inline text contexts) |
export const menuItem = style([svgContainer, svgDefaultSize, { /* ... */ }]);Shared Recipes for Common Patterns
menuItemBase
`@styles/recipes/menu-item.css` — structural recipe for all list item rows (DropdownMenu, Select, Combobox, ComboboxPopup).
import { menuItemBase } from '@styles/recipes/menu-item.css';
export const myItem = style([
menuItemBase({ trailingIndicator: true, fullWidth: true }),
{ selectors: { '&:focus': { backgroundColor: vars.surfaceHover } } },
]);Variants: `trailingIndicator`, `fullWidth`, `inset`, `muted`.
popupSurface + popupShadow*
`@styles/recipes/popup-surface.css` — base style for floating popup containers with the animation keyframe selectors and visual properties already wired up.
import { popupSurface, popupShadowMd } from '@styles/recipes/popup-surface.css';
export const myMenu = style([
popupSurface,
popupShadowMd,
{ minWidth: '12rem', padding: '0.25rem' },
]);Shadow variants: `popupShadowSm` (tooltips, comboboxes), `popupShadowMd` (menus, selects).
InputGroup variant prop
`InputGroup` accepts a `variant` prop:
- `default` — standalone field with border, shadow, and focus ring
- `embedded` — bottom-border-only divider for use inside popup containers
// ComboboxInput uses "embedded" automatically <InputGroup variant="embedded" />
Input bare prop
`Input` accepts a `bare` boolean that strips its standalone border/shadow/focus ring. `InputGroupInput` passes `bare` automatically; consumers should not need it directly.
CSS @layer Discipline
The layer order is: `reset < tokens < base < recipes < utilities`.
- `reset` / `base` — `globalStyle` rules in `reset.css.ts` and `base.css.ts`
- `recipes` — component `style()` and `recipe()` output (target destination)
- `utilities` — `sx()` sprinkles; always overrides component styles
**Migration path:** To place a style in the `recipes` layer, wrap properties inside `'@layer': { recipes: { ... } }`:
export const foo = style({
'@layer': {
recipes: {
color: vars.foreground,
selectors: { '&:hover': { backgroundColor: vars.surfaceHover } },
},
},
});> **Important:** Unlayered styles always beat ALL layered styles. The migration must > be coordinated — mixing layered and unlayered styles for the same property on the > same element will make the unlayered one always win regardless of intent. > Migrate an entire property-ownership group at once.
Overrides Go Through utilities Layer
If a consumer g
Emdash is the Open-Source Agentic Development Environment (🧡 YC W26). Run multiple coding agents in parallel. Use any provider.
Repo: generalaction/emdash
Other agents on emdash.
- acp-runtime
The ACP runtime is the domain service that serves the ACP API contract. It owns the host-scoped dependencies needed to run provider ACP sessions, but it should not mix cross-session routing with per-session state projection.
Open agent - main-process
The main process is organized into domain modules under `src/main/core/`. Each domain typically has a `controller.ts` (RPC handlers) and service/implementation files.
Open agent - overview
All paths are relative to `apps/emdash-desktop/`.
Open agent - renderer
All paths are relative to `apps/emdash-desktop/`.
Open agent - shared
- Agent/provider DTOs: - `src/shared/core/agents/agent-payload.ts` - provider metadata and capabilities are sourced from `packages/plugins/src/agents/registry.ts` - IPC primitives: - `src/shared/ipc/rpc.ts` — typed RPC router, controller, and client - `src/shared/ipc/events.ts`
Open agent - workspace-server
The Workspace Server (`apps/workspace-server/`) is a Node daemon that runs on a remote machine and exposes workspace runtimes (git, files, deps, ACP, …) to Emdash clients over the `@emdash/wire` protocol. Clients connect over an SSH-forwarded Unix socket; the daemon is
Open agent

