Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions docs/design/zen-style-reference.md

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions docs/plugin-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,55 @@ export default client

Both registrations are effects owned by the browser fiber. Cordis removes the renderer, listeners returned from the plugin, and its `<style>` element before a replacement activates. Keep raw browser APIs such as observers and global listeners inside an owned effect or a React effect with cleanup. Browser modules may import React and Cordis; Node built-ins and filesystem/process work belong in the server half.

## Shared interface appearance

Built-in controls import `uiStyles` from `program/plugins/ui/appearance.ts`.
Use its `button`, `iconButton`, `row`, `field`, `input`, `segment`, `dialog`,
`panel`, `tooltip`, `title`, `description`, and `key` classes for their visual
roles. The `interface-appearance` fiber registers the corresponding tokens and
styles directly under the interface, independently of the workspace. This scope
also covers portals mounted on `document.body`. The class catalog imports no CSS,
so editing shared appearance does not replace feature components or their drafts.

Features own positioning, bounds, row grids, wrapping, and specialized content.
Shared roles own neutral colors, typography, rounding, shadows, focus, interaction
states, and default control geometry. Add layout classes alongside the role instead of
repainting it. A decorated search container uses `field`; its borderless inner
control uses `input`. Use `panel` for a flyout and `dialog` for a modal, even when
both have an accessible `role="dialog"`. Native menu/listbox states also share the
row treatment. Keep `aria-selected`, `aria-checked`, `aria-pressed`, and disabled
states accurate so keyboard and pointer interactions agree.

When a specialized control needs custom CSS, consume the `--ui-*` roles from
`ui/appearance-tokens.css`: surface/inset, text/strong/secondary, border/divider,
field fill/border/radius, control fill/radius, hover/pressed/selected, action/on-action,
danger, and focus. Existing primitive `--alto-button-*` hooks remain supported
for geometry and deliberate visual exceptions. Literal accent swatches, status
indicators, source syntax, terminal colors, diagrams, and embedded browser content
retain their own palettes. Window glass and Composer geometry have independent
owners and should not change as a side effect of control styling.

`ui/geometry.css` supplies common target sizes, row padding, and menu insets.
Consume its geometry tokens for feature layouts: `--ui-panel-inset` (12px),
`--ui-dialog-inset` (24px), compact `--ui-row-min-height` (32px), and
`--ui-choice-row-min-height` (52px) for two-line choices. A feature row sets
`--ui-row-min-height: var(--ui-choice-row-min-height)` when it needs that density;
this preserves its target size independently of stylesheet registration order.
Use the shared row padding/gap and field/control padding roles rather than
nearby literal values. A 12px inset accommodates a 12px inner corner within the
24px outer surface. Keep tab and Composer pills as their established shape
families, and allow wrapping or scrolling when the available pane width shrinks.

Pierre renders React review annotations into document slots. Their shared
appearance belongs in `ui/review-annotations.css`; the viewer's `unsafeCSS`
stylesheet reaches its code shadow root and cannot paint those slotted controls.

`tests/interface-appearance-browser.test.ts` exercises real Settings, Cmd+K,
menus, and Composer under both themes, alternate accents, reversed stylesheet
registration, and style replacement. It varies shared role values to catch
feature overrides and verifies query, selection, and draft retention. Run it
alongside the affected feature checks when adding a surface or changing a role.

Fenced Markdown renderers use the same lifetime. Register a catch-all code renderer without `languages`, or a specialized renderer with normalized language names. Specialized renderers fall through to the next matching renderer when they unload or do not recognize the current fence:

