aetna-core 0.3.1

Aetna — backend-agnostic UI library core
Documentation
//! Design tokens — the named, finite vocabulary of values everything else
//! refers to.
//!
//! Tokens are public `const` values, not a runtime struct. That means:
//!
//! - Reading the file once gives you the entire vocabulary.
//! - Components reference tokens by name (`tokens::CARD`) directly,
//!   no theme handle threaded through every call.
//! - Constants are inlined and indistinguishable from any other f32/Color
//!   at runtime — there's no `OnceLock` to initialize.
//!
//! Naming intentionally shadows shadcn/Tailwind so LLM training transfers:
//! `CARD`, `MUTED_FOREGROUND`, `RADIUS_LG`, `SPACE_3`, etc.
//!
//! ## Palette resolution
//!
//! [`Color`] tokens carry a `token: Some(name)` marker. The renderer
//! resolves each tokened color through the active [`crate::Palette`] at
//! paint time, so the rgba seen on screen tracks
//! [`crate::Theme`]`::palette()` rather than the constants in this file.
//! The constants here serve as the **fallback rgba** — used when a token
//! has no palette entry or when the host renders without a theme
//! (`draw_ops` with the default `Theme`, whose default palette mirrors
//! these constants exactly). The fallback values match the default
//! Aetna Dark palette, which is copied from shadcn/ui zinc dark; pick a
//! different palette at runtime via [`crate::Theme::with_palette`] or
//! [`crate::Theme::aetna_light`].

use crate::tree::Color;

// ---- Palette ----
//
// Color tokens carry the default Aetna Dark / shadcn zinc dark rgba as
// their compile-time fallback. Apps swap to a light palette at runtime via
// `Theme::aetna_light()` (see `crate::Palette` for the full token →
// rgba mapping for each palette).

// Core shadcn-shaped semantic colors.
pub const BACKGROUND: Color = Color::token("background", 9, 9, 11, 255);
pub const FOREGROUND: Color = Color::token("foreground", 250, 250, 250, 255);

pub const CARD: Color = Color::token("card", 9, 9, 11, 255);
pub const CARD_FOREGROUND: Color = Color::token("card-foreground", 250, 250, 250, 255);

pub const POPOVER: Color = Color::token("popover", 9, 9, 11, 255);
pub const POPOVER_FOREGROUND: Color = Color::token("popover-foreground", 250, 250, 250, 255);

pub const PRIMARY: Color = Color::token("primary", 250, 250, 250, 255);
pub const PRIMARY_FOREGROUND: Color = Color::token("primary-foreground", 24, 24, 27, 255);

pub const SECONDARY: Color = Color::token("secondary", 39, 39, 42, 255);
pub const SECONDARY_FOREGROUND: Color = Color::token("secondary-foreground", 250, 250, 250, 255);

pub const MUTED: Color = Color::token("muted", 39, 39, 42, 255);
pub const MUTED_FOREGROUND: Color = Color::token("muted-foreground", 161, 161, 170, 255);

pub const ACCENT: Color = Color::token("accent", 39, 39, 42, 255);
pub const ACCENT_FOREGROUND: Color = Color::token("accent-foreground", 250, 250, 250, 255);

pub const DESTRUCTIVE: Color = Color::token("destructive", 127, 29, 29, 255);
pub const DESTRUCTIVE_FOREGROUND: Color =
    Color::token("destructive-foreground", 250, 250, 250, 255);

pub const BORDER: Color = Color::token("border", 39, 39, 42, 255);
pub const INPUT: Color = Color::token("input", 39, 39, 42, 255);
pub const RING: Color = Color::token("ring", 212, 212, 216, 255);

pub const SUCCESS: Color = Color::token("success", 16, 185, 129, 255);
pub const SUCCESS_FOREGROUND: Color = Color::token("success-foreground", 5, 46, 22, 255);
pub const WARNING: Color = Color::token("warning", 245, 158, 11, 255);
pub const WARNING_FOREGROUND: Color = Color::token("warning-foreground", 69, 26, 3, 255);
pub const INFO: Color = Color::token("info", 59, 130, 246, 255);
pub const INFO_FOREGROUND: Color = Color::token("info-foreground", 239, 246, 255, 255);

// Extension colors. These remain semantic, but they describe a specific
// component/domain rather than the reusable shadcn core palette.
pub const OVERLAY_SCRIM: Color = Color::token("overlay-scrim", 0, 0, 0, 204);

