diff --git a/.claude/skills/README.md b/.claude/skills/README.md index 84540b1..888c7e4 100644 --- a/.claude/skills/README.md +++ b/.claude/skills/README.md @@ -130,6 +130,25 @@ Helps debug common GPUI framework issues. - Performance optimization - Memory leak prevention +### 8. GPUI Component Library (`gpui-component/`) +Upstream skill vendored from [longbridge/gpui-component](https://github.com/longbridge/gpui-component/tree/main/skills/gpui-component). Covers the component library CloudBridge builds its UI on (`gpui-component 0.5.0-preview1`). + +**Use when:** +- Building or changing UI with library components (Button, Input, Select, Dialog, Tabs, Sidebar, Table, ...) +- Choosing the right component for a UI need +- Wiring component state or theming +- Making layout, spacing, hierarchy, interaction-state, or interface-copy decisions +- Making architecture, state-ownership, or public API decisions + +**Key Topics:** +- Component catalog with imports and stateless/stateful notes +- Setup patterns (`gpui_component::init`, `Root::new`) +- [design-guides.md](gpui-component/references/design-guides.md) — normative design rules +- [coding-guides.md](gpui-component/references/coding-guides.md) — crate layering, `RenderOnce` vs `Entity`, focus, async +- [usage.md](gpui-component/references/usage.md), [style-guide.md](gpui-component/references/style-guide.md) + +**Note:** This is a copy of upstream content — refresh it from the source repo rather than editing in place. + ## How to Use Skills Skills are reference documents that provide: diff --git a/.claude/skills/gpui-component/SKILL.md b/.claude/skills/gpui-component/SKILL.md new file mode 100644 index 0000000..36f442f --- /dev/null +++ b/.claude/skills/gpui-component/SKILL.md @@ -0,0 +1,168 @@ +--- +name: gpui-component +description: How to use the gpui-component UI library in GPUI applications, and the normative Design and Coding Guides that govern it. Use when building UIs with gpui-component components (Button, Input, Select, Dialog, Tabs, Sidebar, List, Table, etc.), setting up the library, handling component state or theming, finding the right component for a UI need, and also when designing layouts, spacing, visual hierarchy, or interaction states, writing interface copy, or making application architecture, state-ownership, or public API decisions. +--- + +## Read This First + +Before changing UI, interaction, interface language, layout, styling, +components, or application architecture, **read the relevant guide**: + +| Guide | Read before | +| --- | --- | +| [Design Guides](references/design-guides.md) | Choosing components, layout, spacing, hierarchy, color, density, interaction states, overlays, interface copy | +| [Coding Guides](references/coding-guides.md) | Crate layering, `RenderOnce` vs `Entity`, state ownership, `ElementId`, focus, async, public API, testing | + +These guides are requirements, not optional inspiration. Do not copy generic +web conventions, infer a design system from one existing screen, or add a +control merely because the underlying feature exists. Review the finished work +against both guides before considering it complete. + +Read the guide file itself. Do not answer from what this page summarizes, from +an existing screen in the codebase, or from training data — those are the three +ways the guides get quietly ignored. + +### Non-negotiables + +These are a floor, not a substitute. Read the guide for anything past this list. + +- **Never invent an API.** Search the current source for the real signature. + Do not translate a React, CSS, or older-GPUI example by analogy — a + plausible-looking method name that does not exist is the most common + failure mode here. +- **Desktop before web convention.** Keyboard access, window chrome, menus, + dense data views, resizable regions, persistent navigation. +- **`Button` vs `Link`.** `Button` for every in-app command — use `ghost` or + `outline` when it should read quietly. `Link` only for external URLs and + email addresses. +- **Tokens before values.** No raw hex or `rgb(...)` in application UI; use + `cx.theme()` semantic tokens. Use rem-based helpers (`p_2()`, `gap_3()`, + `text_sm()`) so window zoom works. Any spacing number you see quoted is the + current default scale, not a literal to repeat. +- **State must be visible.** Hover, focus, selection, disabled, loading, + validation, and destructive states each need distinct, consistent treatment. +- **Stable identity.** Repeated elements need domain-derived `ElementId`s, not + list indexes. +- **Overlays.** Escape dismisses the topmost surface and returns focus to its + trigger. +- **Copy.** Name the object and the verb — `Delete “Roadmap”?` with a `Delete` + button, not `Are you sure?` with `OK`. + +## Documentation + +- **Full reference**: fetch `https://longbridge.github.io/gpui-component/llms-full.txt` +- **Per-component API**: fetch `https://longbridge.github.io/gpui-component/docs/components/{name}.md` + - e.g. `button.md`, `input.md`, `select.md`, `dialog.md`, `data-table.md` +- **Any site page** can be fetched as Markdown by appending `.md` to the URL + +## Quick Reference + +**Setup** — always required: +```rust +gpui_component::init(cx); // in app.run(), must be first +Root::new(view, window, cx) // first-level view in every window +``` + +**Stateless** — use directly in render: +```rust +Button::new("id").primary().label("OK").on_click(|_, _, _| {}) +``` + +**Stateful** — hold `Entity` in struct, pass ref in render: +```rust +// in new(): let input = cx.new(|cx| InputState::new(window, cx)); +// in render: Input::new(&self.input) +``` + +**Sizes**: `.xsmall()` `.small()` `.medium()` (default) `.large()` + +**Theme**: `cx.theme().primary` · `.background` · `.foreground` · `.border` · `.muted` + +## Component Catalog + +When you need a component, find it here. For full API, fetch its `.md` doc. + +### Input & Form +| Component | Import | Notes | +|-----------|--------|-------| +| `Input` | `input::{Input, InputState}` | Stateful. Text, password, mask, validation | +| `NumberInput` | `input::{NumberInput, NumberInputEvent}` | Stateful. Numeric with step | +| `OtpInput` | `input::OtpInput` | Stateful. One-time password | +| `Select` | `select::{Select, SelectState}` | Stateful. Dropdown picker | +| `Combobox` | `combobox::{Combobox, ComboboxState}` | Stateful. Searchable select | +| `Checkbox` | `checkbox::Checkbox` | Stateless. `on_click(|&bool, ...|)` | +| `Switch` | `switch::Switch` | Stateless. Toggle | +| `Radio` | `radio::{Radio, RadioGroup}` | Stateless. | +| `Slider` | `slider::{Slider, SliderState}` | Stateful. | +| `Toggle` | `button::Toggle` | Stateless. | +| `Rating` | `rating::Rating` | Stateless. | +| `Stepper` | `stepper::Stepper` | Stateless. Increment/decrement | +| `ColorPicker` | `color_picker::{ColorPicker, ColorPickerState}` | Stateful. | +| `DatePicker` | `date_picker::{DatePicker, DatePickerState}` | Stateful. | +| `Form` | `form::{v_form, h_form, field}` | Layout container for form fields | + +### Display & Feedback +| Component | Import | Notes | +|-----------|--------|-------| +| `Button` | `button::{Button, ButtonGroup}` | Stateless. Primary UI action | +| `Icon` | `{Icon, IconName}` | Stateless. Lucide icons | +| `Badge` | `badge::Badge` | Stateless. | +| `Tag` | `tag::Tag` | Stateless. Closable tags | +| `Avatar` | `avatar::Avatar` | Stateless. | +| `Label` | `label::Label` | Stateless. Form label | +| `Kbd` | `kbd::Kbd` | Stateless. Keyboard key display | +| `Alert` | `alert::Alert` | Stateless. Info/success/warning/error | +| `Spinner` | `spinner::Spinner` | Stateless. Loading indicator | +| `Skeleton` | `skeleton::Skeleton` | Stateless. Loading placeholder | +| `Progress` | `progress::{Progress, ProgressCircle}` | Stateless. | +| `Tooltip` | `tooltip::Tooltip` | Via `.tooltip()` on elements | +| `HoverCard` | `hover_card::{HoverCard, HoverCardState}` | Stateful. | +| `Clipboard` | `clipboard::Clipboard` | Stateless. Copy button | + +### Overlay & Popups +| Component | Import | Notes | +|-----------|--------|-------| +| `Dialog` | `dialog::Dialog` + `WindowExt` | Via `window.open_dialog(...)` | +| `AlertDialog` | `WindowExt` | Via `window.open_alert_dialog(...)` | +| `Sheet` | `sheet::Sheet` + `WindowExt` | Side panel, via `window.open_sheet(...)` | +| `Notification` | `notification::Notification` + `WindowExt` | Via `window.push_notification(...)` | +| `Popover` | `popover::Popover` | Floating overlay | +| `Menu` | `menu::{PopupMenu, DropdownMenu}` | Context menus | +| `DropdownButton` | `button::DropdownButton` | Button with dropdown menu | + +### Navigation & Layout +| Component | Import | Notes | +|-----------|--------|-------| +| `Tabs` / `TabBar` | `tab::{Tab, TabBar}` | Tabbed interface | +| `Sidebar` | `sidebar::{Sidebar, SidebarMenu, ...}` | App navigation panel | +| `TitleBar` | `TitleBar` | Window title bar | +| `Breadcrumb` | `breadcrumb::Breadcrumb` | Navigation breadcrumb | +| `Pagination` | `pagination::Pagination` | Page navigation | +| `Accordion` | `accordion::Accordion` | Collapsible sections | +| `Collapsible` | `collapsible::Collapsible` | Single collapsible | +| `GroupBox` | `group_box::GroupBox` | Labeled container | +| `Resizable` | `resizable::{h_resizable, v_resizable, resizable_panel, ResizableState}` | Draggable split panes | +| `Scrollable` | `scroll::Scrollbar` | Custom scrollbar | +| `FocusTrap` | `gpui_base::focus_trap::FocusTrapElement` | Keyboard trap for modals | + +### Data Display +| Component | Import | Notes | +|-----------|--------|-------| +| `DataTable` | `table::{DataTable, TableState, TableDelegate}` | Stateful. Full-featured table | +| `Table` | `table::{Table, ...}` | Simpler table | +| `VirtualList` | `{v_virtual_list, h_virtual_list}` | High-perf large lists | +| `List` | `list::{List, ListState, ListDelegate}` | Stateful. Searchable list | +| `Tree` | `tree::{Tree, TreeState, TreeItem, TreeEntry}` | Stateful. Hierarchy | +| `DescriptionList` | `description_list::DescriptionList` | Key-value pairs | +| `Settings` | `setting::Settings` | Settings panel | + +### Charts +| Component | Import | Notes | +|-----------|--------|-------| +| `Chart` | `chart::{AreaChart, BarChart, LineChart, PieChart, RadarChart}` | Bar, line, area, pie charts | +| `Plot` | `plot::Plot` | `#[derive(IntoPlot)]` for data | + +## Reference Files + +- [usage.md](references/usage.md) — setup patterns, component types, common examples +- [style-guide.md](references/style-guide.md) — code style for contributors diff --git a/.claude/skills/gpui-component/references/coding-guides.md b/.claude/skills/gpui-component/references/coding-guides.md new file mode 100644 index 0000000..46c4022 --- /dev/null +++ b/.claude/skills/gpui-component/references/coding-guides.md @@ -0,0 +1,863 @@ +--- +title: Coding Guides +description: Architecture and coding conventions for maintainable GPUI Component applications +order: -2.2 +--- + +# Coding Guides + +This guide describes the application architecture and code patterns that have +proved durable in GPUI Component. It is written for both engineers and coding +agents. Read [Design Guides](./design-guides.md) first: code structure should +preserve product intent, not replace it. + +This is a normative guide. **Must** marks lifecycle, correctness, or ecosystem +constraints; **should** is the default architecture and requires a concrete +reason to depart from it. Current source and API docs remain authoritative for +exact signatures. + +## Architecture at a glance + +GPUI application architecture layers +GPUI application architecture layers + +Dependencies point downward. Higher layers own domain meaning and orchestration; +lower layers own reusable presentation or behavior. Do not make a reusable +component depend on an application screen, or make `gpui-base` depend on a +theme from GPUI Component. + +Use these boundaries: + +- **app shell:** compose windows and feature crates while keeping feature logic out; +- **feature crate:** keep one capability's model, services, views, commands, dialogs, + and workflow behind one public boundary; +- **app component:** a repeated domain-aware pattern; +- **gpui-component:** themed, general-purpose UI; +- **gpui-base:** reusable behavior and geometry without product presentation. + +### Organize large applications by capability + +In a large Rust application, a feature should usually be a crate, not another +file in a global `views`, `models`, or `modals` directory. Keep the model, +views, commands, dialogs, and workflow for one capability together. A dialog +that edits a workspace belongs to the workspace feature; only the reusable +dialog primitive belongs to the UI library. + +```text +crates/ +├── app/ +│ └── src/main.rs # Compose windows and features +├── workspace/ +│ └── src/ +│ ├── lib.rs # The feature's public boundary +│ ├── model.rs +│ ├── commands.rs +│ ├── workspace_view.rs +│ └── rename_dialog.rs +├── search/ +│ └── src/ +│ ├── lib.rs +│ ├── model.rs +│ ├── commands.rs +│ ├── search_view.rs +│ └── filters.rs +├── settings/ +│ └── src/ +│ ├── lib.rs +│ ├── model.rs +│ ├── settings_view.rs +│ └── account_dialog.rs +└── shared/ + └── src/ + ├── lib.rs + └── recent_items.rs # A stable capability with multiple owners +``` + +Do not invert this into global `models/`, `views/`, `modals/`, and `commands/` +directories. Those folders classify files by implementation role while +scattering every feature across the application. + +The application shell composes feature crates but contains little feature +logic. A feature may depend on stable shared capabilities and UI foundations; +it must not depend on the shell or reach into a sibling feature's internals. +When two features need to communicate, prefer an explicit command, event, data +type, or small shared service over a dependency between their views. Extract a +shared crate only after the capability has a coherent name and more than one +real owner. + +Crate boundaries are engineering boundaries. They let Cargo rebuild and test a +smaller dependency subgraph, make ownership visible in `Cargo.toml`, and limit +the review and regression surface of a change. They also make removal honest: +a feature that cannot be detached without searching through global view and +modal directories was never isolated. + +Do not create a crate for every screen or helper. Split where a capability has +its own state and lifecycle, a stable public seam, or enough implementation to +benefit from independent compilation and tests. Keep dependencies acyclic and +pointing toward smaller, more stable crates. + +## Bootstrap and root ownership + +Initialize GPUI Component once, before creating component-backed views, and put +`Root` at the first level of each window: + +```rust +app.run(move |cx| { + gpui_component::init(cx); + + cx.spawn(async move |cx| { + cx.open_window(WindowOptions::default(), |window, cx| { + let workspace = cx.new(|cx| Workspace::new(window, cx)); + cx.new(|cx| Root::new(workspace, window, cx)) + }) + .expect("failed to open window"); + }) + .detach(); +}); +``` + +`Root` coordinates window-level component facilities such as overlays and +notifications. Do not create a separate root for each page inside one window. +It also coordinates modal focus restoration, focus traps, tooltip/menu layers, +and window-scoped text selection. Bypassing it can produce behavior that looks +correct at rest but fails when overlays nest or focus changes quickly. + +## Understand GPUI's phases and contexts + +GPUI is retained state with declarative rendering. An entity survives across +frames; the element tree returned by `render` is a fresh description of the +current frame. Keep that distinction explicit. + +- `Context` mutates the current entity, creates listeners tied to it, + emits its events, and notifies its observers. +- `App` gives access to application globals and entity reads/updates without + implying ownership by the rendered element. +- `Window` owns focus, actions, input dispatch, element-keyed state, + measurement, and animation-frame requests for that window. +- layout, prepaint, and paint are later phases; use their hooks only when + resolved geometry is genuinely required. + +Never retain `&mut Window`, `&mut App`, or `&mut Context<_>` beyond the call in +which it is provided. Retain typed handles—`Entity`, `WeakEntity`, +`FocusHandle`, scroll handles, or domain IDs—instead. + +## Choose the right unit + +### Use `RenderOnce` for value-like elements + +Use a `RenderOnce`/`IntoElement` component when all inputs can be supplied by +the caller and the element does not need to retain application state between +frames. This is the normal choice for presentational wrappers and small +controls. + +```rust +#[derive(IntoElement)] +struct EmptyState { + title: SharedString, +} + +impl RenderOnce for EmptyState { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + div() + .v_flex() + .gap_2() + .items_center() + .text_color(cx.theme().muted_foreground) + .child(self.title) + } +} +``` + +### Use `Entity` for retained behavior + +Use an entity-backed `Render` view when behavior spans frames or needs +observation, subscriptions, focus, async work, history, measurement, or +incremental updates. Store entities in an owning view rather than recreating +them in `render`. + +```rust +struct SearchView { + query: Entity, +} + +impl SearchView { + fn new(window: &mut Window, cx: &mut Context) -> Self { + let query = cx.new(|cx| InputState::new(window, cx).placeholder("Search…")); + Self { query } + } +} +``` + +Do not turn every visual fragment into an entity. Entity boundaries have +lifecycle and coordination costs; use them where retained identity matters. + +### Elements, views, and behavior systems are different + +Do not force every component into one template. The ecosystem contains: + +- semantic elements such as Button, Checkbox, Link, and Tabs; +- compound behavior roots such as Dialog, Popover, Select, and Combobox; +- entity-backed systems such as Input, Table, Tree, Dock, and notifications; +- infrastructure such as positioning, virtualization, scrolling, focus traps, + motion, history, and measurement. + +An element may be internally complex and still be value-like to its caller. A +stateful system may expose render callbacks so applications own presentation +without reimplementing behavior. Choose the public seam from the behavior, +not from how many `div`s appear in its renderer. + +## State ownership + +Put each state in the narrowest owner that can keep it correct: + +- domain state belongs to a model or feature view; +- transient view state belongs to the view that renders it; +- reusable behavioral state belongs to the component state designed for it; +- tiny element-local state may use GPUI keyed element state; +- shared application services may be stored as GPUI globals. + +Prefer controlled values for ordinary selection and toggles: pass the current +value into the component, receive a requested change, update the owner, and +render again. A callback reports intent; it should not create a second hidden +source of truth. + +```rust +Checkbox::new("show-hidden") + .checked(self.show_hidden) + .label("Show hidden files") + .on_click(cx.listener(|this, checked, _, cx| { + this.show_hidden = *checked; + cx.notify(); + })) +``` + +Call `cx.notify()` after a mutation that changes rendering. Use `cx.emit(...)` +for a semantic event that an owner should handle, and `cx.subscribe(...)` or +`cx.observe(...)` when the lifetime should follow an entity. Keep returned +subscriptions alive when the API requires it. + +Do not notify merely because a value was read or derived. Avoid unconditional +notification from `render`; it schedules another render and can create a +permanent redraw loop. When several fields form one invariant, update them +together and notify once. A reusable state type that cannot receive a context +should make that limitation explicit and require its owner to emit/notify. + +### Avoid state feedback loops + +Text input, selection, filters, and controlled popups commonly have two paths: +an external owner updates the value, and user interaction requests a new value. +Do not send an owner-supplied value back through the user callback during sync. +Track the origin or compare coherent snapshots so each logical change is +reported once. Make callbacks re-entrancy-safe when a callback can synchronously +close, replace, or update the component that invoked it. + +## Stable identity + +An `ElementId` is part of behavior. It gives an element stable identity and keys +element-local or component state. A component may also use it as one input to +its own focus, measurement, or animation identity; focus and scrolling are +otherwise owned by their dedicated handles. + +- Use stable domain IDs for rows, tabs, tree nodes, and repeated controls. +- Namespace child IDs with their owning object when the same control repeats. +- Never derive identity from a translated label or a mutable list index when + items can be inserted or reordered. +- Do not generate a fresh random ID during `render`. + +```rust +Button::new(("delete-project", project.id)) + .danger() + .label("Delete") +``` + +A changed ID means a changed UI identity. Treat that reset as deliberate. + +The same rule applies to transition channels, overlay tokens, scroll handles, +and persistence IDs. If two independently retained behaviors share a key, they +can overwrite each other's state; if one behavior changes keys every frame, it +never accumulates state. + +## Rendering and composition + +Keep `render` declarative: read current state, derive presentation values, and +compose elements. Move domain operations, parsing, and non-trivial mutation to +named methods or services. + +```rust +impl Render for ProjectView { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .v_flex() + .size_full() + .child(self.render_toolbar(cx)) + .child(self.render_content(cx)) + } +} +``` + +Extract a render helper when it names a meaningful region and reduces the +amount of state a reader must hold at once. Extract a new component when the +region has its own reusable contract or retained lifecycle—not merely because +a builder chain is long. + +Use GPUI Component's fluent traits consistently (`Sizable`, `Disableable`, +`Selectable`, and component-specific builders). Prefer `.when(...)` and +`.when_some(...)` for small conditional refinements; use ordinary Rust control +flow when branches represent substantially different interfaces. + +Compose from the standard semantic component before building a custom surface. +Do not reproduce a menu, select, dropdown, or command palette from generic +`div`s merely to match one screenshot. Reusing the component preserves its +item geometry, focus transfer, keyboard navigation, selection, disabled state, +dismissal, and accessibility contract. If the standard component cannot +express a recurring valid pattern, improve its explicit API instead of styling +arbitrary descendants at each call site. + +Render callbacks supplied by application code should be side-effect-free. A +list item renderer, menu builder, or dock panel renderer may run whenever its +owner needs to measure or redraw. It must not perform a business operation, +append data, or register an unbounded subscription. + +## Behavior and presentation boundary + +The durable Base rule is: + +> Base owns reusable behavior and the geometry required to implement it. The +> presentation layer owns the product's visual language. + +“Headless” does not mean “one empty `div`.” Popup collision, keyboard +navigation, editing, virtualization, resize arithmetic, focus trapping, and +dock reconciliation require internal structure and state. Moving that work to +every caller would not create flexibility; it would duplicate fragile +behavior. + +Conversely, Base must not choose brand colors, typography, density, final +icons, component variants, or application composition. Expose presentation +through `Styled`, typed semantic-state styles, explicit parts, child slots, and +item renderers. Do not inspect arbitrary descendants to discover titles, +descriptions, or close buttons—make semantic parts explicit. + +## Theme and styling + +Read semantic values from the active theme and apply layout with GPUI's +`Styled` methods: + +```rust +div() + .bg(cx.theme().background) + .text_color(cx.theme().foreground) + .border_1() + .border_color(cx.theme().border) + .rounded(cx.theme().radius) +``` + +Rules: + +- do not hard-code product colors, corner radii, spacing, or control geometry; +- application code must not introduce raw hex, `rgb`/`rgba`, or `hsla`; read a + semantic color from `cx.theme()` or add the missing role to the product theme; +- application layout should use GPUI's rem-based scale helpers (`p_2()`, + `gap_3()`, `w_64()`, `text_sm()`) instead of direct `px(...)` values; +- use semantic tokens for meaning, not palette position; +- keep state-independent geometry in the ordinary builder chain; +- use GPUI `hover`, `active`, `focus`, and `focus_visible` modifiers for + runtime interaction states; +- use a component's semantic state styles for checked, selected, pressed, or + disabled appearance; +- keep popup ownership in explicit state so its trigger can render the open or + pressed appearance until dismissal; +- guard hover/active refinements when a disabled control must not react; +- keep component variants few and meaningful rather than adding a variant for + every call site. +- bind primary styling to the decision area's real default commit and Enter + action; do not derive it from action count, frequency, or toolbar position. +- keep Badge and Alert variants semantic and scarce. Ordinary metadata stays + neutral; do not map every enum case or section to a different color merely + because a variant exists. + +The effective precedence is instance style, active semantic value states, +disabled state, then GPUI runtime interaction refinements. Later layers only +replace fields they set. + +Prefer `Theme::semantic_tokens()` for new application-owned presentation. The +semantic token surface contains generic color roles plus radius, spacing, +typography, and shadow scales; it deliberately avoids component names. Legacy +component-specific theme values still exist for compatibility but should not +become the extension point for every application widget. + +There is one current ownership caveat: `Theme::spacing_tokens()` projects the +default scale, and `Theme::apply_semantic_tokens(...)` does not store custom +spacing or elevation scales. An application that customizes those scales must +retain its own `SemanticThemeTokens` (or narrower design-system state) and make +that state available to its components. Do not write a custom spacing snapshot +into the global theme and expect a later `cx.theme().semantic_tokens()` call to +return it. + +If code mutates the global GPUI Component theme directly, call +`Theme::sync_base(cx)` afterward so Base-owned scrollbars and resize handles +receive the new projection. `Theme::change(...)` performs this projection as +part of a complete theme change. + +An outward focus ring needs physical room. An ancestor with +`overflow_hidden()` clips it. Prefer layouts that leave room; if a product must +clip heavily, use the theme's focus-ring policy and retain the focused border +instead of silently hiding all keyboard focus. + +### Base font is the application zoom control + +`Root::render` calls `window.set_rem_size(cx.theme().font_size)`. Therefore the +theme's base font is not only body typography; it is the reference length for +the application's rem-based design scale. This deliberately follows the useful +part of Tailwind's model: named type, spacing, and size steps share one relative +base instead of becoming unrelated pixel constants. + +Change zoom by updating the base font and refreshing the window: + +```rust +Theme::global_mut(cx).font_size = px(18.); +Theme::sync_base(cx); +window.refresh(); +``` + +The base font itself is a pixel value because it anchors the scale. Descendant +application UI should normally use relative helpers—`text_sm()`, `gap_2()`, +`px_3()`, `h_8()`, `size_4()`—so type, whitespace, controls, and icons respond +together. A custom component that combines rem-based text with fixed-pixel +padding or icon geometry must document why that part should not zoom. + +Treat every direct `px(...)` and raw color constructor in application UI as a +review finding. Accept it only for a documented physical/platform boundary, +measured runtime geometry, raster/data color, or the theme/token definition +itself. Convenience and matching a screenshot are not valid exceptions. + +Anything cached from resolved layout must include `window.rem_size()` in its +invalidation key, directly or through a revision that changes with it. This +includes wrapped row heights, text shaping/layout, virtual-list measurement, +popup and dialog geometry, icon sizing derived from text, and custom canvas +metrics. The Command component's variable-height rows are an ecosystem example: +they remeasure when rem changes because the same fixed width wraps differently +at a larger base font. + +Do not confuse this application zoom with Dock panel zoom. Dock zoom is a +stateful layout operation that makes one tab group or tile fill the DockArea +while keeping the container chrome and the way back out. It must not modify the +window rem size. + +## Events, actions, and focus + +Use pointer callbacks for pointer-specific behavior. Use GPUI Actions for +commands that should support key bindings, menus, or dispatch from multiple +inputs. Keep action handlers close to the view that owns the command. + +Model one logical desktop command once. A toolbar Button, `DropdownMenu` item, +`ContextMenu` item, menu-bar item, and key binding should dispatch the same +Action or call the same owner method instead of copying five mutations. Derive +their label, icon, shortcut, and enabled state from one command policy where +practical, so the entry points cannot disagree. The menu owns navigation and +dismissal; the feature owner still owns whether the command is allowed and +what it does. + +Preserve semantic roles in the element choice. Use `Button` for commands even +when the desired treatment is quiet—select `outline`, `ghost`, or an icon +presentation instead of replacing it with `Link`. GPUI Component applications +reserve `Link` for targets opened by a browser or mail client, such as a URL, +web document, or email address. Use the relevant navigation component for an +in-app destination and `Button`/`Action` for a command. This is a product +convention, not a limitation of `gpui_base::Link`, whose `open_with` seam can +route a destination elsewhere. + +Only stop propagation when a nested interaction must prevent its parent from +handling the same event. Blanket propagation stops break menus, selection, +dragging, and window-level commands in ways that are difficult to diagnose. + +Make focus ownership explicit: + +- retain a `FocusHandle` in the entity that owns keyboard interaction; +- register key contexts and actions on the appropriate focused region; +- transfer focus when opening an overlay and restore it on dismissal; +- render a visible `focus_visible` state; +- do not request focus unconditionally from `render`. + +Attach a `key_context` and its `on_action` handlers to the same focused region. +Bindings are contextual: a registered Action without the intended focus path +is not a working keyboard interaction. Composite widgets should implement the +complete navigation model—arrow movement, Home/End or page movement where +appropriate, confirmation, cancellation, and Tab behavior—rather than a few +isolated shortcuts. + +Modal surfaces must trap focus and restore the previous valid focus target on +dismissal. Nested overlays dismiss from the top. Handle rapid close/open +sequences without restoring focus through an intermediate, already-closing +surface. + +## Async work and side effects + +Start async work from an event, lifecycle hook, or named method—not as an +unconditional side effect of `render`. Capture weak entities when work should +not keep a closed view alive. When the task completes, update state through the +GPUI context, handle the case where the entity or window no longer exists, and +notify once after the coherent state change. + +Represent async operations with explicit states such as idle, loading, loaded, +and failed. Preserve usable previous data during refresh when possible. Prevent +duplicate destructive submissions and surface recoverable errors in the UI; +do not rely on logs as user feedback. + +Use background executors for expensive parsing or computation, but keep GPUI +entity mutation on the appropriate application context. Results can arrive +after the request, document, view, or selection has changed; attach a revision +or identity and reject stale work rather than applying it to new state. + +## Layout, measurement, and scrolling + +Most UI should use GPUI layout rather than measuring itself. Measurement is a +deep behavior tool for popups, virtualization, editors, resize handles, charts, +and similar components whose correctness depends on resolved geometry. + +- Put measurement and geometry in the layer that owns the behavior. +- Observe bounds in prepaint only when ordinary layout cannot express the + relationship. +- Never mutate unrelated application state every prepaint. +- Treat measured data as frame- or revision-scoped; it can become stale after + typography, rem size, width, theme, or content changes. +- Centralize shared geometry such as popup flipping and viewport clamping so + every overlay follows the same edge policy. + +For alignment invariants, prefer construction over correction: sibling regions +should consume the same spacing token or shared inset instead of repeating +equivalent literals. Add geometry assertions or visual regression coverage for +critical repeated edges, columns, and gaps. Exercise more than the default +window: rem zoom and display scaling can turn fractional coordinates into a +one-physical-pixel drift even when the default screenshot looks aligned. + +Measure the resolved result when reviewing precision, but do not encode a +measured correction as a raw `px(...)` nudge. Trace the mismatch to duplicated +padding, nested insets, border ownership, font metrics, or rounding, then fix +the structural owner. + +Every scrollable region must have one owner. In flex layouts, apply +`min_w_0()` or `min_h_0()` to the flexible child that is allowed to shrink. +Avoid accidental nested scrolling; route wheel input to the intended axis and +preserve platform/wasm differences when an API is not portable. + +Attach `Scrollable` to the element that owns the full panel, editor, or window +viewport so its scrollbar resolves against the region edge. Put content inset +inside that scroll owner rather than wrapping the scroll owner in a padded +container. A scrollbar floating between content and the panel boundary usually +reveals the wrong scroll owner or padding on the wrong layer. + +## Lists, tables, and large data + +Use virtualization when data can grow beyond a small, bounded collection. Keep +row identity separate from visible position and avoid cloning the full data set +on every render. Let a stateful list or table own navigation, selection, scroll +coordination, and visible-range calculation while item renderers own row +presentation. + +Separate: + +- source data and domain IDs; +- filtering/sorting state; +- selection state; +- viewport/scroll state; +- row rendering. + +This keeps updates local and prevents the view tree from becoming the data +model. + +Virtualization is a behavioral contract, not just a performance switch. Item +measurement must be invalidated when width, typography, rem size, or row +content changes. Keyboard selection and scroll-to-item must operate in model +coordinates even when most elements do not exist in the current frame. + +## Public API design + +For reusable components: + +- constructors should establish valid defaults; +- builders take and return `Self` and use domain language; +- callbacks describe requested changes and include pointer events only when + modifiers or pointer details are meaningful; +- evolvable behavioral seams use private fields, builders for construction, + and readers for inspection; +- boolean readers use `is_` or `has_` where a same-named builder exists; +- non-boolean setters use `with_` when readers need the plain field name; +- explicit compound parts are preferable to inspecting arbitrary descendants; +- adding reusable behavior must not force a product-level visual choice. + +Private fields are the default for behavioral state that must evolve without +breaking callers. Public fields are appropriate for deliberately record-like +configuration, theme tokens, geometry, and serialized schemas when direct +construction is part of the contract and the compatibility cost is accepted. +Use `#[non_exhaustive]` when callers may inspect a record but should not depend +on exhaustive construction or matching. + +Keep public module paths stable while reorganizing internals: use a module seam +with deliberate re-exports so folders can change without forcing downstream +imports to change. Prefer platform control terminology and established project +naming over web-framework vocabulary. + +## Platform and capability boundaries + +Do not assume every native or web target supports the same facility. Window +decorations, accessibility bridges, system notifications, clipboard behavior, +scroll gestures, fonts, and timing can differ. Put platform-specific code +behind a narrow capability seam and define the fallback behavior. + +A platform branch must preserve the semantic contract even if presentation +differs. For example, a system notification may have different retraction +support, but the application still needs a coherent delivery state. Test both +the shared state machine and the platform adapter where possible. + +## File and naming conventions + +- Name views and entities after product concepts: `ProjectList`, + `ProjectEditor`, `SettingsState`. +- Name event handlers after intent: `confirm_delete`, `open_project`, + `on_query_changed`. +- Keep one main responsibility per module; split a file when state ownership or + lifecycle can no longer be understood without reading unrelated behavior. +- Keep component module, state, events, and focused tests together when they + change together. +- Document invariants and surprising lifecycle constraints; do not narrate + obvious builder calls. +- Use `rustfmt` and satisfy the workspace's Clippy rules. Avoid broad `allow` + attributes that conceal unrelated warnings. + +### Vocabulary is part of the API + +Use the same word for the same concept across components. Before naming a new +method, search GPUI, `gpui-base`, and GPUI Component for the established term; +prefer macOS/Windows control terminology where the ecosystem has no precedent. +Localized documentation preserves exact API identifiers and established UI +framework terms when translation would reduce precision. Format identifiers as +code, explain retained terms when needed, and do not mix languages merely to +make ordinary prose sound technical. + +| Concept | Naming pattern | Example | +| --- | --- | --- | +| Value-like rendered control | noun | `Button`, `Checkbox`, `Tab` | +| Retained behavioral model | `State` | `InputState`, `TableState` | +| Imperative shared reference | `Handle` | `DialogHandle`, scroll handle | +| Semantic notification | `Event` | `TableEvent`, `SelectEvent` | +| Keyboard command | verb or intent noun | `Confirm`, `Cancel`, `SelectNext` | +| Pluggable data/behavior owner | `Delegate` / `Provider` | `TableDelegate`, `CompletionProvider` | +| Application-supplied presentation | `render_` or `_renderer` | `render_item` | +| Construction | `new`, or a semantic constructor | `new`, `horizontal`, `vertical` | +| Fluent property | noun/adjective | `label`, `disabled`, `selected`, `placement` | +| General non-boolean replacement builder | `with_` | `with_size`, `with_mode` | +| In-place mutation | `set_` | `set_items`, `set_selected_index` | +| Boolean reader | `is_` / `has_` | `is_open`, `is_closable`, `has_selection` | +| Plain value reader | field noun | `placement`, `selected_value` | +| Callback registration | `on_` | `on_click`, `on_open_change` | +| Rendering a named region | `render_` | `render_toolbar`, `render_content` | + +For new APIs, fluent builders omit `set_` because they consume and return +`Self`; mutation through `&mut self` uses `set_`. Preserve established public +names when changing them would cause needless churn. Existing builder names +such as `set_position` are compatibility exceptions, not patterns for new APIs. + +A boolean reader is either `has_`, when the value holds something, or +`is_`, when it describes a state or a permission. Reach for the +adjective whenever the action has one: `is_closable` over `can_close`, +`is_zoomable` over `can_zoom`, `is_copyable` over `can_copy`. When the action +is a verb phrase with no adjective form, name the thing it needs instead: +`has_definition`, not `can_go_to_definition`. Do not add new `can_` readers. + +Boolean builders may use the field name (`disabled(bool)`) while their readers +use `is_disabled()`. For a public seam struct containing non-boolean fields, +use `with_item_ix(...)` for construction and `item_ix()` for reading so setter +and getter names never collide. Prefer `_ix` for new local or internal +zero-based indices, preserve established public terms such as `selected_index`, +and do not introduce `_idx`. If callers never construct the seam value, do not +publish a builder merely for symmetry. + +### Let the enclosing name carry the context + +A name is read inside something. A field is read inside its type and a parameter +inside its method, so neither repeats what encloses it: `with_item_ix(ix)`, not +`with_item_ix(item_ix)`. + +Keep one type's fields at the same level of abbreviation. A single field spelled +out in full becomes the odd one out, and a reader goes looking for the +distinction that made it different. Because a builder is named `with_`, +shortening a field shortens its builder with it and the pair stays matched. + +Shorten only where the enclosing name really does disambiguate. When a short +form is also the established term for a *different* quantity elsewhere in the +ecosystem, say which one you mean in the doc comment rather than lengthening the +identifier — the doc is read at the call, and it can explain what a longer name +could only hint at. + +### Use precise domain words + +- **selected** is persistent membership or the active item; **focused** is the + current keyboard target; **hovered** is pointer presence; **confirmed** is an + activation result. Never use them interchangeably. +- **open/close** describes an overlay or disclosure state; **show/hide** is for + transient presentation requests; **expand/collapse** describes structure. +- **disabled** prevents interaction; **read-only** permits navigation and + selection but prevents editing; **loading** prevents duplicate work while an + operation is pending. +- **index** is a current positional coordinate; **id** is stable identity; + `IndexPath` represents hierarchical position. Do not persist or key + reorderable data by index. +- **value** is controlled domain data; **presentation** is a read-only snapshot + prepared for rendering; **state** is retained behavior. +- **placement** is a side or anchor policy; **position** is resolved geometry. +- **size** is a semantic control tier; **width/height/bounds** are geometry. +- **child/children** follows GPUI composition; named slots such as `header`, + `footer`, `trigger`, and `content` carry additional semantics. + +Avoid vague public names such as `data`, `item2`, `handle_action`, `update_ui`, +`process`, `manager`, or `config` when a narrower domain term exists. `Manager` +is appropriate only when a type truly coordinates a collection or lifecycle, +as `ToastManager` does. + +### Type and module style + +- Rust types and Actions use `UpperCamelCase`; modules, functions, methods, + fields, and local variables use `snake_case`; constants use + `SCREAMING_SNAKE_CASE`. +- A module named after a component owns its public seam. Internal folders may + split state, element, geometry, platform adapter, and tests without leaking + those folder names into imports. +- Use singular module names for one component concept and established + ecosystem names for families (`input`, `table`, `dock`). +- Suffix type-erased wrappers with `Any` only when they erase a real type + boundary, such as `AnyInputState` or `AnyElement`. +- Suffix identifiers with `Id`, zero-based indices with `ix`, and collections + with meaningful plurals. Do not alternate `idx`, `index`, and `ix` in one + subsystem. +- Name predicates positively when possible. A positive `enabled`/`visible` + contract is easier to compose than multiple negatives, but preserve + established API terms such as `disabled` where they match control semantics. + +### Callback and event wording + +Use `on_click` only for a genuine click-level contract. A controlled semantic +primitive in Base should prefer `on_change(next_value, ...)`; a styled +compatibility component may retain `on_click` when pointer details or existing +API expectations matter. Do not invent a `ClickEvent` for a model-driven +change. + +Name before/after lifecycle hooks precisely. `on_will_change` can veto or +prepare; `on_change` observes a requested/current value contract; `on_confirm` +commits a choice; `on_dismiss` closes a transient surface. Document whether a +callback runs before internal state changes, after them, or instead of them, +and whether it may synchronously re-enter the component. + +### Documentation and copy style + +Public docs should begin with what a type does and who owns its state. Examples +must use current, compilable APIs and show stable IDs. Document defaults, +platform limitations, focus behavior, callback ordering, and any requirement +to call `notify`, `emit`, or a theme synchronization method. + +Follow the [interface-language rules](./design-guides.md#interface-language) for +labels, commands, confirmation dialogs, capitalization, and ellipses. Keep one +canonical term for each domain object, command, and state. + +Translation keys describe stable intent (`dialog.delete_project.title`), not a +source-language sentence or a screen coordinate. Never assemble a sentence +from translated fragments or reuse one key for meanings that happen to share +the same English text. + +Localize intent, not syntax. Give every locale control over word order, +pluralization, punctuation, and the amount of context it needs. Review strings +inside the component and with realistic data. Tests or linting should catch +missing keys, unintended CJK text in English resources, three-dot ellipses, +unreviewed ALL CAPS, and inconsistent fixed terms; human review still decides +whether repetition is justified by context. Verify every string inside its +component with realistic content, text expansion, and application zoom. + +## Testing strategy + +Test at the lowest layer that can prove the behavior: + +1. pure tests for state transitions, geometry, parsing, and ordering; +2. GPUI context tests for entities, events, and subscriptions; +3. `VisualTestContext` interaction tests for focus, keyboard, pointer, layout, + and rendered state; +4. example or application smoke tests for complete workflows. + +For an interactive component, cover the semantic contract rather than its +implementation details: pointer and keyboard activation, controlled value +changes, disabled behavior, focus movement, event count/order, stable identity, +and important empty or failure states. Add a regression test before fixing a +bug whenever the failure can be reproduced deterministically. + +For UI behavior that depends on the real window system, test through the +accessibility tree by role, label, value, enabled state, focus, and selection. +Re-read the tree after every state-changing action because element indexes are +snapshots. Use screenshots for visual facts the semantic tree cannot express; +use coordinate input only as a fallback. Report automated and manual evidence +separately. + +## Performance rules + +- Do not mutate state or notify unconditionally in `render`. +- Avoid rebuilding entities, subscriptions, focus handles, and expensive data + structures per frame. +- Notify the narrowest owning entity after a coherent state change. +- Virtualize long collections and render only the visible range. +- Avoid cloning large strings or collections solely to satisfy a closure; + capture stable handles or shared data. +- Measure before adding caches. A cache must have a clear invalidation owner. +- Keep animation work bounded and honor reduced motion. + +## Common failure modes + +Avoid these patterns: + +- one entity containing the entire application's unrelated state; +- business logic and network requests embedded in a long `render` method; +- random or index-based `ElementId` values for reorderable content; +- literal colors and radii that break custom themes; +- custom clickable `div`s where a semantic component already supplies focus, + keyboard, disabled, and accessibility behavior; +- duplicated local state that drifts from a controlled model value; +- `cx.notify()` loops caused by mutation during every render; +- nested scroll containers without explicit ownership; +- a new component variant for a one-off screen; +- confirmation dialogs for reversible, low-risk actions; +- tests that call internal methods but never exercise keyboard or pointer + behavior. + +## Rules for coding agents + +Before editing, an agent must read the nearest implementation, its tests, the +re-export seam, and the relevant component documentation. It must search the +current source for signatures instead of translating a React, CSS, or old GPUI +example by analogy. + +For each change, the agent should be able to name: + +1. the behavior owner and presentation owner; +2. the retained identity and state lifecycle; +3. the pointer, keyboard, focus, and accessibility contract; +4. the layout and overflow owner; +5. the theme tokens and intentional exceptions; +6. the test that would fail if the behavior regressed. + +Generated code must be reviewed and tested by a person. “Compiles” is not a UI +quality bar, and a broad refactor that merely makes generated code look tidy is +not a substitute for matching the repository's architecture. + +## Implementation checklist + +Before opening a change for review, confirm that: + +- state and side-effect ownership are explicit; +- `RenderOnce` versus `Entity` is chosen deliberately; +- repeated elements have stable domain-based IDs; +- theme tokens and component sizes replace isolated visual literals; +- keyboard actions, focus, disabled state, and overlays work together; +- loading, empty, error, and cancellation paths are represented; +- long data sets use an appropriate virtualized component; +- public API additions preserve dependency direction and encapsulation; +- tests prove behavior at the appropriate layer; +- formatting, Clippy, targeted tests, and relevant examples pass. + +See [Getting Started](./getting-started.md) for application setup and the +component pages for current API details. diff --git a/.claude/skills/gpui-component/references/design-guides.md b/.claude/skills/gpui-component/references/design-guides.md new file mode 100644 index 0000000..aca4413 --- /dev/null +++ b/.claude/skills/gpui-component/references/design-guides.md @@ -0,0 +1,890 @@ +--- +title: Design Guides +description: Product and interaction design guidance for GPUI Component applications +order: -2.1 +--- + +# Design Guides + +Use this guide before choosing components or writing layout code. It records +the product judgment accumulated through years of GPUI Component desktop work: +an interface should feel native, restrained, precise, and understandable +without guesswork. + +This is a normative guide. **Must** identifies a correctness or ecosystem +constraint, **should** is the default that needs a concrete reason to override, +and **may** is an optional technique. Component API documentation remains the +authority for individual methods. + +The rules build on behavior in `gpui-base`, the GPUI Component theme and +component system, and familiar desktop interaction. Shadcn contributes useful +methods—open code, composition, and dependable defaults—but does not determine +how a GPUI application should look. When influences conflict, preserve GPUI's +lifecycle constraints and the interaction people already understand. + +## Design thesis + +Build interfaces that feel native, quiet, and precise. Let content, hierarchy, +and interaction carry the experience; decoration should support them rather +than compete with them. + +1. **Clarity before personality.** Make the primary task and next action clear + before adding brand expression. +2. **Composition before invention.** Start with established components and + compose them into product-specific workflows. Create a new primitive only + when its behavior is genuinely new. +3. **Tokens before values.** Colors, radii, typography, and spacing should form + a system. Avoid isolated literals that cannot respond to themes. +4. **Desktop before web convention.** Preserve keyboard access, window chrome, + menus, dense data views, resizable regions, and persistent navigation where + the task benefits from them. +5. **State must be visible.** Hover, focus, selection, disabled, loading, + validation, and destructive states need distinct and consistent treatment. + +## Learning from Shadcn + +Shadcn's most useful contribution is not a particular border color. It is a +way of building a system: + +- own the top layer of the interface instead of fighting a sealed abstraction; +- compose small, predictable parts into product-specific components; +- provide defaults that already form one visual language; +- keep the code and composition legible to both people and AI; +- separate behavior primitives from the styled layer. + +GPUI Component applies those ideas through a Rust library and the split between +`gpui-base` and `gpui-component`. Applications normally compose or wrap the +published components; contributors move genuinely reusable behavior into Base +and keep visual policy above it. + +Do not copy these web assumptions blindly: + +| Web habit | Native GPUI default | +| --- | --- | +| Pointing-hand cursor on every button | Default arrow cursor; pointing hand for links | +| Page navigation as the main structure | Persistent windows, panes, sidebars, tabs, and menus | +| Browser focus and scrolling as a fallback | Explicit focus ownership and region-owned scrolling | +| Mobile-first single column | Resizable desktop shell with a defined minimum window size | +| Hover-revealed critical actions | Keyboard- and pointer-reachable actions that do not depend on hover | +| A row of hover-only icon buttons | A visible primary action plus `DropdownMenu` or `ContextMenu` for secondary commands | +| Link-styled text for application commands | `Button`, `outline`, or `ghost`; Link only for URLs, web resources, or email addresses | +| Large touch density everywhere | Medium density by default; compact only where information work benefits | +| CSS overrides across descendants | Typed builders, semantic parts, and application composition | + +## Start from the task + +Before drawing a screen, write down: + +- the user's primary task; +- the object being viewed or changed; +- the actions that must remain immediately available; +- the information required to make a decision; +- empty, loading, error, offline, read-only, and permission-denied states; +- the keyboard path through the workflow. + +Organize the window around those answers. Do not begin with a dashboard grid or +a component catalogue. A good desktop interface exposes the user's mental +model: documents, accounts, projects, messages, settings, or another stable +object—not the internal service architecture. + +Design the primary task before distributing controls. Its visual weight, +location, and information depth should match its importance to the product. A +core result must not be reduced to a small count, an icon in a corner, or a +weak footer action while secondary content consumes the page. When a result set +is the product's main value, consider a summary region or card that exposes the +count, representative results, meaningful state, and a clear next action. + +For every proposed action, name its visible object, current state, scope, and +result. If the interface does not show or clearly imply those things, the +action is premature. Do not expose a capability merely because the backend has +it; first design how the object enters the user's mental model. + +## Visual language + +### Hierarchy + +Prefer a small number of clear levels: + +- **window or page title** identifies the current object or workspace; +- **section title** separates meaningful regions; +- **body text** carries the work; +- **muted text** provides secondary metadata and help; +- **labels** identify controls and values. + +Use size, weight, spacing, and separators before adding color or containers. +Avoid nesting cards inside cards: most desktop regions need only a background, +a hairline boundary, and intentional spacing. + +Evaluate hierarchy across the whole feature, not component by component. Hide +accent color and decoration during review: the primary task, current selection, +result summary, and next action should still be obvious from structure. A +screen that contains individually plausible controls can still fail when they +do not form one reading order and one decision path. + +Treat emphasis as a limited budget. A local surface needs one clear focal point, +not a field of competing highlights. If everything is colored, badged, bold, +boxed, or promoted to an alert, nothing reads as important. Establish priority +with structure and proximity first; spend stronger color and components only +where a distinction changes what the user notices or does. + +### Color and themes + +Read colors from `cx.theme()` and use them by semantic role: + +- `background` and `foreground` for the main surface and text; +- `group_box`, `popover`, `sidebar`, and their foreground tokens for their + named surfaces; +- `muted` and `muted_foreground` for supporting information; +- `primary` for the principal action or selection emphasis; +- `danger`, `warning`, `success`, and `info` only for their meanings; +- `border`, `input`, and focus-ring tokens for structure and interaction. + +Do not use a semantic status color as decoration. Do not encode meaning by +color alone. Verify every custom surface in light and dark themes and with +custom theme values; never assume that foreground is black or background is +white. + +Use Badge for a short state, count, or classification that benefits from rapid +scanning—not for every label, metadata value, filter, or section title. Keep +most badges neutral; reserve semantic variants for states that truly carry +success, warning, danger, or informational meaning. A row of multicolored +badges is usually a missing hierarchy or grouping decision. + +Application UI should not contain raw hex, `rgb`/`rgba`, or `hsla` colors. +Resolve colors from `cx.theme()` by semantic role. If the required role does +not exist, define it in the product's theme/token layer rather than embedding a +palette value at the call site. Raw colors belong only inside theme definitions +or in audited data/raster content whose color is itself the data. + +### Radius, spacing, and density + +Derive corner radii from the active theme. This preserves a product's ability +to become square or more rounded as one coherent system. Use `radius_full()` +for circles and pills rather than a literal maximum radius. + +Use a compact spacing scale and repeat it. Related label/control pairs should +be closer than separate groups; separate groups should be closer than separate +sections. Prefer component sizes (`xsmall`, `small`, default medium, `large`) +over one-off heights. Use compact variants for toolbars and data-dense screens, +not to squeeze an unclear layout into less space. + +The shared semantic scale is intentionally small: spacing progresses through +roughly 2, 4, 8, 12, 16, 24, and 32 pixels, while typography stays near 12, +14, 16, 18, and 20 pixels. Treat these as relationships rather than permission +to scatter their current values through feature code. GPUI Component currently +projects a fixed default `SpacingTokens` scale from its global `Theme`; unlike +colors and radii, `Theme::apply_semantic_tokens` does not persist a custom +spacing scale. An application that needs different spacing must own that full +token snapshot and use it consistently in its application components. + +### Spatial grammar + +Spacing expresses relationship. Choose a gap from the semantic scale by asking +what the two things mean to each other: + +| Relationship | Typical token | Current scale | Examples | +| --- | --- | --- | --- | +| Optical correction | `xxs` | 2 px | icon baseline, compact separator | +| Parts of one control | `xs` | 4 px | menu icon/label, title/description | +| Closely related controls | `sm` | 8 px | button icon/label, dialog actions | +| One content group | `md` | 12 px | notification columns, compact form rows | +| Separate groups in one section | `lg` | 16 px | panel padding, form groups | +| Separate sections | `xl` | 24 px | major blocks in a page or inspector | +| Major region boundary | `xxl` | 32 px | empty-state breathing room, page bands | + +These values describe the current default scale, not literals to repeat. Use +`cx.theme().spacing_tokens()` or the corresponding GPUI scale helpers for the +ecosystem default. A product-owned scale should preserve the ordering and +relationships and must be passed through the application's own design-system +context rather than assumed to persist in the global GPUI Component theme. + +Use these rules when resolving horizontal and vertical space: + +1. **Inside before outside.** A component's padding belongs to the component; + the gap between components belongs to their parent. +2. **Vertical rhythm shows grouping.** The gap between a title and its + description is smaller than the gap from that description to the next + section. Equal gaps imply equal relationships. +3. **Horizontal space supports scanning.** Repeated rows keep icons, labels, + values, badges, and trailing actions on stable columns. +4. **Leading and trailing are semantic.** Think in reading-order edges even + when the current API uses left/right; this keeps future RTL adaptation + possible. +5. **Do not double padding.** A card placed in an already padded panel should + not automatically add another full panel inset. +6. **Use optical alignment sparingly.** A one- or two-pixel correction is valid + for icon or glyph geometry, but document why it differs from the scale. + +Common compositions in the current system illustrate the relationships: + +- button contents use 4 px at small sizes and 8 px at normal sizes; +- dialog headers and footers use an 8 px internal gap; +- compact list and menu rows use 4 px vertical and 8–12 px horizontal padding; +- sheet headers use about 16 px leading and 12 px trailing space, leaving room + for a close affordance, while footers use 16 px horizontal and 12 px vertical + space; +- notifications use 16 px horizontal padding and a 12 px column gap because + icon, message, and action are distinct groups. + +Do not treat these as copy-and-paste recipes for every surface. They reveal the +system: controls are tighter internally, rows optimize scanning, and +containers spend more space at their boundary than between their contents. + +### Proportion and layer hierarchy + +Start with content requirements, then set proportions. Avoid arbitrary halves +when one pane has a clearly different role. + +- A navigation sidebar should be wide enough for stable labels but visibly + subordinate to the work area. Give it a minimum, preferred, and maximum + width rather than a percentage alone. +- In master–detail layouts, let the collection remain scannable and give the + detail pane the surplus. A roughly one-third/two-thirds starting point is + often useful, but content constraints are authoritative. +- Inspectors and auxiliary sheets should not cover the primary object by + default. They should be resizable or dismissible when their content grows. +- Dialog width comes from the decision: short confirmation, medium form, or a + dedicated window/page for complex work. Do not enlarge a dialog simply to + create whitespace. +- Reserve the strongest elevation for the topmost decision layer. Within a + layer, use background and hairlines—not successively larger shadows—to show + hierarchy. + +Define three size constraints for every major region: the minimum at which its +task still works, a comfortable default, and how it consumes surplus. Persist +user-controlled splits when they represent workflow preference, and clamp +restored values against the current window. + +### Alignment details + +Alignment is a structural system, not a final polish pass. Establish a small +set of alignment spines for each surface: shared leading and trailing edges, +text baselines, center lines, and fixed functional lanes. Elements at the same +level should attach to the same spine from top to bottom or leading to trailing, +even when they are different component types. + +Alignment spines across a desktop surface +Alignment spines across a desktop surface + +Vertical red lines sit beside shared edges or control centers for content, +status, time, and trailing actions. Horizontal lines sit beneath text baselines +or pass through a row center to show bottom and vertical-center alignment. The +compact comparison isolates a one-rendered-pixel drift that must be corrected +at its structural owner. + +- Give sibling regions a shared content inset. A heading, toolbar, list row, + empty state, and footer that describe the same level should not each invent a + slightly different leading edge. +- Repeat column geometry through the whole region. Headers, rows, summaries, + loading states, and inline editors should reserve the same lanes for identity, + metadata, status, numbers, and actions. +- Align related controls across rows and sections. Form labels, fields, + descriptions, and validation messages should reveal a stable vertical grid + when the page is scanned from top to bottom. +- Keep horizontal bands coherent. Items sharing a toolbar, title bar, status + bar, or row should use one baseline or center line instead of individually + tuned offsets. +- Introduce indentation only for real hierarchy, containment, or disclosure. + Decorative indentation makes siblings look subordinate and breaks the + surface's reading line. +- When a nested level ends, return exactly to the parent spine. Do not let + accumulated padding drift across nested containers. +- Preserve the spine through optional content. Missing icons, badges, + descriptions, or trailing actions must not move the remaining labels; use + intentional slots or lanes when cross-row comparison matters. +- Align major regions with one another where their hierarchy matches. Sidebar + headers, content titles, split panes, toolbars, and bottom bars need not share + every coordinate, but coincident levels should form visible continuous lines. + +Not every edge should align. A child can indent, a primary value can lead its +supporting metadata, and a destructive decision can gain separation. Such +exceptions must communicate hierarchy or meaning; they must not result from +uncoordinated component padding. Start with the shared spine, then make the +exception explicit. + +Treat exact alignment and repeated gaps as quality invariants. When two edges +or spaces are intended to be equal, a one-rendered-pixel difference is a defect, +not an acceptable optical approximation. Inspect resolved bounds with a +measurement tool at representative window sizes, zoom levels, and display scale +factors. Compare coordinates and distances; do not approve alignment only from +a casual screenshot. + +The rendered-pixel tolerance is a verification rule, not permission to patch +the code with raw pixel offsets. Equal relationships should resolve from the +same `rem` helper, spacing token, grid definition, or shared component inset. +Fix the common owner when they differ. Account for fractional layout and device +rounding so intended spines land on the same physical pixel instead of drifting +at particular zoom levels. + +- Align text by baselines, not by bounding-box centers, when mixed sizes share + a row. +- Center icons in a fixed slot so labels do not move when icons differ in + intrinsic width. +- Right-align comparable numbers; left-align prose and identifiers unless the + locale requires otherwise. +- Keep trailing row actions and disclosure indicators in fixed-width lanes. +- Align form controls by their interactive frame, not by help text below them. +- Use `justify_between` only when the two sides truly own opposite edges; it + should not disguise missing structure in the middle. +- Hairlines belong on the boundary owner. Two adjacent regions must not each + draw the same separator. +- A scrollbar belongs to the region that scrolls and sits against that panel, + editor, or window's trailing edge. Content padding may inset text and rows; + it must not pull the scrollbar into the middle of the surface. Reserve a + deliberate scrollbar gutter when content needs clearance. + +### Density tiers + +Medium is the ecosystem default. Change density for the whole local context, +not one isolated control: + +- **comfortable / large:** onboarding, sparse forms, prominent decisions; +- **standard / medium:** most application chrome and workflows; +- **compact / small:** toolbars, menus, tables, and repeated professional data; +- **extra compact / xsmall:** exceptional high-density utilities, never the + automatic choice for an entire application. + +The current controls demonstrate a bounded scale rather than arbitrary sizing: +buttons commonly move through approximately 20, 24, and 32 px frames; input +and data controls may extend to about 44 px at large size; table rows use about +26, 30, 32, and 40 px. Use the component's `Size` API so typography, icon, +padding, and hit target change together. A custom height that changes only the +outer box is usually incomplete. + +### Zoom, base font, and `rem` + +A well-designed `rem` system preserves hierarchy while the interface zooms. +Zoom is successful when the relationship between title and body, control and +icon, inner and outer spacing, primary and secondary regions still feels the +same at every scale—not merely when every object becomes larger. + +GPUI Component adopts the relative-scale idea familiar from Tailwind. The +theme's base `font_size` becomes the window's `rem` through `Root`, and GPUI +scale helpers such as `text_sm()`, `gap_2()`, `p_4()`, `h_8()`, and `size_4()` +resolve against it. This gives typography, spacing, controls, and icons one +shared zoom axis. + +Design in ratios: + +- type steps keep the same hierarchy around the base body size; +- spacing steps keep the same grouping relationships around the type; +- control frames, icons, and hit targets scale with their labels; +- pane minima and comfortable widths account for the scaled content; +- corner radii and focus treatment remain optically consistent with the + control frame. + +Do not implement zoom by changing text size alone. A larger label inside a +fixed-height button, a larger document inside fixed pane minima, or larger rows +inside a stale virtual-list measurement destroys the original rhythm and can +clip content. Conversely, multiplying every physical pixel—including +hairlines—can make the interface visually heavy. + +As a rule, application layout should not call `px(...)` directly. Use GPUI's +rem-based scale helpers (`p_2`, `gap_3`, `w_64`, `text_sm`, and related +builders) or semantic component sizes. Use fixed pixels only when the value +represents a physical or raster boundary: +a one-device-pixel hairline, platform window inset, bitmap dimension, minimum +hit-test tolerance, or geometry that must match an external surface. These are +audited, documented exceptions. Product spacing, typography, icon size, and +ordinary control geometry stay on the relative scale. + +Test interface zoom at several base-font values, not just the default. Verify +hierarchy, wrapping, truncation, minimum window size, pane resizing, focus-ring +clearance, popup placement, and virtualized row measurement. Also distinguish +interface zoom from Dock's panel zoom: Dock zoom makes one container fill its +area while retaining its chrome; it does not change `rem` or application scale. + +### Surfaces and elevation + +Use elevation to explain stacking, not importance. The base window surface is +flat; separators and background contrast define its regions. Popovers, menus, +dialogs, and notifications may use progressively stronger shadows because they +sit above other content. Do not put a shadow on every card. + +All surfaces of the same kind should share one treatment. GPUI Component, for +example, deliberately gives popup families one themed popover surface so +Popover, Select, Combobox, DatePicker, and menus do not drift apart. When an +application invents another anchored surface, reuse that semantic treatment +instead of approximating it with unrelated border and shadow literals. + +### Typography and icons + +Use the platform UI font for interface text and monospace only for code, +identifiers, shortcuts, and aligned numeric data. Keep body text readable and +avoid excessive uppercase or letter spacing, especially for CJK text. + +Use one icon family in a product. Icons supplement labels; they should not +replace unfamiliar actions with guesswork. Icon-only buttons require a tooltip +and an accessible name. Use filled or colored icons to communicate a state, +not merely to make a toolbar lively. + +## Layout patterns + +### Choose a stable shell + +Most applications should use one of these shells: + +- **single workspace:** toolbar or title bar above one primary view; +- **sidebar workspace:** persistent navigation beside a changing detail view; +- **master–detail:** resizable collection and detail panes; +- **document workspace:** tabs or a dock area for multiple long-lived objects; +- **utility window:** one focused task with a short, fixed action path. + +Keep global navigation stable while content changes. Give the primary work area +the remaining space with `flex_1()` and `min_w_0()` / `min_h_0()` where +overflowing children must shrink. Use `Scrollable`, `VirtualList`, `Table`, or +`DockArea` for their intended behavior instead of rebuilding scrolling or pane +management from nested `div`s. + +### Responsive desktop windows + +Desktop does not mean fixed-size. Decide what happens as a window narrows: + +1. preserve the primary task; +2. allow resizable regions to reach a documented minimum; +3. collapse secondary labels or inspectors; +4. move low-frequency actions into a menu; +5. scroll only the region whose content actually overflows. + +Do not hide an action without providing another path to it. Avoid making the +entire window scroll when only a list or document body should scroll. + +GPUI flex layouts have the same intrinsic-size pressure found in other layout +systems: a `flex_1()` child may still refuse to shrink around long content. +Design and implementation must agree on which panes may shrink, truncate, wrap, +or scroll. A clipped region also clips an outward focus ring; never trade away +keyboard visibility merely to simplify overflow. + +### Forms and settings + +Use a visible label for each field and place help or validation next to the +field it describes. Align related fields, but do not force long labels into a +narrow fixed column. Use the appropriate control: `Checkbox` for independent +choices, `RadioGroup` for a small visible set, `Select` for a longer set, and +`Switch` for a setting that takes effect immediately. + +Disable submission while an operation is in flight, keep the user's input, and +show the result near the action. Reserve dialogs for short, focused decisions; +use a full page or sheet for workflows that need exploration or many fields. + +## Components and composition + +Follow the Shadcn principle that components are building material rather than a +sealed design system. GPUI Component supplies coherent defaults, while the +application owns composition and product semantics. + +- Use component variants by meaning. Primary is reserved for the explicit + default commit in a decision area—normally the action invoked by Enter. A + lone, frequent, or desirable action is not automatically primary. An `Add` + command in a management toolbar normally uses a default Button; a form's + default `Create` commit may use primary. Use `danger` for destructive + commitment and `ghost` for quiet toolbar actions. +- Prefer explicit compound parts and render callbacks over styling arbitrary + descendants. +- Keep a repeated pattern consistent across the product. Wrap it in an + application component when it carries domain language or policy. +- Use the standard component for its semantic role. A menu, dropdown menu, + popover, select, and command palette are not interchangeable boxes; each owns + different selection, focus, keyboard, dismissal, and layout contracts. +- Preserve the component family's geometry. Menu rows share vertical and + horizontal padding, height, icon and checkmark slots, separators, radius, and + state treatment. Do not imitate one menu with a custom popup whose spacing + only approximates the system. +- Do not wrap a library component merely to rename every method or freeze all + of its capabilities. +- Move reusable behavior without product styling to `gpui-base`; keep themed, + opinionated presentation in GPUI Component or the application. + +## Interaction states + +### Make the result understandable before the click + +A control should predict its result. Use familiar desktop controls and +placement so people can act without learning the interface first. Its label +names the action and object, its state shows availability, and its feedback +confirms the same outcome. + +Do not label a Button `Save` if it opens a configuration flow, or `Delete` if +it only removes an item from a group. Name the scope when context does not make +it clear. Respond immediately to activation, prevent duplicate submission +during longer work, and show the result near the object that changed. Add a +success message only when the result itself is not visible. + +Every interactive control should be designed for: + +| State | Design requirement | +| --- | --- | +| Rest | Clear affordance without visual noise | +| Hover | Subtle pointer feedback, never the only cue | +| Pressed | Immediate press feedback | +| Open / pressed | Persistent feedback while an attached popup is open | +| Focus visible | High-contrast keyboard focus ring | +| Selected / checked | Persistent state distinct from hover | +| Disabled | Lower emphasis and no misleading hover/pressed response | +| Loading | Preserve context, prevent duplicate action, explain long waits | +| Error | State what happened and how to recover | + +Use GPUI's focus system and Actions for commands that should work from the +keyboard. Match familiar desktop shortcuts, expose shortcuts in menus or +tooltips, and keep focus in a logical place after opening or dismissing an +overlay. + +Selection is part of the information model, not optional polish. Tabs, +segmented choices, selectable rows, filters, and navigation destinations must +show a persistent selected state. A Button that owns a dropdown must remain +visibly pressed or open until the popup closes; hover alone cannot explain the +relationship between trigger and surface. + +For destructive actions, distinguish between reversible and irreversible work. +Prefer undo or a temporary notification for reversible changes. Use an +`AlertDialog` when the consequence is serious and cannot be undone; name the +specific object and consequence in the confirmation copy. + +### Pointer conventions + +Use the default arrow cursor for buttons, checkboxes, menu items, tabs, and +other native controls. Use a pointing hand for links and content that behaves +as a link. Use text, resize, grab, and prohibited cursors only when they +describe the active manipulation. A cursor reinforces an affordance; it does +not replace the control's visible state or accessible role. + +Keep hover effects modest because keyboard and accessibility interaction has no +hover. Do not reveal the only copy of a destructive or essential action on +hover. Contextual row actions may become quieter at rest if the same commands +remain available through selection, keyboard, or a context menu. + +### Prefer desktop command surfaces over hover toolbars + +Use command frequency and scope to choose where an action lives: + +- keep the primary or frequent action visible as a labeled Button or familiar + toolbar control; +- put secondary actions for the current region behind a visible + `DropdownMenu` trigger; +- put commands that act on the object under the pointer in a `ContextMenu`; +- expose the same important command through an Action/key binding when it has a + natural keyboard form; +- use a hover-revealed icon only as a shortcut to a command that remains + reachable elsewhere. + +This is more than a visual preference. GPUI Component's menu system already +owns directional keyboard navigation, confirmation and cancellation, disabled +items, separators, submenus, shortcut presentation, focus transfer and +restoration, and nested-menu dismissal. A custom strip of hover buttons must +rebuild those behaviors and is invisible to keyboard-only and many assistive +technology workflows. + +Choose `DropdownMenu` when users need a visible indication that more commands +exist—for example a toolbar overflow, document actions, or account menu. Choose +`ContextMenu` for selection- or object-scoped commands such as rename, +duplicate, reveal, or remove. The context menu must not be the only way to +perform an essential command; provide a menu-bar, toolbar, keyboard, or detail +view path as appropriate. + +Do not put every action into a menu to make a screen look minimal. Discovery +and speed matter: the main action stays visible, dangerous items remain clearly +labeled and separated, and a menu item should use the same verb, icon, shortcut, +enabled state, and result everywhere it appears. + +### Button means application action; Link means external resource + +Use a Button when activation changes application state, confirms a decision, +opens a tool, submits data, or runs a command. Choose its treatment by local +hierarchy: + +- primary Button for the one emphasized commitment in a decision area; +- default Button for ordinary visible actions; +- outline Button when an action needs a clear boundary with less emphasis; +- ghost Button for familiar, low-emphasis toolbar and inline actions; +- icon Button only for a well-known symbol, with an accessible name and tooltip. + +Do not assign primary because a Button is the only action on screen, because it +is placed at the top right, or because the team wants more clicks. Primary +communicates default commitment and keyboard behavior. If activation is merely +an ordinary command such as adding an item, opening a tool, or refreshing a +view, use a default, outline, or ghost Button according to its local hierarchy. + +Use an underlined Link only for an external resource target: a URL, web page, +online documentation, or email address. It uses the pointing-hand cursor +because its contract is leaving the current application context for that +resource. Do not use Link styling to make a functional command look quiet. A +link-shaped Delete, Save, Refresh, Add, Open-menu, or in-app navigation action +hides the control's affordance and exposes the wrong accessibility role. + +“View” does not make an in-app destination a Link. A full report, analysis, +details panel, or local record still opens through a Button, row, card, tab, or +disclosure control. Use concise context-aware labels such as `Full analysis` +when the containing card already establishes what opens; reserve underlining +for a resource that actually opens in a browser or mail client. + +All internal navigation—sidebar rows, tabs, breadcrumbs, list items, opening a +local view, or switching workspaces—must use the corresponding native component +or a Button/Action. Visual emphasis is chosen through Button variant or the +navigation component's selected state, never by lying about semantics. + +## Feedback and overlays + +Choose the smallest surface that fits the decision: + +- tooltip: a short explanation or shortcut; +- popover: contextual controls that do not interrupt the task; +- menu: a compact list of actions; +- notification: asynchronous status that does not require a decision; +- dialog: a focused decision or short form; +- alert dialog: explicit confirmation of a consequential action; +- sheet: supplementary work that benefits from more persistent space. + +An Alert interrupts the visual hierarchy even when it does not open a modal. +Use it for important, exceptional information that needs attention in the +current task, not as a decorated container for ordinary descriptions, tips, or +empty space. Prefer inline help, muted text, or a normal section when the +content does not require immediate notice or action. + +Avoid stacking overlays. Escape should dismiss the topmost dismissible layer, +and focus should return to the trigger or the next logical target. + +An overlay action must refer to an object or state the overlay actually shows. +For example, expose `Clear history` only when a distinct recent-history section +is visible and contains entries. Search results, recent items, and favorites +are different collections; label and separate them instead of merging them +into one unexplained list. Hide an inapplicable action or disable it with a +useful reason—do not park an ambiguous trash icon in a footer. + +Footer space is not a catch-all for capabilities that lacked a place in the +design. A footer may present shortcuts, status, or actions that apply to the +whole surface, but each item must answer: what is its object, why is it +available now, what scope does it affect, and what visible state changes after +activation? + +## Motion + +Motion explains change; it is not ambient decoration. Use short transitions for +appearance, dismissal, expansion, and spatial continuity. Avoid animating large +layout changes when opacity or transform communicates the same relationship. +Honor reduced-motion preferences, never require animation to understand state, +and do not add a default animation to every component. + +Motion policy belongs to the styled or application layer. Base may own the +lifecycle mechanism or geometry needed for a transition, but it should not +decide that every product fades or slides. Give independently animated values +stable identity, and make interruption reverse smoothly from the currently +sampled value rather than restarting from an old endpoint. + +## Designing data-heavy interfaces + +Dense does not mean cramped. In tables, trees, command palettes, editors, and +docks: + +- keep headers and primary row identity visually stable; +- align comparable values and use tabular numerals where appropriate; +- distinguish focus, hover, active row, and multi-selection; +- keep sorting and filtering visible and reversible; +- preserve selection by domain identity across filtering and reordering; +- virtualize large collections without changing keyboard semantics; +- use progressive disclosure for secondary columns and inspectors; +- provide a useful empty state that explains the next action. + +Choose a table for comparison across consistent fields, a list for scanning +heterogeneous items, a tree for real hierarchy, and a dock only when users need +to arrange long-lived tools or documents. Do not use a complex data component +as a visual style. + +## Interface language + +Words are part of the interface architecture. Write the vocabulary for a +feature as a system—destinations, objects, commands, states, and outcomes—not as +isolated translations of implementation features. Prefer the shortest wording +that remains accurate in its actual context. + +### Let context carry context + +Do not repeat information that the surrounding surface already establishes. A +sidebar destination is usually the object or domain itself: use `Users`, not +`User Management`; `Shortcuts`, not `Shortcut Configuration Management`. A +column whose rows already contain actions can omit a generic `Operation` +heading. A dialog titled `Delete “Roadmap”?` does not need body text that asks +the same question again. + +This is context economy, not deletion for its own sake. Add text when it changes +the decision: identify the affected scope, an irreversible consequence, an +unusual prerequisite, or a way to recover. Every extra word should answer a +question the current layout does not already answer. + +Use nouns for destinations and objects (`Users`, `Appearance`, `Orders`), verbs +for commands (`Save`, `Duplicate`, `Export`), and adjectives or short phrases +for states (`Offline`, `Up to date`, `Pending review`). Avoid wrappers such as +`Management`, `Module`, `Page`, `Function`, `Operation`, and `System` unless the +word distinguishes a real domain concept. + +### Write each language, do not translate its shape + +Start from shared intent, hierarchy, and terminology, then compose each locale +as natural interface language. Do not preserve the source language's word +order, number of words, politeness filler, or grammatical category. English +`Users` can express a Chinese feature concept that would literally expand to +“user management”; fidelity means preserving purpose, not preserving tokens. + +Remove words supplied by the enclosing information architecture. Inside a +`Settings` surface, a destination is often simply `Account`, not `Account +Settings` and never the unnatural singular `Account Setting`. The correct +English label is chosen from its role and neighbors, not from the standalone +source phrase. + +Maintain a small product lexicon for recurring objects, commands, and states. +Use the same term in the toolbar, menu, context menu, dialog, shortcut search, +and documentation unless the context genuinely changes its meaning. Review +copy in the rendered surface: neighboring labels often reveal repetition or +inconsistent scope that a locale file cannot. + +In localized technical writing, preserve an established framework term when a +translation would be less precise. Keep API identifiers in their original form +and format them as code. Do not retain ordinary foreign words merely to sound +technical. Explain a retained term on first use when needed, then use the same +form throughout the interface, documentation, and API examples. + +### Buttons and confirmation dialogs + +Button labels are short by default—usually one or two words—and describe the +result, not the gesture or the component. Prefer `Save`, `Move`, or `Delete` to +`Click to save`, `Perform move`, or `Confirm deletion`. Use `Cancel` consistently +for the action that leaves without committing. Reserve `OK` for acknowledging +purely informational content. + +Short is a default, not a character limit. A deliberately longer label is +better when its words expose a consequence or distinguish choices that users +could otherwise confuse, for example `Delete from this group` versus `Delete +everywhere`, or `Restart without saving`. Length must buy decision-critical +information; it must not restate the dialog title or body. + +Use the most specific concise result as the confirmation label when possible: + +| Context | Weak | Prefer | +| --- | --- | --- | +| Delete dialog | `Yes`, `Sure`, `Confirm deletion` | `Delete` | +| Unsaved changes | `Confirm`, `Yes` | `Discard changes` | +| Pure acknowledgement | `Confirm operation` | `OK` or `Done` | +| Complex consent whose result has no clear verb | `Yes` | `Confirm` | + +`Confirm` is a useful fallback when the surrounding dialog fully names a +complex commitment and no shorter result verb is accurate. It should not +replace a clear command. `Sure` is conversational rather than a stable English +command and is too ambiguous for the standard vocabulary. + +A confirmation dialog should form one compact decision: + +- title: the decision or condition, such as `Delete “Roadmap”?`; +- body: only new scope, consequence, or recovery information; +- actions: `Cancel` and the result, such as `Delete`; +- destructive styling: applied to the destructive result, not substituted for + precise wording. + +Avoid generic titles such as `Notice`, `Warning`, `Error`, and `Confirmation` +when the actual condition can be named. Avoid ritual phrases such as “Are you +sure you want to…”, “Would you like to…”, “Please note that…”, and “successfully” +when the structure or state already communicates them. Courtesy should come +from a calm, respectful tone, not repeated `please`. + +### Capitalization, punctuation, and symbols + +Use sentence case for English UI by default: `Reset layout`, not `Reset Layout` +or `RESET LAYOUT`. Preserve proper nouns and established acronyms. Follow a +platform convention such as title case for native menu commands only when the +platform integration benefits from it, and apply that convention consistently +within the component class. + +ALL CAPS can provide restrained typographic emphasis for very short section +labels, eyebrows, statuses, established acronyms, and code-like identifiers. +Its compact shape and measured tracking can form a level similar to bold type, +but it does not belong on Buttons, long headings, sentences, or dense lists. Do +not combine uppercase, strong color, and bold weight in the same region, and do +not transform every string automatically: product names, acronyms, and localized +content must preserve their intended casing. + +Labels, buttons, menu items, tabs, headings, placeholders, and short states do +not take a final period. Complete explanatory, warning, and error sentences do. +Avoid exclamation marks in routine success and failure messages. In Chinese, +use full-width punctuation in sentences and omit terminal punctuation from +short control labels by the same semantic rule. + +Use the single ellipsis character (`…`), not three periods. Append it to every +Button or MenuItem that opens a dialog, sheet, or separate window, and to a +command that requires more input or choices before it can complete, such as +`Settings…` or `Export…`. An immediately executed command does not take an +ellipsis. Use an indeterminate progress indicator, not decorative dots, to +communicate ongoing work. + +Errors should say what happened and, when useful, the next recovery action. +Success feedback should name the resulting state only when that state is not +already visible. Prefer `Couldn’t save. Check your connection and try again.` +to a technical code or a long apology; omit a `Saved successfully` toast when +the document visibly becomes saved. + +## Internationalization and platform fit + +Copy must survive expansion, CJK typography, and different shortcut notation. +Do not size a control from one English label. Keep text out of raster assets, +avoid concatenating translated fragments, and let labels wrap or truncate only +where the product defines a recovery path such as a tooltip. + +Respect platform differences that carry meaning: Command versus Control, +native window decorations, system appearance, scrollbar behavior, menus, and +notification capabilities. Keep the product's information architecture stable +across platforms, but do not erase familiar platform behavior for superficial +pixel equality. + +## Guidance for AI-generated interfaces + +An AI changing a GPUI interface should first inspect the nearest feature, +theme tokens, and component documentation. It should state the primary task, +state owner, component composition, and keyboard path before generating code. +It must not infer an API from React/Shadcn examples or invent a GPUI method +because the name seems plausible. + +AI output is incomplete until a human can explain why the hierarchy, density, +component choice, and exceptional literal values belong in this product. A +visually plausible screenshot is not proof: keyboard behavior, focus, dynamic +content, themes, resizing, and failure states are part of the design. + +## Accessibility checklist + +Before considering a screen complete, verify that: + +- every action is reachable and operable by keyboard; +- focus order follows visual and task order; +- focus remains visible and is restored after overlays; +- controls have names, and icon-only controls have tooltips; +- text and meaningful boundaries have sufficient contrast; +- status is not communicated by color alone; +- disabled and read-only states are distinguishable; +- labels, errors, and descriptions remain near their controls; +- content remains usable with longer translations and larger text; +- pointer targets are comfortably sized even in a dense layout. + +## Design review checklist + +A review does not inventory components; it judges whether the interface made +the right decisions. Ask, in order: + +1. **Is the task clear?** Can a new user recognize the purpose, primary action, + and next step without learning, guessing, or experimenting? +2. **Does every action keep its promise?** Do the label, control, state, scope, + feedback, and result describe one consistent outcome? +3. **Is hierarchy decisive and restrained?** Does the core feature receive the + space it deserves while strong color, bold type, badges, alerts, and primary + Buttons remain scarce? +4. **Could the interface do less, better?** Can an entry point, option, or state + be removed, combined, or deferred without weakening the complete task? +5. **Is the structure exact?** Do peers share alignment spines, equal gaps stay + equal to the rendered pixel, and scrollbars sit at the edge of their actual + scrolling region? +6. **Does it follow the component system?** Do standard controls retain their + geometry, states, keyboard behavior, and dismissal model, with appearance + supplied by theme and scale tokens? +7. **Does it remain usable in every state and constraint?** Verify keyboard and + focus behavior, empty/loading/failure/permission states, longer translations, + zoom, minimum window size, and reduced motion. +8. **Has it been tested in a real window?** Complete the task with real + components, copy, and representative content—not only an ideal screenshot. + +Continue with [Coding Guides](./coding-guides.md) to translate these design +decisions into GPUI architecture and code. diff --git a/.claude/skills/gpui-component/references/style-guide.md b/.claude/skills/gpui-component/references/style-guide.md new file mode 100644 index 0000000..40b797c --- /dev/null +++ b/.claude/skills/gpui-component/references/style-guide.md @@ -0,0 +1,364 @@ +# GPUI Component Code Style Guide + +Based on analysis of `Button`, `Checkbox`, `Input`, `Select`, and other components in `crates/ui/src`. + +**Contents:** [Component Structure](#component-structure) · [Required Traits](#required-trait-implementations) · [Optional Traits](#optional-traits) · [Variants Pattern](#variants-pattern) · [Callback Signatures](#callback-signatures) · [Import Organization](#import-organization) · [Doc Comments](#doc-comments) · [Applying User Style Overrides](#applying-user-style-overrides) · [FluentBuilder Conditionals](#fluentbuilder-for-conditionals) · [Theme Colors](#theme-colors) · [Size Handling](#size-handling) · [Checklist](#checklist-for-new-components) + +## Component Structure + +### Standard Stateless Component + +```rust +use std::rc::Rc; + +use crate::{ActiveTheme, Disableable, Sizable, Size, StyledExt as _, /* ... */}; +use gpui::{ + AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, + ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, + StyleRefinement, Styled, Window, div, prelude::FluentBuilder as _, +}; + +/// A MyComponent element. +#[derive(IntoElement)] +pub struct MyComponent { + // 1. Identity + id: ElementId, + base: Div, + style: StyleRefinement, + + // 2. Configuration + size: Size, + disabled: bool, + selected: bool, + tab_stop: bool, + tab_index: isize, + + // 3. Content + label: Option, + children: Vec, + + // 4. Callbacks (last) + on_click: Option>, +} + +impl MyComponent { + /// Create a new MyComponent with the given id. + pub fn new(id: impl Into) -> Self { + Self { + id: id.into(), + base: div(), + style: StyleRefinement::default(), + size: Size::default(), + disabled: false, + selected: false, + tab_stop: true, + tab_index: 0, + label: None, + children: Vec::new(), + on_click: None, + } + } + + /// Set the label. + pub fn label(mut self, label: impl Into) -> Self { + self.label = Some(label.into()); + self + } + + /// Set the click handler. + pub fn on_click(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { + self.on_click = Some(Rc::new(handler)); + self + } +} +``` + +### Stateful Component (Interactive, Needs `.id()`) + +Components with mouse interactions (hover, click tracking) use `Stateful
`: + +```rust +use gpui::{Stateful, StatefulInteractiveElement as _, /* ... */}; + +#[derive(IntoElement)] +pub struct Button { + id: ElementId, + base: Stateful
, // Not Div — needs stateful for interaction tracking + // ... +} + +impl Button { + pub fn new(id: impl Into) -> Self { + let id = id.into(); + Self { + id: id.clone(), + base: div().flex_shrink_0().id(id), // .id() makes it Stateful
+ // ... + } + } +} + +impl InteractiveElement for Button { + fn interactivity(&mut self) -> &mut Interactivity { + self.base.interactivity() + } +} +``` + +--- + +## Required Trait Implementations + +```rust +// All components that accept children +impl ParentElement for MyComponent { + fn extend(&mut self, elements: impl IntoIterator) { + self.children.extend(elements) + } +} + +// All components with styleable outer div +impl Styled for MyComponent { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + +// For interactive components (mouse events, hover, click) +impl InteractiveElement for MyComponent { + fn interactivity(&mut self) -> &mut Interactivity { + self.base.interactivity() + } +} + +// Required if InteractiveElement is implemented +impl StatefulInteractiveElement for MyComponent {} + +// Rendering +impl RenderOnce for MyComponent { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + self.base + .id(self.id) + // Apply user style overrides last + .refine_style(&self.style) + .children(self.children) + } +} +``` + +--- + +## Optional Traits + +```rust +impl Disableable for MyComponent { + fn disabled(mut self, disabled: bool) -> Self { + self.disabled = disabled; + self + } +} + +impl Selectable for MyComponent { + fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } + fn is_selected(&self) -> bool { + self.selected + } +} + +impl Sizable for MyComponent { + fn with_size(mut self, size: impl Into) -> Self { + self.size = size.into(); + self + } +} +``` + +Implementing `Sizable` gives `.xsmall()`, `.small()`, `.medium()`, `.large()` for free via `StyleSized`. + +--- + +## Variants Pattern + +Use a `Variants` trait with default method impls: + +```rust +#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)] +pub enum AlertVariant { + #[default] + Info, + Success, + Warning, + Error, +} + +pub trait AlertVariants: Sized { + fn with_variant(self, variant: AlertVariant) -> Self; + + fn info(self) -> Self { self.with_variant(AlertVariant::Info) } + fn success(self) -> Self { self.with_variant(AlertVariant::Success) } + fn warning(self) -> Self { self.with_variant(AlertVariant::Warning) } + fn error(self) -> Self { self.with_variant(AlertVariant::Error) } +} + +impl AlertVariants for MyAlert { + fn with_variant(mut self, variant: AlertVariant) -> Self { + self.variant = variant; + self + } +} +``` + +--- + +## Callback Signatures + +```rust +// Click event (ClickEvent first) +on_click: Option> + +// State change (state value first) +on_change: Option> +on_change: Option> +on_change: Option> +``` + +Always `Rc` — components are cloned and called multiple times. + +--- + +## Import Organization + +```rust +// 1. std +use std::rc::Rc; + +// 2. crate imports (project internals) +use crate::{ + ActiveTheme, Disableable, Icon, IconName, + Selectable, Sizable, Size, StyledExt as _, + h_flex, v_flex, +}; + +// 3. gpui imports +use gpui::{ + AnyElement, App, Div, ElementId, InteractiveElement, IntoElement, + ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, + StyleRefinement, Styled, Window, div, + prelude::FluentBuilder as _, + px, rems, relative, +}; +``` + +--- + +## Doc Comments + +```rust +/// A Checkbox element. ← struct: one-line with capital, period +#[derive(IntoElement)] +pub struct Checkbox { ... } + +impl Checkbox { + /// Create a new Checkbox with the given id. ← constructor + pub fn new(id: impl Into) -> Self { ... } + + /// Set the label for the checkbox. ← setter + pub fn label(mut self, label: impl Into) -> Self { ... } + + /// Set the click handler for the checkbox. + /// + /// The `&bool` parameter indicates the new checked state after the click. + pub fn on_click(mut self, ...) -> Self { ... } +} +``` + +- Struct doc: `/// A {Name} element.` +- Constructor: `/// Create a new {Name} with the given id.` +- Setters: `/// Set the {field}.` +- No redundant comments — only document non-obvious behavior + +--- + +## Applying User Style Overrides + +Use `refine_style` to merge user's `Styled` calls onto the root element: + +```rust +impl RenderOnce for MyComponent { + fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { + div() + .flex() + .items_center() + // Apply component defaults first, then user overrides + .refine_style(&self.style) + .children(self.children) + } +} +``` + +--- + +## FluentBuilder for Conditionals + +```rust +div() + .when(self.disabled, |this| this.opacity(0.5).cursor_not_allowed()) + .when(self.selected, |this| this.bg(cx.theme().primary)) + .when_some(self.label.as_ref(), |this, label| { + this.child(div().child(label.clone())) + }) +``` + +Always `use gpui::prelude::FluentBuilder as _;` for `.when()` / `.when_some()`. + +--- + +## Theme Colors + +```rust +// In render, access via cx.theme() (requires ActiveTheme import) +use crate::ActiveTheme; + +div() + .bg(cx.theme().surface) + .text_color(cx.theme().foreground) + .border_color(cx.theme().border) + .when(is_active, |el| el.bg(cx.theme().primary)) +``` + +--- + +## Size Handling + +```rust +// Get pixel values based on Size +let (width, height) = self.size.input_size(); + +// Or use match +let font_size = match self.size { + Size::XSmall => rems(0.75), + Size::Small => rems(0.875), + Size::Medium | Size::Size(_) => rems(1.0), + Size::Large => rems(1.125), +}; +``` + +--- + +## Checklist for New Components + +- [ ] `#[derive(IntoElement)]` +- [ ] Fields: `id: ElementId`, `base: Div` (or `Stateful
`), `style: StyleRefinement` +- [ ] `impl RenderOnce` — calls `.refine_style(&self.style)` on root element +- [ ] `impl Styled` returning `&mut self.style` +- [ ] `impl ParentElement` if accepts children +- [ ] `impl InteractiveElement` + `StatefulInteractiveElement` if interactive +- [ ] `impl Sizable` if has size variants +- [ ] `impl Disableable` if can be disabled +- [ ] `impl Selectable` if can be selected +- [ ] Callbacks as `Option>` +- [ ] Doc comment on struct and public methods +- [ ] Import `prelude::FluentBuilder as _` diff --git a/.claude/skills/gpui-component/references/usage.md b/.claude/skills/gpui-component/references/usage.md new file mode 100644 index 0000000..d09d137 --- /dev/null +++ b/.claude/skills/gpui-component/references/usage.md @@ -0,0 +1,402 @@ +# gpui-component Usage Guide + +**Contents:** [Setup](#setup) · [Component Types](#component-types) · [Common Components](#common-components) (Button, Input, Select, Checkbox, Icon, Dialog, Notification, Tabs, Tooltip, Form, List) · [Theming](#theming) · [Layout Helpers](#layout-helpers) · [Overlay Layers](#overlay-layers-dialogs-sheets-notifications) · [Shared Traits](#shared-traits) + +## Setup + +### 1. Cargo.toml + +```toml +[dependencies] +gpui = { git = "https://github.com/zed-industries/zed" } +gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit"] } +gpui-component = { git = "https://github.com/longbridge/gpui-component" } +gpui-component-assets = { git = "https://github.com/longbridge/gpui-component" } # optional icons +``` + +### 2. Initialization + +```rust +fn main() { + gpui_platform::application() + .with_assets(gpui_component_assets::Assets) + .run(move |cx| { + gpui_component::init(cx); // MUST be first + + cx.spawn(async move |cx| { + cx.open_window(WindowOptions::default(), |window, cx| { + let view = cx.new(|_| MyApp); + cx.new(|cx| Root::new(view, window, cx)) // Root wraps first view + }).expect("Failed to open window"); + }).detach(); + }); +} +``` + +**`Root` is required** as the first-level child of every window — it enables dialogs, sheets, and notifications. + +--- + +## Component Types + +### Stateless (most components) + +Used directly in `render`, no stored state: + +```rust +use gpui_component::button::Button; + +impl Render for MyView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + Button::new("btn").primary().label("Submit") + .on_click(|_, _, _| println!("clicked")) + } +} +``` + +### Stateful (Input, Select, Combobox, etc.) + +Require an `Entity` stored in your view: + +```rust +use gpui_component::input::{Input, InputState}; + +struct MyView { + name: Entity, +} + +impl MyView { + fn new(window: &mut Window, cx: &mut Context) -> Self { + Self { + name: cx.new(|cx| InputState::new(window, cx).placeholder("Your name")), + } + } +} + +impl Render for MyView { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + Input::new(&self.name) + } +} +``` + +--- + +## Common Components + +### Button + +```rust +use gpui_component::button::{Button, ButtonGroup}; + +// Variants +Button::new("btn").label("Default") +Button::new("btn").primary().label("Primary") +Button::new("btn").danger().label("Delete") +Button::new("btn").warning().label("Warning") +Button::new("btn").success().label("Success") +Button::new("btn").ghost().label("Ghost") +Button::new("btn").link().label("Link") + +// States +Button::new("btn").label("Text").disabled(true) +Button::new("btn").label("Text").loading(true) +Button::new("btn").label("Text").selected(true) + +// With icon +Button::new("btn").icon(IconName::Plus).label("Add") + +// Sizes +Button::new("btn").xsmall().label("XS") +Button::new("btn").small().label("S") +Button::new("btn").large().label("L") + +// Group +ButtonGroup::new("group") + .child(Button::new("a").label("A")) + .child(Button::new("b").label("B")) + .on_click(|indices, _, _| { /* selected indices */ }) +``` + +### Input + +```rust +use gpui_component::input::{Input, InputState}; + +// State setup (in new/init) +let input = cx.new(|cx| InputState::new(window, cx) + .placeholder("Enter text...") + .default_value("Hello") +); + +// Render +Input::new(&input) +Input::new(&input).cleanable(true) // clear button +Input::new(&input).disabled(true) +Input::new(&input).prefix(Icon::new(IconName::Search).small()) +Input::new(&input).suffix(Button::new("b").ghost().icon(IconName::X).xsmall()) +Input::new(&input).content_type(InputContentType::Password) +Input::new(&input).mask_toggle() // password reveal toggle +Input::new(&input).appearance(false) // remove default border/bg + +// Reading value +let value = input.read(cx).value(); + +// Events +cx.subscribe_in(&input, window, |view, state, event, window, cx| { + match event { + InputEvent::Change => { let v = state.read(cx).value(); } + InputEvent::PressEnter { .. } => { /* submit */ } + InputEvent::Focus | InputEvent::Blur => {} + } +}); +``` + +### Select + +```rust +use gpui_component::select::{Select, SelectState}; + +// Simple string list +let state = cx.new(|cx| { + SelectState::new(vec!["Apple", "Orange", "Banana"], Some(IndexPath::default()), window, cx) +}); + +// Render +Select::new(&state) +Select::new(&state).placeholder("Pick one") + +// Reading selection +let selected = state.read(cx).selected_item(); +``` + +### Checkbox / Switch / Radio + +```rust +use gpui_component::{Checkbox, Switch}; + +// Stateless (controlled) +Checkbox::new("cb").checked(self.checked) + .on_click(|checked, _, cx| { /* &bool */ }) + +Switch::new("sw").checked(self.enabled) + .on_click(|checked, _, cx| {}) +``` + +### Icon + +```rust +use gpui_component::{Icon, IconName}; + +Icon::new(IconName::Check) +Icon::new(IconName::Search).small() +Icon::new(IconName::Plus).large().text_color(cx.theme().primary) +``` + +### Dialog + +```rust +use gpui_component::dialog::{Dialog, DialogAction, DialogClose, DialogFooter}; + +// Open from window context. `footer` takes an element, not a closure. +// DialogClose dismisses the dialog, so no manual close call is needed. +window.open_dialog(cx, |dialog, _, _| { + dialog + .title("Export Report") + .child("Choose a destination for the exported file.") + .footer( + DialogFooter::new() + .gap_2() + .child(DialogClose::new().child( + Button::new("cancel").label("Cancel").outline(), + )) + .child(DialogAction::new().child( + Button::new("export").label("Export").primary(), + )), + ) +}); +``` + +### AlertDialog + +Use `AlertDialog` — not `Dialog` — to confirm a consequential action. It is not +overlay-closable and has no close button, so the choice must be made. Name the +object in the title and the result on the confirming button; see the Design +Guides for the copy rules. + +```rust +use gpui_component::{button::ButtonVariant, dialog::DialogButtonProps}; + +window.open_alert_dialog(cx, |alert, _, _| { + alert + .title("Remove “Roadmap”?") + .description("Files on disk aren’t deleted.") + .button_props( + DialogButtonProps::default() + .ok_text("Remove") + .ok_variant(ButtonVariant::Danger) + .on_ok(|_, _, _| true), + ) +}); +``` + +### Notification + +```rust +// Simple string message +window.push_notification("Saved successfully!", cx); + +// With type variant +window.push_notification( + Notification::new("Upload complete").info().message("File uploaded"), + cx, +); +``` + +### Tabs + +```rust +use gpui_component::tab::{Tab, TabBar}; + +TabBar::new("tabs") + .child(Tab::new("tab1").child("Overview")) + .child(Tab::new("tab2").child("Settings")) + .child(Tab::new("tab3").child("Logs")) +``` + +### Tooltip + +```rust +// On any element with .id(), add .tooltip(): +div() + .id("my-btn") + .tooltip(|window, cx| Tooltip::new("Delete item").build(window, cx)) + .child("Delete") + +// Or on a Button directly: +Button::new("btn").icon(IconName::Trash).tooltip("Delete") +``` + +### Form + +```rust +use gpui_component::form::{v_form, h_form, field}; + +// Vertical form +v_form() + .child(field().label("Name").child(Input::new(&self.name))) + .child(field().label("Email").child(Input::new(&self.email))) + .child(Button::new("submit").primary().label("Submit")) + +// Horizontal label alignment +h_form() + .child(field().label("Username").child(Input::new(&self.username))) +``` + +### List (searchable, virtualized) + +```rust +use gpui_component::list::{List, ListState, ListDelegate, ListItem, ListEvent}; + +// Implement ListDelegate for your data type, then: +let list_state = cx.new(|cx| ListState::new(MyDelegate::new(), window, cx)); + +// Render +List::new(&list_state) +// Events +cx.subscribe(&list_state, |this, _, event, cx| { + if let ListEvent::Select(index_path) = event { + // handle selection + } +}); +``` + +--- + +## Theming + +```rust +use gpui_component::ActiveTheme as _; + +// Access colors +cx.theme().primary +cx.theme().background +cx.theme().foreground +cx.theme().border +cx.theme().surface +cx.theme().muted +cx.theme().destructive + +// Use in styles +div() + .bg(cx.theme().surface) + .text_color(cx.theme().foreground) + .border_color(cx.theme().border) +``` + +### Switch Theme + +```rust +use gpui_component::Theme; + +// Toggle light/dark +cx.update_global::(|theme, cx| { + theme.toggle_mode(cx); +}); + +// Load a named theme +Theme::global_mut(cx).apply_config(&theme_config); +``` + +--- + +## Layout Helpers + +gpui-component extends GPUI with convenient layout methods: + +```rust +h_flex() // div().flex().flex_row().items_center() +v_flex() // div().flex().flex_col() + +// Common patterns +h_flex().gap_2().items_center() + .child(Icon::new(IconName::User)) + .child(label("Username")) + +v_flex().gap_4().p_4() + .child(Input::new(&self.name)) + .child(Input::new(&self.email)) + .child(Button::new("submit").primary().label("Submit")) +``` + +--- + +## Overlay Layers (Dialogs, Sheets, Notifications) + +To render overlays, add these to your first-level view's render: + +```rust +impl Render for MyApp { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .size_full() + .child(self.main_content(window, cx)) + .children(Root::render_dialog_layer(cx)) + .children(Root::render_sheet_layer(cx)) + .children(Root::render_notification_layer(cx)) + } +} +``` + +--- + +## Shared Traits + +All components follow the builder pattern `Component::new("id").method().method()`: +- `Sizable`: `.xsmall()` / `.small()` / `.medium()` (default) / `.large()` +- `Disableable`: `.disabled(bool)` +- `Selectable`: `.selected(bool)` +- `Styled`: any GPUI style methods (`.w()`, `.bg()`, `.p_2()`, etc.) + +For any component not covered here, fetch its doc from: +`https://longbridge.github.io/gpui-component/docs/components/{name}.md` diff --git a/Cargo.lock b/Cargo.lock index 389b41b..416d176 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2718,13 +2718,14 @@ dependencies = [ [[package]] name = "gpui-component" -version = "0.5.0-preview2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b600febdc128afd24febf1bc4fa3a6b704e6d8521e67aa5e5a245117ff6a1b9" +checksum = "d021d46b4088d3d93a57ccdf443da85695a77272108caca2f6fe5369f584966a" dependencies = [ "aho-corasick", "anyhow", "chrono", + "core-text", "enum-iterator", "gpui", "gpui-component-macros", @@ -2757,9 +2758,9 @@ dependencies = [ [[package]] name = "gpui-component-assets" -version = "0.5.0-preview2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a00977ad46e08cfafb0e500647fce9b2a8de0e04daca9890860f1b9dc889c3" +checksum = "afc6e4c6551a1a12d4e8b69c3e8eba3cef43331c8c87898a0d4d040c78c6865e" dependencies = [ "anyhow", "gpui", @@ -2768,9 +2769,9 @@ dependencies = [ [[package]] name = "gpui-component-macros" -version = "0.5.0-preview2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c73cc07eab9c69be8858f156043f635ba257ce5d28ddfb1b7586350bdeb918c4" +checksum = "86fbc2d84bf91717b171320e6adc600d91ccb3ed259448f3b006787633c1c615" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index ec1d6f4..e0623fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,8 @@ rust-version = "1.75" [dependencies] # GPUI framework gpui = "0.2" -gpui-component = "0.5.0-preview1" -gpui-component-assets = "0.5.0-preview1" +gpui-component = "0.5.1" +gpui-component-assets = "0.5.1" # Sync HTTP client (avoids aws-lc-sys compilation issues) ureq = { version = "3", features = ["json"] } diff --git a/src/app.rs b/src/app.rs index 7d25291..97e5256 100644 --- a/src/app.rs +++ b/src/app.rs @@ -46,7 +46,7 @@ impl CloudBridgeApp { .w(px(220.0)) .h_full() .border_r_1() - .border_color(cx.theme().border) + .border_color(cx.theme().sidebar_border) .bg(cx.theme().sidebar) .p_4() .v_flex() @@ -55,11 +55,11 @@ impl CloudBridgeApp { div() .text_xl() .font_weight(FontWeight::BOLD) - .text_color(cx.theme().foreground) + .text_color(cx.theme().sidebar_foreground) .child("CloudBridge") .pb_4() .border_b_1() - .border_color(cx.theme().border) + .border_color(cx.theme().sidebar_border) .mb_4(), ) .child(self.nav_item( @@ -92,16 +92,18 @@ impl CloudBridgeApp { is_active: bool, cx: &Context, ) -> impl IntoElement { + // Mirrors gpui-component's own SidebarMenuItem: `accent` is a surface + // token, so the active label has to use `sidebar_accent_foreground`. let bg = if is_active { - cx.theme().accent.opacity(0.1) + cx.theme().sidebar_accent } else { transparent_black() }; let text_color = if is_active { - cx.theme().accent + cx.theme().sidebar_accent_foreground } else { - cx.theme().foreground + cx.theme().sidebar_foreground }; div() @@ -111,7 +113,7 @@ impl CloudBridgeApp { .rounded_md() .cursor_pointer() .bg(bg) - .hover(|s| s.bg(cx.theme().accent.opacity(0.05))) + .hover(|s| s.bg(cx.theme().sidebar_accent.opacity(0.8))) .text_color(text_color) .child(label) .on_click(cx.listener(move |this, _, _, cx| { diff --git a/src/cloud/aws.rs b/src/cloud/aws.rs index e28a129..16e311f 100644 --- a/src/cloud/aws.rs +++ b/src/cloud/aws.rs @@ -76,7 +76,7 @@ impl AwsCloudService { all_headers.push(("x-amz-content-sha256".to_string(), payload_hash.clone())); // Sort by lowercase key - all_headers.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase())); + all_headers.sort_by_key(|(name, _)| name.to_lowercase()); let canonical_headers: String = all_headers .iter() @@ -363,7 +363,7 @@ impl AwsCloudService { all_headers.push(("x-amz-content-sha256".to_string(), payload_hash.clone())); // Sort by lowercase key - all_headers.sort_by(|a, b| a.0.to_lowercase().cmp(&b.0.to_lowercase())); + all_headers.sort_by_key(|(name, _)| name.to_lowercase()); let canonical_headers: String = all_headers .iter() diff --git a/src/cloud/mod.rs b/src/cloud/mod.rs index f498591..64fe9d4 100644 --- a/src/cloud/mod.rs +++ b/src/cloud/mod.rs @@ -138,6 +138,8 @@ pub struct CostTrend { } /// Budget information +// TODO(v0.2.0): drop this allow once the budget UI is wired up +#[allow(dead_code)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BudgetInfo { /// Account ID @@ -155,6 +157,8 @@ pub struct BudgetInfo { } /// Budget status (comparison of budget vs actual) +// TODO(v0.2.0): drop this allow once the budget UI is wired up +#[allow(dead_code)] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BudgetStatus { /// Account ID diff --git a/src/config.rs b/src/config.rs index d19d4e2..fa63769 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,29 +6,39 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; +/// Default data refresh interval, in minutes +pub const DEFAULT_REFRESH_INTERVAL_MINUTES: u32 = 60; + /// Application configuration -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppConfig { /// Encryption key (for encrypting AK/SK) pub encryption_key: Option, /// Theme settings pub theme: ThemeConfig, - /// Data refresh interval (minutes) + /// Data refresh interval (minutes). + /// + /// Persisted but not acted on yet: nothing schedules a refresh from it, + /// so it has no Settings UI either. Wire both up together. pub refresh_interval_minutes: u32, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ThemeConfig { - /// Whether to use dark mode - pub dark_mode: bool, -} - -impl Default for ThemeConfig { +impl Default for AppConfig { fn default() -> Self { - Self { dark_mode: true } + Self { + encryption_key: None, + theme: ThemeConfig::default(), + refresh_interval_minutes: DEFAULT_REFRESH_INTERVAL_MINUTES, + } } } +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ThemeConfig { + /// Whether to use dark mode. Defaults to light. + pub dark_mode: bool, +} + /// Get application data directory pub fn get_app_data_dir() -> Result { // Use simpler path: AppData/Roaming/CloudBridge/ on Windows @@ -64,7 +74,12 @@ pub fn load_config() -> Result { if config_path.exists() { let content = fs::read_to_string(&config_path)?; - let config: AppConfig = serde_json::from_str(&content)?; + let mut config: AppConfig = serde_json::from_str(&content)?; + // Configs written before AppConfig had a real Default stored 0 here, + // which is not a usable interval. + if config.refresh_interval_minutes == 0 { + config.refresh_interval_minutes = DEFAULT_REFRESH_INTERVAL_MINUTES; + } Ok(config) } else { // Return default config diff --git a/src/crypto.rs b/src/crypto.rs index 8def653..eaa1c84 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -47,6 +47,10 @@ impl CryptoManager { } /// Encrypt data + /// + /// Unused since secrets moved to the OS keyring; kept as the counterpart of + /// `decrypt`, which still reads legacy encrypted blobs during migration. + #[allow(dead_code)] pub fn encrypt(&self, plaintext: &str) -> Result { let mut nonce_bytes = [0u8; NONCE_SIZE]; OsRng.fill_bytes(&mut nonce_bytes); diff --git a/src/db.rs b/src/db.rs index 91a32d8..8a351b0 100644 --- a/src/db.rs +++ b/src/db.rs @@ -625,6 +625,7 @@ pub fn clear_all_cache() -> Result<()> { // ==================== Budget Functions ==================== /// Save or update budget for an account +#[allow(dead_code)] // TODO(v0.2.0): remove once the budget UI calls this pub fn save_budget(budget: &BudgetInfo) -> Result<()> { let db = get_connection()?; let conn = db.as_ref().unwrap(); @@ -650,6 +651,7 @@ pub fn save_budget(budget: &BudgetInfo) -> Result<()> { } /// Get budget for an account +#[allow(dead_code)] // TODO(v0.2.0): remove once the budget UI calls this pub fn get_budget(account_id: &str) -> Result> { let db = get_connection()?; let conn = db.as_ref().unwrap(); @@ -688,6 +690,7 @@ pub fn get_budget(account_id: &str) -> Result> { } /// Get all budgets +#[allow(dead_code)] // TODO(v0.2.0): remove once the budget UI calls this pub fn get_all_budgets() -> Result> { let db = get_connection()?; let conn = db.as_ref().unwrap(); @@ -724,17 +727,22 @@ pub fn get_all_budgets() -> Result> { } /// Delete budget for an account +#[allow(dead_code)] // TODO(v0.2.0): remove once the budget UI calls this pub fn delete_budget(account_id: &str) -> Result<()> { let db = get_connection()?; let conn = db.as_ref().unwrap(); - conn.execute("DELETE FROM budgets WHERE account_id = ?", params![account_id])?; + conn.execute( + "DELETE FROM budgets WHERE account_id = ?", + params![account_id], + )?; tracing::info!("Deleted budget for account {}", account_id); Ok(()) } /// Get budget status (compares budget with current costs) +#[allow(dead_code)] // TODO(v0.2.0): remove once the budget UI calls this pub fn get_budget_status(account_id: &str) -> Result> { // Get budget let budget = match get_budget(account_id)? { @@ -750,11 +758,10 @@ pub fn get_budget_status(account_id: &str) -> Result> { .ok_or_else(|| anyhow::anyhow!("Account not found"))?; // Get cached cost summary - let cost_summary = get_cached_cost_summary_with_account(account_id, &account.name, &account.provider)?; + let cost_summary = + get_cached_cost_summary_with_account(account_id, &account.name, &account.provider)?; - let current_cost = cost_summary - .map(|cs| cs.current_month_cost) - .unwrap_or(0.0); + let current_cost = cost_summary.map(|cs| cs.current_month_cost).unwrap_or(0.0); // Calculate metrics let percentage_used = if budget.monthly_budget > 0.0 { @@ -779,6 +786,7 @@ pub fn get_budget_status(account_id: &str) -> Result> { } /// Get all budget statuses +#[allow(dead_code)] // TODO(v0.2.0): remove once the budget UI calls this pub fn get_all_budget_statuses() -> Result> { let budgets = get_all_budgets()?; let mut statuses = Vec::new(); diff --git a/src/main.rs b/src/main.rs index 5e61a52..c3a45bf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,6 +35,19 @@ fn main() { // Initialize GPUI Component gpui_component::init(cx); + // Apply the persisted theme before the first window opens, so the app + // does not flash the default appearance on startup. + let dark_mode = config::load_config().unwrap_or_default().theme.dark_mode; + Theme::change( + if dark_mode { + ThemeMode::Dark + } else { + ThemeMode::Light + }, + None, + cx, + ); + cx.spawn(async move |cx| { // Initialize database if let Err(e) = db::init_database() { diff --git a/src/ui/accounts.rs b/src/ui/accounts.rs index e6c4db9..4c04a79 100644 --- a/src/ui/accounts.rs +++ b/src/ui/accounts.rs @@ -104,38 +104,38 @@ impl AccountsView { // Update input placeholders based on cloud provider match provider { CloudProvider::AWS => { - self.ak_input.update(cx, |state, _cx| { - state.set_placeholder("Access Key ID"); + self.ak_input.update(cx, |state, cx| { + state.set_placeholder("Access Key ID", window, cx); }); - self.sk_input.update(cx, |state, _cx| { - state.set_placeholder("Secret Access Key"); + self.sk_input.update(cx, |state, cx| { + state.set_placeholder("Secret Access Key", window, cx); }); - self.region_input.update(cx, |state, _cx| { - state.set_placeholder("Region (optional, default us-east-1)"); - state.set_default_value("us-east-1"); + self.region_input.update(cx, |state, cx| { + state.set_placeholder("Region (optional, default us-east-1)", window, cx); + state.set_value("us-east-1", window, cx); }); } CloudProvider::Aliyun => { - self.ak_input.update(cx, |state, _cx| { - state.set_placeholder("AccessKey ID"); + self.ak_input.update(cx, |state, cx| { + state.set_placeholder("AccessKey ID", window, cx); }); - self.sk_input.update(cx, |state, _cx| { - state.set_placeholder("AccessKey Secret"); + self.sk_input.update(cx, |state, cx| { + state.set_placeholder("AccessKey Secret", window, cx); }); - self.region_input.update(cx, |state, _cx| { - state.set_placeholder("Region (optional, default cn-hangzhou)"); - state.set_default_value("cn-hangzhou"); + self.region_input.update(cx, |state, cx| { + state.set_placeholder("Region (optional, default cn-hangzhou)", window, cx); + state.set_value("cn-hangzhou", window, cx); }); } CloudProvider::DeepSeek => { - self.ak_input.update(cx, |state, _cx| { - state.set_placeholder("API Key"); + self.ak_input.update(cx, |state, cx| { + state.set_placeholder("API Key", window, cx); }); - self.sk_input.update(cx, |state, _cx| { - state.set_placeholder("(Not required, leave empty)"); + self.sk_input.update(cx, |state, cx| { + state.set_placeholder("(Not required, leave empty)", window, cx); }); - self.region_input.update(cx, |state, _cx| { - state.set_placeholder("(Not required)"); + self.region_input.update(cx, |state, cx| { + state.set_placeholder("(Not required)", window, cx); }); } diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 7f0b901..49ea5b7 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -24,8 +24,17 @@ impl SettingsView { } } - fn toggle_dark_mode(&mut self, cx: &mut Context) { - self.config.theme.dark_mode = !self.config.theme.dark_mode; + fn set_dark_mode(&mut self, dark: bool, window: &mut Window, cx: &mut Context) { + self.config.theme.dark_mode = dark; + Theme::change( + if dark { + ThemeMode::Dark + } else { + ThemeMode::Light + }, + Some(window), + cx, + ); self.save_config(cx); } @@ -103,36 +112,13 @@ impl Render for SettingsView { .child( Switch::new("dark-mode") .checked(dark_mode) - .on_click(cx.listener(|this, _, _, cx| { - this.toggle_dark_mode(cx); + .on_click(cx.listener(|this, checked: &bool, window, cx| { + this.set_dark_mode(*checked, window, cx); })), ), cx, ), ) - // Data settings - .child( - self.render_section( - "Data", - div().v_flex().gap_3().child( - div().h_flex().justify_between().items_center().child( - div() - .v_flex() - .child(div().child("Data Refresh Interval")) - .child( - div() - .text_sm() - .text_color(cx.theme().muted_foreground) - .child(format!( - "{} minutes", - self.config.refresh_interval_minutes - )), - ), - ), - ), - cx, - ), - ) // About .child( self.render_section( @@ -149,7 +135,7 @@ impl Render for SettingsView { .text_color(cx.theme().muted_foreground) .child("Version:"), ) - .child(div().child("0.1.0")), + .child(div().child(env!("CARGO_PKG_VERSION"))), ) .child( div()