```tsx
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "alto",
"version": "0.1.2",
"version": "0.1.3",
"private": true,
"type": "module",
"main": "dist/node/desktop/main.js",
Expand Down
43 changes: 43 additions & 0 deletions program/cordis.json
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,14 @@
}
]
},
{
"id": "interface-appearance",
"name": "Interface Appearance",
"description": "Shares typography, control states, fields, and floating surfaces across the application.",
"client": "plugins/interface-appearance.client.ts",
"enabled": true,
"config": {}
},
{
"id": "search",
"name": "Quick Search",
Expand Down Expand Up @@ -649,6 +657,41 @@
"enabled": true,
"config": {}
},
{
"id": "workspace-tabs-appearance",
"name": "Workspace Appearance",
"description": "Shares surface colors, typography, and alignment across workspace chrome and content.",
"client": "plugins/workspace-tabs-appearance.client.ts",
"enabled": true,
"config": {}
},
{
"id": "workspace-material",
"name": "macOS Window Material",
"description": "Applies native translucency consistently to workspace chrome while keeping content opaque.",
"module": "plugins/workspace-material.ts",
"client": "plugins/workspace-material.client.ts",
"enabled": true,
"config": {}
},
{
"id": "vertical-tabs",
"name": "Vertical Tabs",
"description": "Adds a resizable side tab column that can collapse to icons, and switches between it and the top tabs.",
"client": "plugins/vertical-tabs.client.tsx",
"enabled": false,
"config": {},
"children": [
{
"id": "hotkeys-vertical-tabs",
"name": "Vertical Tabs Shortcuts",
"description": "Switches tab orientation with Command-Shift-B and collapses the side tab column with Command-B.",
"client": "plugins/hotkeys-vertical-tabs.client.ts",
"enabled": true,
"config": {}
}
]
},
{
"id": "seal",
"name": "Seal",
Expand Down
14 changes: 8 additions & 6 deletions program/plugins/agent-switcher.client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type ReactNode,
} from 'react'
import { createPortal } from 'react-dom'
import { uiStyles } from './ui/appearance.js'
import { clientStyles, type BrowserPlugin, type ClientUiService } from '../../src/client/plugin-api.js'
import { errorMessage } from '../../src/shared/protocol.js'
import { AGENT_SWITCHER_ACTION, AGENT_SWITCHER_OVERLAY, AGENT_SWITCHER_TOGGLE } from './agent-switcher-api.js'
Expand Down Expand Up @@ -164,15 +165,16 @@ export function AgentSwitcherDialog({ session, ui }: { session: ClientSessionSer
<ConversationPaneOverlay className={`${clientStyles.overlayLayer} agent-switcher-layer`} onMouseDown={close}>
<section
id="agent-switcher-dialog"
className={`${clientStyles.floatingPanel} agent-switcher-panel`}
className={`${clientStyles.floatingPanel} ${uiStyles.dialog} agent-switcher-panel`}
role="dialog"
aria-modal="true"
aria-label="Agent, model, and reasoning"
onMouseDown={(event) => event.stopPropagation()}
>
<div className="agent-switcher-search">
<div className={`${uiStyles.field} agent-switcher-search`}>
<Search size={17} aria-hidden="true" />
<input
className={uiStyles.input}
ref={input}
value={query}
aria-label="Search agents and models"
Expand Down Expand Up @@ -202,7 +204,7 @@ export function AgentSwitcherDialog({ session, ui }: { session: ClientSessionSer
else optionRefs.current.delete(option.key)
}}
id={optionDomId(option.key)}
className={`agent-switcher-option${active ? ' is-active' : ''}${option.current ? ' is-current' : ''}`}
className={`${uiStyles.row} agent-switcher-option${active ? ' is-active' : ''}${option.current ? ' is-current' : ''}`}
type="button"
role="option"
aria-selected={active}
Expand All @@ -215,8 +217,8 @@ export function AgentSwitcherDialog({ session, ui }: { session: ClientSessionSer
key={option.key}
>
<span className="agent-switcher-option-copy">
<strong>{option.modelLabel}</strong>
<span>{option.providerLabel} · {option.providerLocation}</span>
<strong className={uiStyles.title}>{option.modelLabel}</strong>
<span className={uiStyles.description}>{option.providerLabel} · {option.providerLocation}</span>
</span>
<span className="agent-switcher-option-meta">
{option.disabledReason
Expand All @@ -236,7 +238,7 @@ export function AgentSwitcherDialog({ session, ui }: { session: ClientSessionSer
<div className="agent-switcher-efforts" role="group" aria-label="Reasoning effort">
{selected.efforts.map((choice) => (
<button
className="agent-switcher-effort"
className={`${uiStyles.segment} agent-switcher-effort`}
type="button"
aria-pressed={choice.value === effort}
title={choice.description}
Expand Down
Loading
Loading