/// Themed link color. Picked up automatically by `.link(url)` runs
/// (and any `RunStyle.link.is_some()` run, regardless of how it was
/// constructed). Distinct from `PRIMARY` so an underlined link reads
/// as a link, not an action accent — brighter on dark, darker on light.
pub const LINK_FOREGROUND: Color = Color::token("link-foreground", 96, 165, 250, 255);

// ---- Spacing ----
//
// Spacing follows Tailwind's numeric scale so layout code reads like
// the UI examples LLMs have seen most often: `gap-3` is 12 px, `p-4`
// is 16 px, `mt-2` is 8 px, etc.
pub const SPACE_0: f32 = 0.0;
pub const SPACE_1: f32 = 4.0;
pub const SPACE_2: f32 = 8.0;
pub const SPACE_3: f32 = 12.0;
pub const SPACE_4: f32 = 16.0;
pub const SPACE_5: f32 = 20.0;
pub const SPACE_6: f32 = 24.0;
pub const SPACE_7: f32 = 28.0;
pub const SPACE_8: f32 = 32.0;
pub const SPACE_10: f32 = 40.0;
pub const SPACE_12: f32 = 48.0;

// ---- Pinned-pane sizing ----
//
// Conventional starting widths for a resizable sidebar (file tree,
// settings nav, inspector). Sourced from VS Code (~240px), Slack
// (~270px), and Finder (~250px) — wide enough that label content
// stays readable, narrow enough that the main work area still
// dominates. `_MIN` is the floor below which file/label content
// truncates badly; `_MAX` is the ceiling above which a sidebar
// stops being a sidebar.
pub const SIDEBAR_WIDTH: f32 = 256.0;
pub const SIDEBAR_WIDTH_MIN: f32 = 180.0;
pub const SIDEBAR_WIDTH_MAX: f32 = 480.0;

// ---- Control sizing ----
//
// Shared row height for input-tier controls — buttons, selects, text
// inputs, tabs, pagination, command-palette rows. Form layouts depend
// on these aligning across widget kinds, so the value lives at the
// token tier rather than each widget hardcoding 32.0. Use this when
// sizing a parent container that has to fit a control row, or when
// composing a new control-shaped widget.
//
// Matches Tailwind/shadcn `h-8` (32 px) — the default for `Button`,
// `Input`, `Select`, etc. Square icon-only controls (icon button,
// pagination cell) use this as both width and height.
pub const CONTROL_HEIGHT: f32 = 32.0;

// ---- Radius ----
pub const RADIUS_SM: f32 = 4.0;
pub const RADIUS_MD: f32 = 8.0;
pub const RADIUS_LG: f32 = 12.0;
pub const RADIUS_PILL: f32 = 999.0;

// ---- Scrollbar thumb (overlay indicator on scrollable viewports) ----
/// Visible thumb width when idle. Kept thin so it doesn't crowd
/// content; the hitbox is wider so the thumb still feels grabbable
/// (Fitts's law).
pub const SCROLLBAR_THUMB_WIDTH: f32 = 6.0;
/// Visible thumb width while hovered or being dragged. The thumb
/// expands toward the viewport interior so the cursor sits inside
/// it instead of pinning the right edge.
pub const SCROLLBAR_THUMB_WIDTH_ACTIVE: f32 = 10.0;
/// Track / thumb hitbox width — the column on the right edge that
/// accepts pointer presses for thumb-grab and click-to-page. Always
/// wider than the visible thumb (idle or active) so grabbing a
/// thin idle thumb is still easy. Matches the shadcn ScrollArea
/// track width convention.
pub const SCROLLBAR_HITBOX_WIDTH: f32 = 14.0;
pub const SCROLLBAR_TRACK_INSET: f32 = 2.0;
pub const SCROLLBAR_THUMB_MIN_H: f32 = 24.0;
/// Idle thumb fill — subtle on background/card.
pub const SCROLLBAR_THUMB_FILL: Color = Color::token("scrollbar-thumb", 113, 113, 122, 120);
/// Active (hovered or dragged) thumb fill — fully opaque accent.
pub const SCROLLBAR_THUMB_FILL_ACTIVE: Color =
    Color::token("scrollbar-thumb-active", 161, 161, 170, 220);

