{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "embed",
  "type": "registry:item",
  "title": "Embed",
  "description": "Link previews, message attachments and quoted posts under a chat message.",
  "files": [
    {
      "path": "registry/theme/primitives.rs",
      "type": "registry:file",
      "target": "~/src/ui/primitives.rs",
      "content": "//! Shared visual recipes between the theme tokens and the components.\n//!\n//! shadcn expresses these as Tailwind class strings; here each one is a function\n//! that applies the same styles to any GPUI element, so every component draws\n//! surfaces, controls, focus, text and images the same way, and a change to a\n//! recipe reaches the whole catalog. The registry lint fails components that\n//! rewrite one of these by hand. Everything takes `&UiTheme` so it works in\n//! `RenderOnce` and `Render` alike without re-reading the global.\n\nuse gpui_icons::{LucideIcon, lucide};\nuse gpui_kit::{\n    AnyElement, Axis, BoxShadow, Div, FontWeight, ImageSource, Img, InteractiveElement,\n    IntoElement, MouseButton, ObjectFit, ParentElement, Pixels, Rgba, SharedString,\n    StatefulInteractiveElement, StyleRefinement, Styled, StyledImage as _, Window, div, img, px,\n};\n\nuse super::{UiTextStyle, UiTheme};\n\n/// Text sizes from the type scale, each with its line height and the body font.\npub mod text {\n    use super::*;\n\n    /// Applies one step of the type scale and the body font.\n    pub fn style<T: Styled>(element: T, theme: &UiTheme, style: UiTextStyle) -> T {\n        let style = theme.text_style(style);\n        element\n            .font_family(theme.fonts.body.clone())\n            .text_size(style.size)\n            .line_height(style.line_height)\n    }\n    /// Captions, key caps and timestamps.\n    pub fn xs<T: Styled>(element: T, theme: &UiTheme) -> T {\n        style(element, theme, theme.typography.xs)\n    }\n    /// Controls and body copy; the Nova default.\n    pub fn sm<T: Styled>(element: T, theme: &UiTheme) -> T {\n        style(element, theme, theme.typography.sm)\n    }\n    /// Prose and dialog titles.\n    pub fn base<T: Styled>(element: T, theme: &UiTheme) -> T {\n        style(element, theme, theme.typography.base)\n    }\n    /// Section headings.\n    pub fn lg<T: Styled>(element: T, theme: &UiTheme) -> T {\n        style(element, theme, theme.typography.lg)\n    }\n    /// Page headings.\n    pub fn xl<T: Styled>(element: T, theme: &UiTheme) -> T {\n        style(element, theme, theme.typography.xl)\n    }\n    /// A medium-weight heading in the heading font at the given step.\n    pub fn heading<T: Styled>(element: T, theme: &UiTheme, size: UiTextStyle) -> T {\n        style(element, theme, size)\n            .font_family(theme.fonts.heading.clone())\n            .font_weight(FontWeight::MEDIUM)\n    }\n    /// Switches to the monospace font, keeping the current size.\n    pub fn mono<T: Styled>(element: T, theme: &UiTheme) -> T {\n        element.font_family(theme.fonts.mono.clone())\n    }\n    /// Muted supporting text at the small size.\n    pub fn muted<T: Styled>(element: T, theme: &UiTheme) -> T {\n        sm(element, theme).text_color(theme.colors.muted_foreground)\n    }\n}\n\n/// Icon sizes in spacing units, so icons scale with control density.\npub mod icon {\n    use super::*;\n\n    /// 12px: inside badges and tags.\n    pub fn xs(theme: &UiTheme) -> Pixels {\n        theme.space(3.)\n    }\n    /// 14px: inside small buttons and chips.\n    pub fn sm(theme: &UiTheme) -> Pixels {\n        theme.space(3.5)\n    }\n    /// 16px: the default inside controls and menu items.\n    pub fn md(theme: &UiTheme) -> Pixels {\n        theme.space(4.)\n    }\n    /// 20px: list leading icons.\n    pub fn lg(theme: &UiTheme) -> Pixels {\n        theme.space(5.)\n    }\n    /// 24px: empty states and headers.\n    pub fn xl(theme: &UiTheme) -> Pixels {\n        theme.space(6.)\n    }\n}\n\n/// Keyboard focus treatment: shadcn Nova's `ring-3 ring-ring/50` outside the\n/// border, with the border taking the ring color. Apply through\n/// `focus_visible` so pointer presses leave it alone.\npub mod focus {\n    use super::*;\n\n    /// The ring as a box shadow, for elements that draw their own border.\n    pub fn shadow(color: Rgba) -> Vec<BoxShadow> {\n        vec![BoxShadow::new(px(0.), px(0.), color.into()).spread_radius(px(3.))]\n    }\n    /// The focus ring for an ordinary control.\n    pub fn ring(style: StyleRefinement, theme: &UiTheme) -> StyleRefinement {\n        let control = theme.control();\n        style\n            .border_color(theme.colors.ring)\n            .shadow(shadow(control.ring))\n    }\n    /// The focus ring for an invalid control, in the destructive color.\n    pub fn invalid_ring(style: StyleRefinement, theme: &UiTheme) -> StyleRefinement {\n        let control = theme.control();\n        style\n            .border_color(theme.colors.destructive)\n            .shadow(shadow(control.invalid_ring))\n    }\n    /// Picks the valid or invalid ring.\n    pub fn ring_for(style: StyleRefinement, theme: &UiTheme, invalid: bool) -> StyleRefinement {\n        if invalid {\n            invalid_ring(style, theme)\n        } else {\n            ring(style, theme)\n        }\n    }\n    /// Draws the ring on `element` while `focused`, for controls whose focus is\n    /// tracked on a parent (a grouped checkbox or radio draws it on its box).\n    pub fn shown<T: Styled>(element: T, theme: &UiTheme, focused: bool) -> T {\n        shown_for(element, theme, focused, false)\n    }\n    /// [`shown`] with the invalid ring when `invalid`.\n    pub fn shown_for<T: Styled>(element: T, theme: &UiTheme, focused: bool, invalid: bool) -> T {\n        if focused {\n            super::super::apply_style(\n                element,\n                &ring_for(StyleRefinement::default(), theme, invalid),\n            )\n        } else {\n            element\n        }\n    }\n}\n\n/// Disabled and interactive cursor treatment.\npub mod disabled {\n    use super::*;\n\n    /// Nova's `disabled:opacity-50` plus the blocked cursor.\n    pub fn style<T: Styled>(element: T) -> T {\n        element.opacity(0.5).cursor_not_allowed()\n    }\n    /// A pointer cursor when enabled, the disabled treatment otherwise.\n    pub fn interactive<T: Styled>(element: T, disabled: bool) -> T {\n        if disabled {\n            style(element)\n        } else {\n            element.cursor_pointer()\n        }\n    }\n    /// Half opacity while `dimmed`, for content that is unavailable rather than\n    /// a control that is disabled: uploading pictures, disabled list rows.\n    pub fn dim<T: Styled>(element: T, dimmed: bool) -> T {\n        if dimmed {\n            element.opacity(0.5)\n        } else {\n            element\n        }\n    }\n}\n\n/// Elevated surfaces. One rule per kind, so every popover, menu, dialog, card\n/// and tooltip in the catalog shares border, radius, shadow and text defaults.\npub mod surface {\n    use super::*;\n\n    fn popover_base<T: Styled>(element: T, theme: &UiTheme) -> T {\n        text::sm(element, theme)\n            .border_1()\n            .border_color(theme.control().surface_border)\n            .bg(theme.colors.popover)\n            .text_color(theme.colors.popover_foreground)\n    }\n    /// Popovers, hover cards, select and combobox lists: `rounded-lg shadow-md`.\n    pub fn popover<T: Styled>(element: T, theme: &UiTheme) -> T {\n        popover_base(element, theme)\n            .rounded(theme.radius.lg)\n            .shadow(theme.shadows.md.to_vec())\n    }\n    /// Menus and context menus: a popover with Nova's 4px inset.\n    pub fn menu<T: Styled>(element: T, theme: &UiTheme) -> T {\n        popover(element, theme).p(theme.space(1.))\n    }\n    /// Dialogs and command palettes: `rounded-xl shadow-lg`.\n    pub fn modal<T: Styled>(element: T, theme: &UiTheme) -> T {\n        popover_base(element, theme)\n            .rounded(theme.radius.xl)\n            .shadow(theme.shadows.lg.to_vec())\n    }\n    /// A sheet or drawer panel: the modal surface without a radius, which the\n    /// caller rounds on the open edge.\n    pub fn sheet<T: Styled>(element: T, theme: &UiTheme) -> T {\n        popover_base(element, theme).border_color(theme.colors.border)\n    }\n    /// Toasts: `rounded-2xl` on the popover surface with the plain border.\n    pub fn toast<T: Styled>(element: T, theme: &UiTheme) -> T {\n        popover_base(element, theme)\n            .rounded(theme.radius.two_xl)\n            .border_color(theme.colors.border)\n            .shadow(theme.shadows.lg.to_vec())\n    }\n    /// Cards and empty states: `rounded-xl border shadow-sm` on the card color.\n    pub fn card<T: Styled>(element: T, theme: &UiTheme) -> T {\n        text::sm(element, theme)\n            .rounded(theme.radius.xl)\n            .border_1()\n            .border_color(theme.colors.border)\n            .bg(theme.colors.card)\n            .text_color(theme.colors.card_foreground)\n            .shadow(theme.shadows.sm.to_vec())\n    }\n    /// A floating action bar over content, as on a hovered message.\n    pub fn floating<T: Styled>(element: T, theme: &UiTheme) -> T {\n        element\n            .rounded(theme.radius.lg)\n            .border_1()\n            .border_color(theme.colors.border)\n            .bg(theme.colors.background)\n            .shadow(theme.shadows.sm.to_vec())\n    }\n    /// Tooltips: inverted colors at the small radius and caption size.\n    pub fn tooltip<T: Styled>(element: T, theme: &UiTheme) -> T {\n        text::xs(element, theme)\n            .rounded(theme.radius.sm)\n            .px(theme.space(3.))\n            .py(theme.space(1.5))\n            .bg(theme.colors.foreground)\n            .text_color(theme.colors.background)\n    }\n    /// A one-pixel rule in the border color, across or down.\n    pub fn divider<T: Styled>(element: T, theme: &UiTheme, axis: Axis) -> T {\n        let element = match axis {\n            Axis::Horizontal => element.w_full().h(px(1.)),\n            Axis::Vertical => element.h_full().w(px(1.)),\n        };\n        element.bg(theme.colors.border)\n    }\n}\n\n/// Form control pieces shared by single controls and their grouped variants.\npub mod control {\n    use super::*;\n\n    /// Corner radius of a 16px check box: Nova's `rounded-[4px]` at the default radius.\n    pub fn box_radius(theme: &UiTheme) -> Pixels {\n        theme.radius.sm * (2. / 3.)\n    }\n\n    /// The frame every text-like control shares: inputs, textareas, selects,\n    /// number and OTP cells. Large radius, input border and fill, small text.\n    /// Callers set the size: `h(theme.space(8.))` and `px(theme.space(2.5))`\n    /// for a single-line control.\n    pub fn frame<T: Styled>(element: T, theme: &UiTheme, invalid: bool) -> T {\n        let control = theme.control();\n        text::sm(element, theme)\n            .rounded(theme.radius.lg)\n            .border_1()\n            .border_color(if invalid {\n                theme.colors.destructive\n            } else {\n                control.border\n            })\n            .bg(control.background)\n            .text_color(theme.colors.foreground)\n    }\n\n    /// Styles `element` as the 16px check box with its mark, for checkboxes and\n    /// grouped checkboxes. The element is the interactive root of the control.\n    pub fn checkbox_box<T: Styled + ParentElement>(\n        element: T,\n        theme: &UiTheme,\n        checked: bool,\n        indeterminate: bool,\n    ) -> T {\n        let colors = theme.colors;\n        let control = theme.control();\n        let selected = checked || indeterminate;\n        let element = element\n            .flex()\n            .flex_shrink_0()\n            .items_center()\n            .justify_center()\n            .size(theme.space(4.))\n            .rounded(box_radius(theme))\n            .border_1()\n            .border_color(if selected {\n                colors.primary\n            } else {\n                control.border\n            })\n            .bg(if selected {\n                colors.primary\n            } else {\n                control.background\n            });\n        if selected {\n            element.child(\n                lucide(if indeterminate {\n                    LucideIcon::Minus\n                } else {\n                    LucideIcon::Check\n                })\n                .size(icon::sm(theme))\n                .text_color(colors.primary_foreground),\n            )\n        } else {\n            element\n        }\n    }\n\n    /// Styles `element` as the 16px radio circle with its dot.\n    pub fn radio_dot<T: Styled + ParentElement>(element: T, theme: &UiTheme, checked: bool) -> T {\n        let colors = theme.colors;\n        let control = theme.control();\n        let element = element\n            .flex()\n            .flex_shrink_0()\n            .items_center()\n            .justify_center()\n            .size(theme.space(4.))\n            .rounded_full()\n            .border_1()\n            .border_color(if checked {\n                colors.primary\n            } else {\n                control.border\n            })\n            .bg(if checked {\n                colors.primary\n            } else {\n                control.background\n            });\n        if checked {\n            element.child(\n                div()\n                    .size(theme.space(2.))\n                    .rounded_full()\n                    .bg(colors.primary_foreground),\n            )\n        } else {\n            element\n        }\n    }\n\n    /// The toggle look shared by Toggle and Toggle Group items: transparent at\n    /// rest, muted when hovered or pressed, an input border when outlined.\n    pub fn toggle<T: Styled>(\n        element: T,\n        theme: &UiTheme,\n        pressed: bool,\n        disabled: bool,\n        outline: bool,\n    ) -> T {\n        let colors = theme.colors;\n        let control = theme.control();\n        let element = text::sm(element, theme)\n            .border_1()\n            .border_color(if outline {\n                control.border\n            } else {\n                colors.background.opacity(0.)\n            })\n            .text_color(colors.foreground)\n            .bg(if pressed {\n                control.pressed\n            } else {\n                colors.background.opacity(0.)\n            });\n        disabled::interactive(element, disabled)\n    }\n}\n\n/// The switch track and thumb colors for a checked or unchecked switch.\npub fn switch_colors(theme: &UiTheme, checked: bool) -> (Rgba, Rgba) {\n    let control = theme.control();\n    if checked {\n        (theme.colors.primary, control.thumb_checked)\n    } else {\n        (control.track, control.thumb)\n    }\n}\n\n/// An image inside a rounded frame. GPUI clips children to rectangles, so the\n/// frame rounds itself and the image rounds to the frame's inner radius; the\n/// image fills the frame's box and never sizes the frame from its aspect ratio.\npub fn clipped_image(\n    source: impl Into<ImageSource>,\n    radius: Pixels,\n    fit: ObjectFit,\n    theme: &UiTheme,\n) -> Div {\n    clipped_image_with(source, radius, fit, theme, |image| image)\n}\n\n/// [`clipped_image`] with access to the picture, for loading and fallback views.\npub fn clipped_image_with(\n    source: impl Into<ImageSource>,\n    radius: Pixels,\n    fit: ObjectFit,\n    theme: &UiTheme,\n    configure: impl FnOnce(Img) -> Img,\n) -> Div {\n    div()\n        .relative()\n        .flex_shrink_0()\n        .overflow_hidden()\n        .rounded(radius)\n        .bg(theme.colors.muted)\n        .child(configure(\n            img(source)\n                .absolute()\n                .inset_0()\n                .size_full()\n                .rounded(radius)\n                .object_fit(fit),\n        ))\n}\n\n/// A circular image, as in avatars.\npub fn round_image(source: impl Into<ImageSource>, size: Pixels, theme: &UiTheme) -> Div {\n    clipped_image(source, size / 2., ObjectFit::Cover, theme).size(size)\n}\n\n/// Dismisses an overlay from a press outside it or the Escape key.\npub mod dismiss {\n    use super::*;\n\n    /// Runs `handler` when the pointer presses outside `element` or Escape is\n    /// pressed while focus is inside it.\n    pub fn on_outside<E: StatefulInteractiveElement>(\n        element: E,\n        handler: impl Fn(&mut Window, &mut gpui_kit::App) + 'static,\n    ) -> E {\n        let handler = std::rc::Rc::new(handler);\n        let on_press = handler.clone();\n        element\n            .on_mouse_down_out(move |event, window, cx| {\n                if event.button == MouseButton::Left {\n                    on_press(window, cx)\n                }\n            })\n            .on_key_down(move |event, window, cx| {\n                if event.keystroke.key == \"escape\" {\n                    handler(window, cx);\n                    cx.stop_propagation();\n                }\n            })\n    }\n}\n\n/// Hover-revealed content that never changes layout: the element keeps its\n/// size and only its opacity follows `visible`.\npub fn hover_reveal<T: Styled>(element: T, visible: bool) -> T {\n    element.opacity(if visible { 1. } else { 0. })\n}\n\n/// Content shown while the pointer is over the enclosing `.group(name)`\n/// element, with no state and no re-render: opacity 0 at rest, 1 on hover.\n/// Pass `pinned` to keep it visible, as for a selected row's actions.\npub fn hover_reveal_in_group<T: Styled + InteractiveElement>(\n    element: T,\n    group: impl Into<SharedString>,\n    pinned: bool,\n) -> T {\n    if pinned {\n        element\n    } else {\n        element\n            .opacity(0.)\n            .group_hover(group, |style| style.opacity(1.))\n    }\n}\n\n/// Erases an element's type after styling, for callers that store children.\npub fn erase(element: impl IntoElement) -> AnyElement {\n    element.into_any_element()\n}\n"
    },
    {
      "path": "registry/theme/theme.rs",
      "type": "registry:file",
      "target": "~/src/ui/theme.rs",
      "content": "//! The app-owned shadcn Neutral theme for gpuicn.\n//!\n//! Source: shadcn/ui 4.19.0 at\n//! `1773ecfeeb4a04366978d353e69b5c7ded78dcb2`, Nova style.\n\nuse std::{sync::Arc, time::Duration};\n\nuse gpui_kit::{\n    App, BoxShadow, ElementId, Global, Pixels, Rgba, SharedString, Styled, Subscription, Window,\n    WindowAppearance, black, px,\n};\n\n#[path = \"primitives.rs\"]\npub mod primitives;\n\ngpui_kit::actions!(\n    gpuicn,\n    [\n        /// Move to the next keyboard tab stop.\n        FocusNext,\n        /// Move to the previous keyboard tab stop.\n        FocusPrevious\n    ]\n);\n\nstruct Initialized;\nimpl Global for Initialized {}\n\n/// Installs component actions, keyboard traversal and the default theme once.\n/// Call this from application startup for both copied source and crate usage.\npub fn init(cx: &mut App) {\n    if cx.has_global::<Initialized>() {\n        return;\n    }\n    seed_component_fonts(cx);\n    gpui_kit::init(cx);\n    cx.set_global(Initialized);\n    if !cx.has_global::<UiTheme>() {\n        UiTheme::set(cx, UiTheme::neutral_light());\n    } else {\n        let theme = UiTheme::read(cx).clone();\n        sync_component_theme(cx, theme);\n    }\n    cx.bind_keys([\n        gpui_kit::KeyBinding::new(\"tab\", FocusNext, None),\n        gpui_kit::KeyBinding::new(\"shift-tab\", FocusPrevious, None),\n    ]);\n    cx.on_action(|_: &FocusNext, cx| advance_focus(false, cx));\n    cx.on_action(|_: &FocusPrevious, cx| advance_focus(true, cx));\n    // Scoped component bindings take precedence over these window-wide defaults.\n    #[cfg(target_family = \"wasm\")]\n    {\n        // Browser previews also accept macOS editing shortcuts.\n        use gpui_kit::KeyBinding;\n        use gpui_kit::base::input::{\n            Copy, Cut, MoveEnd, MoveHome, MoveToNextWord, MoveToPreviousWord, Paste, Redo,\n            SelectAll, SelectToNextWordEnd, SelectToPreviousWordStart, Undo,\n        };\n        cx.bind_keys([\n            KeyBinding::new(\"cmd-a\", SelectAll, Some(\"Input\")),\n            KeyBinding::new(\"cmd-c\", Copy, Some(\"Input\")),\n            KeyBinding::new(\"cmd-v\", Paste, Some(\"Input\")),\n            KeyBinding::new(\"cmd-x\", Cut, Some(\"Input\")),\n            KeyBinding::new(\"cmd-left\", MoveHome, Some(\"Input\")),\n            KeyBinding::new(\"cmd-right\", MoveEnd, Some(\"Input\")),\n            KeyBinding::new(\"cmd-z\", Undo, Some(\"Input\")),\n            KeyBinding::new(\"cmd-shift-z\", Redo, Some(\"Input\")),\n            KeyBinding::new(\"alt-left\", MoveToPreviousWord, Some(\"Input\")),\n            KeyBinding::new(\"alt-right\", MoveToNextWord, Some(\"Input\")),\n            KeyBinding::new(\"alt-shift-left\", SelectToPreviousWordStart, Some(\"Input\")),\n            KeyBinding::new(\"alt-shift-right\", SelectToNextWordEnd, Some(\"Input\")),\n        ]);\n    }\n}\n\nfn seed_component_fonts(cx: &mut App) {\n    use gpui_kit::component::Theme;\n\n    if cx.has_global::<Theme>() {\n        return;\n    }\n    let fonts = if cx.has_global::<UiTheme>() {\n        UiTheme::read(cx).fonts.clone()\n    } else {\n        UiTheme::neutral_light().fonts\n    };\n    let mut component = Theme::default();\n    component.font_family = fonts.body;\n    component.mono_font_family = fonts.mono;\n    cx.set_global(component);\n}\n\nfn advance_focus(reverse: bool, cx: &mut App) {\n    let Some(handle) = cx.active_window() else {\n        return;\n    };\n    // Action dispatch still holds the window borrow until this callback returns.\n    cx.defer(move |cx| {\n        let _ = handle.update(cx, |_, window, cx| {\n            if reverse {\n                window.focus_prev(cx);\n            } else {\n                window.focus_next(cx);\n            }\n        });\n    });\n}\n\n/// The active color mode.\n#[derive(Clone, Copy, Debug, Eq, PartialEq)]\npub enum ThemeMode {\n    /// The Neutral light palette.\n    Light,\n    /// The Neutral dark palette.\n    Dark,\n}\n\n/// Which of an application's two palettes is shown.\n#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]\npub enum Appearance {\n    /// Always the light palette.\n    Light,\n    /// Always the dark palette.\n    Dark,\n    /// The palette matching the operating system's window appearance.\n    #[default]\n    System,\n}\n\nimpl Appearance {\n    /// The mode this appearance shows under the given system appearance.\n    pub fn resolve(self, system: WindowAppearance) -> ThemeMode {\n        match self {\n            Appearance::Light => ThemeMode::Light,\n            Appearance::Dark => ThemeMode::Dark,\n            Appearance::System => match system {\n                WindowAppearance::Dark | WindowAppearance::VibrantDark => ThemeMode::Dark,\n                WindowAppearance::Light | WindowAppearance::VibrantLight => ThemeMode::Light,\n            },\n        }\n    }\n}\n\n/// The application's light and dark themes and which one it shows.\n#[derive(Clone)]\nstruct ThemePair {\n    light: UiTheme,\n    dark: UiTheme,\n    appearance: Appearance,\n}\nimpl Global for ThemePair {}\n\n/// Swaps palettes when the operating system's appearance changes while the\n/// application follows it. Call it for each window the application opens and\n/// keep the subscription; `app::run` does this for the first window.\npub fn follow_system(window: &mut Window, cx: &mut App) -> Subscription {\n    UiTheme::apply_appearance(cx);\n    window.observe_window_appearance(|_, cx| UiTheme::apply_appearance(cx))\n}\n\n/// Semantic colors consumed by installed components.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiColors {\n    /// App background.\n    pub background: Rgba,\n    /// Default foreground.\n    pub foreground: Rgba,\n    /// Card background.\n    pub card: Rgba,\n    /// Card foreground.\n    pub card_foreground: Rgba,\n    /// Popover background.\n    pub popover: Rgba,\n    /// Popover foreground.\n    pub popover_foreground: Rgba,\n    /// Primary control background.\n    pub primary: Rgba,\n    /// Primary control foreground.\n    pub primary_foreground: Rgba,\n    /// Secondary control background.\n    pub secondary: Rgba,\n    /// Secondary control foreground.\n    pub secondary_foreground: Rgba,\n    /// Muted background.\n    pub muted: Rgba,\n    /// Muted foreground.\n    pub muted_foreground: Rgba,\n    /// Accent background.\n    pub accent: Rgba,\n    /// Accent foreground.\n    pub accent_foreground: Rgba,\n    /// Destructive action color.\n    pub destructive: Rgba,\n    /// Text on a solid `destructive` surface.\n    pub destructive_foreground: Rgba,\n    /// Positive status, such as a completed upload or an online presence dot.\n    pub success: Rgba,\n    /// Text on a `success` fill.\n    pub success_foreground: Rgba,\n    /// Cautionary status, such as a permalink highlight or a pending state.\n    pub warning: Rgba,\n    /// Text on a `warning` fill.\n    pub warning_foreground: Rgba,\n    /// Informational status.\n    pub info: Rgba,\n    /// Text on an `info` fill.\n    pub info_foreground: Rgba,\n    /// Default border.\n    pub border: Rgba,\n    /// Input border.\n    pub input: Rgba,\n    /// Focus ring.\n    pub ring: Rgba,\n    /// First chart color.\n    pub chart_1: Rgba,\n    /// Second chart color.\n    pub chart_2: Rgba,\n    /// Third chart color.\n    pub chart_3: Rgba,\n    /// Fourth chart color.\n    pub chart_4: Rgba,\n    /// Fifth chart color.\n    pub chart_5: Rgba,\n    /// Sidebar background.\n    pub sidebar: Rgba,\n    /// Sidebar foreground.\n    pub sidebar_foreground: Rgba,\n    /// Sidebar primary background.\n    pub sidebar_primary: Rgba,\n    /// Sidebar primary foreground.\n    pub sidebar_primary_foreground: Rgba,\n    /// Sidebar accent background.\n    pub sidebar_accent: Rgba,\n    /// Sidebar accent foreground.\n    pub sidebar_accent_foreground: Rgba,\n    /// Sidebar border.\n    pub sidebar_border: Rgba,\n    /// Sidebar focus border color.\n    pub sidebar_ring: Rgba,\n    /// Scrim behind modal surfaces.\n    pub overlay: Rgba,\n}\n\n/// Theme font family names. Applications own font loading.\n#[derive(Clone, Debug, PartialEq)]\npub struct UiFonts {\n    /// Default body family.\n    pub body: SharedString,\n    /// Default heading family.\n    pub heading: SharedString,\n    /// Default monospace family.\n    pub mono: SharedString,\n}\n\n/// Corner radii derived from shadcn's 10px base radius.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiRadius {\n    /// Backward-compatible alias for [`Self::lg`].\n    pub base: Pixels,\n    /// Six pixels (`radius * 0.6`).\n    pub sm: Pixels,\n    /// Eight pixels (`radius * 0.8`).\n    pub md: Pixels,\n    /// Ten pixels (the configured base radius).\n    pub lg: Pixels,\n    /// Fourteen pixels (`radius * 1.4`).\n    pub xl: Pixels,\n    /// Eighteen pixels (`radius * 1.8`).\n    pub two_xl: Pixels,\n    /// Twenty-two pixels (`radius * 2.2`).\n    pub three_xl: Pixels,\n    /// Twenty-six pixels (`radius * 2.6`).\n    pub four_xl: Pixels,\n}\n\nimpl UiRadius {\n    /// Derives the complete Nova radius scale from one nonnegative base radius.\n    pub fn new(base: Pixels) -> Self {\n        assert!(f32::from(base).is_finite() && base >= px(0.));\n        Self {\n            base,\n            sm: base * 0.6,\n            md: base * 0.8,\n            lg: base,\n            xl: base * 1.4,\n            two_xl: base * 1.8,\n            three_xl: base * 2.2,\n            four_xl: base * 2.6,\n        }\n    }\n}\n\n/// Shared UI transition durations. Honor these alongside `App::reduce_motion()`.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiMotion {\n    /// Thumb and small-state transitions (150ms by default).\n    pub fast: Duration,\n    /// Panel/layout transitions (200ms by default).\n    pub normal: Duration,\n    /// Application preference; true removes movement without removing state feedback.\n    pub reduced: bool,\n    /// Shared easing for reversible state transitions.\n    pub easing: UiEasing,\n}\nimpl Default for UiMotion {\n    fn default() -> Self {\n        Self {\n            fast: Duration::from_millis(150),\n            normal: Duration::from_millis(200),\n            reduced: false,\n            easing: UiEasing::EaseInOut,\n        }\n    }\n}\n\n/// Curves supported by the shared native transition helper.\n#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]\npub enum UiEasing {\n    /// Constant-speed motion, as used by shadcn sidebar layout transitions.\n    Linear,\n    /// Fast response followed by a smooth stop.\n    EaseOut,\n    /// Smooth acceleration and deceleration for changes within a layout.\n    #[default]\n    EaseInOut,\n    /// Quick start and a long settle (`cubic-bezier(0.22, 1, 0.36, 1)`), for\n    /// surfaces that slide in from an edge, such as drawers and sheets.\n    Emphasized,\n}\nimpl UiEasing {\n    fn sample(self, progress: f32) -> f32 {\n        match self {\n            Self::Linear => progress,\n            Self::EaseOut => gpui_kit::ease_out_quint()(progress),\n            Self::EaseInOut => gpui_kit::ease_in_out(progress),\n            Self::Emphasized => gpui_kit::base::motion::Easing::CubicBezier {\n                x1: 0.22,\n                y1: 1.,\n                x2: 0.36,\n                y2: 1.,\n            }\n            .sample(progress),\n        }\n    }\n}\n\n/// Returns a smoothly retargetable value for one stable, caller-named property.\n/// Starts at the target on mount; rapid reversals continue from the current value.\n/// Honors theme and GPUI reduced-motion settings and schedules no idle frames.\n/// Use for state changes, never for pointer-drag coordinates or keyboard navigation.\n/// Native layout transitions perform layout work; retain expensive child entities.\npub fn transition_value(\n    id: impl Into<ElementId>,\n    target: f32,\n    duration: Duration,\n    window: &mut Window,\n    cx: &mut App,\n) -> f32 {\n    let easing = UiTheme::read(cx).motion.easing;\n    transition_with(id, target, duration, easing, window, cx)\n}\n\n/// [`transition_value`] with an explicit curve, for the few surfaces whose motion\n/// differs from the theme default (drawers slide with [`UiEasing::Emphasized`]).\npub fn transition_with(\n    id: impl Into<ElementId>,\n    target: f32,\n    duration: Duration,\n    easing: UiEasing,\n    window: &mut Window,\n    cx: &mut App,\n) -> f32 {\n    assert!(target.is_finite(), \"transition targets must be finite\");\n    gpui_kit::base::motion::transition(\n        id.into(),\n        target,\n        motion_policy(duration, easing, cx),\n        window,\n        cx,\n    )\n}\n\n/// Shares the theme's disclosure and notification timing with Kit's presence lifecycle.\n/// Returns a sample whose `progress` runs 0→1 while entering and 1→0 while leaving, so\n/// a surface fades or scales with it; `should_render()` keeps the element mounted\n/// until the exit transition has finished.\n#[doc(hidden)]\npub fn presence(\n    id: impl Into<ElementId>,\n    present: bool,\n    window: &mut Window,\n    cx: &mut App,\n) -> gpui_kit::base::motion::PresenceSample {\n    let motion = UiTheme::read(cx).motion;\n    presence_with(id, present, motion.fast, motion.easing, window, cx)\n}\n\n/// [`presence`] with explicit timing and curve, for panels that take the theme's\n/// `normal` duration or a different easing.\n#[doc(hidden)]\npub fn presence_with(\n    id: impl Into<ElementId>,\n    present: bool,\n    duration: Duration,\n    easing: UiEasing,\n    window: &mut Window,\n    cx: &mut App,\n) -> gpui_kit::base::motion::PresenceSample {\n    gpui_kit::base::motion::Presence::new(id.into(), present)\n        .transition(motion_policy(duration, easing, cx))\n        .sample(window, cx)\n}\n\n/// The one place motion honors the theme and platform reduced-motion settings.\nfn motion_policy(\n    duration: Duration,\n    easing: UiEasing,\n    cx: &App,\n) -> gpui_kit::base::motion::Transition {\n    let duration = if UiTheme::read(cx).motion.reduced || cx.reduce_motion() {\n        Duration::ZERO\n    } else {\n        duration\n    };\n    gpui_kit::base::motion::Transition::new(duration).ease(move |progress| easing.sample(progress))\n}\n\n/// Shared shadcn elevation tokens. Each level is shared, so cloning a theme\n/// every render costs no allocation; `to_vec()` the level you apply.\n#[derive(Clone, Debug, PartialEq)]\npub struct UiShadows {\n    /// Tailwind `shadow-sm`.\n    pub sm: Arc<[BoxShadow]>,\n    /// Tailwind `shadow-md`.\n    pub md: Arc<[BoxShadow]>,\n    /// Tailwind `shadow-lg`.\n    pub lg: Arc<[BoxShadow]>,\n}\n\n/// One step of the type scale: a text size and the line height that goes with it.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiTextStyle {\n    /// Font size before `text_scale`.\n    pub size: Pixels,\n    /// Line height before `text_scale`.\n    pub line_height: Pixels,\n}\n\n/// The type scale, Tailwind's by default. Components read sizes from here through\n/// `primitives::text`, so `text_scale` and a custom scale reach every label.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiTypography {\n    /// Captions and key caps (12/16).\n    pub xs: UiTextStyle,\n    /// Controls and body copy in dense desktop layouts (14/20); the Nova default.\n    pub sm: UiTextStyle,\n    /// Prose and dialog titles (16/24).\n    pub base: UiTextStyle,\n    /// Section headings (18/28).\n    pub lg: UiTextStyle,\n    /// Page headings (20/28).\n    pub xl: UiTextStyle,\n}\n\nimpl Default for UiTypography {\n    fn default() -> Self {\n        let style = |size: f32, line_height: f32| UiTextStyle {\n            size: px(size),\n            line_height: px(line_height),\n        };\n        Self {\n            xs: style(12., 16.),\n            sm: style(14., 20.),\n            base: style(16., 24.),\n            lg: style(18., 28.),\n            xl: style(20., 28.),\n        }\n    }\n}\n\n/// Window widths at which layouts change shape.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiBreakpoints {\n    /// Below this width, sidebars become sheets and drawers fill the window (768px).\n    pub mobile: Pixels,\n    /// Below this width, side drawers take three quarters of the window (640px).\n    pub compact: Pixels,\n}\n\nimpl Default for UiBreakpoints {\n    fn default() -> Self {\n        Self {\n            mobile: px(768.),\n            compact: px(640.),\n        }\n    }\n}\n\n/// Colors every form control shares, derived once from the palette and mode so\n/// no component chooses its own dark-mode alpha. These follow shadcn Nova's\n/// `dark:bg-input/30`, `dark:hover:bg-input/50` and `dark:border-input` rules.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct ControlColors {\n    /// Resting fill of inputs, checkboxes, radios and outline buttons.\n    pub background: Rgba,\n    /// Resting border of the same controls.\n    pub border: Rgba,\n    /// Hover fill of outline, ghost and toggle controls.\n    pub hover: Rgba,\n    /// Hover fill of ghost buttons and plain icon buttons.\n    pub ghost_hover: Rgba,\n    /// Fill of a pressed toggle or selected tab.\n    pub pressed: Rgba,\n    /// Unchecked switch track.\n    pub track: Rgba,\n    /// Switch thumb on the unchecked track.\n    pub thumb: Rgba,\n    /// Switch thumb on the checked (`primary`) track.\n    pub thumb_checked: Rgba,\n    /// Tinted destructive fill, for destructive buttons, badges and tags.\n    pub destructive_background: Rgba,\n    /// Hover of the tinted destructive fill.\n    pub destructive_hover: Rgba,\n    /// Hairline around tinted destructive chips.\n    pub destructive_border: Rgba,\n    /// Focus ring color at Nova's 50% opacity.\n    pub ring: Rgba,\n    /// Focus ring of an invalid control.\n    pub invalid_ring: Rgba,\n    /// Hairline around popovers and dialogs (`ring-foreground/10`).\n    pub surface_border: Rgba,\n}\n\n/// Base spacing unit from the pinned Nova style.\n#[derive(Clone, Copy, Debug, PartialEq)]\npub struct UiSpacing {\n    /// Four pixels.\n    pub unit: Pixels,\n}\n\n/// The application-owned theme stored in GPUI global state.\n#[derive(Clone, Debug, PartialEq)]\npub struct UiTheme {\n    /// Active color mode.\n    pub mode: ThemeMode,\n    /// Active semantic colors.\n    pub colors: UiColors,\n    /// Font family names.\n    pub fonts: UiFonts,\n    /// Base corner radius.\n    pub radius: UiRadius,\n    /// Base spacing unit.\n    pub spacing: UiSpacing,\n    /// Shared elevation tokens.\n    pub shadows: UiShadows,\n    /// Shared motion durations and reduced-motion preference.\n    pub motion: UiMotion,\n    /// Typography scale, independent of control density (1 by default).\n    pub text_scale: f32,\n    /// Text sizes and line heights, multiplied by `text_scale` when applied.\n    pub typography: UiTypography,\n    /// Window widths at which layouts change shape.\n    pub breakpoints: UiBreakpoints,\n}\n\nimpl Global for UiTheme {}\n\nimpl Default for UiTheme {\n    fn default() -> Self {\n        Self::neutral_light()\n    }\n}\n\nimpl UiTheme {\n    /// Creates the pinned Neutral light theme.\n    pub fn neutral_light() -> Self {\n        Self::new(ThemeMode::Light, neutral_light_colors())\n    }\n\n    /// Creates the pinned Neutral dark theme.\n    pub fn neutral_dark() -> Self {\n        Self::new(ThemeMode::Dark, neutral_dark_colors())\n    }\n\n    /// Installs light and dark themes together and shows the one the current\n    /// [`Appearance`] selects (System until [`UiTheme::set_appearance`] says\n    /// otherwise). [`follow_system`] keeps the pair in step with the OS.\n    pub fn set_pair(cx: &mut App, light: Self, dark: Self) {\n        let appearance = cx\n            .try_global::<ThemePair>()\n            .map_or(Appearance::System, |pair| pair.appearance);\n        cx.set_global(ThemePair {\n            light,\n            dark,\n            appearance,\n        });\n        Self::apply_appearance(cx);\n    }\n\n    /// Chooses which of the installed pair to show. Without a pair, the\n    /// pinned Neutral palettes stand in.\n    pub fn set_appearance(cx: &mut App, appearance: Appearance) {\n        match cx.try_global::<ThemePair>() {\n            Some(_) => cx.global_mut::<ThemePair>().appearance = appearance,\n            None => cx.set_global(ThemePair {\n                light: Self::neutral_light(),\n                dark: Self::neutral_dark(),\n                appearance,\n            }),\n        }\n        Self::apply_appearance(cx);\n    }\n\n    /// The appearance the application asked for, System when none was set.\n    pub fn appearance(cx: &App) -> Appearance {\n        cx.try_global::<ThemePair>()\n            .map_or(Appearance::System, |pair| pair.appearance)\n    }\n\n    /// Installs the pair member the appearance selects, if a pair is installed.\n    fn apply_appearance(cx: &mut App) {\n        let Some(pair) = cx.try_global::<ThemePair>().cloned() else {\n            return;\n        };\n        let theme = match pair.appearance.resolve(cx.window_appearance()) {\n            ThemeMode::Light => pair.light,\n            ThemeMode::Dark => pair.dark,\n        };\n        let current = cx.try_global::<Self>();\n        if current\n            .is_some_and(|current| current.mode == theme.mode && current.colors == theme.colors)\n        {\n            return;\n        }\n        Self::set(cx, theme);\n    }\n\n    /// Installs a theme into the application.\n    pub fn set(cx: &mut App, theme: Self) {\n        assert!(f32::from(theme.spacing.unit).is_finite() && theme.spacing.unit > px(0.));\n        assert!(theme.text_scale.is_finite() && theme.text_scale > 0.);\n        sync_component_theme(cx, theme.clone());\n        cx.set_global(theme);\n        cx.refresh_windows();\n    }\n\n    /// Reads the installed theme.\n    ///\n    /// Panics when the application has not installed a theme.\n    #[track_caller]\n    pub fn read(cx: &App) -> &Self {\n        cx.try_global::<Self>().expect(\n            \"gpuicn theme is not installed: call `ui::theme::init(cx)` once at application startup\",\n        )\n    }\n\n    /// Builds a theme from shadcn theme CSS, such as the output of the shadcn\n    /// theme picker or tweakcn.\n    ///\n    /// Light reads `:root`; dark reads `:root` and then `.dark`, like the\n    /// browser cascade. Recognized `--background`, `--primary`, `--sidebar-*`,\n    /// `--chart-*` and `--radius` declarations replace the Neutral defaults;\n    /// other declarations are ignored. Colors may use `oklch()`, `hsl()`,\n    /// bare HSL triplets, `rgb()` or hex. Build both modes and call\n    /// [`UiTheme::set`] to switch; [`UiTheme::switch`] restores Neutral colors.\n    pub fn from_shadcn_css(css: &str, mode: ThemeMode) -> Result<Self, ThemeCssError> {\n        let blocks = css_blocks(css);\n        let mut theme = match mode {\n            ThemeMode::Light => Self::neutral_light(),\n            ThemeMode::Dark => Self::neutral_dark(),\n        };\n        let mut selectors = vec![\":root\"];\n        if mode == ThemeMode::Dark {\n            selectors.push(\".dark\");\n        }\n        for selector in selectors {\n            let declarations = blocks\n                .iter()\n                .filter(|(name, _)| name.split(',').any(|part| part.trim() == selector))\n                .flat_map(|(_, body)| body.split(';'))\n                .collect::<Vec<_>>();\n            if declarations.is_empty() {\n                return Err(ThemeCssError(format!(\"no `{selector}` block found\")));\n            }\n            for declaration in declarations {\n                let Some((name, value)) = declaration.split_once(':') else {\n                    continue;\n                };\n                let (name, value) = (name.trim(), value.trim());\n                let Some(name) = name.strip_prefix(\"--\") else {\n                    continue;\n                };\n                if name == \"radius\" {\n                    theme.radius = UiRadius::new(css_length(value).ok_or_else(|| {\n                        ThemeCssError(format!(\"unsupported length `--radius: {value}`\"))\n                    })?);\n                } else if let Some(slot) = theme.colors.slot(name) {\n                    *slot = css_color(value).ok_or_else(|| {\n                        ThemeCssError(format!(\"unsupported color `--{name}: {value}`\"))\n                    })?;\n                }\n            }\n        }\n        Ok(theme)\n    }\n\n    /// Selects the pinned palette, preserving non-color tokens or initializing defaults.\n    pub fn switch(cx: &mut App, mode: ThemeMode) {\n        let mut theme = cx\n            .try_global::<Self>()\n            .cloned()\n            .unwrap_or_else(Self::neutral_light);\n        theme.mode = mode;\n        theme.colors = match mode {\n            ThemeMode::Light => neutral_light_colors(),\n            ThemeMode::Dark => neutral_dark_colors(),\n        };\n        Self::set(cx, theme);\n    }\n\n    /// Returns a multiple of the configured spacing unit (2 means 8px by default).\n    pub fn space(&self, units: f32) -> Pixels {\n        self.spacing.unit * units\n    }\n\n    /// Scales typography independently of spacing; sizes use Nova's default pixels.\n    pub fn text(&self, size: f32) -> Pixels {\n        px(size * self.text_scale)\n    }\n\n    fn new(mode: ThemeMode, colors: UiColors) -> Self {\n        Self {\n            mode,\n            colors,\n            fonts: UiFonts {\n                body: \"Geist\".into(),\n                heading: \"Geist\".into(),\n                mono: \"Geist Mono\".into(),\n            },\n            radius: UiRadius::new(px(10.)),\n            motion: UiMotion::default(),\n            text_scale: 1.,\n            typography: UiTypography::default(),\n            breakpoints: UiBreakpoints::default(),\n            spacing: UiSpacing { unit: px(4.) },\n            shadows: UiShadows {\n                sm: Arc::from([\n                    BoxShadow::new(px(0.), px(1.), black().alpha(0.10)).blur_radius(px(3.)),\n                    BoxShadow::new(px(0.), px(1.), black().alpha(0.10))\n                        .blur_radius(px(2.))\n                        .spread_radius(px(-1.)),\n                ]),\n                md: Arc::from([\n                    BoxShadow::new(px(0.), px(4.), black().alpha(0.10))\n                        .blur_radius(px(6.))\n                        .spread_radius(px(-1.)),\n                    BoxShadow::new(px(0.), px(2.), black().alpha(0.10))\n                        .blur_radius(px(4.))\n                        .spread_radius(px(-2.)),\n                ]),\n                lg: Arc::from([\n                    BoxShadow::new(px(0.), px(10.), black().alpha(0.10))\n                        .blur_radius(px(15.))\n                        .spread_radius(px(-3.)),\n                    BoxShadow::new(px(0.), px(4.), black().alpha(0.10))\n                        .blur_radius(px(6.))\n                        .spread_radius(px(-4.)),\n                ]),\n            },\n        }\n    }\n\n    /// The shared form-control colors for the active mode. See [`ControlColors`].\n    pub fn control(&self) -> ControlColors {\n        let colors = self.colors;\n        let dark = self.mode == ThemeMode::Dark;\n        ControlColors {\n            background: if dark {\n                colors.input.opacity(0.30)\n            } else {\n                colors.background.opacity(0.)\n            },\n            border: colors.input,\n            hover: if dark {\n                colors.input.opacity(0.50)\n            } else {\n                colors.muted\n            },\n            ghost_hover: if dark {\n                colors.muted.opacity(0.50)\n            } else {\n                colors.muted\n            },\n            pressed: colors.muted,\n            track: if dark {\n                colors.input.opacity(0.80)\n            } else {\n                colors.input\n            },\n            thumb: if dark {\n                colors.foreground\n            } else {\n                colors.background\n            },\n            thumb_checked: if dark {\n                colors.primary_foreground\n            } else {\n                colors.background\n            },\n            destructive_background: colors.destructive.opacity(if dark { 0.20 } else { 0.10 }),\n            destructive_hover: colors.destructive.opacity(if dark { 0.30 } else { 0.20 }),\n            destructive_border: colors.destructive.opacity(0.20),\n            ring: colors.ring.opacity(0.50),\n            invalid_ring: colors.destructive.opacity(0.20),\n            surface_border: colors.foreground.opacity(0.10),\n        }\n    }\n\n    /// A step of the type scale multiplied by `text_scale`.\n    pub fn text_style(&self, style: UiTextStyle) -> UiTextStyle {\n        UiTextStyle {\n            size: style.size * self.text_scale,\n            line_height: style.line_height * self.text_scale,\n        }\n    }\n}\n\nfn sync_component_theme(cx: &mut App, theme: UiTheme) {\n    use gpui_kit::component::{Theme, ThemeTokens};\n\n    if !cx.has_global::<Theme>() {\n        return;\n    }\n\n    let colors = theme.colors;\n    let component = Theme::global_mut(cx);\n    component.mode = match theme.mode {\n        ThemeMode::Light => gpui_kit::component::ThemeMode::Light,\n        ThemeMode::Dark => gpui_kit::component::ThemeMode::Dark,\n    };\n    // Start from Kit's palette for this mode, so colors gpuicn doesn't set\n    // below, such as scrollbar tracks, never keep the other mode's values.\n    component.colors = *match theme.mode {\n        ThemeMode::Light => gpui_kit::component::ThemeColor::light(),\n        ThemeMode::Dark => gpui_kit::component::ThemeColor::dark(),\n    };\n    component.font_family = theme.fonts.body;\n    component.mono_font_family = theme.fonts.mono;\n    component.radius = theme.radius.md;\n    component.radius_lg = theme.radius.lg;\n    component.background = colors.background.into();\n    component.foreground = colors.foreground.into();\n    component.popover = colors.popover.into();\n    component.popover_foreground = colors.popover_foreground.into();\n    component.primary = colors.primary.into();\n    component.primary_foreground = colors.primary_foreground.into();\n    component.secondary = colors.secondary.into();\n    component.secondary_foreground = colors.secondary_foreground.into();\n    component.muted = colors.muted.into();\n    component.muted_foreground = colors.muted_foreground.into();\n    component.accent = colors.accent.into();\n    component.accent_foreground = colors.accent_foreground.into();\n    component.primary_hover = colors.primary.opacity(0.9).into();\n    component.primary_active = colors.primary.opacity(0.8).into();\n    component.secondary_hover = colors.secondary.opacity(0.8).into();\n    component.secondary_active = colors.secondary.opacity(0.7).into();\n    component.border = colors.border.into();\n    component.input = colors.input.into();\n    component.ring = colors.ring.into();\n    component.overlay = colors.overlay.into();\n    component.caret = colors.foreground.into();\n    component.window_border = colors.border.into();\n    component.danger = colors.destructive.into();\n    component.danger_foreground = colors.destructive_foreground.into();\n    component.danger_hover = colors.destructive.opacity(0.9).into();\n    component.danger_active = colors.destructive.opacity(0.8).into();\n    component.success = colors.success.into();\n    component.success_foreground = colors.success_foreground.into();\n    component.success_hover = colors.success.opacity(0.9).into();\n    component.success_active = colors.success.opacity(0.8).into();\n    component.warning = colors.warning.into();\n    component.warning_foreground = colors.warning_foreground.into();\n    component.warning_hover = colors.warning.opacity(0.9).into();\n    component.warning_active = colors.warning.opacity(0.8).into();\n    component.info = colors.info.into();\n    component.info_foreground = colors.info_foreground.into();\n    component.info_hover = colors.info.opacity(0.9).into();\n    component.info_active = colors.info.opacity(0.8).into();\n    component.chart_1 = colors.chart_1.into();\n    component.chart_2 = colors.chart_2.into();\n    component.chart_3 = colors.chart_3.into();\n    component.chart_4 = colors.chart_4.into();\n    component.chart_5 = colors.chart_5.into();\n    component.chart_bullish = colors.success.into();\n    component.chart_bearish = colors.destructive.into();\n    component.chart_grid = colors.border.opacity(0.6).into();\n    // Buttons: Kit's plain button is shadcn `outline`; the variants follow their token.\n    component.button = colors.background.into();\n    component.button_foreground = colors.foreground.into();\n    component.button_hover = colors.accent.into();\n    component.button_active = colors.muted.into();\n    component.button_primary = component.primary;\n    component.button_primary_foreground = component.primary_foreground;\n    component.button_primary_hover = component.primary_hover;\n    component.button_primary_active = component.primary_active;\n    component.button_secondary = component.secondary;\n    component.button_secondary_foreground = component.secondary_foreground;\n    component.button_secondary_hover = component.secondary_hover;\n    component.button_secondary_active = component.secondary_active;\n    component.button_danger = component.danger;\n    component.button_danger_foreground = component.danger_foreground;\n    component.button_danger_hover = component.danger_hover;\n    component.button_danger_active = component.danger_active;\n    component.button_success = component.success;\n    component.button_success_foreground = component.success_foreground;\n    component.button_success_hover = component.success_hover;\n    component.button_success_active = component.success_active;\n    component.button_warning = component.warning;\n    component.button_warning_foreground = component.warning_foreground;\n    component.button_warning_hover = component.warning_hover;\n    component.button_warning_active = component.warning_active;\n    component.button_info = component.info;\n    component.button_info_foreground = component.info_foreground;\n    component.button_info_hover = component.info_hover;\n    component.button_info_active = component.info_active;\n    // Nova scrollbars float over content with no track.\n    component.scrollbar = gpui_kit::transparent_black();\n    component.scrollbar_thumb = colors.border.into();\n    component.scrollbar_thumb_hover = colors.muted_foreground.opacity(0.72).into();\n    component.sidebar = colors.sidebar.into();\n    component.sidebar_foreground = colors.sidebar_foreground.into();\n    component.sidebar_border = colors.sidebar_border.into();\n    component.sidebar_accent = colors.sidebar_accent.into();\n    component.sidebar_accent_foreground = colors.sidebar_accent_foreground.into();\n    component.sidebar_primary = colors.sidebar_primary.into();\n    component.sidebar_primary_foreground = colors.sidebar_primary_foreground.into();\n    component.title_bar = colors.sidebar.into();\n    component.title_bar_border = colors.border.into();\n    component.status_bar = colors.sidebar.into();\n    component.status_bar_border = colors.border.into();\n    // Rich text: links, selection, table headings and code highlighting.\n    component.link = colors.primary.into();\n    component.link_hover = colors.primary.opacity(0.9).into();\n    component.link_active = colors.primary.opacity(0.8).into();\n    component.selection = colors.ring.opacity(0.3).into();\n    component.description_list_label = colors.muted.into();\n    component.description_list_label_foreground = colors.muted_foreground.into();\n    component.group_box = colors.card.into();\n    component.group_box_foreground = colors.card_foreground.into();\n    component.accordion = colors.card.into();\n    // Lists and tables share the `muted` zebra and the `accent` hover of menus.\n    component.colors.list = colors.background.into();\n    component.list_even = colors.muted.opacity(0.5).into();\n    component.list_head = colors.muted.into();\n    component.list_hover = colors.accent.into();\n    component.list_active = colors.accent.into();\n    component.list_active_border = colors.ring.into();\n    component.table = colors.background.into();\n    component.table_even = colors.muted.opacity(0.5).into();\n    component.table_hover = colors.muted.opacity(0.5).into();\n    component.table_active = colors.accent.into();\n    component.table_active_border = colors.ring.into();\n    component.table_row_border = colors.border.into();\n    component.table_head = colors.muted.into();\n    component.table_head_foreground = colors.foreground.into();\n    component.table_foot = colors.muted.opacity(0.5).into();\n    component.table_foot_foreground = colors.foreground.into();\n    // Controls: shadcn tracks are `input`, thumbs and active tabs are `background`.\n    component.switch = colors.input.into();\n    component.switch_thumb = colors.background.into();\n    component.slider_bar = colors.primary.into();\n    component.slider_thumb = colors.background.into();\n    component.progress_bar = colors.primary.into();\n    component.skeleton = colors.accent.into();\n    component.tab_bar = colors.muted.into();\n    component.tab_bar_segmented = colors.muted.into();\n    component.tab_foreground = colors.muted_foreground.into();\n    component.tab_active = colors.background.into();\n    component.tab_active_foreground = colors.foreground.into();\n    component.drag_border = colors.ring.into();\n    component.drop_target = colors.ring.opacity(0.15).into();\n    component.highlight_theme = match theme.mode {\n        ThemeMode::Light => gpui_kit::component::highlighter::HighlightTheme::default_light(),\n        ThemeMode::Dark => gpui_kit::component::highlighter::HighlightTheme::default_dark(),\n    };\n    component.tokens = ThemeTokens::from(&component.colors);\n\n    Theme::sync_base(cx);\n}\n\n/// Why [`UiTheme::from_shadcn_css`] could not read a theme.\n#[derive(Clone, Debug, PartialEq)]\npub struct ThemeCssError(pub String);\n\nimpl std::fmt::Display for ThemeCssError {\n    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        write!(formatter, \"shadcn theme CSS: {}\", self.0)\n    }\n}\n\nimpl std::error::Error for ThemeCssError {}\n\nimpl UiColors {\n    #[doc(hidden)]\n    pub fn slot(&mut self, variable: &str) -> Option<&mut Rgba> {\n        Some(match variable {\n            \"background\" => &mut self.background,\n            \"foreground\" => &mut self.foreground,\n            \"card\" => &mut self.card,\n            \"card-foreground\" => &mut self.card_foreground,\n            \"popover\" => &mut self.popover,\n            \"popover-foreground\" => &mut self.popover_foreground,\n            \"primary\" => &mut self.primary,\n            \"primary-foreground\" => &mut self.primary_foreground,\n            \"secondary\" => &mut self.secondary,\n            \"secondary-foreground\" => &mut self.secondary_foreground,\n            \"muted\" => &mut self.muted,\n            \"muted-foreground\" => &mut self.muted_foreground,\n            \"accent\" => &mut self.accent,\n            \"accent-foreground\" => &mut self.accent_foreground,\n            \"destructive\" => &mut self.destructive,\n            \"destructive-foreground\" => &mut self.destructive_foreground,\n            \"success\" => &mut self.success,\n            \"success-foreground\" => &mut self.success_foreground,\n            \"warning\" => &mut self.warning,\n            \"warning-foreground\" => &mut self.warning_foreground,\n            \"info\" => &mut self.info,\n            \"info-foreground\" => &mut self.info_foreground,\n            \"border\" => &mut self.border,\n            \"input\" => &mut self.input,\n            \"ring\" => &mut self.ring,\n            \"chart-1\" => &mut self.chart_1,\n            \"chart-2\" => &mut self.chart_2,\n            \"chart-3\" => &mut self.chart_3,\n            \"chart-4\" => &mut self.chart_4,\n            \"chart-5\" => &mut self.chart_5,\n            \"sidebar\" | \"sidebar-background\" => &mut self.sidebar,\n            \"sidebar-foreground\" => &mut self.sidebar_foreground,\n            \"sidebar-primary\" => &mut self.sidebar_primary,\n            \"sidebar-primary-foreground\" => &mut self.sidebar_primary_foreground,\n            \"sidebar-accent\" => &mut self.sidebar_accent,\n            \"sidebar-accent-foreground\" => &mut self.sidebar_accent_foreground,\n            \"sidebar-border\" => &mut self.sidebar_border,\n            \"sidebar-ring\" => &mut self.sidebar_ring,\n            _ => return None,\n        })\n    }\n}\n\n/// Innermost `selector { body }` pairs with comments removed.\nfn css_blocks(css: &str) -> Vec<(String, String)> {\n    let mut text = String::with_capacity(css.len());\n    let mut rest = css;\n    while let Some(start) = rest.find(\"/*\") {\n        text.push_str(&rest[..start]);\n        rest = rest[start + 2..]\n            .split_once(\"*/\")\n            .map_or(\"\", |(_, after)| after);\n    }\n    text.push_str(rest);\n    let mut blocks = Vec::new();\n    let mut selector_start = 0;\n    let mut open = None;\n    for (index, character) in text.char_indices() {\n        match character {\n            '{' => {\n                open = Some((text[selector_start..index].trim().to_owned(), index + 1));\n                selector_start = index + 1;\n            }\n            '}' => {\n                if let Some((selector, body)) = open.take() {\n                    blocks.push((selector, text[body..index].to_owned()));\n                }\n                selector_start = index + 1;\n            }\n            ';' if open.is_none() => selector_start = index + 1,\n            _ => {}\n        }\n    }\n    blocks\n}\n\nfn css_length(value: &str) -> Option<Pixels> {\n    let value = value.trim();\n    let pixels = if let Some(rem) = value.strip_suffix(\"rem\") {\n        rem.trim().parse::<f32>().ok()? * 16.\n    } else if let Some(pixels) = value.strip_suffix(\"px\") {\n        pixels.trim().parse::<f32>().ok()?\n    } else {\n        value.parse::<f32>().ok().filter(|value| *value == 0.)?\n    };\n    (pixels.is_finite() && pixels >= 0.).then(|| px(pixels))\n}\n\n/// A CSS number or percentage; `percent` is the value of `100%`.\nfn css_number(value: &str, percent: f64) -> Option<f64> {\n    let value = value.trim();\n    let number = match value.strip_suffix('%') {\n        Some(value) => value.trim().parse::<f64>().ok()? / 100. * percent,\n        None => value\n            .strip_suffix(\"deg\")\n            .unwrap_or(value)\n            .parse::<f64>()\n            .ok()?,\n    };\n    number.is_finite().then_some(number)\n}\n\nfn css_color(value: &str) -> Option<Rgba> {\n    let value = value.trim().trim_end_matches(\"!important\").trim();\n    if let Some(hex) = value.strip_prefix('#') {\n        return css_hex(hex);\n    }\n    let (function, arguments) = match value.split_once('(') {\n        Some((function, arguments)) => (\n            function.trim().to_ascii_lowercase(),\n            arguments.strip_suffix(')')?,\n        ),\n        // shadcn v3 themes store bare HSL triplets such as `0 0% 100%`.\n        None => (\"hsl\".to_owned(), value),\n    };\n    let (channels, alpha) = match arguments.split_once('/') {\n        Some((channels, alpha)) => (channels, css_number(alpha, 1.)?),\n        None => (arguments, 1.),\n    };\n    let channels: Vec<_> = channels\n        .split(|character: char| character == ',' || character.is_whitespace())\n        .filter(|part| !part.is_empty())\n        .collect();\n    let (channels, alpha) = match channels.len() {\n        3 => (channels, alpha),\n        4 => (channels[..3].to_vec(), css_number(channels[3], 1.)?),\n        _ => return None,\n    };\n    let color = match function.as_str() {\n        \"oklch\" => oklch(\n            css_number(channels[0], 1.)?,\n            css_number(channels[1], 0.4)?,\n            css_number(channels[2], 1.)?,\n        ),\n        \"hsl\" | \"hsla\" => hsl(\n            css_number(channels[0], 1.)?,\n            css_number(channels[1], 1.)?,\n            css_number(channels[2], 1.)?,\n        ),\n        \"rgb\" | \"rgba\" => [\n            css_number(channels[0], 255.)? / 255.,\n            css_number(channels[1], 255.)? / 255.,\n            css_number(channels[2], 255.)? / 255.,\n        ],\n        _ => return None,\n    };\n    Some(Rgba {\n        r: color[0].clamp(0., 1.) as f32,\n        g: color[1].clamp(0., 1.) as f32,\n        b: color[2].clamp(0., 1.) as f32,\n        a: alpha.clamp(0., 1.) as f32,\n    })\n}\n\nfn css_hex(hex: &str) -> Option<Rgba> {\n    if !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) {\n        return None;\n    }\n    let digits: Vec<u8> = match hex.len() {\n        3 | 4 => hex\n            .chars()\n            .map(|digit| u8::from_str_radix(&format!(\"{digit}{digit}\"), 16).ok())\n            .collect::<Option<_>>()?,\n        6 | 8 => (0..hex.len())\n            .step_by(2)\n            .map(|index| u8::from_str_radix(&hex[index..index + 2], 16).ok())\n            .collect::<Option<_>>()?,\n        _ => return None,\n    };\n    Some(Rgba {\n        r: digits[0] as f32 / 255.,\n        g: digits[1] as f32 / 255.,\n        b: digits[2] as f32 / 255.,\n        a: digits.get(3).map_or(1., |alpha| *alpha as f32 / 255.),\n    })\n}\n\n/// OKLCH to gamma-encoded sRGB, matching the pinned Neutral conversion when chroma is 0.\nfn oklch(lightness: f64, chroma: f64, hue: f64) -> [f64; 3] {\n    let (a, b) = (\n        chroma * hue.to_radians().cos(),\n        chroma * hue.to_radians().sin(),\n    );\n    let l = (lightness + 0.396_337_777_4 * a + 0.215_803_757_3 * b).powi(3);\n    let m = (lightness - 0.105_561_345_8 * a - 0.063_854_172_8 * b).powi(3);\n    let s = (lightness - 0.089_484_177_5 * a - 1.291_485_548 * b).powi(3);\n    [\n        4.076_741_662_1 * l - 3.307_711_591_3 * m + 0.230_969_929_2 * s,\n        -1.268_438_004_6 * l + 2.609_757_401_1 * m - 0.341_319_396_5 * s,\n        -0.004_196_086_3 * l - 0.703_418_614_7 * m + 1.707_614_701 * s,\n    ]\n    .map(srgb_channel)\n}\n\nfn hsl(hue: f64, saturation: f64, lightness: f64) -> [f64; 3] {\n    let hue = hue.rem_euclid(360.) / 60.;\n    let chroma = (1. - (2. * lightness - 1.).abs()) * saturation;\n    let x = chroma * (1. - (hue % 2. - 1.).abs());\n    let (r, g, b) = match hue as u32 {\n        0 => (chroma, x, 0.),\n        1 => (x, chroma, 0.),\n        2 => (0., chroma, x),\n        3 => (0., x, chroma),\n        4 => (x, 0., chroma),\n        _ => (chroma, 0., x),\n    };\n    let base = lightness - chroma / 2.;\n    [r + base, g + base, b + base]\n}\n\nfn neutral_light_colors() -> UiColors {\n    UiColors {\n        background: neutral(1.0),\n        foreground: neutral(0.145),\n        card: neutral(1.0),\n        card_foreground: neutral(0.145),\n        popover: neutral(1.0),\n        popover_foreground: neutral(0.145),\n        primary: neutral(0.205),\n        primary_foreground: neutral(0.985),\n        secondary: neutral(0.97),\n        secondary_foreground: neutral(0.205),\n        muted: neutral(0.97),\n        muted_foreground: neutral(0.556),\n        accent: neutral(0.97),\n        accent_foreground: neutral(0.205),\n        destructive: srgb(0xdc, 0x26, 0x26),\n        destructive_foreground: neutral(0.985),\n        success: srgb(0x16, 0xa3, 0x4a),\n        success_foreground: neutral(0.985),\n        warning: srgb(0xd9, 0x77, 0x06),\n        warning_foreground: neutral(0.985),\n        info: srgb(0x25, 0x63, 0xeb),\n        info_foreground: neutral(0.985),\n        border: neutral(0.922),\n        input: neutral(0.922),\n        ring: neutral(0.708),\n        chart_1: neutral(0.87),\n        chart_2: neutral(0.556),\n        chart_3: neutral(0.439),\n        chart_4: neutral(0.371),\n        chart_5: neutral(0.269),\n        sidebar: neutral(0.985),\n        sidebar_foreground: neutral(0.145),\n        sidebar_primary: neutral(0.205),\n        sidebar_primary_foreground: neutral(0.985),\n        sidebar_accent: neutral(0.97),\n        sidebar_accent_foreground: neutral(0.205),\n        sidebar_border: neutral(0.922),\n        sidebar_ring: neutral(0.708),\n        overlay: black().alpha(0.10).into(),\n    }\n}\n\nfn neutral_dark_colors() -> UiColors {\n    UiColors {\n        background: neutral(0.145),\n        foreground: neutral(0.985),\n        card: neutral(0.205),\n        card_foreground: neutral(0.985),\n        popover: neutral(0.205),\n        popover_foreground: neutral(0.985),\n        primary: neutral(0.922),\n        primary_foreground: neutral(0.205),\n        secondary: neutral(0.269),\n        secondary_foreground: neutral(0.985),\n        muted: neutral(0.269),\n        muted_foreground: neutral(0.708),\n        accent: neutral(0.269),\n        accent_foreground: neutral(0.985),\n        destructive: srgb(0xff, 0x64, 0x67),\n        destructive_foreground: neutral(0.985),\n        success: srgb(0x4a, 0xde, 0x80),\n        success_foreground: neutral(0.145),\n        warning: srgb(0xfb, 0xbf, 0x24),\n        warning_foreground: neutral(0.145),\n        info: srgb(0x60, 0xa5, 0xfa),\n        info_foreground: neutral(0.145),\n        border: neutral_alpha(1.0, 0.10),\n        input: neutral_alpha(1.0, 0.15),\n        ring: neutral(0.556),\n        chart_1: neutral(0.87),\n        chart_2: neutral(0.556),\n        chart_3: neutral(0.439),\n        chart_4: neutral(0.371),\n        chart_5: neutral(0.269),\n        sidebar: neutral(0.205),\n        sidebar_foreground: neutral(0.985),\n        sidebar_primary: srgb(0x14, 0x47, 0xe6),\n        sidebar_primary_foreground: neutral(0.985),\n        sidebar_accent: neutral(0.269),\n        sidebar_accent_foreground: neutral(0.985),\n        sidebar_border: neutral_alpha(1.0, 0.10),\n        sidebar_ring: neutral(0.556),\n        // shadcn pairs black/10 with a backdrop blur GPUI lacks; without it a\n        // 10% scrim disappears on a near-black page.\n        overlay: black().alpha(0.50).into(),\n    }\n}\n\n#[doc(hidden)]\npub fn neutral(l: f64) -> Rgba {\n    neutral_alpha(l, 1.0)\n}\n\nfn neutral_alpha(l: f64, alpha: f64) -> Rgba {\n    let channel = srgb_channel(l * l * l) as f32;\n\n    Rgba {\n        r: channel,\n        g: channel,\n        b: channel,\n        a: alpha.clamp(0.0, 1.0) as f32,\n    }\n}\n\nconst fn srgb(red: u8, green: u8, blue: u8) -> Rgba {\n    Rgba {\n        r: red as f32 / 255.0,\n        g: green as f32 / 255.0,\n        b: blue as f32 / 255.0,\n        a: 1.0,\n    }\n}\n\nfn srgb_channel(channel: f64) -> f64 {\n    if channel >= 0.003_130_8 {\n        (1.055 * channel.powf(1.0 / 2.4) - 0.055).clamp(0.0, 1.0)\n    } else {\n        (12.92 * channel).clamp(0.0, 1.0)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn converts_pinned_neutral_tokens() {\n        assert_eq!(\n            neutral(1.0),\n            Rgba {\n                r: 1.0,\n                g: 1.0,\n                b: 1.0,\n                a: 1.0,\n            }\n        );\n        assert_eq!(\n            neutral(0.0),\n            Rgba {\n                r: 0.0,\n                g: 0.0,\n                b: 0.0,\n                a: 1.0,\n            }\n        );\n\n        assert_eq!(neutral_light_colors().destructive, srgb(0xdc, 0x26, 0x26));\n        assert_eq!(neutral_dark_colors().destructive, srgb(0xff, 0x64, 0x67));\n        assert_eq!(\n            neutral_dark_colors().sidebar_primary,\n            srgb(0x14, 0x47, 0xe6)\n        );\n    }\n\n    fn close(left: Rgba, right: Rgba) -> bool {\n        [\n            (left.r, right.r),\n            (left.g, right.g),\n            (left.b, right.b),\n            (left.a, right.a),\n        ]\n        .iter()\n        .all(|(left, right)| (left - right).abs() < 1.5 / 255.)\n    }\n\n    #[test]\n    fn imports_shadcn_theme_css() {\n        let css = r#\"\n            @layer base {\n              :root {\n                --radius: 0.5rem; /* 8px */\n                --background: oklch(1 0 0);\n                --foreground: oklch(0.145 0 0);\n                --primary: #2563eb;\n                --ring: rgb(37 99 235 / 50%);\n                --sidebar-background: 0 0% 98%;\n                --font-sans: Inter, sans-serif;\n              }\n              .dark {\n                --background: oklch(0.145 0 0);\n                --border: oklch(1 0 0 / 10%);\n                --sidebar-primary: oklch(0.488 0.243 264.376);\n                --chart-1: hsl(220 70% 50%);\n              }\n            }\n        \"#;\n        let light = UiTheme::from_shadcn_css(css, ThemeMode::Light).unwrap();\n        assert_eq!(light.mode, ThemeMode::Light);\n        assert_eq!(light.radius.lg, px(8.));\n        assert!(close(light.colors.background, neutral(1.0)));\n        assert!(close(light.colors.foreground, neutral(0.145)));\n        assert_eq!(light.colors.primary, srgb(0x25, 0x63, 0xeb));\n        assert!(close(\n            light.colors.ring,\n            Rgba {\n                a: 0.5,\n                ..srgb(37, 99, 235)\n            }\n        ));\n        assert!(close(light.colors.sidebar, neutral(0.985)));\n        // Variables the CSS leaves out keep the Neutral defaults.\n        assert_eq!(light.colors.muted, neutral_light_colors().muted);\n\n        let dark = UiTheme::from_shadcn_css(css, ThemeMode::Dark).unwrap();\n        assert_eq!(dark.mode, ThemeMode::Dark);\n        assert!(close(dark.colors.background, neutral(0.145)));\n        // `.dark` inherits `:root` values it does not override.\n        assert_eq!(dark.colors.primary, srgb(0x25, 0x63, 0xeb));\n        assert!(close(dark.colors.border, neutral_alpha(1.0, 0.10)));\n        assert!(close(dark.colors.sidebar_primary, srgb(0x14, 0x47, 0xe6)));\n        assert!(close(dark.colors.chart_1, srgb(0x26, 0x63, 0xd9)));\n\n        assert!(UiTheme::from_shadcn_css(\":root { --primary: blue; }\", ThemeMode::Light).is_err());\n        assert!(UiTheme::from_shadcn_css(\":root { --radius: 1em; }\", ThemeMode::Light).is_err());\n        assert!(UiTheme::from_shadcn_css(\":root { }\", ThemeMode::Dark).is_err());\n        assert_eq!(\n            css_hex(\"fff8\"),\n            Some(Rgba {\n                r: 1.,\n                g: 1.,\n                b: 1.,\n                a: 0x88 as f32 / 255.\n            })\n        );\n    }\n\n    #[test]\n    fn preserves_pinned_alpha_tokens() {\n        let dark = neutral_dark_colors();\n        assert_eq!(dark.border.a, 0.10);\n        assert_eq!(dark.input.a, 0.15);\n        assert_eq!(dark.sidebar_border.a, 0.10);\n        assert!((dark.input.opacity(0.30).a - 0.045).abs() < f32::EPSILON);\n    }\n\n    #[test]\n    fn derives_the_shadcn_radius_and_shadow_scales() {\n        let theme = UiTheme::neutral_light();\n        assert_eq!(theme.radius.sm, px(6.));\n        assert_eq!(theme.radius.md, px(8.));\n        assert_eq!(theme.radius.lg, px(10.));\n        assert_eq!(theme.radius.xl, px(14.));\n        assert_eq!(theme.shadows.sm.len(), 2);\n        assert_eq!(theme.shadows.md.len(), 2);\n        assert_eq!(theme.shadows.lg.len(), 2);\n    }\n\n    #[gpui_kit::test]\n    fn keeps_component_state_tokens_in_sync_with_ui_theme(cx: &mut gpui_kit::TestAppContext) {\n        cx.update(|cx| {\n            init(cx);\n            let component = gpui_kit::component::Theme::global(cx);\n            assert_eq!(component.mode, gpui_kit::component::ThemeMode::Light);\n            assert_eq!(\n                component.button_hover,\n                UiTheme::read(cx).colors.accent.into()\n            );\n\n            UiTheme::switch(cx, ThemeMode::Dark);\n            let component = gpui_kit::component::Theme::global(cx);\n            assert_eq!(component.mode, gpui_kit::component::ThemeMode::Dark);\n            assert_eq!(component.ring, UiTheme::read(cx).colors.ring.into());\n\n            let mut custom = UiTheme::read(cx).clone();\n            custom.colors.accent = srgb(0x22, 0x66, 0xaa);\n            custom.fonts.body = \"Custom Sans\".into();\n            custom.radius = UiRadius::new(px(12.));\n            let expected_accent = custom.colors.accent;\n            UiTheme::set(cx, custom);\n\n            let component = gpui_kit::component::Theme::global(cx);\n            assert_eq!(component.button_hover, expected_accent.into());\n            assert_eq!(component.font_family, \"Custom Sans\");\n            assert_eq!(component.radius, px(9.6));\n            assert_eq!(\n                component.tokens.button_hover.background,\n                expected_accent.into()\n            );\n\n            init(cx);\n            assert_eq!(\n                gpui_kit::component::Theme::global(cx).button_hover,\n                expected_accent.into()\n            );\n        });\n    }\n}\n\n// Refine the actual control after default styling; no extra layout or focus node.\n#[doc(hidden)]\npub fn apply_style<T: Styled>(mut element: T, style: &gpui_kit::StyleRefinement) -> T {\n    use gpui_kit::Refineable as _;\n    element.style().refine(style);\n    element\n}\n\n#[derive(gpui_kit::IntoElement)]\n#[doc(hidden)]\npub struct DisclosureIcon {\n    icon: gpui_kit::Svg,\n    open: bool,\n}\n#[doc(hidden)]\npub fn disclosure_icon(icon: gpui_kit::Svg, open: bool) -> DisclosureIcon {\n    DisclosureIcon { icon, open }\n}\nimpl gpui_kit::RenderOnce for DisclosureIcon {\n    fn render(self, window: &mut Window, cx: &mut App) -> impl gpui_kit::IntoElement {\n        let duration = UiTheme::read(cx).motion.fast;\n        let value = transition_value(\n            \"disclosure-rotation\",\n            if self.open { 1. } else { 0. },\n            duration,\n            window,\n            cx,\n        );\n        self.icon\n            .with_transformation(gpui_kit::Transformation::rotate(gpui_kit::radians(\n                value * std::f32::consts::PI,\n            )))\n    }\n}\n\n#[cfg(test)]\nmod motion_tests {\n    use super::*;\n    use gpui_kit::{\n        AppContext as _, Context, IntoElement, Render, TestAppContext, VisualTestContext, div,\n    };\n\n    struct View {\n        target: f32,\n        sampled: f32,\n    }\n    impl Render for View {\n        fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {\n            self.sampled = transition_value(\n                \"test-motion\",\n                self.target,\n                Duration::from_millis(200),\n                window,\n                cx,\n            );\n            div()\n        }\n    }\n\n    #[gpui_kit::test]\n    fn theme_reduced_motion_snaps_an_active_upstream_transition(cx: &mut TestAppContext) {\n        cx.update(init);\n        let window = cx.add_window(|_, _| View {\n            target: 0.,\n            sampled: 0.,\n        });\n        let mut visual = VisualTestContext::from_window(window.into(), cx);\n        visual.update(|window, cx| window.draw(cx).clear(cx));\n        window\n            .update(cx, |view, _, cx| {\n                view.target = 1.;\n                cx.notify();\n            })\n            .unwrap();\n        visual.update(|window, cx| window.draw(cx).clear(cx));\n        assert_eq!(\n            cx.read_window(&window, |view, cx| view.read(cx).sampled)\n                .unwrap(),\n            0.\n        );\n        cx.update(|cx| {\n            let mut theme = UiTheme::read(cx).clone();\n            theme.motion.reduced = true;\n            UiTheme::set(cx, theme);\n        });\n        visual.update(|window, cx| window.draw(cx).clear(cx));\n        assert_eq!(\n            cx.read_window(&window, |view, cx| view.read(cx).sampled)\n                .unwrap(),\n            1.\n        );\n    }\n}\n\n/// Shared focus traversal for the tab, radio and toggle collections.\n#[doc(hidden)]\npub struct RovingFocus {\n    pub handles: Vec<Option<gpui_kit::FocusHandle>>,\n    active: gpui_kit::Entity<Option<gpui_kit::FocusHandle>>,\n}\nimpl RovingFocus {\n    pub fn new(\n        id: gpui_kit::ElementId,\n        items: &[(gpui_kit::ElementId, bool)],\n        selected: Option<usize>,\n        window: &mut gpui_kit::Window,\n        cx: &mut App,\n    ) -> Self {\n        let active =\n            window.use_keyed_state((id, \"active\"), cx, |_, _| None::<gpui_kit::FocusHandle>);\n        let mut handles: Vec<_> = items\n            .iter()\n            .map(|(id, disabled)| {\n                let handle = window\n                    .use_keyed_state((id.clone(), \"focus\"), cx, |_, cx| cx.focus_handle())\n                    .read(cx)\n                    .clone();\n                handle.clone().tab_stop(false);\n                (!disabled).then_some(handle)\n            })\n            .collect();\n        let index = handles\n            .iter()\n            .position(|h| h.as_ref().is_some_and(|h| h.is_focused(window)))\n            .or(selected.filter(|&i| handles.get(i).is_some_and(Option::is_some)))\n            .or_else(|| {\n                handles\n                    .iter()\n                    .position(|h| h.is_some() && h.as_ref() == active.read(cx).as_ref())\n            })\n            .or_else(|| handles.iter().position(Option::is_some));\n        if let Some(index) = index {\n            let handle = handles[index].as_ref().unwrap().clone().tab_stop(true);\n            handles[index] = Some(handle.clone());\n            active.update(cx, |value, _| *value = Some(handle));\n        }\n        Self { handles, active }\n    }\n    pub fn key(\n        &self,\n        event: &gpui_kit::KeyDownEvent,\n        axis: Option<gpui_kit::Axis>,\n        window: &mut gpui_kit::Window,\n        cx: &mut App,\n    ) -> Option<usize> {\n        if event.keystroke.modifiers.modified() {\n            return None;\n        }\n        let enabled: Vec<_> = self\n            .handles\n            .iter()\n            .enumerate()\n            .filter_map(|(i, h)| h.as_ref().map(|h| (i, h)))\n            .collect();\n        let current = enabled.iter().position(|(_, h)| h.is_focused(window))?;\n        let next = match event.keystroke.key.as_str() {\n            \"home\" => 0,\n            \"end\" => enabled.len() - 1,\n            \"left\" if axis != Some(gpui_kit::Axis::Vertical) => {\n                (current + enabled.len() - 1) % enabled.len()\n            }\n            \"right\" if axis != Some(gpui_kit::Axis::Vertical) => (current + 1) % enabled.len(),\n            \"up\" if axis != Some(gpui_kit::Axis::Horizontal) => {\n                (current + enabled.len() - 1) % enabled.len()\n            }\n            \"down\" if axis != Some(gpui_kit::Axis::Horizontal) => (current + 1) % enabled.len(),\n            _ => return None,\n        };\n        enabled[current].1.clone().tab_stop(false);\n        let handle = enabled[next].1.clone().tab_stop(true);\n        handle.focus(window, cx);\n        self.active.update(cx, |value, _| *value = Some(handle));\n        window.refresh();\n        cx.stop_propagation();\n        Some(enabled[next].0)\n    }\n}\n"
    },
    {
      "path": "registry/avatar/avatar.rs",
      "type": "registry:file",
      "target": "~/src/ui/avatar.rs",
      "content": "//! Nova-styled Avatar with native image loading and error fallback.\n\nuse gpui_kit::{\n    App, ElementId, ImageSource, InteractiveElement as _, IntoElement, ObjectFit, ParentElement,\n    RenderOnce, Rgba, SharedString, StatefulInteractiveElement as _, Styled, StyledImage, Window,\n    div, px,\n};\n\nuse gpui_kit::base::TestSupportExt as _;\n\nuse super::theme::{UiTheme, primitives::clipped_image_with};\n\n/// A compact, circular Avatar with an optional image and fallback content.\n#[derive(IntoElement)]\npub struct Avatar {\n    style: gpui_kit::StyleRefinement,\n    id: ElementId,\n    image: Option<ImageSource>,\n    fallback: SharedString,\n    aria_label: Option<SharedString>,\n    size: AvatarSize,\n    status: Option<AvatarStatus>,\n    ring_color: Option<Rgba>,\n}\n\n/// A presence state drawn as a dot over the avatar's bottom-right corner.\n///\n/// The colors come from the theme: `success` for online, `warning` for away,\n/// `destructive` for do not disturb, and a hollow `muted_foreground` ring for\n/// offline. Set those tokens to change every presence dot at once.\n#[derive(Clone, Debug, PartialEq)]\npub enum AvatarStatus {\n    /// Active now: a filled `success` dot.\n    Online,\n    /// Idle or away: a filled `warning` dot.\n    Away,\n    /// Do not disturb: a filled `destructive` dot.\n    Dnd,\n    /// Not connected: a hollow ring, so it reads without color.\n    Offline,\n    /// An app-defined state with its own color and the words read after the name.\n    Custom {\n        /// The dot fill.\n        color: Rgba,\n        /// The status read after the accessible name, such as \"in a meeting\".\n        label: SharedString,\n    },\n}\n\nimpl AvatarStatus {\n    /// The words appended to the avatar's accessible name.\n    pub fn label(&self) -> SharedString {\n        match self {\n            Self::Online => \"online\".into(),\n            Self::Away => \"away\".into(),\n            Self::Dnd => \"do not disturb\".into(),\n            Self::Offline => \"offline\".into(),\n            Self::Custom { label, .. } => label.clone(),\n        }\n    }\n}\n\n#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]\n/// Nova avatar dimensions.\npub enum AvatarSize {\n    /// Compact dimensions.\n    Sm,\n    #[default]\n    /// The default Nova presentation.\n    Default,\n    /// Large dimensions.\n    Lg,\n}\n\nimpl Avatar {\n    /// Creates a `Avatar` with a stable caller-owned ID.\n    pub fn new(id: impl Into<ElementId>) -> Self {\n        Self {\n            style: gpui_kit::StyleRefinement::default(),\n            id: id.into(),\n            image: None,\n            fallback: SharedString::default(),\n            aria_label: None,\n            size: AvatarSize::Default,\n            status: None,\n            ring_color: None,\n        }\n    }\n\n    /// Sets the text shown while the image is unavailable.\n    pub fn fallback(mut self, text: impl Into<SharedString>) -> Self {\n        self.fallback = text.into();\n        self\n    }\n    /// Sets the avatar image source.\n    pub fn image(mut self, source: impl Into<ImageSource>) -> Self {\n        self.image = Some(source.into());\n        self\n    }\n    /// Sets the accessible name of the control.\n    pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {\n        self.aria_label = Some(label.into());\n        self\n    }\n    /// Selects the Nova control dimensions.\n    pub fn size(mut self, size: AvatarSize) -> Self {\n        self.size = size;\n        self\n    }\n    /// Draws a presence dot at the bottom-right corner, sized to the avatar,\n    /// and appends the status to the accessible name (\"Ana Lopez, away\").\n    pub fn status(mut self, status: AvatarStatus) -> Self {\n        self.status = Some(status);\n        self\n    }\n    /// Sets the ring drawn behind the presence dot. It should match the\n    /// surface under the avatar; the default is the `background` token, so\n    /// pass `colors.sidebar` for avatars in a sidebar.\n    pub fn ring_color(mut self, color: impl Into<Rgba>) -> Self {\n        self.ring_color = Some(color.into());\n        self\n    }\n}\n\n/// The presence dot and its surface ring, sized for an avatar of `size` pixels.\nfn status_dot(status: &AvatarStatus, size: f32, ring: Rgba, theme: &UiTheme) -> gpui_kit::Div {\n    // 8, 10 and 12px dots for the 24, 32 and 40px avatars.\n    let diameter = theme.space(size / 16. + 0.5);\n    let ring_width = px(if size < 32. { 1.5 } else { 2. });\n    let dot = div().size_full().rounded_full();\n    let dot = match status {\n        AvatarStatus::Online => dot.bg(theme.colors.success),\n        AvatarStatus::Away => dot.bg(theme.colors.warning),\n        AvatarStatus::Dnd => dot.bg(theme.colors.destructive),\n        AvatarStatus::Custom { color, .. } => dot.bg(*color),\n        AvatarStatus::Offline => dot\n            .bg(ring)\n            .border(ring_width)\n            .border_color(theme.colors.muted_foreground),\n    };\n    div()\n        .debug_selector(|| \"avatar-status\".into())\n        .absolute()\n        .right(px(-1.))\n        .bottom(px(-1.))\n        .size(diameter + ring_width * 2.)\n        .p(ring_width)\n        .rounded_full()\n        .bg(ring)\n        .child(dot)\n}\n\nimpl RenderOnce for Avatar {\n    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {\n        let theme = UiTheme::read(cx).clone();\n        let size = match self.size {\n            AvatarSize::Sm => 24.,\n            AvatarSize::Default => 32.,\n            AvatarSize::Lg => 40.,\n        };\n        let image_size = theme.space(size / 4.);\n        let dot = self.status.as_ref().map(|status| {\n            status_dot(\n                status,\n                size,\n                self.ring_color.unwrap_or(theme.colors.background),\n                &theme,\n            )\n        });\n        let label = match (self.aria_label, &self.status) {\n            (Some(label), Some(status)) => Some(format!(\"{label}, {}\", status.label()).into()),\n            (label, _) => label,\n        };\n        let frame_theme = theme.clone();\n        let fallback = move || {\n            div()\n                .size_full()\n                .flex()\n                .items_center()\n                .justify_center()\n                .rounded_full()\n                .bg(theme.colors.muted)\n                .text_color(theme.colors.muted_foreground)\n                .text_size(theme.text(if self.size == AvatarSize::Sm {\n                    12.\n                } else {\n                    14.\n                }))\n                .child(self.fallback.clone())\n                .into_any_element()\n        };\n        let root = match self.image {\n            Some(source) => {\n                let loading = fallback.clone();\n                clipped_image_with(\n                    source,\n                    image_size / 2.,\n                    ObjectFit::Cover,\n                    &frame_theme,\n                    |image| image.with_loading(loading).with_fallback(fallback),\n                )\n            }\n            None => div().rounded_full().overflow_hidden().child(fallback()),\n        }\n        .size_full();\n        // The dot sits outside the circular clip, so the frame is a plain box.\n        let root = div()\n            .id(self.id)\n            .test_support()\n            .relative()\n            .size(image_size)\n            .flex_shrink_0()\n            .child(root)\n            .children(dot);\n        let root = if let Some(label) = label {\n            root.aria_label(label)\n        } else {\n            root\n        };\n        super::theme::apply_style(root, &self.style)\n    }\n}\n\nimpl gpui_kit::Styled for Avatar {\n    fn style(&mut self) -> &mut gpui_kit::StyleRefinement {\n        &mut self.style\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use gpui_kit::{\n        Context, Render, TestAppContext, VisualTestContext, prelude::FluentBuilder as _,\n        test::TestQueryExt as _,\n    };\n\n    struct People {\n        status: Option<AvatarStatus>,\n        size: AvatarSize,\n    }\n\n    impl Render for People {\n        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {\n            div().p(px(8.)).child(\n                Avatar::new(\"ana\")\n                    .aria_label(\"Ana Lopez\")\n                    .fallback(\"AL\")\n                    .size(self.size)\n                    .when_some(self.status.clone(), |avatar, status| avatar.status(status)),\n            )\n        }\n    }\n\n    fn draw(\n        status: Option<AvatarStatus>,\n        size: AvatarSize,\n        cx: &mut TestAppContext,\n    ) -> VisualTestContext {\n        cx.update(super::super::theme::init);\n        let window = cx.add_window(move |_, _| People { status, size });\n        let mut cx = VisualTestContext::from_window(window.into(), cx);\n        cx.update(|window, cx| window.draw(cx).clear(cx));\n        cx\n    }\n\n    #[gpui_kit::test]\n    fn the_status_joins_the_accessible_name(cx: &mut TestAppContext) {\n        let mut cx = draw(Some(AvatarStatus::Away), AvatarSize::Default, cx);\n        cx.update(|window, _| {\n            window.find_by_label(\"Ana Lopez, away\");\n        });\n        assert!(cx.debug_bounds(\"avatar-status\").is_some());\n    }\n\n    #[gpui_kit::test]\n    fn no_status_draws_no_dot(cx: &mut TestAppContext) {\n        let mut cx = draw(None, AvatarSize::Default, cx);\n        cx.update(|window, _| {\n            window.find_by_label(\"Ana Lopez\");\n        });\n        assert!(cx.debug_bounds(\"avatar-status\").is_none());\n    }\n\n    #[gpui_kit::test]\n    fn the_dot_scales_with_the_avatar_and_sits_on_its_corner(cx: &mut TestAppContext) {\n        for (size, avatar, dot) in [\n            (AvatarSize::Sm, 24., 8. + 3.),\n            (AvatarSize::Default, 32., 10. + 4.),\n            (AvatarSize::Lg, 40., 12. + 4.),\n        ] {\n            let mut cx = draw(Some(AvatarStatus::Offline), size, cx);\n            let bounds = cx.debug_bounds(\"avatar-status\").expect(\"the dot is drawn\");\n            assert_eq!(bounds.size.width, px(dot), \"{size:?}\");\n            // Bottom-right corner of the avatar at (8, 8), overhanging by 1px.\n            assert_eq!(bounds.right(), px(8. + avatar + 1.), \"{size:?}\");\n            assert_eq!(bounds.bottom(), px(8. + avatar + 1.), \"{size:?}\");\n        }\n    }\n\n    #[test]\n    fn statuses_read_as_words() {\n        assert_eq!(AvatarStatus::Dnd.label(), \"do not disturb\");\n        let custom = AvatarStatus::Custom {\n            color: gpui_kit::Rgba::default(),\n            label: \"in a meeting\".into(),\n        };\n        assert_eq!(custom.label(), \"in a meeting\");\n    }\n}\n"
    },
    {
      "path": "registry/button/button.rs",
      "type": "registry:file",
      "target": "~/src/ui/button.rs",
      "content": "//! The shadcn Nova Button visual port.\n//!\n//! Visual source: shadcn/ui 4.19.0 `button.tsx` and `style-nova.css` at\n//! `1773ecfeeb4a04366978d353e69b5c7ded78dcb2`. Interaction comes from the\n//! GPUI Kit headless button.\n\nuse std::rc::Rc;\n\nuse gpui_kit::base::Button as BaseButton;\nuse gpui_kit::{\n    AnyElement, App, ClickEvent, ElementId, FontWeight, IntoElement, ParentElement, RenderOnce,\n    SharedString, Styled, Window, px,\n};\n\nuse super::theme::{\n    UiRadius, UiTheme,\n    primitives::{disabled as disabled_style, focus},\n};\n\ntype ButtonClickHandler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;\ntype ButtonStyleHandler = Rc<dyn Fn(BaseButton) -> BaseButton>;\n\n/// The pinned shadcn Button visual variant.\n#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]\npub enum ButtonVariant {\n    /// Primary action.\n    #[default]\n    Default,\n    /// Bordered action.\n    Outline,\n    /// Secondary action.\n    Secondary,\n    /// Low-emphasis action.\n    Ghost,\n    /// Destructive action.\n    Destructive,\n    /// Link-styled action.\n    Link,\n}\n\n/// The pinned shadcn Button size.\n#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]\npub enum ButtonSize {\n    /// 24px-high extra-small button.\n    Xs,\n    /// 28px-high small button.\n    Sm,\n    /// 32px-high default button.\n    #[default]\n    Default,\n    /// 36px-high large button.\n    Lg,\n    /// 24px square icon button.\n    IconXs,\n    /// 28px square icon button.\n    IconSm,\n    /// 32px square icon button.\n    Icon,\n    /// 36px square icon button.\n    IconLg,\n}\n\n#[derive(Clone, Copy, Debug, PartialEq)]\nstruct ButtonMetrics {\n    height: f32,\n    horizontal_padding: f32,\n    gap: f32,\n    radius: f32,\n    text_size: f32,\n    icon_only: bool,\n}\n\nimpl ButtonSize {\n    fn metrics(self, radii: UiRadius) -> ButtonMetrics {\n        let radius = f32::from(radii.lg);\n        let radius_md = f32::from(radii.md);\n        match self {\n            Self::Xs => ButtonMetrics::new(24.0, 8.0, 4.0, radius_md.min(10.0), 12.0, false),\n            Self::Sm => ButtonMetrics::new(28.0, 10.0, 4.0, radius_md.min(12.0), 12.8, false),\n            Self::Default => ButtonMetrics::new(32.0, 10.0, 6.0, radius, 14.0, false),\n            Self::Lg => ButtonMetrics::new(36.0, 10.0, 6.0, radius, 14.0, false),\n            Self::IconXs => ButtonMetrics::new(24.0, 0.0, 0.0, radius_md.min(10.0), 14.0, true),\n            Self::IconSm => ButtonMetrics::new(28.0, 0.0, 0.0, radius_md.min(12.0), 14.0, true),\n            Self::Icon => ButtonMetrics::new(32.0, 0.0, 0.0, radius, 14.0, true),\n            Self::IconLg => ButtonMetrics::new(36.0, 0.0, 0.0, radius, 14.0, true),\n        }\n    }\n}\n\nimpl ButtonMetrics {\n    const fn new(\n        height: f32,\n        horizontal_padding: f32,\n        gap: f32,\n        radius: f32,\n        text_size: f32,\n        icon_only: bool,\n    ) -> Self {\n        Self {\n            height,\n            horizontal_padding,\n            gap,\n            radius,\n            text_size,\n            icon_only,\n        }\n    }\n}\n\n/// A styled Button that keeps GPUI Kit's pointer and keyboard behavior.\n#[derive(IntoElement)]\npub struct Button {\n    style: gpui_kit::StyleRefinement,\n    id: ElementId,\n    variant: ButtonVariant,\n    size: ButtonSize,\n    disabled: bool,\n    aria_label: Option<SharedString>,\n    on_click: Option<ButtonClickHandler>,\n    style_with: Option<ButtonStyleHandler>,\n    children: Vec<AnyElement>,\n}\n\nimpl Button {\n    /// Creates a Button with a caller-owned stable ID.\n    pub fn new(id: impl Into<ElementId>) -> Self {\n        Self {\n            style: gpui_kit::StyleRefinement::default(),\n            id: id.into(),\n            variant: ButtonVariant::default(),\n            size: ButtonSize::default(),\n            disabled: false,\n            aria_label: None,\n            on_click: None,\n            style_with: None,\n            children: Vec::new(),\n        }\n    }\n\n    /// Sets the visual variant.\n    pub fn variant(mut self, variant: ButtonVariant) -> Self {\n        self.variant = variant;\n        self\n    }\n\n    /// Sets the visual size.\n    pub fn size(mut self, size: ButtonSize) -> Self {\n        self.size = size;\n        self\n    }\n\n    /// Prevents activation and removes the Button from tab order.\n    pub fn disabled(mut self, disabled: bool) -> Self {\n        self.disabled = disabled;\n        self\n    }\n\n    /// Sets the accessible name for icon or custom content. Use `label` for plain text.\n    pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {\n        self.aria_label = Some(label.into());\n        self\n    }\n\n    /// Adds visible text and uses it as the accessible name unless one was supplied.\n    pub fn label(mut self, label: impl Into<SharedString>) -> Self {\n        let label = label.into();\n        self.aria_label.get_or_insert_with(|| label.clone());\n        self.children.push(label.into_any_element());\n        self\n    }\n\n    /// Runs for pointer, Enter, or Space activation.\n    pub fn on_click(\n        mut self,\n        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,\n    ) -> Self {\n        self.on_click = Some(Rc::new(handler));\n        self\n    }\n\n    /// Replaces the visual styling while retaining button interaction.\n    /// Include a visible keyboard focus treatment in custom styles.\n    pub fn style_with(mut self, handler: impl Fn(BaseButton) -> BaseButton + 'static) -> Self {\n        self.style_with = Some(Rc::new(handler));\n        self\n    }\n}\n\nimpl ParentElement for Button {\n    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {\n        self.children.extend(elements);\n    }\n}\n\nimpl RenderOnce for Button {\n    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {\n        let theme = UiTheme::read(cx).clone();\n        let variant = self.variant;\n        let size = self.size;\n        let root = BaseButton::new(self.id).disabled(self.disabled);\n        let root = match self.style_with {\n            Some(style) => style(root),\n            None => style_button(root, self.disabled, variant, size, &theme),\n        };\n        let mut root = super::theme::apply_style(root, &self.style).children(self.children);\n\n        if let Some(label) = self.aria_label {\n            root = root.accessibility_label(label);\n        }\n        if let Some(handler) = self.on_click {\n            root = root.on_click(move |event, window, cx| handler(event, window, cx));\n        }\n\n        root\n    }\n}\n\n#[doc(hidden)]\npub fn style_button<T: Styled + gpui_kit::InteractiveElement + gpui_kit::prelude::FluentBuilder>(\n    base: T,\n    disabled: bool,\n    variant: ButtonVariant,\n    size: ButtonSize,\n    theme: &UiTheme,\n) -> T {\n    let colors = theme.colors;\n    let control = theme.control();\n    let metrics = size.metrics(theme.radius);\n    let invalid_ring = variant == ButtonVariant::Destructive;\n    let focus_theme = theme.clone();\n\n    let base = base\n        .flex()\n        .flex_shrink_0()\n        .items_center()\n        .justify_center()\n        .whitespace_nowrap()\n        .h(theme.space(metrics.height / 4.))\n        .gap(theme.space(metrics.gap / 4.))\n        .rounded(px(metrics.radius))\n        .border_1()\n        .border_color(colors.background.opacity(0.0))\n        .font_family(theme.fonts.body.clone())\n        .font_weight(FontWeight::MEDIUM)\n        .text_size(theme.text(metrics.text_size))\n        .focus_visible(move |style| focus::ring_for(style, &focus_theme, invalid_ring))\n        .when(metrics.icon_only, |base| {\n            base.w(theme.space(metrics.height / 4.)).p_0()\n        })\n        .when(!metrics.icon_only, |base| {\n            base.px(theme.space(metrics.horizontal_padding / 4.))\n        });\n\n    let base = match variant {\n        ButtonVariant::Default => base\n            .bg(colors.primary)\n            .text_color(colors.primary_foreground)\n            .when(!disabled, |base| {\n                base.hover(move |style| style.bg(colors.primary.opacity(0.80)))\n            }),\n        ButtonVariant::Outline => base\n            .bg(control.background)\n            .text_color(colors.foreground)\n            .border_color(control.border)\n            .when(!disabled, |base| {\n                base.hover(move |style| style.bg(control.hover).text_color(colors.foreground))\n            }),\n        ButtonVariant::Secondary => {\n            let hover = colors\n                .secondary\n                .blend(colors.secondary_foreground.opacity(0.08));\n            base.bg(colors.secondary)\n                .text_color(colors.secondary_foreground)\n                .when(!disabled, |base| base.hover(move |style| style.bg(hover)))\n        }\n        ButtonVariant::Ghost => base\n            .bg(colors.background.opacity(0.0))\n            .text_color(colors.foreground)\n            .when(!disabled, |base| {\n                base.hover(move |style| style.bg(control.ghost_hover).text_color(colors.foreground))\n            }),\n        ButtonVariant::Destructive => base\n            .bg(control.destructive_background)\n            .text_color(colors.destructive)\n            .when(!disabled, |base| {\n                base.hover(move |style| style.bg(control.destructive_hover))\n            }),\n        ButtonVariant::Link => base\n            .bg(colors.background.opacity(0.0))\n            .text_color(colors.primary)\n            .when(!disabled, |base| base.hover(|style| style.underline())),\n    };\n\n    disabled_style::interactive(base, disabled)\n}\n\nimpl gpui_kit::Styled for Button {\n    fn style(&mut self) -> &mut gpui_kit::StyleRefinement {\n        &mut self.style\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn all_variants_expose_clickable_and_disabled_cursors() {\n        for theme in [UiTheme::neutral_light(), UiTheme::neutral_dark()] {\n            for variant in [\n                ButtonVariant::Default,\n                ButtonVariant::Outline,\n                ButtonVariant::Secondary,\n                ButtonVariant::Ghost,\n                ButtonVariant::Destructive,\n                ButtonVariant::Link,\n            ] {\n                for disabled in [false, true] {\n                    let mut button = style_button(\n                        gpui_kit::div(),\n                        disabled,\n                        variant,\n                        ButtonSize::Default,\n                        &theme,\n                    );\n                    assert_eq!(\n                        button.style().mouse_cursor,\n                        Some(if disabled {\n                            gpui_kit::CursorStyle::OperationNotAllowed\n                        } else {\n                            gpui_kit::CursorStyle::PointingHand\n                        })\n                    );\n                }\n            }\n        }\n    }\n\n    #[test]\n    fn nova_sizes_are_exact() {\n        let sizes = [\n            (ButtonSize::Xs, 24.0, 8.0, 4.0, 8.0),\n            (ButtonSize::Sm, 28.0, 10.0, 4.0, 8.0),\n            (ButtonSize::Default, 32.0, 10.0, 6.0, 10.0),\n            (ButtonSize::Lg, 36.0, 10.0, 6.0, 10.0),\n            (ButtonSize::IconXs, 24.0, 0.0, 0.0, 8.0),\n            (ButtonSize::IconSm, 28.0, 0.0, 0.0, 8.0),\n            (ButtonSize::Icon, 32.0, 0.0, 0.0, 10.0),\n            (ButtonSize::IconLg, 36.0, 0.0, 0.0, 10.0),\n        ];\n\n        for (size, height, padding, gap, radius) in sizes {\n            let metrics = size.metrics(UiRadius::new(px(10.0)));\n            assert_eq!(metrics.height, height);\n            assert_eq!(metrics.horizontal_padding, padding);\n            assert_eq!(metrics.gap, gap);\n            assert_eq!(metrics.radius, radius);\n        }\n    }\n}\n"
    },
    {
      "path": "registry/tooltip/tooltip.rs",
      "type": "registry:file",
      "target": "~/src/ui/tooltip.rs",
      "content": "//! Nova tooltips for pointer hover and keyboard focus.\nuse super::theme::{UiTheme, primitives::surface};\nuse gpui_kit::base::{Button, ElementExt as _, Tooltip, TooltipOverlay, TooltipRequest};\nuse gpui_kit::{\n    AnyWindowHandle, App, AppContext as _, Bounds, Context, ElementId, Entity, FocusHandle, Global,\n    InteractiveElement as _, ParentElement as _, Pixels, SharedString,\n    StatefulInteractiveElement as _, Subscription, WeakEntity, Window, div,\n};\n\n/// Adds a tooltip to one existing button, preserving a single focus target.\n/// The button must retain its own accessible name.\npub fn tooltip(\n    id: impl Into<ElementId>,\n    label: impl Into<SharedString>,\n    trigger: Button,\n    window: &mut Window,\n    cx: &mut App,\n) -> gpui_kit::Div {\n    let state = window.use_keyed_state(id.into(), cx, TooltipState::new);\n    state.update(cx, |state, _| state.label = label.into());\n    let focus = state.read(cx).focus.clone();\n    let overlay = state.read(cx).overlay.clone();\n    let hover = state.clone();\n    let measured = state.clone();\n    div()\n        .child(\n            trigger\n                .track_focus(&focus)\n                .on_prepaint(move |bounds, _, cx| {\n                    measured.update(cx, |state, _| state.bounds = bounds)\n                })\n                .on_hover(move |hovered, window, cx| {\n                    hover.update(cx, |state, cx| {\n                        state.hovered = *hovered;\n                        state.update_visibility(window, cx);\n                    })\n                }),\n        )\n        .child(overlay)\n}\n\nstruct TooltipState {\n    focus: FocusHandle,\n    overlay: Entity<TooltipOverlay>,\n    bounds: Bounds<Pixels>,\n    label: SharedString,\n    hovered: bool,\n    active: bool,\n    dismissed: bool,\n    _subscriptions: [Subscription; 2],\n}\n\n/// One Escape interceptor for every tooltip in the application, instead of\n/// one keystroke hook per tooltip. Active tooltips register here while shown.\n#[derive(Default)]\nstruct EscapeDismissal {\n    shown: Vec<(AnyWindowHandle, WeakEntity<TooltipState>)>,\n    _subscription: Option<Subscription>,\n}\nimpl Global for EscapeDismissal {}\nimpl EscapeDismissal {\n    fn install(cx: &mut App) {\n        if cx.default_global::<Self>()._subscription.is_some() {\n            return;\n        }\n        let subscription = cx.intercept_keystrokes(|event, window, cx| {\n            if event.keystroke.key != \"escape\" {\n                return;\n            }\n            let handle = window.window_handle();\n            let shown = std::mem::take(&mut cx.global_mut::<Self>().shown);\n            let mut dismissed = false;\n            for (owner, state) in shown {\n                if owner != handle {\n                    cx.global_mut::<Self>().shown.push((owner, state));\n                    continue;\n                }\n                state\n                    .update(cx, |state, cx| {\n                        if state.active {\n                            state.overlay.update(cx, |overlay, cx| overlay.hide(cx));\n                            state.active = false;\n                            state.dismissed = true;\n                            dismissed = true;\n                        }\n                    })\n                    .ok();\n            }\n            if dismissed {\n                cx.stop_propagation();\n            }\n        });\n        cx.global_mut::<Self>()._subscription = Some(subscription);\n    }\n    fn shown(window: &Window, state: WeakEntity<TooltipState>, cx: &mut App) {\n        let handle = window.window_handle();\n        let shown = &mut cx.global_mut::<Self>().shown;\n        shown.retain(|(_, other)| {\n            other.upgrade().is_some() && other.entity_id() != state.entity_id()\n        });\n        shown.push((handle, state));\n    }\n    fn hidden(state: &WeakEntity<TooltipState>, cx: &mut App) {\n        cx.global_mut::<Self>()\n            .shown\n            .retain(|(_, other)| other.entity_id() != state.entity_id());\n    }\n}\n\nimpl TooltipState {\n    fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {\n        EscapeDismissal::install(cx);\n        let focus = cx.focus_handle();\n        let subscriptions = [\n            cx.on_focus(&focus, window, |state, window, cx| {\n                state.update_visibility(window, cx)\n            }),\n            cx.on_blur(&focus, window, |state, window, cx| {\n                state.update_visibility(window, cx)\n            }),\n        ];\n        Self {\n            focus,\n            overlay: cx.new(|_| TooltipOverlay::new()),\n            bounds: Bounds::default(),\n            label: SharedString::default(),\n            hovered: false,\n            active: false,\n            dismissed: false,\n            _subscriptions: subscriptions,\n        }\n    }\n    fn update_visibility(&mut self, window: &mut Window, cx: &mut Context<Self>) {\n        let active = self.hovered || self.focus.is_focused(window);\n        if !active {\n            self.dismissed = false;\n        }\n        let active = active && !self.dismissed;\n        if active == self.active {\n            return;\n        }\n        self.active = active;\n        if active {\n            let label = self.label.clone();\n            let request =\n                TooltipRequest::new(self.bounds, move |_, cx| text_tooltip(label.clone(), cx));\n            self.overlay\n                .update(cx, |overlay, cx| overlay.request_show(request, window, cx));\n            EscapeDismissal::shown(window, cx.weak_entity(), cx);\n        } else {\n            self.overlay.update(cx, |overlay, cx| overlay.hide(cx));\n            EscapeDismissal::hidden(&cx.weak_entity(), cx);\n        }\n    }\n}\n\n/// Creates a tooltip view for GPUI's native `.tooltip(...)` attachment point.\n/// Use this on an existing interactive control to avoid nesting another button\n/// and tab stop. The host control must retain its own full accessible name.\npub fn text_tooltip(label: gpui_kit::SharedString, cx: &mut App) -> gpui_kit::AnyView {\n    use gpui_kit::AppContext as _;\n    cx.new(|_| TextTooltip(label)).into()\n}\nstruct TextTooltip(gpui_kit::SharedString);\nimpl gpui_kit::Render for TextTooltip {\n    fn render(\n        &mut self,\n        _: &mut gpui_kit::Window,\n        cx: &mut gpui_kit::Context<Self>,\n    ) -> impl gpui_kit::IntoElement {\n        use gpui_kit::ParentElement as _;\n        let theme = UiTheme::read(cx);\n        surface::tooltip(Tooltip::new(\"tooltip-content\"), theme).child(\n            div()\n                .debug_selector(|| \"tooltip-label\".into())\n                .child(self.0.clone()),\n        )\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use gpui_kit::{IntoElement, Modifiers, Render, Styled, TestAppContext, px};\n    use std::time::Duration;\n\n    #[gpui_kit::test]\n    fn focus_and_hover_show_tooltip_and_escape_dismisses_it(cx: &mut TestAppContext) {\n        struct Probe;\n        impl Render for Probe {\n            fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {\n                div()\n                    .child(tooltip(\n                        \"test-tooltip\",\n                        \"Help\",\n                        Button::new(\"help\").size(px(100.)).child(\"Help\"),\n                        window,\n                        cx,\n                    ))\n                    .child(Button::new(\"after\").child(\"After\"))\n            }\n        }\n        cx.update(super::super::theme::init);\n        let (_, visual) = cx.add_window_view(|_, _| Probe);\n        visual.update(|window, cx| {\n            window.activate_window();\n            window.draw(cx).clear(cx);\n        });\n        visual.simulate_mouse_move(\n            gpui_kit::point(px(200.), px(200.)),\n            None,\n            Modifiers::default(),\n        );\n        visual.simulate_keystrokes(\"tab\");\n        visual.executor().advance_clock(Duration::from_millis(550));\n        visual.update(|window, cx| window.draw(cx).clear(cx));\n        assert!(visual.debug_bounds(\"tooltip-label\").is_some());\n        visual.simulate_keystrokes(\"escape\");\n        assert!(visual.debug_bounds(\"tooltip-label\").is_none());\n        visual.simulate_keystrokes(\"tab shift-tab\");\n        visual.executor().advance_clock(Duration::from_millis(550));\n        visual.update(|window, cx| window.draw(cx).clear(cx));\n        assert!(visual.debug_bounds(\"tooltip-label\").is_some());\n        visual.simulate_keystrokes(\"tab\");\n        visual.update(|window, cx| window.draw(cx).clear(cx));\n        assert!(visual.debug_bounds(\"tooltip-label\").is_none());\n        // Hover the first control while keyboard focus stays on the second.\n        visual.simulate_mouse_move(\n            gpui_kit::point(px(10.), px(10.)),\n            None,\n            Modifiers::default(),\n        );\n        visual.executor().advance_clock(Duration::from_millis(550));\n        visual.update(|window, cx| window.draw(cx).clear(cx));\n        assert!(visual.debug_bounds(\"tooltip-label\").is_some());\n        visual.simulate_keystrokes(\"escape\");\n        assert!(visual.debug_bounds(\"tooltip-label\").is_none());\n    }\n}\n"
    },
    {
      "path": "registry/message/message.rs",
      "type": "registry:file",
      "target": "~/src/ui/message.rs",
      "content": "//! Nova rows for team-chat transcripts: messages, reactions and dividers.\n//!\n//! The layout follows left-aligned team chat (Slack, Mattermost): a 32px avatar\n//! gutter, a name and time header, and body content. Consecutive messages from\n//! one sender collapse into continuation rows.\n\nuse std::rc::Rc;\n\nuse gpui_icons::{LucideIcon, lucide};\nuse gpui_kit::{\n    AnyElement, AnyView, App, Axis, Bounds, ClickEvent, ElementId, FontWeight, ImageSource,\n    InteractiveElement as _, IntoElement, ParentElement, Pixels, RenderOnce, Role, SharedString,\n    StatefulInteractiveElement as _, Styled, Window, div, img, prelude::FluentBuilder as _, px,\n    relative,\n};\n\nuse gpui_kit::base::ElementExt as _;\n\nuse super::button::{Button, ButtonSize, ButtonVariant};\nuse super::theme::{\n    UiTheme, apply_style,\n    primitives::{focus, hover_reveal_in_group, surface, text},\n};\nuse super::tooltip::text_tooltip;\n\ntype Handler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;\ntype AuthorHandler = Rc<dyn Fn(&ClickEvent, Bounds<Pixels>, &mut Window, &mut App) + 'static>;\ntype ReactionHandler = Rc<dyn Fn(&SharedString, &mut Window, &mut App) + 'static>;\ntype ReactionTooltip = Rc<dyn Fn(&Reaction, &mut Window, &mut App) -> AnyView + 'static>;\n\n/// How a message row is emphasized.\n#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]\npub enum MessageHighlight {\n    #[default]\n    /// No emphasis.\n    None,\n    /// Mentions the reader: a tinted row and a primary bar in the gutter.\n    Mention,\n    /// Selected for keyboard actions; also keeps the action bar visible.\n    Selected,\n    /// The target of a permalink the reader followed: an amber tint and bar.\n    Permalink,\n}\n\n/// How much room a row takes.\n#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]\npub enum MessageDensity {\n    #[default]\n    /// Avatar gutter, a header line with the name and time, then the body.\n    Standard,\n    /// One line per post, as Mattermost's compact display: the time always\n    /// sits in the gutter, the name starts the body line, and rows are tighter.\n    Compact,\n}\n\n/// Delivery state of the reader's own message.\n#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]\npub enum MessageStatus {\n    #[default]\n    /// Delivered.\n    Sent,\n    /// Still sending; shown at reduced opacity.\n    Pending,\n    /// Failed to send; shows Retry and Delete when handlers are set.\n    Failed,\n}\n\n/// One reaction chip: an emoji (or short text), its count and whether the reader reacted.\n#[derive(Clone)]\npub struct Reaction {\n    /// The emoji or short code shown in the chip; also the value toggled.\n    pub emoji: SharedString,\n    /// How many people reacted.\n    pub count: usize,\n    /// Whether the reader is one of them.\n    pub reacted: bool,\n    /// The accessible name, such as \"Arya and 2 others reacted with :tada:\".\n    pub label: Option<SharedString>,\n    /// A custom emoji image shown instead of `emoji`, supplied by the application.\n    pub image: Option<ImageSource>,\n    /// Text shown in a tooltip on hover, such as who reacted.\n    pub tooltip: Option<SharedString>,\n}\n\nimpl Reaction {\n    /// Creates a reaction chip.\n    pub fn new(emoji: impl Into<SharedString>, count: usize, reacted: bool) -> Self {\n        Self {\n            emoji: emoji.into(),\n            count,\n            reacted,\n            label: None,\n            image: None,\n            tooltip: None,\n        }\n    }\n    /// Sets the accessible name, for example who reacted.\n    pub fn label(mut self, label: impl Into<SharedString>) -> Self {\n        self.label = Some(label.into());\n        self\n    }\n    /// Shows a custom emoji image, such as a decoded image from the application's\n    /// media cache. `emoji` stays the toggled value and the fallback name.\n    pub fn image(mut self, image: impl Into<ImageSource>) -> Self {\n        self.image = Some(image.into());\n        self\n    }\n    /// Shows text in a tooltip on hover, for example \"Arya, Sam and 3 others\".\n    /// `Message::reaction_tooltip` replaces it with a custom view.\n    pub fn tooltip(mut self, text: impl Into<SharedString>) -> Self {\n        self.tooltip = Some(text.into());\n        self\n    }\n}\n\n/// A transcript row with avatar, header, body, reactions and a hover action bar.\n#[derive(IntoElement)]\npub struct Message {\n    style: gpui_kit::StyleRefinement,\n    id: ElementId,\n    avatar: Option<AnyElement>,\n    author: Option<SharedString>,\n    author_element: Option<AnyElement>,\n    on_author_click: Option<AuthorHandler>,\n    timestamp: Option<SharedString>,\n    timestamp_tooltip: Option<SharedString>,\n    gutter_time: Option<SharedString>,\n    on_gutter_click: Option<Handler>,\n    header: Vec<AnyElement>,\n    children: Vec<AnyElement>,\n    reactions: Vec<Reaction>,\n    on_reaction: Option<ReactionHandler>,\n    reaction_tooltip: Option<ReactionTooltip>,\n    on_add_reaction: Option<Handler>,\n    actions: Option<AnyElement>,\n    description: Option<SharedString>,\n    continuation: bool,\n    density: MessageDensity,\n    highlight: MessageHighlight,\n    status: MessageStatus,\n    on_retry: Option<Handler>,\n    on_delete: Option<Handler>,\n}\n\nimpl Message {\n    /// Creates a message row with a stable caller-owned ID, such as the post ID.\n    pub fn new(id: impl Into<ElementId>) -> Self {\n        Self {\n            style: gpui_kit::StyleRefinement::default(),\n            id: id.into(),\n            avatar: None,\n            author: None,\n            author_element: None,\n            on_author_click: None,\n            timestamp: None,\n            timestamp_tooltip: None,\n            gutter_time: None,\n            on_gutter_click: None,\n            header: Vec::new(),\n            children: Vec::new(),\n            reactions: Vec::new(),\n            on_reaction: None,\n            reaction_tooltip: None,\n            on_add_reaction: None,\n            actions: None,\n            description: None,\n            continuation: false,\n            density: MessageDensity::Standard,\n            highlight: MessageHighlight::None,\n            status: MessageStatus::Sent,\n            on_retry: None,\n            on_delete: None,\n        }\n    }\n    /// Chooses the standard or compact row layout.\n    pub fn density(mut self, density: MessageDensity) -> Self {\n        self.density = density;\n        self\n    }\n    /// Sets the sender's avatar, usually an `Avatar`.\n    pub fn avatar(mut self, avatar: impl IntoElement) -> Self {\n        self.avatar = Some(avatar.into_any_element());\n        self\n    }\n    /// Sets the sender's display name.\n    pub fn author(mut self, author: impl Into<SharedString>) -> Self {\n        self.author = Some(author.into());\n        self\n    }\n    /// Makes the name a link-styled button that the reader can click, or\n    /// reach with Tab and press with Enter or Space, such as to open the\n    /// sender's profile card. The handler gets the click and the name's\n    /// bounds in window coordinates, to anchor a popover.\n    pub fn on_author_click(\n        mut self,\n        handler: impl Fn(&ClickEvent, Bounds<Pixels>, &mut Window, &mut App) + 'static,\n    ) -> Self {\n        self.on_author_click = Some(Rc::new(handler));\n        self\n    }\n    /// Draws this element in place of the name, such as the app's own popover\n    /// trigger around the name, the way `avatar` takes any element. It\n    /// inherits the name's weight, size and color, and replaces both `author`\n    /// and `on_author_click`.\n    pub fn author_element(mut self, element: impl IntoElement) -> Self {\n        self.author_element = Some(element.into_any_element());\n        self\n    }\n    /// Sets the time shown beside the name, such as \"10:42 AM\".\n    pub fn timestamp(mut self, timestamp: impl Into<SharedString>) -> Self {\n        self.timestamp = Some(timestamp.into());\n        self\n    }\n    /// Shows the full date and time in a tooltip when the reader hovers the\n    /// time, such as \"Tuesday, 3 November 2026 at 16:40\". It also serves the\n    /// `gutter_time` of continuation and compact rows.\n    pub fn timestamp_tooltip(mut self, text: impl Into<SharedString>) -> Self {\n        self.timestamp_tooltip = Some(text.into());\n        self\n    }\n    /// Runs when the reader clicks the gutter column (the avatar on the first\n    /// row of a run, the empty gutter or hover time on continuation rows), for\n    /// selecting the row with the pointer while clicks on the body stay free\n    /// for text selection and links.\n    pub fn on_gutter_click(\n        mut self,\n        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,\n    ) -> Self {\n        self.on_gutter_click = Some(Rc::new(handler));\n        self\n    }\n    /// Adds an element after the time in the header, such as a \"Bot\" tag.\n    pub fn header_child(mut self, child: impl IntoElement) -> Self {\n        self.header.push(child.into_any_element());\n        self\n    }\n    /// Collapses the row under the previous message from the same sender. The\n    /// avatar and header are hidden and `gutter_time` shows on hover instead.\n    pub fn continuation(mut self, continuation: bool) -> Self {\n        self.continuation = continuation;\n        self\n    }\n    /// Sets the short time shown in the gutter of a hovered continuation row.\n    pub fn gutter_time(mut self, time: impl Into<SharedString>) -> Self {\n        self.gutter_time = Some(time.into());\n        self\n    }\n    /// Emphasizes the row as a mention of the reader or as keyboard-selected.\n    pub fn highlight(mut self, highlight: MessageHighlight) -> Self {\n        self.highlight = highlight;\n        self\n    }\n    /// Sets the delivery state of the reader's own message.\n    pub fn status(mut self, status: MessageStatus) -> Self {\n        self.status = status;\n        self\n    }\n    /// Shows a Retry action on a failed message.\n    pub fn on_retry(\n        mut self,\n        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,\n    ) -> Self {\n        self.on_retry = Some(Rc::new(handler));\n        self\n    }\n    /// Shows a Delete action on a failed message.\n    pub fn on_delete(\n        mut self,\n        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,\n    ) -> Self {\n        self.on_delete = Some(Rc::new(handler));\n        self\n    }\n    /// Sets the reaction chips shown under the body.\n    pub fn reactions(mut self, reactions: impl IntoIterator<Item = Reaction>) -> Self {\n        self.reactions = reactions.into_iter().collect();\n        self\n    }\n    /// Runs with a chip's emoji when the reader toggles that reaction.\n    pub fn on_reaction(\n        mut self,\n        handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,\n    ) -> Self {\n        self.on_reaction = Some(Rc::new(handler));\n        self\n    }\n    /// Builds the tooltip shown when a reaction chip is hovered, for example a\n    /// list of who reacted with avatars. Without it, a chip with\n    /// `Reaction::tooltip` text shows that text.\n    pub fn reaction_tooltip(\n        mut self,\n        builder: impl Fn(&Reaction, &mut Window, &mut App) -> AnyView + 'static,\n    ) -> Self {\n        self.reaction_tooltip = Some(Rc::new(builder));\n        self\n    }\n    /// Sets the description assistive technology reads for the row, such as\n    /// \"Arya, 10:42 AM, 2 reactions, edited\".\n    pub fn aria_description(mut self, description: impl Into<SharedString>) -> Self {\n        self.description = Some(description.into());\n        self\n    }\n    /// Adds an \"Add reaction\" button after the chips, for opening an emoji picker.\n    pub fn on_add_reaction(\n        mut self,\n        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,\n    ) -> Self {\n        self.on_add_reaction = Some(Rc::new(handler));\n        self\n    }\n    /// Sets the floating action bar shown while the row is hovered or selected,\n    /// usually `message_actions` holding `message_action` buttons.\n    pub fn actions(mut self, actions: impl IntoElement) -> Self {\n        self.actions = Some(actions.into_any_element());\n        self\n    }\n}\n\nimpl ParentElement for Message {\n    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {\n        self.children.extend(elements);\n    }\n}\n\nimpl Styled for Message {\n    fn style(&mut self) -> &mut gpui_kit::StyleRefinement {\n        &mut self.style\n    }\n}\n\nimpl RenderOnce for Message {\n    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {\n        let theme = UiTheme::read(cx).clone();\n        let colors = theme.colors;\n        let group: SharedString = format!(\"message-{}\", self.id).into();\n        let selected = self.highlight == MessageHighlight::Selected;\n        let compact = self.density == MessageDensity::Compact;\n        let gutter = if compact {\n            theme.space(11.)\n        } else {\n            theme.space(8.)\n        };\n\n        let gutter_content = if compact {\n            // Compact rows always show the time, so the gutter has a fixed\n            // width and the body lines up from row to row.\n            div().w(gutter).flex_shrink_0().pt(px(2.)).children(\n                self.gutter_time\n                    .clone()\n                    .or(self.timestamp.clone())\n                    .map(|time| {\n                        time_tooltip(\n                            text::xs(div(), &theme).id((self.id.clone(), \"gutter-time\")),\n                            self.timestamp_tooltip.clone(),\n                        )\n                        .line_height(theme.text(20.))\n                        .text_color(colors.muted_foreground)\n                        .whitespace_nowrap()\n                        .child(time)\n                    }),\n            )\n        } else if self.continuation {\n            // The hover time floats in the gutter so showing it never changes\n            // the row's height.\n            div().w(gutter).flex_shrink_0().relative().children(\n                self.gutter_time\n                    .clone()\n                    .or(self.timestamp.clone())\n                    .map(|time| {\n                        hover_reveal_in_group(\n                            time_tooltip(\n                                text::xs(div(), &theme).id((self.id.clone(), \"gutter-time\")),\n                                self.timestamp_tooltip.clone(),\n                            )\n                            .absolute()\n                            .top(px(2.))\n                            .right_0()\n                            .line_height(theme.text(20.))\n                            .text_color(colors.muted_foreground)\n                            .whitespace_nowrap()\n                            .child(time),\n                            group.clone(),\n                            false,\n                        )\n                    }),\n            )\n        } else {\n            div()\n                .w(gutter)\n                .flex_shrink_0()\n                .pt(px(2.))\n                .children(self.avatar)\n        };\n        let gutter_content = gutter_content.id((self.id.clone(), \"gutter\")).when_some(\n            self.on_gutter_click.clone(),\n            |gutter, handler| {\n                gutter\n                    .cursor_pointer()\n                    .on_click(move |event, window, cx| handler(event, window, cx))\n            },\n        );\n\n        // The name, as plain text, a link-styled button or the caller's element.\n        let shows_author = !self.continuation;\n        // Clipping would cut off the hover underline of a clickable name.\n        let clip = self.author_element.is_none() && self.on_author_click.is_none();\n        let mut author = shows_author\n            .then(|| match (self.author_element, self.author.clone()) {\n                (Some(element), _) => Some(element),\n                (None, Some(name)) => Some(match self.on_author_click.clone() {\n                    Some(handler) => author_button(&self.id, name, handler, &theme),\n                    None => name.into_any_element(),\n                }),\n                (None, None) => None,\n            })\n            .flatten();\n\n        let mut header_children = self.header;\n        let header = (!self.continuation && !compact).then(|| {\n            div()\n                .flex()\n                .items_baseline()\n                .gap(theme.space(2.))\n                .min_w_0()\n                .children(author.take().map(|author| {\n                    div()\n                        .font_weight(FontWeight::SEMIBOLD)\n                        .text_size(theme.text(14.))\n                        .text_color(colors.foreground)\n                        .min_w_0()\n                        .when(clip, |name| name.overflow_hidden())\n                        .text_ellipsis()\n                        .whitespace_nowrap()\n                        .child(author)\n                }))\n                .children(self.timestamp.clone().map(|time| {\n                    time_tooltip(\n                        div().id((self.id.clone(), \"timestamp\")),\n                        self.timestamp_tooltip.clone(),\n                    )\n                    .flex_shrink_0()\n                    .text_size(theme.text(12.))\n                    .text_color(colors.muted_foreground)\n                    .child(time)\n                }))\n                .children(std::mem::take(&mut header_children))\n        });\n\n        let failed = (self.status == MessageStatus::Failed).then(|| {\n            let link = |id: &'static str, label: &'static str, handler: Handler| {\n                Button::new((self.id.clone(), id))\n                    .variant(ButtonVariant::Link)\n                    .size(ButtonSize::Xs)\n                    .h(theme.space(5.))\n                    .px_0()\n                    .label(label)\n                    .on_click(move |event, window, cx| handler(event, window, cx))\n            };\n            div()\n                .flex()\n                .items_center()\n                .gap(theme.space(2.))\n                .text_size(theme.text(12.))\n                .text_color(colors.destructive)\n                .child(\n                    lucide(LucideIcon::CircleAlert)\n                        .size(theme.space(3.5))\n                        .text_color(colors.destructive),\n                )\n                .child(\"Message failed to send.\")\n                .when_some(self.on_retry.clone(), |row, handler| {\n                    row.child(link(\"retry\", \"Retry\", handler))\n                })\n                .when_some(self.on_delete.clone(), |row, handler| {\n                    row.child(link(\"delete\", \"Delete\", handler))\n                })\n        });\n\n        let reactions = (!self.reactions.is_empty() || self.on_add_reaction.is_some()).then(|| {\n            let on_reaction = self.on_reaction.clone();\n            let tooltip = self.reaction_tooltip.clone();\n            div()\n                .flex()\n                .flex_wrap()\n                .gap(theme.space(1.))\n                .pt(theme.space(1.))\n                .children(self.reactions.iter().map(|reaction| {\n                    reaction_chip(\n                        &self.id,\n                        reaction,\n                        on_reaction.clone(),\n                        tooltip.clone(),\n                        &theme,\n                    )\n                }))\n                .when_some(self.on_add_reaction.clone(), |row, handler| {\n                    row.child(\n                        Button::new((self.id.clone(), \"add-reaction\"))\n                            .variant(ButtonVariant::Ghost)\n                            .size(ButtonSize::IconXs)\n                            .rounded_full()\n                            .aria_label(\"Add reaction\")\n                            .on_click(move |event, window, cx| handler(event, window, cx))\n                            .child(\n                                lucide(LucideIcon::FaceSlightlySmilingPlus)\n                                    .size(theme.space(3.5))\n                                    .text_color(colors.muted_foreground),\n                            ),\n                    )\n                })\n        });\n\n        // Compact rows start the body line with the name; continuation rows\n        // keep only the time in the gutter.\n        let inline_author = (compact && !self.continuation)\n            .then(|| author.take())\n            .flatten()\n            .map(|author| {\n                div()\n                    .font_weight(FontWeight::SEMIBOLD)\n                    .text_color(colors.foreground)\n                    .whitespace_nowrap()\n                    .child(author)\n            });\n        let inline_header =\n            (compact && !self.continuation && !header_children.is_empty()).then(|| {\n                div()\n                    .flex()\n                    .items_center()\n                    .gap(theme.space(1.))\n                    .children(header_children)\n            });\n        let body = div()\n            .flex_1()\n            .min_w_0()\n            .flex()\n            .flex_col()\n            .gap(theme.space(0.5))\n            .children(header)\n            .child(\n                div()\n                    .text_size(theme.text(14.))\n                    .line_height(relative(1.45))\n                    .text_color(colors.foreground)\n                    .when(compact, |content| {\n                        content\n                            .flex()\n                            .flex_wrap()\n                            .items_baseline()\n                            .gap(theme.space(1.5))\n                    })\n                    .children(inline_author)\n                    .children(inline_header)\n                    .children(self.children),\n            )\n            .children(failed)\n            .children(reactions);\n\n        let permalink = colors.warning;\n        let root = div()\n            .id(self.id.clone())\n            .group(group.clone())\n            .role(Role::ListItem)\n            .relative()\n            .w_full()\n            .flex()\n            .gap(theme.space(2.))\n            .px(theme.space(4.))\n            .py(theme.space(if compact { 0.25 } else { 0.5 }))\n            .when(!self.continuation && !compact, |row| {\n                row.pt(theme.space(2.))\n            })\n            .border_l_2()\n            .border_color(colors.primary.opacity(0.))\n            .when(!selected, |row| {\n                row.hover(move |style| style.bg(colors.muted.opacity(0.5)))\n            })\n            .when(self.highlight == MessageHighlight::Mention, |row| {\n                row.bg(colors.primary.opacity(0.06))\n                    .border_color(colors.primary)\n            })\n            .when(selected, |row| row.bg(colors.muted))\n            .when(self.highlight == MessageHighlight::Permalink, |row| {\n                row.bg(permalink.opacity(0.14)).border_color(permalink)\n            })\n            .when_some(self.description, |row, description| {\n                row.aria_description(description)\n            })\n            .when(self.status == MessageStatus::Pending, |row| {\n                row.opacity(0.6)\n            })\n            .child(gutter_content)\n            .child(body)\n            .when_some(self.actions, |row, actions| {\n                row.child(hover_reveal_in_group(\n                    div()\n                        .id((self.id.clone(), \"actions\"))\n                        .absolute()\n                        .top(-theme.space(3.))\n                        .right(theme.space(4.))\n                        .child(actions),\n                    group.clone(),\n                    selected,\n                ))\n            });\n        apply_style(root, &self.style)\n    }\n}\n\n/// Adds the full-date tooltip to a time label when one is set.\nfn time_tooltip(\n    label: gpui_kit::Stateful<gpui_kit::Div>,\n    tooltip: Option<SharedString>,\n) -> gpui_kit::Stateful<gpui_kit::Div> {\n    label.when_some(tooltip, |label, text| {\n        label.tooltip(move |_, cx| text_tooltip(text.clone(), cx))\n    })\n}\n\nfn reaction_chip(\n    message: &ElementId,\n    reaction: &Reaction,\n    on_reaction: Option<ReactionHandler>,\n    tooltip: Option<ReactionTooltip>,\n    theme: &UiTheme,\n) -> impl IntoElement {\n    let colors = theme.colors;\n    let emoji = reaction.emoji.clone();\n    let label = reaction\n        .label\n        .clone()\n        .unwrap_or_else(|| format!(\"{} {}\", reaction.emoji, reaction.count).into());\n    let key = SharedString::from(format!(\"reaction-{}\", reaction.emoji));\n    let glyph = match reaction.image.clone() {\n        Some(image) => img(image)\n            .size(theme.space(4.))\n            .flex_shrink_0()\n            .into_any_element(),\n        None => div()\n            .text_size(theme.text(14.))\n            .child(reaction.emoji.clone())\n            .into_any_element(),\n    };\n    let chip = Button::new((message.clone(), key.clone()))\n        .variant(ButtonVariant::Outline)\n        .size(ButtonSize::Xs)\n        .h(theme.space(6.))\n        .px(theme.space(2.))\n        .gap(theme.space(1.5))\n        .rounded_full()\n        .when(reaction.reacted, |chip| {\n            chip.bg(colors.primary.opacity(0.1))\n                .border_color(colors.primary.opacity(0.5))\n        })\n        .aria_label(label)\n        .on_click(move |_, window, cx| {\n            if let Some(handler) = &on_reaction {\n                handler(&emoji, window, cx);\n            }\n        })\n        .child(glyph)\n        .child(\n            div()\n                .text_size(theme.text(12.))\n                .font_weight(FontWeight::MEDIUM)\n                .text_color(if reaction.reacted {\n                    colors.primary\n                } else {\n                    colors.muted_foreground\n                })\n                .child(reaction.count.to_string()),\n        );\n    let tooltip: Option<ReactionTooltip> = match (tooltip, reaction.tooltip.clone()) {\n        (Some(builder), _) => Some(builder),\n        (None, Some(text)) => Some(Rc::new(\n            move |_: &Reaction, _: &mut Window, cx: &mut App| text_tooltip(text.clone(), cx),\n        )),\n        (None, None) => None,\n    };\n    let reaction = reaction.clone();\n    div()\n        .id((\n            message.clone(),\n            SharedString::from(format!(\"{key}-tooltip\")),\n        ))\n        .when_some(tooltip, move |wrapper, builder| {\n            wrapper.tooltip(move |window, cx| builder(&reaction, window, cx))\n        })\n        .child(chip)\n}\n\n/// The sender's name as a link-styled button that reports its own bounds.\nfn author_button(\n    id: &ElementId,\n    name: SharedString,\n    handler: AuthorHandler,\n    theme: &UiTheme,\n) -> AnyElement {\n    let bounds = Rc::new(std::cell::Cell::new(Bounds::default()));\n    let measured = bounds.clone();\n    let theme = theme.clone();\n    Button::new((id.clone(), \"author\"))\n        .aria_label(name.clone())\n        .style_with(move |base| {\n            let focus_theme = theme.clone();\n            base.role(Role::Link)\n                .rounded(theme.radius.sm)\n                .justify_start()\n                .cursor_pointer()\n                .hover(|style| style.underline())\n                .focus_visible(move |style| focus::ring(style, &focus_theme))\n        })\n        .on_click(move |event, window, cx| handler(event, bounds.get(), window, cx))\n        .child(name)\n        .on_prepaint(move |bounds, _, _| measured.set(bounds))\n        .into_any_element()\n}\n\n/// The floating bar that holds a message's hover actions.\npub fn message_actions(cx: &App) -> gpui_kit::Div {\n    let theme = UiTheme::read(cx);\n    div()\n        .flex()\n        .items_center()\n        .gap(theme.space(0.5))\n        .p(theme.space(0.5))\n        .rounded(theme.radius.lg)\n        .border_1()\n        .border_color(theme.colors.border)\n        .bg(theme.colors.background)\n        .shadow(theme.shadows.sm.to_vec())\n}\n\n/// An icon button for `message_actions`, named for assistive technology.\npub fn message_action(\n    id: impl Into<ElementId>,\n    icon: LucideIcon,\n    label: impl Into<SharedString>,\n    cx: &App,\n) -> Button {\n    let theme = UiTheme::read(cx);\n    Button::new(id)\n        .variant(ButtonVariant::Ghost)\n        .size(ButtonSize::IconSm)\n        .aria_label(label)\n        .child(\n            lucide(icon)\n                .size(theme.space(4.))\n                .text_color(theme.colors.muted_foreground),\n        )\n}\n\n/// A centered date label between two rules, such as \"Today\" or \"Monday, October 27\".\npub fn day_divider(\n    id: impl Into<ElementId>,\n    label: impl Into<SharedString>,\n    cx: &App,\n) -> impl IntoElement {\n    let theme = UiTheme::read(cx);\n    let rule = || surface::divider(div().flex_1(), theme, Axis::Horizontal);\n    div()\n        .id(id)\n        .role(Role::Splitter)\n        .w_full()\n        .flex()\n        .items_center()\n        .gap(theme.space(3.))\n        .px(theme.space(4.))\n        .py(theme.space(2.))\n        .child(rule())\n        .child(\n            div()\n                .px(theme.space(3.))\n                .h(theme.space(6.))\n                .flex()\n                .items_center()\n                .rounded_full()\n                .border_1()\n                .border_color(theme.colors.border)\n                .bg(theme.colors.background)\n                .text_size(theme.text(12.))\n                .font_weight(FontWeight::MEDIUM)\n                .text_color(theme.colors.foreground)\n                .child(label.into()),\n        )\n        .child(rule())\n}\n\n/// The destructive rule that marks where unread messages start.\npub fn new_messages_divider(\n    id: impl Into<ElementId>,\n    label: impl Into<SharedString>,\n    cx: &App,\n) -> impl IntoElement {\n    let theme = UiTheme::read(cx);\n    div()\n        .id(id)\n        .role(Role::Splitter)\n        .w_full()\n        .flex()\n        .items_center()\n        .gap(theme.space(2.))\n        .px(theme.space(4.))\n        .py(theme.space(1.))\n        .child(\n            surface::divider(div().flex_1(), theme, Axis::Horizontal).bg(theme.colors.destructive),\n        )\n        .child(\n            div()\n                .text_size(theme.text(12.))\n                .font_weight(FontWeight::SEMIBOLD)\n                .text_color(theme.colors.destructive)\n                .child(label.into()),\n        )\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n    use gpui_kit::{Context, Pixels, Render, TestAppContext, VisualTestContext};\n\n    struct Probe(MessageDensity);\n\n    impl Render for Probe {\n        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {\n            div()\n                .flex()\n                .flex_col()\n                .w(px(600.))\n                .child(\n                    div().debug_selector(|| \"post\".into()).child(\n                        Message::new(\"post\")\n                            .density(self.0)\n                            .author(\"Arya\")\n                            .timestamp(\"10:42\")\n                            .header_child(div().child(\"Bot\"))\n                            .child(\"First line\"),\n                    ),\n                )\n                .child(\n                    div().debug_selector(|| \"follow\".into()).child(\n                        Message::new(\"follow\")\n                            .density(self.0)\n                            .continuation(true)\n                            .gutter_time(\"10:43\")\n                            .child(\"Second\"),\n                    ),\n                )\n        }\n    }\n\n    fn row_heights(cx: &mut TestAppContext, density: MessageDensity) -> (Pixels, Pixels) {\n        let window = cx.add_window(|_, _| Probe(density));\n        let mut cx = VisualTestContext::from_window(window.into(), cx);\n        cx.update(|window, cx| window.draw(cx).clear(cx));\n        let height = |cx: &mut VisualTestContext, id: &'static str| {\n            cx.debug_bounds(id)\n                .unwrap_or_else(|| panic!(\"{id} drew\"))\n                .size\n                .height\n        };\n        (height(&mut cx, \"post\"), height(&mut cx, \"follow\"))\n    }\n\n    #[gpui_kit::test]\n    fn compact_rows_are_one_line_and_shorter(cx: &mut TestAppContext) {\n        cx.update(super::super::theme::init);\n        let (standard_first, standard_follow) = row_heights(cx, MessageDensity::Standard);\n        let (compact_first, compact_follow) = row_heights(cx, MessageDensity::Compact);\n        assert!(\n            compact_first < standard_first,\n            \"compact first row {compact_first:?} should be shorter than {standard_first:?}\"\n        );\n        assert!(compact_follow < standard_follow);\n        // One line: the name sits inline, so the first row is as tall as a continuation row.\n        assert_eq!(compact_first, compact_follow);\n    }\n\n    struct GutterProbe(Rc<std::cell::Cell<usize>>);\n\n    impl Render for GutterProbe {\n        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {\n            let clicks = self.0.clone();\n            div().w(px(600.)).child(\n                div().debug_selector(|| \"row\".into()).child(\n                    Message::new(\"row\")\n                        .continuation(true)\n                        .gutter_time(\"10:43\")\n                        .timestamp_tooltip(\"Tuesday, 3 November 2026 at 10:43\")\n                        .on_gutter_click(move |_, _, _| clicks.set(clicks.get() + 1))\n                        .child(\"Second\"),\n                ),\n            )\n        }\n    }\n\n    /// The gutter of a continuation row is a click target, and the body is not\n    /// (devaryakjha/gpuicn#98).\n    #[gpui_kit::test]\n    fn the_gutter_selects_the_row_and_the_body_does_not(cx: &mut TestAppContext) {\n        cx.update(super::super::theme::init);\n        let clicks = Rc::new(std::cell::Cell::new(0));\n        let probe = clicks.clone();\n        let window = cx.add_window(move |_, _| GutterProbe(probe));\n        let mut cx = VisualTestContext::from_window(window.into(), cx);\n        cx.update(|window, cx| window.draw(cx).clear(cx));\n        let row = cx.debug_bounds(\"row\").expect(\"row drew\");\n        let y = row.center().y;\n        // The gutter starts after the row's 16px padding and 2px bar.\n        cx.simulate_click(\n            gpui_kit::point(row.left() + px(30.), y),\n            gpui_kit::Modifiers::default(),\n        );\n        assert_eq!(clicks.get(), 1);\n        cx.simulate_click(\n            gpui_kit::point(row.left() + px(200.), y),\n            gpui_kit::Modifiers::default(),\n        );\n        assert_eq!(clicks.get(), 1);\n    }\n\n    type Clicked = Rc<std::cell::RefCell<Vec<Bounds<Pixels>>>>;\n\n    struct AuthorProbe(Clicked, MessageDensity);\n\n    impl Render for AuthorProbe {\n        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {\n            let clicks = self.0.clone();\n            div()\n                .w(px(600.))\n                .flex()\n                .flex_col()\n                .child(\n                    Message::new(\"post\")\n                        .density(self.1)\n                        .author(\"Arya\")\n                        .on_author_click(move |_, bounds, _, _| clicks.borrow_mut().push(bounds))\n                        .timestamp(\"10:42\")\n                        .child(\"Hello\"),\n                )\n                .child(\n                    Message::new(\"custom\")\n                        .density(self.1)\n                        .author(\"ignored\")\n                        .author_element(div().debug_selector(|| \"custom-name\".into()).child(\"Dev\"))\n                        .child(\"Hi\"),\n                )\n        }\n    }\n\n    /// The name opens something with the pointer or the keyboard and reports\n    /// its bounds, and `author_element` replaces it (devaryakjha/gpuicn#123).\n    #[gpui_kit::test]\n    fn the_author_name_is_a_button_with_its_bounds(cx: &mut TestAppContext) {\n        use gpui_kit::test::TestQueryExt as _;\n        use gpui_kit::{KeyDownEvent, KeyUpEvent, Keystroke};\n        cx.update(super::super::theme::init);\n        for density in [MessageDensity::Standard, MessageDensity::Compact] {\n            let clicks = Clicked::default();\n            let probe = clicks.clone();\n            let window = cx.add_window(move |window, _| {\n                window.activate_window();\n                AuthorProbe(probe, density)\n            });\n            let mut cx = VisualTestContext::from_window(window.into(), cx);\n            cx.update(|window, cx| window.draw(cx).clear(cx));\n            assert!(\n                cx.debug_bounds(\"custom-name\").is_some(),\n                \"the custom element drew\"\n            );\n\n            let name = cx.update(|window, _| {\n                let name = window.find_by_label(\"Arya\");\n                assert_eq!(name.role(), Some(Role::Link));\n                name.bounds()\n            });\n            cx.simulate_click(name.center(), gpui_kit::Modifiers::default());\n            assert_eq!(\n                *clicks.borrow(),\n                [name],\n                \"{density:?}: the click reports the name's bounds\"\n            );\n\n            cx.simulate_keystrokes(\"tab\");\n            cx.update(|window, cx| window.draw(cx).clear(cx));\n            let keystroke = Keystroke::parse(\"enter\").unwrap();\n            cx.simulate_event(KeyDownEvent {\n                keystroke: keystroke.clone(),\n                is_held: false,\n                prefer_character_input: false,\n            });\n            cx.simulate_event(KeyUpEvent { keystroke });\n            assert_eq!(\n                clicks.borrow().len(),\n                2,\n                \"{density:?}: Enter on the focused name\"\n            );\n        }\n    }\n}\n"
    },
    {
      "path": "registry/embed/embed.rs",
      "type": "registry:file",
      "target": "~/src/ui/embed.rs",
      "content": "//! Nova embed cards under a chat message: link previews (Open Graph) and\n//! message attachments (the integration format with a colored bar, fields and\n//! a footer) and quoted posts for permalinks and forwards.\n//!\n//! shadcn/ui has no embed card; both cards are drawn on the card tokens with\n//! the same density steps as `Message`. Titles that open something are GPUI\n//! Kit buttons with the link role, so they take focus and activate on Enter.\n//! gpuicn never fetches: images arrive as `ImageSource` with their pixel size\n//! so the card reserves its box before the bytes load. When the application\n//! asks before loading remote images, an [`ImagePlaceholder`] takes the\n//! image's box until the person chooses to load it.\n\nuse std::rc::Rc;\n\nuse gpui_icons::{LucideIcon, lucide};\nuse gpui_kit::{\n    AnyElement, App, ClickEvent, ElementId, FontWeight, ImageSource, InteractiveElement as _,\n    IntoElement, ObjectFit, ParentElement, Pixels, RenderOnce, Rgba, Role, SharedString,\n    StatefulInteractiveElement as _, Styled, Window, div, prelude::FluentBuilder as _, px,\n    relative,\n};\n\nuse gpui_kit::base::ElementExt as _;\n\nuse super::avatar::{Avatar, AvatarSize};\nuse super::button::{Button, ButtonSize, ButtonVariant};\npub use super::message::MessageDensity;\nuse super::theme::{\n    UiTheme, apply_style,\n    primitives::{clipped_image, focus, icon, round_image, text},\n};\n\ntype Handler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;\ntype ToggleHandler = Rc<dyn Fn(&bool, &mut Window, &mut App) + 'static>;\n\n/// Images narrower than this, in source pixels, show as a thumbnail beside\n/// the text instead of a large image below it, as in Mattermost's webapp.\npub const LARGE_IMAGE_MIN_WIDTH: u32 = 150;\n\n/// Where a link preview draws its image.\n#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]\npub enum LinkPreviewImageLayout {\n    #[default]\n    /// A thumbnail when the image is narrower than [`LARGE_IMAGE_MIN_WIDTH`]\n    /// or roughly square (or taller than wide); a large image otherwise.\n    Auto,\n    /// The full-width image below the text.\n    Large,\n    /// A small square on the right of the text.\n    Thumbnail,\n}\n\nimpl LinkPreviewImageLayout {\n    /// The layout `Auto` resolves to for an image of `width` × `height` pixels.\n    pub fn resolve(self, width: u32, height: u32) -> Self {\n        match self {\n            Self::Auto => {\n                let square = height > 0 && (width as f32) < height as f32 * 1.2;\n                if width < LARGE_IMAGE_MIN_WIDTH || square {\n                    Self::Thumbnail\n                } else {\n                    Self::Large\n                }\n            }\n            layout => layout,\n        }\n    }\n}\n\n#[derive(Clone)]\nstruct SizedImage {\n    source: ImageSource,\n    width: u32,\n    height: u32,\n}\n\n/// A click-to-load stand-in for a remote image, for applications that ask\n/// before loading one. It takes the box the image would take, so nothing\n/// moves when the image arrives, and shows an image icon, \"Load image\" and\n/// the image's host. It is a button named \"Load image from {host}\" that\n/// runs `on_load` once on click, Enter or Space; the caller then fetches the\n/// image and passes it through `image` on the next render.\n#[derive(Clone)]\npub struct ImagePlaceholder {\n    host: SharedString,\n    size: Option<(u32, u32)>,\n    label: SharedString,\n    aria_label: Option<SharedString>,\n    on_load: Handler,\n}\n\nimpl ImagePlaceholder {\n    /// Creates a placeholder for an image served from `host`, such as\n    /// \"github.com\". `on_load` runs once, when the person asks to load it.\n    pub fn new(\n        host: impl Into<SharedString>,\n        on_load: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,\n    ) -> Self {\n        Self {\n            host: host.into(),\n            size: None,\n            label: \"Load image\".into(),\n            aria_label: None,\n            on_load: Rc::new(on_load),\n        }\n    }\n    /// The image's pixel size from the server's metadata, so the placeholder\n    /// takes the image's box and layout. Without it the box is 16:9 and large.\n    pub fn size(mut self, width: u32, height: u32) -> Self {\n        self.size = Some((width, height));\n        self\n    }\n    /// Replaces the visible \"Load image\" text, such as for a translation.\n    pub fn label(mut self, label: impl Into<SharedString>) -> Self {\n        self.label = label.into();\n        self\n    }\n    /// Replaces the accessible name, \"Load image from {host}\" by default.\n    pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {\n        self.aria_label = Some(label.into());\n        self\n    }\n    /// The pixel size layouts resolve against; 16:9 when unknown.\n    fn layout_size(&self) -> (u32, u32) {\n        self.size.unwrap_or((1600, 900))\n    }\n}\n\n/// What fills a card's image slot.\n#[derive(Clone)]\nenum ImageSlot {\n    Image(SizedImage),\n    Placeholder(ImagePlaceholder),\n}\n\n/// What fills a card's thumbnail square.\nenum ThumbnailSlot {\n    Image(ImageSource),\n    Placeholder(ImagePlaceholder),\n}\n\nimpl ImageSlot {\n    fn size(&self) -> (u32, u32) {\n        match self {\n            Self::Image(image) => (image.width, image.height),\n            Self::Placeholder(placeholder) => placeholder.layout_size(),\n        }\n    }\n}\n\n/// Density-aware inner padding and spacing, matching `Message`.\nfn metrics(theme: &UiTheme, density: MessageDensity) -> (Pixels, Pixels) {\n    match density {\n        MessageDensity::Standard => (theme.space(3.), theme.space(1.5)),\n        MessageDensity::Compact => (theme.space(2.), theme.space(1.)),\n    }\n}\n\n/// The box for an image of `width` × `height` pixels: its aspect ratio, scaled\n/// down (never up) to fit the cap.\nfn fit_within(width: f32, height: f32, max: (Pixels, Pixels)) -> (Pixels, Pixels) {\n    let scale = (f32::from(max.0) / width)\n        .min(f32::from(max.1) / height)\n        .min(1.);\n    (px((width * scale).round()), px((height * scale).round()))\n}\n\n/// The box of a large image of `width` × `height` pixels. The width also\n/// shrinks with a narrow card, and the aspect ratio keeps the height in step.\nfn large_box(\n    id: ElementId,\n    width: u32,\n    height: u32,\n    theme: &UiTheme,\n) -> gpui_kit::Stateful<gpui_kit::Div> {\n    let (width, height) = (width.max(1) as f32, height.max(1) as f32);\n    let (box_width, _) = fit_within(width, height, (theme.space(114.), theme.space(72.)));\n    div()\n        .id(id)\n        .w(box_width)\n        .max_w_full()\n        .aspect_ratio(width / height)\n        .flex_shrink_0()\n}\n\n/// A large image whose box is reserved from its pixel size.\nfn large_image(\n    id: ElementId,\n    image: SizedImage,\n    on_click: Option<Handler>,\n    theme: &UiTheme,\n) -> AnyElement {\n    large_box(id, image.width, image.height, theme)\n        .debug_selector(|| \"embed-image\".into())\n        .child(clipped_image(image.source, theme.radius.md, ObjectFit::Cover, theme).size_full())\n        .when_some(on_click, |frame, handler| {\n            frame\n                .cursor_pointer()\n                .on_click(move |event, window, cx| handler(event, window, cx))\n        })\n        .into_any_element()\n}\n\n/// The box of a small square image beside the text.\nfn thumbnail_box(id: ElementId, theme: &UiTheme) -> gpui_kit::Stateful<gpui_kit::Div> {\n    div().id(id).flex_shrink_0().size(theme.space(20.))\n}\n\n/// A small square image beside the text.\nfn thumbnail(\n    id: ElementId,\n    source: ImageSource,\n    on_click: Option<Handler>,\n    theme: &UiTheme,\n) -> AnyElement {\n    thumbnail_box(id, theme)\n        .debug_selector(|| \"embed-thumbnail\".into())\n        .child(clipped_image(source, theme.radius.md, ObjectFit::Cover, theme).size_full())\n        .when_some(on_click, |frame, handler| {\n            frame\n                .cursor_pointer()\n                .on_click(move |event, window, cx| handler(event, window, cx))\n        })\n        .into_any_element()\n}\n\n/// The placeholder's button filling an image box: a muted surface with the\n/// image icon, the label and the host, which runs `on_load` only once.\nfn placeholder_button(\n    id: ElementId,\n    placeholder: ImagePlaceholder,\n    window: &mut Window,\n    cx: &mut App,\n    theme: &UiTheme,\n) -> Button {\n    let colors = theme.colors;\n    let loaded = window.use_keyed_state((id.clone(), \"loaded\"), cx, |_, _| false);\n    let aria_label = placeholder\n        .aria_label\n        .unwrap_or_else(|| format!(\"Load image from {}\", placeholder.host).into());\n    let on_load = placeholder.on_load;\n    let style_theme = theme.clone();\n    Button::new(id)\n        .aria_label(aria_label)\n        .style_with(move |base| {\n            let theme = style_theme.clone();\n            let focus_theme = theme.clone();\n            base.size_full()\n                .flex()\n                .flex_col()\n                .items_center()\n                .justify_center()\n                .gap(theme.space(1.))\n                .p(theme.space(2.))\n                .overflow_hidden()\n                .rounded(theme.radius.md)\n                .bg(theme.colors.muted)\n                .cursor_pointer()\n                .hover(move |style| style.bg(theme.colors.accent))\n                .focus_visible(move |style| focus::ring(style, &focus_theme))\n        })\n        .on_click(move |event, window, cx| {\n            if *loaded.read(cx) {\n                return;\n            }\n            loaded.update(cx, |loaded, _| *loaded = true);\n            on_load(event, window, cx);\n        })\n        .child(\n            lucide(LucideIcon::Image)\n                .flex_shrink_0()\n                .size(icon::lg(theme))\n                .text_color(colors.muted_foreground),\n        )\n        .child(\n            text::xs(div(), theme)\n                .max_w_full()\n                .font_weight(FontWeight::MEDIUM)\n                .overflow_hidden()\n                .text_ellipsis()\n                .whitespace_nowrap()\n                .child(placeholder.label),\n        )\n        .child(\n            text::xs(div(), theme)\n                .max_w_full()\n                .text_color(colors.muted_foreground)\n                .overflow_hidden()\n                .text_ellipsis()\n                .whitespace_nowrap()\n                .child(placeholder.host),\n        )\n}\n\n/// A placeholder in a large image's box.\nfn large_placeholder(\n    id: ElementId,\n    placeholder: ImagePlaceholder,\n    window: &mut Window,\n    cx: &mut App,\n    theme: &UiTheme,\n) -> AnyElement {\n    let (width, height) = placeholder.layout_size();\n    large_box((id.clone(), \"frame\").into(), width, height, theme)\n        .debug_selector(|| \"embed-image-placeholder\".into())\n        .child(placeholder_button(id, placeholder, window, cx, theme))\n        .into_any_element()\n}\n\n/// A placeholder in a thumbnail's box.\nfn thumbnail_placeholder(\n    id: ElementId,\n    placeholder: ImagePlaceholder,\n    window: &mut Window,\n    cx: &mut App,\n    theme: &UiTheme,\n) -> AnyElement {\n    thumbnail_box((id.clone(), \"frame\").into(), theme)\n        .debug_selector(|| \"embed-thumbnail-placeholder\".into())\n        .child(placeholder_button(id, placeholder, window, cx, theme))\n        .into_any_element()\n}\n\n/// A title clamped to `lines`; a focusable link when `on_click` is set.\nfn title_text(\n    id: ElementId,\n    title: SharedString,\n    lines: usize,\n    on_click: Option<Handler>,\n    theme: &UiTheme,\n) -> AnyElement {\n    match on_click {\n        Some(handler) => {\n            let theme = theme.clone();\n            Button::new(id)\n                .aria_label(title.clone())\n                .style_with(move |base| {\n                    let focus_theme = theme.clone();\n                    let base = base.role(Role::Link);\n                    text::sm(base, &theme)\n                        .font_weight(FontWeight::SEMIBOLD)\n                        .text_color(theme.colors.primary)\n                        .line_clamp(lines)\n                        .rounded(theme.radius.sm)\n                        // A block, so the title wraps at the card's width\n                        // and clamps instead of growing past it.\n                        .block()\n                        .w_full()\n                        .text_left()\n                        .cursor_pointer()\n                        .hover(|style| style.underline())\n                        .focus_visible(move |style| focus::ring(style, &focus_theme))\n                })\n                .on_click(move |event, window, cx| handler(event, window, cx))\n                .child(title)\n                .into_any_element()\n        }\n        None => text::sm(div().id(id), theme)\n            .font_weight(FontWeight::SEMIBOLD)\n            .line_clamp(lines)\n            .child(title)\n            .into_any_element(),\n    }\n}\n\nfn icon_button(\n    id: ElementId,\n    icon: LucideIcon,\n    label: &'static str,\n    on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,\n    theme: &UiTheme,\n) -> Button {\n    Button::new(id)\n        .variant(ButtonVariant::Ghost)\n        .size(ButtonSize::IconXs)\n        .aria_label(label)\n        .on_click(on_click)\n        .child(\n            lucide(icon)\n                .size(theme.space(3.5))\n                .text_color(theme.colors.muted_foreground),\n        )\n}\n\n/// The card frame both embeds share.\nfn card_frame(\n    id: ElementId,\n    label: SharedString,\n    theme: &UiTheme,\n) -> gpui_kit::Stateful<gpui_kit::Div> {\n    text::sm(div().id(id), theme)\n        .role(Role::Group)\n        .aria_label(label)\n        .w_full()\n        .max_w(theme.space(120.))\n        .min_w_0()\n        .text_color(theme.colors.card_foreground)\n}\n\n/// An Open Graph link preview: site name, linked title, description and an\n/// optional image, with collapse and remove controls.\n#[derive(IntoElement)]\npub struct LinkPreview {\n    style: gpui_kit::StyleRefinement,\n    id: ElementId,\n    title: SharedString,\n    site_name: Option<SharedString>,\n    description: Option<SharedString>,\n    image: Option<ImageSlot>,\n    image_layout: LinkPreviewImageLayout,\n    collapsed: bool,\n    density: MessageDensity,\n    on_click: Option<Handler>,\n    on_toggle: Option<ToggleHandler>,\n    on_remove: Option<Handler>,\n}\n\nimpl LinkPreview {\n    /// Creates a preview with a stable caller-owned ID and the page title,\n    /// which also names the card for assistive technology.\n    pub fn new(id: impl Into<ElementId>, title: impl Into<SharedString>) -> Self {\n        Self {\n            style: gpui_kit::StyleRefinement::default(),\n            id: id.into(),\n            title: title.into(),\n            site_name: None,\n            description: None,\n            image: None,\n            image_layout: LinkPreviewImageLayout::Auto,\n            collapsed: false,\n            density: MessageDensity::Standard,\n            on_click: None,\n            on_toggle: None,\n            on_remove: None,\n        }\n    }\n    /// Sets the small muted line above the title, such as \"GitHub\".\n    pub fn site_name(mut self, site_name: impl Into<SharedString>) -> Self {\n        self.site_name = Some(site_name.into());\n        self\n    }\n    /// Sets the description, clamped to three lines.\n    pub fn description(mut self, description: impl Into<SharedString>) -> Self {\n        self.description = Some(description.into());\n        self\n    }\n    /// Adds the page image with its pixel size from the server's metadata, so\n    /// the card reserves the image's box before it loads.\n    pub fn image(mut self, source: impl Into<ImageSource>, width: u32, height: u32) -> Self {\n        self.image = Some(ImageSlot::Image(SizedImage {\n            source: source.into(),\n            width,\n            height,\n        }));\n        self\n    }\n    /// Shows a click-to-load placeholder in the image's place, with the same\n    /// box and layout the image would take. Replaces `image`; pass the image\n    /// through `image` once it has loaded.\n    pub fn image_placeholder(mut self, placeholder: ImagePlaceholder) -> Self {\n        self.image = Some(ImageSlot::Placeholder(placeholder));\n        self\n    }\n    /// Chooses between a large image and a thumbnail. Defaults to `Auto`.\n    pub fn image_layout(mut self, layout: LinkPreviewImageLayout) -> Self {\n        self.image_layout = layout;\n        self\n    }\n    /// Hides the image, as Mattermost's per-post \"collapse preview\". The\n    /// text stays.\n    pub fn collapsed(mut self, collapsed: bool) -> Self {\n        self.collapsed = collapsed;\n        self\n    }\n    /// Matches the padding to the message list's density.\n    pub fn density(mut self, density: MessageDensity) -> Self {\n        self.density = density;\n        self\n    }\n    /// Runs when the title or the image is activated; open the link here.\n    pub fn on_click(\n        mut self,\n        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,\n    ) -> Self {\n        self.on_click = Some(Rc::new(handler));\n        self\n    }\n    /// Adds a chevron that collapses or expands the image, named \"Collapse\n    /// preview\" or \"Expand preview\". Receives the new collapsed state; the\n    /// caller stores it and passes it back through `collapsed`. Shown only\n    /// when the preview has an image or an image placeholder.\n    pub fn on_toggle(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {\n        self.on_toggle = Some(Rc::new(handler));\n        self\n    }\n    /// Adds a \"Remove preview\" button, for the post's author.\n    pub fn on_remove(\n        mut self,\n        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,\n    ) -> Self {\n        self.on_remove = Some(Rc::new(handler));\n        self\n    }\n}\n\nimpl Styled for LinkPreview {\n    fn style(&mut self) -> &mut gpui_kit::StyleRefinement {\n        &mut self.style\n    }\n}\n\nimpl RenderOnce for LinkPreview {\n    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {\n        let theme = UiTheme::read(cx).clone();\n        let colors = theme.colors;\n        let (padding, gap) = metrics(&theme, self.density);\n        let id = self.id;\n        let collapsed = self.collapsed;\n\n        let toggle = self\n            .on_toggle\n            .filter(|_| self.image.is_some())\n            .map(|handler| {\n                let (icon, label) = if collapsed {\n                    (LucideIcon::ChevronRight, \"Expand preview\")\n                } else {\n                    (LucideIcon::ChevronDown, \"Collapse preview\")\n                };\n                icon_button(\n                    (id.clone(), \"toggle\").into(),\n                    icon,\n                    label,\n                    move |_, window, cx| handler(&!collapsed, window, cx),\n                    &theme,\n                )\n            });\n        let remove = self.on_remove.map(|handler| {\n            icon_button(\n                (id.clone(), \"remove\").into(),\n                LucideIcon::X,\n                \"Remove preview\",\n                move |event, window, cx| handler(event, window, cx),\n                &theme,\n            )\n        });\n        let header =\n            (self.site_name.is_some() || toggle.is_some() || remove.is_some()).then(|| {\n                div()\n                    .flex()\n                    .items_center()\n                    .gap(theme.space(1.))\n                    .min_h(theme.space(5.))\n                    .child(\n                        text::xs(div(), &theme)\n                            .flex_1()\n                            .min_w_0()\n                            .text_color(colors.muted_foreground)\n                            .overflow_hidden()\n                            .text_ellipsis()\n                            .whitespace_nowrap()\n                            .children(self.site_name),\n                    )\n                    .children(\n                        toggle.map(|toggle| {\n                            div().debug_selector(|| \"embed-toggle\".into()).child(toggle)\n                        }),\n                    )\n                    .children(remove)\n            });\n\n        let (large, thumb) = match self.image.filter(|_| !collapsed) {\n            Some(slot) => {\n                let (width, height) = slot.size();\n                let as_thumbnail =\n                    self.image_layout.resolve(width, height) == LinkPreviewImageLayout::Thumbnail;\n                match (slot, as_thumbnail) {\n                    (ImageSlot::Image(image), true) => (\n                        None,\n                        Some(thumbnail(\n                            (id.clone(), \"thumbnail\").into(),\n                            image.source,\n                            self.on_click.clone(),\n                            &theme,\n                        )),\n                    ),\n                    (ImageSlot::Image(image), false) => (\n                        Some(large_image(\n                            (id.clone(), \"image\").into(),\n                            image,\n                            self.on_click.clone(),\n                            &theme,\n                        )),\n                        None,\n                    ),\n                    (ImageSlot::Placeholder(placeholder), true) => (\n                        None,\n                        Some(thumbnail_placeholder(\n                            (id.clone(), \"thumbnail-placeholder\").into(),\n                            placeholder,\n                            window,\n                            cx,\n                            &theme,\n                        )),\n                    ),\n                    (ImageSlot::Placeholder(placeholder), false) => (\n                        Some(large_placeholder(\n                            (id.clone(), \"image-placeholder\").into(),\n                            placeholder,\n                            window,\n                            cx,\n                            &theme,\n                        )),\n                        None,\n                    ),\n                }\n            }\n            None => (None, None),\n        };\n\n        let card = card_frame(id.clone(), self.title.clone(), &theme)\n            .flex()\n            .flex_col()\n            .gap(gap)\n            .p(padding)\n            .rounded(theme.radius.lg)\n            .border_1()\n            .border_color(colors.border)\n            .bg(colors.card)\n            .children(header)\n            .child(\n                div()\n                    .flex()\n                    .items_start()\n                    .gap(theme.space(3.))\n                    .child(\n                        div()\n                            .flex_1()\n                            .min_w_0()\n                            .flex()\n                            .flex_col()\n                            .gap(theme.space(1.))\n                            .child(title_text(\n                                (id.clone(), \"title\").into(),\n                                self.title,\n                                2,\n                                self.on_click,\n                                &theme,\n                            ))\n                            .children(self.description.map(|description| {\n                                text::muted(div(), &theme).line_clamp(3).child(description)\n                            })),\n                    )\n                    .children(thumb),\n            )\n            .children(large);\n        apply_style(card, &self.style)\n    }\n}\n\n#[derive(Clone, Copy)]\nenum EmbedColor {\n    Custom(Rgba),\n    Good,\n    Warning,\n    Danger,\n}\n\nstruct EmbedField {\n    label: SharedString,\n    value: AnyElement,\n    short: bool,\n}\n\n/// A message attachment card, Mattermost's and Slack's integration format: a\n/// colored bar, then pretext, author, title, body, fields, image and footer.\n/// Every part is optional.\n#[derive(IntoElement)]\npub struct MessageEmbed {\n    style: gpui_kit::StyleRefinement,\n    id: ElementId,\n    color: Option<EmbedColor>,\n    pretext: Option<AnyElement>,\n    author: Option<SharedString>,\n    author_icon: Option<ImageSource>,\n    on_author_click: Option<Handler>,\n    title: Option<SharedString>,\n    on_title_click: Option<Handler>,\n    body: Option<AnyElement>,\n    fields: Vec<EmbedField>,\n    image: Option<ImageSlot>,\n    on_image_click: Option<Handler>,\n    thumbnail: Option<ThumbnailSlot>,\n    footer: Option<SharedString>,\n    footer_icon: Option<ImageSource>,\n    timestamp: Option<SharedString>,\n    density: MessageDensity,\n}\n\nimpl MessageEmbed {\n    /// Creates an empty card with a stable caller-owned ID.\n    pub fn new(id: impl Into<ElementId>) -> Self {\n        Self {\n            style: gpui_kit::StyleRefinement::default(),\n            id: id.into(),\n            color: None,\n            pretext: None,\n            author: None,\n            author_icon: None,\n            on_author_click: None,\n            title: None,\n            on_title_click: None,\n            body: None,\n            fields: Vec::new(),\n            image: None,\n            on_image_click: None,\n            thumbnail: None,\n            footer: None,\n            footer_icon: None,\n            timestamp: None,\n            density: MessageDensity::Standard,\n        }\n    }\n    /// Colors the bar. Defaults to the border color.\n    pub fn color(mut self, color: impl Into<Rgba>) -> Self {\n        self.color = Some(EmbedColor::Custom(color.into()));\n        self\n    }\n    /// Colors the bar from the integration's string: `#rgb`, `#rrggbb` (the\n    /// `#` is optional), or `good`, `warning` and `danger`, which map to the\n    /// theme's status colors. Anything else keeps the default.\n    pub fn color_str(mut self, color: &str) -> Self {\n        let color = color.trim();\n        self.color = match color.to_ascii_lowercase().as_str() {\n            \"good\" => Some(EmbedColor::Good),\n            \"warning\" => Some(EmbedColor::Warning),\n            \"danger\" => Some(EmbedColor::Danger),\n            _ => {\n                let hex = color.strip_prefix('#').unwrap_or(color);\n                let valid =\n                    matches!(hex.len(), 3 | 6) && hex.chars().all(|c| c.is_ascii_hexdigit());\n                valid\n                    .then(|| Rgba::try_from(format!(\"#{hex}\").as_str()).ok())\n                    .flatten()\n                    .map(EmbedColor::Custom)\n                    .or(self.color)\n            }\n        };\n        self\n    }\n    /// Content above the card, such as rendered Markdown pretext.\n    pub fn pretext(mut self, pretext: impl IntoElement) -> Self {\n        self.pretext = Some(pretext.into_any_element());\n        self\n    }\n    /// Sets the author line's name.\n    pub fn author(mut self, name: impl Into<SharedString>) -> Self {\n        self.author = Some(name.into());\n        self\n    }\n    /// Sets the small round icon before the author's name.\n    pub fn author_icon(mut self, source: impl Into<ImageSource>) -> Self {\n        self.author_icon = Some(source.into());\n        self\n    }\n    /// Makes the author's name a link.\n    pub fn on_author_click(\n        mut self,\n        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,\n    ) -> Self {\n        self.on_author_click = Some(Rc::new(handler));\n        self\n    }\n    /// Sets the title, which also names the card for assistive technology.\n    pub fn title(mut self, title: impl Into<SharedString>) -> Self {\n        self.title = Some(title.into());\n        self\n    }\n    /// Makes the title a link.\n    pub fn on_title_click(\n        mut self,\n        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,\n    ) -> Self {\n        self.on_title_click = Some(Rc::new(handler));\n        self\n    }\n    /// The card's text; render the attachment's Markdown into it.\n    pub fn body(mut self, body: impl IntoElement) -> Self {\n        self.body = Some(body.into_any_element());\n        self\n    }\n    /// Adds a field. Short fields sit two to a row; a long one takes the row.\n    pub fn field(\n        mut self,\n        label: impl Into<SharedString>,\n        value: impl IntoElement,\n        short: bool,\n    ) -> Self {\n        self.fields.push(EmbedField {\n            label: label.into(),\n            value: value.into_any_element(),\n            short,\n        });\n        self\n    }\n    /// Adds an image below the fields with its pixel size, so the card\n    /// reserves the image's box before it loads.\n    pub fn image(mut self, source: impl Into<ImageSource>, width: u32, height: u32) -> Self {\n        self.image = Some(ImageSlot::Image(SizedImage {\n            source: source.into(),\n            width,\n            height,\n        }));\n        self\n    }\n    /// Shows a click-to-load placeholder in the image's place, in the box the\n    /// image would take. Replaces `image`; pass the image through `image` once\n    /// it has loaded.\n    pub fn image_placeholder(mut self, placeholder: ImagePlaceholder) -> Self {\n        self.image = Some(ImageSlot::Placeholder(placeholder));\n        self\n    }\n    /// Runs when the image is clicked, such as to open a preview.\n    pub fn on_image_click(\n        mut self,\n        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,\n    ) -> Self {\n        self.on_image_click = Some(Rc::new(handler));\n        self\n    }\n    /// Adds a small square image on the right of the card.\n    pub fn thumbnail(mut self, source: impl Into<ImageSource>) -> Self {\n        self.thumbnail = Some(ThumbnailSlot::Image(source.into()));\n        self\n    }\n    /// Shows a click-to-load placeholder in the thumbnail's square. Replaces\n    /// `thumbnail`; pass the image through `thumbnail` once it has loaded.\n    pub fn thumbnail_placeholder(mut self, placeholder: ImagePlaceholder) -> Self {\n        self.thumbnail = Some(ThumbnailSlot::Placeholder(placeholder));\n        self\n    }\n    /// Sets the footer text.\n    pub fn footer(mut self, footer: impl Into<SharedString>) -> Self {\n        self.footer = Some(footer.into());\n        self\n    }\n    /// Sets the small round icon before the footer.\n    pub fn footer_icon(mut self, source: impl Into<ImageSource>) -> Self {\n        self.footer_icon = Some(source.into());\n        self\n    }\n    /// Sets the footer's time, already formatted by the caller.\n    pub fn timestamp(mut self, timestamp: impl Into<SharedString>) -> Self {\n        self.timestamp = Some(timestamp.into());\n        self\n    }\n    /// Matches the padding to the message list's density.\n    pub fn density(mut self, density: MessageDensity) -> Self {\n        self.density = density;\n        self\n    }\n}\n\nimpl Styled for MessageEmbed {\n    fn style(&mut self) -> &mut gpui_kit::StyleRefinement {\n        &mut self.style\n    }\n}\n\nimpl RenderOnce for MessageEmbed {\n    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {\n        let theme = UiTheme::read(cx).clone();\n        let colors = theme.colors;\n        let (padding, gap) = metrics(&theme, self.density);\n        let id = self.id;\n        let bar = match self.color {\n            Some(EmbedColor::Custom(color)) => color,\n            Some(EmbedColor::Good) => colors.success,\n            Some(EmbedColor::Warning) => colors.warning,\n            Some(EmbedColor::Danger) => colors.destructive,\n            None => colors.border,\n        };\n        let label = self\n            .title\n            .clone()\n            .or_else(|| self.author.clone())\n            .unwrap_or_else(|| \"Attachment\".into());\n        let small_icon = theme.space(4.);\n\n        let author = self.author.map(|name| {\n            div()\n                .flex()\n                .items_center()\n                .gap(theme.space(1.5))\n                .children(\n                    self.author_icon\n                        .map(|source| round_image(source, small_icon, &theme)),\n                )\n                .child(match self.on_author_click {\n                    Some(handler) => {\n                        let theme = theme.clone();\n                        Button::new((id.clone(), \"author\"))\n                            .aria_label(name.clone())\n                            .style_with(move |base| {\n                                let focus_theme = theme.clone();\n                                text::xs(base.role(Role::Link), &theme)\n                                    .font_weight(FontWeight::MEDIUM)\n                                    .rounded(theme.radius.sm)\n                                    .justify_start()\n                                    .cursor_pointer()\n                                    .hover(|style| style.underline())\n                                    .focus_visible(move |style| focus::ring(style, &focus_theme))\n                            })\n                            .on_click(move |event, window, cx| handler(event, window, cx))\n                            .child(name)\n                            .into_any_element()\n                    }\n                    None => text::xs(div(), &theme)\n                        .font_weight(FontWeight::MEDIUM)\n                        .child(name)\n                        .into_any_element(),\n                })\n        });\n        let title = self.title.map(|title| {\n            title_text(\n                (id.clone(), \"title\").into(),\n                title,\n                2,\n                self.on_title_click,\n                &theme,\n            )\n        });\n        let fields = (!self.fields.is_empty()).then(|| {\n            div()\n                .flex()\n                .flex_wrap()\n                .children(self.fields.into_iter().map(|field| {\n                    div()\n                        .when(field.short, |cell| cell.w(relative(0.5)))\n                        .when(!field.short, |cell| cell.w_full())\n                        .min_w_0()\n                        .pr(theme.space(3.))\n                        .py(theme.space(1.))\n                        .flex()\n                        .flex_col()\n                        .gap(theme.space(0.5))\n                        .child(\n                            text::xs(div(), &theme)\n                                .font_weight(FontWeight::SEMIBOLD)\n                                .child(field.label),\n                        )\n                        .child(text::sm(div(), &theme).child(field.value))\n                }))\n        });\n        let image = self.image.map(|slot| match slot {\n            ImageSlot::Image(image) => large_image(\n                (id.clone(), \"image\").into(),\n                image,\n                self.on_image_click,\n                &theme,\n            ),\n            ImageSlot::Placeholder(placeholder) => large_placeholder(\n                (id.clone(), \"image-placeholder\").into(),\n                placeholder,\n                window,\n                cx,\n                &theme,\n            ),\n        });\n        let thumb = self.thumbnail.map(|slot| match slot {\n            ThumbnailSlot::Image(source) => {\n                thumbnail((id.clone(), \"thumbnail\").into(), source, None, &theme)\n            }\n            ThumbnailSlot::Placeholder(placeholder) => thumbnail_placeholder(\n                (id.clone(), \"thumbnail-placeholder\").into(),\n                placeholder,\n                window,\n                cx,\n                &theme,\n            ),\n        });\n        let footer = (self.footer.is_some() || self.timestamp.is_some()).then(|| {\n            let line: SharedString = match (self.footer, self.timestamp) {\n                (Some(footer), Some(time)) => format!(\"{footer} · {time}\").into(),\n                (Some(footer), None) => footer,\n                (None, Some(time)) => time,\n                (None, None) => SharedString::default(),\n            };\n            div()\n                .flex()\n                .items_center()\n                .gap(theme.space(1.5))\n                .children(\n                    self.footer_icon\n                        .map(|source| round_image(source, small_icon, &theme)),\n                )\n                .child(\n                    text::xs(div(), &theme)\n                        .text_color(colors.muted_foreground)\n                        .child(line),\n                )\n        });\n\n        let card = card_frame(id.clone(), label, &theme)\n            .flex()\n            .flex_col()\n            .gap(theme.space(1.))\n            .children(self.pretext.map(|pretext| div().child(pretext)))\n            .child(\n                div()\n                    .flex()\n                    .child(\n                        // GPUI clips to rectangles, so the bar rounds its own\n                        // left corners and the content rounds the right ones.\n                        div()\n                            .debug_selector(|| \"embed-bar\".into())\n                            .w(theme.space(1.))\n                            .flex_shrink_0()\n                            .rounded_l(theme.radius.lg)\n                            .bg(bar),\n                    )\n                    .child(\n                        div()\n                            .flex_1()\n                            .min_w_0()\n                            .flex()\n                            .items_start()\n                            .gap(theme.space(3.))\n                            .p(padding)\n                            .rounded_r(theme.radius.lg)\n                            .border_1()\n                            .border_l_0()\n                            .border_color(colors.border)\n                            .bg(colors.card)\n                            .child(\n                                div()\n                                    .flex_1()\n                                    .min_w_0()\n                                    .flex()\n                                    .flex_col()\n                                    .gap(gap)\n                                    .children(author)\n                                    .children(title)\n                                    .children(self.body.map(|body| div().child(body)))\n                                    .children(fields)\n                                    .children(image)\n                                    .children(footer),\n                            )\n                            .children(thumb),\n                    ),\n            );\n        apply_style(card, &self.style)\n    }\n}\n\n/// How many lines of a quoted body show before \"Show more\".\npub const QUOTED_MESSAGE_LINES: usize = 4;\n\n/// A quoted post: a pasted permalink or a forwarded message, drawn as its own\n/// card next to [`LinkPreview`] and [`MessageEmbed`].\n///\n/// One line holds the author's avatar, name and time with a trailing Jump\n/// link; a muted line says where it was first posted; the body is clamped\n/// to a few lines with a \"Show more\" toggle, and an optional strip below it\n/// holds the quoted post's files or images. The card is named \"Quoted\n/// message from <author>\" for assistive technology.\n#[derive(IntoElement)]\npub struct QuotedMessage {\n    style: gpui_kit::StyleRefinement,\n    id: ElementId,\n    author: SharedString,\n    avatar: Option<ImageSource>,\n    initials: Option<SharedString>,\n    timestamp: Option<SharedString>,\n    channel: Option<SharedString>,\n    body: Option<AnyElement>,\n    attachments: Option<AnyElement>,\n    max_lines: usize,\n    density: MessageDensity,\n    on_jump: Option<Handler>,\n}\n\nimpl QuotedMessage {\n    /// Creates a quoted post by `author` with a stable caller-owned ID.\n    pub fn new(id: impl Into<ElementId>, author: impl Into<SharedString>) -> Self {\n        Self {\n            style: gpui_kit::StyleRefinement::default(),\n            id: id.into(),\n            author: author.into(),\n            avatar: None,\n            initials: None,\n            timestamp: None,\n            channel: None,\n            body: None,\n            attachments: None,\n            max_lines: QUOTED_MESSAGE_LINES,\n            density: MessageDensity::Standard,\n            on_jump: None,\n        }\n    }\n    /// Sets the author's avatar image; the initials show while it loads.\n    pub fn avatar(mut self, source: impl Into<ImageSource>) -> Self {\n        self.avatar = Some(source.into());\n        self\n    }\n    /// Sets the initials drawn when there is no avatar image. Defaults to the\n    /// first letter of the author's name.\n    pub fn initials(mut self, initials: impl Into<SharedString>) -> Self {\n        self.initials = Some(initials.into());\n        self\n    }\n    /// Sets the time shown after the author's name, already formatted.\n    pub fn timestamp(mut self, timestamp: impl Into<SharedString>) -> Self {\n        self.timestamp = Some(timestamp.into());\n        self\n    }\n    /// Names the channel the post came from, shown as \"Originally posted in\n    /// <channel>\". Leave it out for direct messages.\n    pub fn channel(mut self, channel: impl Into<SharedString>) -> Self {\n        self.channel = Some(channel.into());\n        self\n    }\n    /// Sets the quoted text, usually rendered Markdown.\n    pub fn body(mut self, body: impl IntoElement) -> Self {\n        self.body = Some(body.into_any_element());\n        self\n    }\n    /// Sets the strip of the quoted post's files or images under the body.\n    pub fn attachments(mut self, attachments: impl IntoElement) -> Self {\n        self.attachments = Some(attachments.into_any_element());\n        self\n    }\n    /// Sets how many body lines show before \"Show more\"; at least one.\n    pub fn max_lines(mut self, lines: usize) -> Self {\n        self.max_lines = lines.max(1);\n        self\n    }\n    /// Matches the density of the message list around the card.\n    pub fn density(mut self, density: MessageDensity) -> Self {\n        self.density = density;\n        self\n    }\n    /// Shows a Jump link that opens the original post.\n    pub fn on_jump(\n        mut self,\n        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,\n    ) -> Self {\n        self.on_jump = Some(Rc::new(handler));\n        self\n    }\n}\n\nimpl Styled for QuotedMessage {\n    fn style(&mut self) -> &mut gpui_kit::StyleRefinement {\n        &mut self.style\n    }\n}\n\n/// Whether the body is expanded, and whether it is taller than the clamp.\n#[derive(Default)]\nstruct QuoteState {\n    expanded: bool,\n    overflows: bool,\n}\n\n/// A small link-styled button, as Jump and Show more are.\nfn link_button(\n    id: ElementId,\n    label: SharedString,\n    aria_label: SharedString,\n    expanded: Option<bool>,\n    on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,\n    theme: &UiTheme,\n) -> Button {\n    let theme = theme.clone();\n    Button::new(id)\n        .aria_label(aria_label)\n        .style_with(move |base| {\n            let focus_theme = theme.clone();\n            let base = match expanded {\n                Some(expanded) => base.aria_expanded(expanded),\n                None => base.role(Role::Link),\n            };\n            text::xs(base, &theme)\n                .font_weight(FontWeight::MEDIUM)\n                .text_color(theme.colors.primary)\n                .rounded(theme.radius.sm)\n                .flex_shrink_0()\n                .cursor_pointer()\n                .hover(|style| style.underline())\n                .focus_visible(move |style| focus::ring(style, &focus_theme))\n        })\n        .on_click(on_click)\n        .child(label)\n}\n\nimpl RenderOnce for QuotedMessage {\n    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {\n        let theme = UiTheme::read(cx).clone();\n        let colors = theme.colors;\n        let (padding, gap) = metrics(&theme, self.density);\n        let id = self.id;\n        let state = window.use_keyed_state((id.clone(), \"quote\"), cx, |_, _| QuoteState::default());\n        let (expanded, overflows) = {\n            let state = state.read(cx);\n            (state.expanded, state.overflows)\n        };\n        let line_height = theme.text_style(theme.typography.sm).line_height;\n        let clamp = line_height * self.max_lines as f32;\n\n        let initials = self.initials.clone().unwrap_or_else(|| {\n            self.author\n                .chars()\n                .next()\n                .map(|c| c.to_uppercase().to_string())\n                .unwrap_or_default()\n                .into()\n        });\n        let avatar = Avatar::new((id.clone(), \"avatar\"))\n            .size(AvatarSize::Sm)\n            .fallback(initials)\n            .when_some(self.avatar, |avatar, source| avatar.image(source));\n        let author = self.author.clone();\n        let jump = self.on_jump.map(|handler| {\n            div()\n                .debug_selector(|| \"quote-jump\".into())\n                .child(link_button(\n                    (id.clone(), \"jump\").into(),\n                    \"Jump\".into(),\n                    format!(\"Jump to the message from {author}\").into(),\n                    None,\n                    move |event, window, cx| handler(event, window, cx),\n                    &theme,\n                ))\n        });\n        let header = div()\n            .flex()\n            .items_center()\n            .gap(theme.space(2.))\n            .min_w_0()\n            .child(avatar)\n            .child(\n                div()\n                    .flex_1()\n                    .min_w_0()\n                    .flex()\n                    .items_baseline()\n                    .gap(theme.space(2.))\n                    .overflow_hidden()\n                    .child(\n                        text::sm(div(), &theme)\n                            .font_weight(FontWeight::SEMIBOLD)\n                            .flex_shrink_0()\n                            .child(self.author.clone()),\n                    )\n                    .children(self.timestamp.map(|time| {\n                        text::xs(div(), &theme)\n                            .text_color(colors.muted_foreground)\n                            .whitespace_nowrap()\n                            .overflow_hidden()\n                            .text_ellipsis()\n                            .child(time)\n                    })),\n            )\n            .children(jump);\n        let origin = self.channel.map(|channel| {\n            text::xs(div(), &theme)\n                .debug_selector(|| \"quote-origin\".into())\n                .text_color(colors.muted_foreground)\n                .overflow_hidden()\n                .text_ellipsis()\n                .whitespace_nowrap()\n                .child(format!(\"Originally posted in {channel}\"))\n        });\n        let body = self.body.map(|body| {\n            let measured = state.clone();\n            div()\n                .flex()\n                .flex_col()\n                .gap(theme.space(1.))\n                .child(\n                    text::sm(div(), &theme)\n                        .debug_selector(|| \"quote-body\".into())\n                        .when(!expanded, |frame| frame.max_h(clamp).overflow_hidden())\n                        .child(\n                            div()\n                                .on_prepaint(move |bounds, window, cx| {\n                                    // The text's natural height, measured\n                                    // inside the clamp, decides whether\n                                    // there is more to show.\n                                    let overflows = bounds.size.height > clamp + px(0.5);\n                                    if measured.read(cx).overflows != overflows {\n                                        measured.update(cx, |state, _| state.overflows = overflows);\n                                        window.refresh();\n                                    }\n                                })\n                                .child(body),\n                        ),\n                )\n                .when(overflows, |column| {\n                    let toggle = state.clone();\n                    let label: SharedString =\n                        if expanded { \"Show less\" } else { \"Show more\" }.into();\n                    column.child(div().debug_selector(|| \"quote-toggle\".into()).flex().child(\n                        link_button(\n                            (id.clone(), \"more\").into(),\n                            label.clone(),\n                            label,\n                            Some(expanded),\n                            move |_, window, cx| {\n                                toggle.update(cx, |state, _| state.expanded = !state.expanded);\n                                window.refresh();\n                            },\n                            &theme,\n                        ),\n                    ))\n                })\n        });\n\n        let card = card_frame(\n            id.clone(),\n            format!(\"Quoted message from {}\", self.author).into(),\n            &theme,\n        )\n        .flex()\n        .flex_col()\n        .gap(gap)\n        .p(padding)\n        .rounded(theme.radius.lg)\n        .border_1()\n        .border_color(colors.border)\n        .bg(colors.card)\n        .child(header)\n        .children(origin)\n        .children(body)\n        .children(\n            self.attachments\n                .map(|strip| div().flex().flex_wrap().gap(gap).child(strip)),\n        );\n        apply_style(card, &self.style)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use std::{cell::Cell, rc::Rc};\n\n    use super::*;\n    use gpui_kit::{Context, Render, TestAppContext, VisualTestContext};\n\n    #[test]\n    fn auto_layout_puts_small_and_square_images_beside_the_text() {\n        use LinkPreviewImageLayout::*;\n        assert_eq!(Auto.resolve(1200, 630), Large);\n        assert_eq!(Auto.resolve(120, 60), Thumbnail);\n        assert_eq!(Auto.resolve(400, 400), Thumbnail);\n        assert_eq!(Auto.resolve(300, 600), Thumbnail);\n        assert_eq!(Large.resolve(100, 100), Large);\n        assert_eq!(Thumbnail.resolve(1200, 630), Thumbnail);\n    }\n\n    #[test]\n    fn bar_colors_parse_hex_and_status_names() {\n        let custom = |embed: MessageEmbed| match embed.color {\n            Some(EmbedColor::Custom(color)) => Some(color),\n            _ => None,\n        };\n        assert_eq!(\n            custom(MessageEmbed::new(\"a\").color_str(\"#ff0000\")),\n            Some(Rgba::try_from(\"#ff0000\").unwrap())\n        );\n        assert_eq!(\n            custom(MessageEmbed::new(\"a\").color_str(\"0f0\")),\n            Some(Rgba::try_from(\"#00ff00\").unwrap())\n        );\n        assert!(matches!(\n            MessageEmbed::new(\"a\").color_str(\"Danger\").color,\n            Some(EmbedColor::Danger)\n        ));\n        assert!(MessageEmbed::new(\"a\").color_str(\"blue\").color.is_none());\n        assert!(MessageEmbed::new(\"a\").color_str(\"#12345\").color.is_none());\n    }\n\n    struct Probe {\n        collapsed: bool,\n        toggled: Rc<Cell<Option<bool>>>,\n    }\n\n    impl Render for Probe {\n        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {\n            let toggled = self.toggled.clone();\n            div()\n                .w(px(600.))\n                .flex()\n                .flex_col()\n                .gap(px(8.))\n                .child(\n                    LinkPreview::new(\"preview\", \"gpuicn 0.9.1 released\")\n                        .site_name(\"GitHub\")\n                        .description(\"Seven component hooks for Matty.\")\n                        .image(\"missing.png\", 1200, 630)\n                        .collapsed(self.collapsed)\n                        .on_click(|_, _, _| {})\n                        .on_toggle(move |collapsed, _, _| toggled.set(Some(*collapsed)))\n                        .on_remove(|_, _, _| {}),\n                )\n                .child(\n                    MessageEmbed::new(\"embed\")\n                        .color_str(\"good\")\n                        .pretext(\"Build finished\")\n                        .author(\"CI\")\n                        .title(\"verify #812\")\n                        .on_title_click(|_, _, _| {})\n                        .body(\"All checks passed.\")\n                        .field(\"Branch\", \"main\", true)\n                        .field(\"Duration\", \"4m 12s\", true)\n                        .field(\"Commit\", \"Fix the dialog scroll\", false)\n                        .footer(\"GitHub Actions\")\n                        .timestamp(\"10:42\"),\n                )\n        }\n    }\n\n    fn probe(\n        cx: &mut TestAppContext,\n        collapsed: bool,\n    ) -> (Rc<Cell<Option<bool>>>, VisualTestContext) {\n        cx.update(super::super::theme::init);\n        let toggled = Rc::new(Cell::new(None));\n        let window = cx.add_window({\n            let toggled = toggled.clone();\n            move |_, _| Probe { collapsed, toggled }\n        });\n        (toggled, VisualTestContext::from_window(window.into(), cx))\n    }\n\n    #[gpui_kit::test]\n    fn draws_in_both_themes(cx: &mut TestAppContext) {\n        let (_, mut cx) = probe(cx, false);\n        for mode in [\n            super::super::theme::ThemeMode::Light,\n            super::super::theme::ThemeMode::Dark,\n        ] {\n            cx.update(|_, cx| UiTheme::switch(cx, mode));\n            cx.update(|window, cx| window.draw(cx).clear(cx));\n        }\n        assert!(cx.debug_bounds(\"embed-bar\").is_some());\n    }\n\n    #[gpui_kit::test]\n    fn large_images_reserve_their_box_before_loading(cx: &mut TestAppContext) {\n        let (_, mut cx) = probe(cx, false);\n        cx.update(|window, cx| window.draw(cx).clear(cx));\n        let image = cx.debug_bounds(\"embed-image\").unwrap();\n        // 1200 × 630 fills the card's content width (480px less padding and\n        // border) and keeps its aspect ratio, so the height is known up front.\n        assert_eq!(image.size.width, px(454.));\n        let expected = 454. * 630. / 1200.;\n        assert!((f32::from(image.size.height) - expected).abs() < 1.);\n    }\n\n    struct PlaceholderProbe {\n        loads: Rc<Cell<usize>>,\n        size: Option<(u32, u32)>,\n    }\n\n    impl Render for PlaceholderProbe {\n        fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {\n            let loads = self.loads.clone();\n            let mut placeholder =\n                ImagePlaceholder::new(\"github.com\", move |_, _, _| loads.set(loads.get() + 1));\n            if let Some((width, height)) = self.size {\n                placeholder = placeholder.size(width, height);\n            }\n            div().w(px(600.)).child(\n                LinkPreview::new(\"preview\", \"gpuicn 0.9.3 released\")\n                    .site_name(\"GitHub\")\n                    .image_placeholder(placeholder),\n            )\n        }\n    }\n\n    fn placeholder_probe(\n        cx: &mut TestAppContext,\n        size: Option<(u32, u32)>,\n    ) -> (Rc<Cell<usize>>, VisualTestContext) {\n        cx.update(super::super::theme::init);\n        let loads = Rc::new(Cell::new(0));\n        let window = cx.add_window({\n            let loads = loads.clone();\n            move |_, _| PlaceholderProbe { loads, size }\n        });\n        let mut cx = VisualTestContext::from_window(window.into(), cx);\n        cx.update(|window, cx| window.draw(cx).clear(cx));\n        (loads, cx)\n    }\n\n    #[gpui_kit::test]\n    fn placeholders_take_the_images_box(cx: &mut TestAppContext) {\n        // The same 1200 × 630 box the loaded image takes.\n        let (_, mut sized) = placeholder_probe(cx, Some((1200, 630)));\n        let frame = sized.debug_bounds(\"embed-image-placeholder\").unwrap();\n        assert_eq!(frame.size.width, px(454.));\n        assert!((f32::from(frame.size.height) - 454. * 630. / 1200.).abs() < 1.);\n    }\n\n    #[gpui_kit::test]\n    fn placeholders_without_a_size_are_16_by_9(cx: &mut TestAppContext) {\n        let (_, mut cx) = placeholder_probe(cx, None);\n        let frame = cx.debug_bounds(\"embed-image-placeholder\").unwrap();\n        assert_eq!(frame.size.width, px(454.));\n        assert!((f32::from(frame.size.height) - 454. * 9. / 16.).abs() < 1.);\n    }\n\n    #[gpui_kit::test]\n    fn square_placeholders_sit_in_the_thumbnail(cx: &mut TestAppContext) {\n        let (_, mut cx) = placeholder_probe(cx, Some((400, 400)));\n        assert!(cx.debug_bounds(\"embed-image-placeholder\").is_none());\n        let frame = cx.debug_bounds(\"embed-thumbnail-placeholder\").unwrap();\n        assert_eq!(frame.size.width, frame.size.height);\n    }\n\n    #[gpui_kit::test]\n    fn the_placeholder_loads_once(cx: &mut TestAppContext) {\n        let (loads, mut cx) = placeholder_probe(cx, None);\n        let frame = cx.debug_bounds(\"embed-image-placeholder\").unwrap();\n        cx.simulate_click(frame.center(), Default::default());\n        cx.simulate_click(frame.center(), Default::default());\n        assert_eq!(loads.get(), 1);\n    }\n\n    #[gpui_kit::test]\n    fn enter_loads_the_focused_placeholder(cx: &mut TestAppContext) {\n        cx.update(super::super::theme::init);\n        let loads = Rc::new(Cell::new(0));\n        let window = cx.add_window({\n            let loads = loads.clone();\n            move |window, _| {\n                window.activate_window();\n                PlaceholderProbe { loads, size: None }\n            }\n        });\n        let mut cx = VisualTestContext::from_window(window.into(), cx);\n        cx.run_until_parked();\n        for keys in [\"tab\", \"enter\", \"space\"] {\n            cx.update(|window, cx| window.draw(cx).clear(cx));\n            cx.simulate_keystrokes(keys);\n            cx.update(|window, cx| {\n                window.dispatch_event(\n                    gpui_kit::PlatformInput::KeyUp(gpui_kit::KeyUpEvent {\n                        keystroke: gpui_kit::Keystroke::parse(keys).unwrap(),\n                    }),\n                    cx,\n                );\n            });\n            cx.run_until_parked();\n            if keys == \"enter\" {\n                assert_eq!(loads.get(), 1, \"Enter runs on_load\");\n            }\n        }\n        assert_eq!(loads.get(), 1, \"on_load runs once\");\n    }\n\n    #[gpui_kit::test]\n    fn the_toggle_reports_the_new_state_and_collapsing_hides_the_image(cx: &mut TestAppContext) {\n        let (toggled, mut cx) = probe(cx, true);\n        cx.update(|window, cx| window.draw(cx).clear(cx));\n        assert!(cx.debug_bounds(\"embed-image\").is_none());\n        assert!(cx.debug_bounds(\"embed-thumbnail\").is_none());\n        let toggle = cx.debug_bounds(\"embed-toggle\").unwrap();\n        cx.simulate_click(toggle.center(), Default::default());\n        assert_eq!(\n            toggled.get(),\n            Some(false),\n            \"expanding reports collapsed = false\"\n        );\n    }\n\n    struct Quote {\n        jumps: Rc<Cell<usize>>,\n        body: &'static str,\n    }\n\n    impl Render for Quote {\n        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {\n            let jumps = self.jumps.clone();\n            div().w(px(420.)).child(\n                QuotedMessage::new(\"quote\", \"Margaret Hamilton\")\n                    .timestamp(\"Yesterday at 4:12 PM\")\n                    .channel(\"~town-square\")\n                    .max_lines(2)\n                    .body(div().child(self.body))\n                    .on_jump(move |_, _, _| jumps.set(jumps.get() + 1)),\n            )\n        }\n    }\n\n    fn quote(body: &'static str, cx: &mut TestAppContext) -> (Rc<Cell<usize>>, VisualTestContext) {\n        cx.update(super::super::theme::init);\n        let jumps = Rc::new(Cell::new(0));\n        let window = cx.add_window({\n            let jumps = jumps.clone();\n            move |_, _| Quote { jumps, body }\n        });\n        let mut cx = VisualTestContext::from_window(window.into(), cx);\n        cx.update(|window, cx| {\n            window.draw(cx).clear(cx);\n            window.draw(cx).clear(cx);\n        });\n        (jumps, cx)\n    }\n\n    const LONG: &str = \"The landing software flagged an overflow, and the computer \\\n        shed its lowest-priority work to keep the guidance running. That was the \\\n        design: the priority scheduler restarted the jobs that mattered, and the \\\n        crew carried on with the descent while Houston confirmed the alarm was safe.\";\n\n    #[gpui_kit::test]\n    fn a_long_quote_clamps_and_show_more_expands_it(cx: &mut TestAppContext) {\n        let (_, mut cx) = quote(LONG, cx);\n        let clamped = cx.debug_bounds(\"quote-body\").unwrap();\n        let line = UiTheme::default()\n            .text_style(UiTheme::default().typography.sm)\n            .line_height;\n        assert_eq!(clamped.size.height, line * 2., \"two lines\");\n        assert!(cx.debug_bounds(\"quote-origin\").is_some());\n        let toggle = cx.debug_bounds(\"quote-toggle\").expect(\"Show more appears\");\n        let on_label = toggle.origin + gpui_kit::point(px(8.), toggle.size.height / 2.);\n        cx.simulate_click(on_label, Default::default());\n        cx.update(|window, cx| window.draw(cx).clear(cx));\n        let expanded = cx.debug_bounds(\"quote-body\").unwrap();\n        assert!(expanded.size.height > clamped.size.height, \"{expanded:?}\");\n    }\n\n    #[gpui_kit::test]\n    fn a_short_quote_has_no_show_more_and_jump_reports(cx: &mut TestAppContext) {\n        let (jumps, mut cx) = quote(\"Lunch?\", cx);\n        assert!(cx.debug_bounds(\"quote-toggle\").is_none());\n        cx.update(|window, _| {\n            use gpui_kit::test::TestQueryExt as _;\n            window.find_by_label(\"Jump to the message from Margaret Hamilton\");\n        });\n        let jump = cx.debug_bounds(\"quote-jump\").unwrap();\n        cx.simulate_click(jump.center(), Default::default());\n        assert_eq!(jumps.get(), 1);\n    }\n}\n"
    }
  ]
}