// ---- Shadow (passed to renderer as a "level"; backend interprets) ----
pub const SHADOW_SM: f32 = 4.0;
pub const SHADOW_MD: f32 = 12.0;
pub const SHADOW_LG: f32 = 24.0;

// ---- Typography ----
//
// Font-size tokens are pairs, matching Tailwind's default type scale:
// a `text-sm` token is 14/20, `text-2xl` is 24/32, and so on. Text
// roles should choose one of these tokens rather than setting a raw
// font size and letting measurement infer a line height later.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TypeToken {
    pub size: f32,
    pub line_height: f32,
}

pub const TEXT_XS: TypeToken = TypeToken {
    size: 12.0,
    line_height: 16.0,
};
pub const TEXT_SM: TypeToken = TypeToken {
    size: 14.0,
    line_height: 20.0,
};
pub const TEXT_BASE: TypeToken = TypeToken {
    size: 16.0,
    line_height: 24.0,
};
pub const TEXT_LG: TypeToken = TypeToken {
    size: 18.0,
    line_height: 28.0,
};
pub const TEXT_XL: TypeToken = TypeToken {
    size: 20.0,
    line_height: 28.0,
};
pub const TEXT_2XL: TypeToken = TypeToken {
    size: 24.0,
    line_height: 32.0,
};
pub const TEXT_3XL: TypeToken = TypeToken {
    size: 30.0,
    line_height: 36.0,
};

pub fn type_token_for_size(size: f32) -> Option<TypeToken> {
    [
        TEXT_XS, TEXT_SM, TEXT_BASE, TEXT_LG, TEXT_XL, TEXT_2XL, TEXT_3XL,
    ]
    .into_iter()
    .find(|token| (token.size - size).abs() < f32::EPSILON)
}

pub fn line_height_for_size(size: f32) -> f32 {
    type_token_for_size(size)
        .map(|token| token.line_height)
        .unwrap_or((size * 1.3).ceil())
}

// ---- Icons ----
//
// Common lucide/shadcn icon boxes. `ICON_SM` is Tailwind `size-4`;
// `ICON_XS` maps to the common `size-3.5` treatment used in dense
// cells and compact status rows.
pub const ICON_XS: f32 = 14.0;
pub const ICON_SM: f32 = 16.0;
pub const ICON_MD: f32 = 20.0;

// ---- State styling ----
//
// Visual deltas applied when an element is in a non-default interaction
// state. Renderer consumes these.

/// How much to darken a fill on press, as a 0..1 factor.
pub const PRESS_DARKEN: f32 = 0.12;
/// How much to lighten a fill on hover, as a 0..1 factor.
pub const HOVER_LIGHTEN: f32 = 0.06;
/// Peak alpha contribution from a fully-eased hover envelope on a
/// surface with no resting fill (`.ghost()`, inactive tab triggers,
/// `.outline()`). Hover/press envelopes only modulate an existing
/// fill, so without a synthesized state-only fill these surfaces show
/// no interaction feedback. Mirrors the shadcn idiom
/// `hover:bg-accent active:bg-accent/80` — transparent at rest, a faint
/// raised surface fades in on interaction.
pub const STATE_FILL_HOVER_ALPHA: f32 = 0.40;
/// Additional peak alpha contribution from a fully-eased press
/// envelope. Sums with [`STATE_FILL_HOVER_ALPHA`] (clamped to 1.0) so
/// a press while hovered reads slightly more committed than hover
/// alone, but still quieter than the active/current treatment.
pub const STATE_FILL_PRESS_ALPHA: f32 = 0.25;
/// Opacity multiplier when an element is disabled.
pub const DISABLED_ALPHA: f32 = 0.5;
/// Ring outset (additional focus stroke beyond the element bounds).
pub const RING_WIDTH: f32 = 2.0;
/// Background tint for selected text in `text_input` / `text_area`.
/// Tinted accent at low alpha so glyphs stay readable through the
/// selection rectangle.
pub const SELECTION_BG: Color = Color::token("selection-bg", 96, 165, 250, 96);
/// Selection-band fill applied while a text input lacks focus. A
/// neutral, low-saturation cousin of [`SELECTION_BG`]; the painter
/// lerps from this toward `SELECTION_BG` as the input regains focus
/// (see [`crate::tree::El::dim_fill`]). Matches the macOS convention
/// where unfocused selection reads as gray rather than blue.
pub const SELECTION_BG_UNFOCUSED: Color = Color::token("selection-bg-unfocused", 113, 113, 122, 64);