# Entangle UI — Full LLM Documentation > Concatenated documentation for entangle-ui. Generated from the Astro docs source. Each section starts with `# ` and is separated by `---`. ========================= # Overview ========================= --- # Entangle UI > React component library for professional editor interfaces _Source: /index_ ========================= # Getting Started ========================= --- # Installation > How to install and set up Entangle UI in your project. _Source: /getting-started/installation_ ## Install Entangle UI ```bash npm install entangle-ui@alpha ``` ### Peer Dependencies Entangle UI requires the following peer dependencies: ```bash npm install react react-dom @base-ui/react @floating-ui/react ``` | Package | Version | | -------------------- | ---------- | | `react` | `>=19.1.0` | | `react-dom` | `>=19.1.0` | | `@base-ui/react` | `^1.1.0` | | `@floating-ui/react` | `^0.27.17` | :::note Entangle UI is ESM-only (`"type": "module"`). Make sure your bundler supports ES modules. ::: ## Deep Imports Color palettes are available via a dedicated export for better tree-shaking: ```tsx import { MATERIAL_PALETTE, TAILWIND_PALETTE } from 'entangle-ui/palettes'; ``` Available palettes: `MATERIAL_PALETTE`, `TAILWIND_PALETTE`, `PASTEL_PALETTE`, `EARTH_PALETTE`, `NEON_PALETTE`, `MONOCHROME_PALETTE`, `SKIN_TONES_PALETTE`, `VINTAGE_PALETTE`, `PROFESSIONAL_PALETTES`. ## TypeScript All components export their prop types. Utility types are also available: ```tsx import type { Tokens, ThemeVars, Prettify, DeepPartial, LiteralUnion, } from 'entangle-ui'; ``` ## Utilities ```tsx import { cx, cn } from 'entangle-ui'; // cx — join class names, filtering out falsy values <div className={cx('base', isActive && 'active', className)} />; ``` ## Next Steps - [Quick Start](/getting-started/quick-start) — build your first component - [Theming](/guides/theming) — customize the theme --- # Introduction > Entangle UI is a React component library for building professional editor interfaces — 3D tools, node editors, parameter systems, and more. _Source: /getting-started/introduction_ Entangle UI is a specialized React component library for building **professional editor interfaces** — 3D tools, node editors, parameter systems, creative applications, and more. ## Why Entangle UI? - **Editor-focused** — components designed for dense, information-rich UIs, not marketing pages - **Dark-first** — all default values target dark backgrounds suitable for creative tools - **Zero-runtime CSS** — powered by [Vanilla Extract](https://vanilla-extract.style/), styles compile to static CSS at build time - **Type-safe** — full TypeScript support with strict types for all props and theme tokens - **Tree-shakeable** — ESM-only, `preserveModules`, and `/*#__PURE__*/` annotations ensure only used code is bundled ## Component Catalog ### Primitives Basic building blocks for any interface. | Component | Description | | ------------- | ----------------------------------------------------------------- | | `Button` | Primary action trigger with `default`, `ghost`, `filled` variants | | `IconButton` | Square button optimized for icon-only actions | | `Input` | Text input with label, icons, error states | | `Text` | Typography component with semantic variants | | `Paper` | Surface container with elevation and nesting | | `Icon` | SVG icon wrapper with consistent sizing | | `Checkbox` | Toggle control with indeterminate state | | `Switch` | Binary toggle switch | | `Tooltip` | Accessible hover/focus tooltip | | `Popover` | Floating content anchored to a trigger | | `Collapsible` | Expandable/collapsible content section | ### Layout Composable layout primitives with responsive breakpoints. | Component | Description | | -------------- | ---------------------------------------------------- | | `Stack` | Flex-based stacking with spacing multiplier | | `Flex` | Full flexbox control with responsive direction | | `Grid` | CSS Grid layout | | `Spacer` | Empty spacing element | | `ScrollArea` | Custom-styled scrollbar container | | `SplitPane` | Resizable split panel | | `Accordion` | Multiple collapsible sections with single/multi mode | | `PanelSurface` | Panel with header/body/footer slots | ### Controls Advanced interactive controls for editor workflows. | Component | Description | | ----------------- | ----------------------------------------------- | | `Slider` | Value slider with keyboard and modifier support | | `NumberInput` | Numeric input with increment/decrement | | `Select` | Dropdown with groups and search | | `VectorInput` | Multi-axis numeric input (2D/3D vectors) | | `ColorPicker` | Full color picker with modes and palettes | | `CartesianPicker` | 2D coordinate picker | | `CurveEditor` | Bezier curve editor with keyframe manipulation | | `TreeView` | Hierarchical data with expand/collapse | ### Navigation | Component | Description | | ------------- | ------------------------------------------------------------------------- | | `Menu` | Composable dropdown menu (`Menu.Trigger`, `Menu.Content`, `Menu.Item`, …) | | `ContextMenu` | Right-click context menu | | `Tabs` | Tab navigation with `TabList`, `Tab`, `TabPanel` | ### Feedback | Component | Description | | --------------- | ------------------------------------ | | `Dialog` | Modal dialog with header/body/footer | | `ToastProvider` | Toast notification system | | `useToast` | Hook to show toasts programmatically | ### Shell (Application Layout) Full-application layout components designed for professional editor interfaces. | Component | Description | | --------------- | -------------------------------------------------------------- | | `AppShell` | Root layout with `.MenuBar`, `.Toolbar`, `.Dock`, `.StatusBar` | | `MenuBar` | Top-level application menu bar | | `Toolbar` | Action toolbar with buttons, toggles, groups | | `StatusBar` | Bottom information bar | | `FloatingPanel` | Draggable, resizable floating window | ### Editor | Component | Description | | ------------------- | ------------------------------------- | | `ChatPanel` | AI chat interface panel | | `PropertyInspector` | Inspector panel for object properties | | `ViewportGizmo` | 3D viewport orientation gizmo | ## Next Steps - [Installation](/getting-started/installation) — install Entangle UI and its peer dependencies - [Quick Start](/getting-started/quick-start) — build your first component in minutes - [Theming](/guides/theming) — customize colors, tokens, and create branded themes - [Styling](/guides/styling) — learn the Vanilla Extract styling system - [Docs for LLMs](/llms-txt) — plain-text documentation for AI coding assistants --- # Quick Start > Get up and running with Entangle UI in under five minutes. _Source: /getting-started/quick-start_ Get up and running with Entangle UI in under five minutes. ## Basic Setup Import the dark theme CSS to register all `--etui-*` CSS custom properties on `:root`. No wrapper component is needed for the default look: ```tsx import 'entangle-ui/theme'; import { Button } from 'entangle-ui'; function App() { return <Button variant="filled">Save</Button>; } ``` The default dark theme is applied automatically — no extra configuration required. ### With Custom Overrides Override tokens with plain CSS on any selector: ```css .my-theme { --etui-color-accent-primary: #2aa1ff; --etui-color-bg-primary: #0d1117; } ``` Or use `VanillaThemeProvider` for scoped overrides: ```tsx import 'entangle-ui/theme'; import { VanillaThemeProvider } from 'entangle-ui'; function App() { return ( <VanillaThemeProvider className="my-theme"> <YourApp /> </VanillaThemeProvider> ); } ``` ## Your First Component ```tsx import { Button, Stack, Text } from 'entangle-ui'; function Welcome() { return ( <Stack direction="column" spacing={3} align="center"> <Text variant="heading">Welcome to Entangle UI</Text> <Stack direction="row" spacing={2}> <Button variant="default">Cancel</Button> <Button variant="filled">Get Started</Button> </Stack> </Stack> ); } ``` ## Common Patterns ### Sizing Most components support a consistent `size` prop: ```tsx <Button size="sm">Small</Button> <Button size="md">Medium</Button> {/* default */} <Button size="lg">Large</Button> <Input size="sm" placeholder="Small input" /> <Slider size="md" min={0} max={100} value={50} /> ``` | Size | Typical Height | | ---- | -------------- | | `sm` | 20px | | `md` | 24px | | `lg` | 32px | ### Responsive Layout `Stack` and `Flex` support responsive direction changes at four breakpoints: ```tsx <Stack direction="column" md="row" spacing={2}> <Sidebar /> <Content /> </Stack> ``` | Breakpoint | Width | | ---------- | ------ | | `sm` | 576px | | `md` | 768px | | `lg` | 992px | | `xl` | 1200px | ### Form Inputs Combine `Input` with form components for labeled fields with validation: ```tsx <Input label="Project Name" placeholder="My Project" helperText="Choose a unique name" error={!!nameError} errorMessage={nameError} required value={name} onChange={setName} /> ``` ### Application Shell Build a complete editor layout: ```tsx import { AppShell, MenuBar, Toolbar, StatusBar, useAppShell, } from 'entangle-ui'; function Editor() { const appShell = useAppShell(); return ( <AppShell> <AppShell.MenuBar> <MenuBar>{/* menu items */}</MenuBar> </AppShell.MenuBar> <AppShell.Toolbar> <Toolbar> <Toolbar.Button icon={<SaveIcon />}>Save</Toolbar.Button> <Toolbar.Separator /> <Toolbar.Toggle icon={<GridIcon />}>Grid</Toolbar.Toggle> </Toolbar> </AppShell.Toolbar> <AppShell.Dock>{/* panels */}</AppShell.Dock> <AppShell.StatusBar> <StatusBar> <StatusBar.Section>Ready</StatusBar.Section> </StatusBar> </AppShell.StatusBar> </AppShell> ); } ``` ## Next Steps - [Theming](/guides/theming) — customize colors, tokens, and create branded themes - [Styling](/guides/styling) — learn the Vanilla Extract styling system ========================= # Guides ========================= --- # Accessibility > How Entangle UI handles accessibility concerns shared by every component, including reduced-motion support, focus rings, and keyboard semantics. _Source: /guides/accessibility_ Entangle UI is designed for professional editor interfaces — surfaces where users spend hours every day. Accessibility is treated as a baseline, not a feature: every primitive ships with the focus, ARIA, and motion behavior expected of a 1.0 component library. This page documents the cross-cutting policies; component-specific notes (keyboard maps, ARIA roles) live on each component's own page. ## Reduced motion The library honors the user's OS-level [`prefers-reduced-motion: reduce`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion) media query everywhere it ships autonomous motion. The rule we apply, drawn from [WCAG 2.1 SC 2.3.3 — Animation from Interactions](https://www.w3.org/TR/WCAG21/#animation-from-interactions): > Motion is for communication, not decoration. When the user asks the OS to reduce motion, every animation that does not carry meaning is disabled or replaced with a static visual. ### What is disabled under reduced motion These are autonomous or transition-driven motion patterns. With `prefers-reduced-motion: reduce` enabled, they snap to the final state instead of animating: - **Loading indicators** — `Spinner` (ring, pulse, dots), `Button` and `IconButton` loading spinners, `ProgressBar` indeterminate stripe and circular rotation - **Skeleton placeholders** — pulse and wave shimmer - **Chat typing indicator** — bouncing dot loop and pulse bar - **Dialog mount/unmount** — overlay fade and panel scale-in/out - **Toast** — slide-in entry and the auto-dismiss progress bar (the timer still runs; only the visual indicator is hidden) - **Popover** — opacity + scale entrance - **Tooltip** — opacity + scale entrance, regardless of the `animation` prop - **Select / dropdown** — `scaleY` open animation - **Accordion / Collapsible** — chevron rotation and `grid-template-rows` height transition - **Switch** — thumb travel - **Radio** — inner-dot scale-in - **Checkbox** — check-mark scale + opacity transition - **Avatar (`interactive`)** — hover scale - **Slider** — thumb hover scale, value tooltip translate - **Color picker presets / palette swatches** — hover scale - **TreeView / PropertySection / Select / Accordion / Collapsible / ChatPanel tool-call** — chevron rotations - **Defensive `transition: all` blocks** — Button, Checkbox box, IconButton, TextArea, Tabs, Select trigger, VectorInput, InputWrapper ### What is preserved under reduced motion These are direct manipulation or non-motion changes — disabling them would break the UI rather than help: - **Drag, scrub, gizmo rotation, color-area picking, slider drag** — interactive direct manipulation - **Focus rings** — instant box-shadow, never animated - **Hover color / background changes** — color is not motion in the WCAG sense - **`opacity` transitions on focus rings or banners** — fade in/out without translation is not vestibular motion ### Authoring custom components When you build a custom component on top of Entangle's tokens, follow the same pattern. For Vanilla Extract styles: ```ts import { style, keyframes } from '@vanilla-extract/css'; import { vars } from 'entangle-ui/theme'; const slideIn = keyframes({ from: { opacity: 0, transform: 'translateY(8px)' }, to: { opacity: 1, transform: 'translateY(0)' }, }); export const banner = style({ animation: `${slideIn} ${vars.transitions.normal} ease-out`, transition: `transform ${vars.transitions.fast}`, '@media': { '(prefers-reduced-motion: reduce)': { animation: 'none', transition: 'none', }, }, }); ``` For inline styles or imperative animation (`requestAnimationFrame`, FLIP, custom physics), gate the animation behind a `matchMedia` check: ```tsx const prefersReducedMotion = typeof window !== 'undefined' && window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (prefersReducedMotion) { setState(finalState); // snap } else { animateTo(finalState); // animate } ``` The shared utilities exported from `entangle-ui` (`animSpin`, `animPulse`, `animBlink`, `animFadeIn`, `animWave`) already include the media query — prefer them over reinventing keyframes. See [Animations](/guides/animations) for the full list. ### Verifying behavior In Chrome / Edge DevTools: open the Rendering panel (`Cmd/Ctrl + Shift + P` → "Show Rendering") and toggle **Emulate CSS media feature `prefers-reduced-motion: reduce`**. Walk through the affected components — every animation in the list above should stop or snap. --- # Animations > Shared keyframes and utility classes for spinners, pulses, blinks, and fade-ins. Each utility honors prefers-reduced-motion. _Source: /guides/animations_ Entangle ships a small set of shared keyframes and utility classes so components don't redefine the same animations over and over. They are exposed as both raw keyframe references (for use inside your own Vanilla Extract recipes) and ready-to-apply class names. Every utility class respects `prefers-reduced-motion: reduce` by halting the animation. ## Import ```tsx import { animSpin, animPulse, animBlink, animFadeIn, spinKeyframe, pulseKeyframe, blinkKeyframe, fadeInKeyframe, } from 'entangle-ui'; ``` ## Utility classes Apply directly to any element via `className`: ```tsx import { animSpin } from 'entangle-ui'; <svg className={animSpin}> <circle cx="8" cy="8" r="6" stroke="currentColor" fill="none" /> </svg>; ``` | Utility | Animation | Typical use | | ------------ | ------------------------------------------------ | ----------------------------------- | | `animSpin` | 1s linear infinite rotate (0deg → 360deg) | Loading rings, refresh icons | | `animPulse` | 1.5s ease-in-out infinite opacity (1 → 0.4 → 1) | Skeleton placeholders, soft pings | | `animBlink` | 1s steps(1) infinite opacity toggle | Cursor blinks, attention prompts | | `animFadeIn` | One-shot opacity 0 → 1, normal transition timing | Mount transitions, toast appearance | When the user has `prefers-reduced-motion: reduce` enabled in their OS, every utility class above sets `animation: none` automatically. ## Keyframes for custom recipes Use the exported keyframe references when you want to compose your own animation in a Vanilla Extract recipe: ```ts // my-component.css.ts import { style } from '@vanilla-extract/css'; import { spinKeyframe } from 'entangle-ui'; export const slowSpin = style({ animation: `${spinKeyframe} 4s linear infinite`, }); ``` This keeps the keyframe definition shared across the bundle (smaller CSS) and avoids drift from copy-pasted versions. ## Reduced motion The `prefers-reduced-motion` media query is the source of truth — utility classes react to it automatically. If you build your own animation with the keyframes, opt into the same behavior: ```ts import { style } from '@vanilla-extract/css'; import { pulseKeyframe } from 'entangle-ui'; export const myPulse = style({ animation: `${pulseKeyframe} 1.5s ease-in-out infinite`, '@media': { '(prefers-reduced-motion: reduce)': { animation: 'none', }, }, }); ``` ## Where they're used internally | Component | Keyframe | | ------------------- | --------------- | | `Spinner` (`ring`) | `spinKeyframe` | | `Spinner` (`pulse`) | `pulseKeyframe` | | `Spinner` (`dots`) | `pulseKeyframe` | `Button`'s built-in loading spinner and `ChatTypingIndicator` keep their own local keyframes for now and will migrate to these utilities in a follow-up patch — switch to the shared utilities first when authoring new components. --- # Styling > Learn how to style Entangle UI components using Vanilla Extract and theme tokens. _Source: /guides/styling_ Entangle UI uses **Vanilla Extract** for all styling — a zero-runtime CSS-in-JS framework where styles are written in `.css.ts` files and compiled to static CSS at build time. ## Style Files Every component has its own `.css.ts` file: ``` Button/ ├── Button.tsx ├── Button.css.ts ← styles ├── Button.test.tsx └── index.ts ``` ## Basic Styles Use `style()` for atomic class names: ```typescript // Card.css.ts import { style } from '@vanilla-extract/css'; import { vars } from '@/theme'; export const cardStyle = style({ background: vars.colors.surface.default, color: vars.colors.text.primary, padding: vars.spacing.lg, borderRadius: vars.borderRadius.md, border: `1px solid ${vars.colors.border.default}`, transition: `all ${vars.transitions.normal}`, ':hover': { background: vars.colors.surface.hover, boxShadow: vars.shadows.md, }, ':focus-visible': { boxShadow: vars.shadows.focus, }, }); ``` ```tsx // Card.tsx import { cardStyle } from './Card.css'; export const Card = ({ children }) => ( <div className={cardStyle}>{children}</div> ); ``` :::tip Always use `vars.*` tokens — never hardcode colors, spacing, or other visual values. ::: ## Recipes (Variant-Based Styling) Recipes from `@vanilla-extract/recipes` define multi-variant component styles: ```typescript // Button.css.ts import { recipe } from '@vanilla-extract/recipes'; import { vars } from '@/theme'; export const buttonRecipe = recipe({ base: { margin: 0, border: 'none', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: vars.typography.fontFamily.sans, transition: `all ${vars.transitions.normal}`, ':disabled': { cursor: 'not-allowed', opacity: 0.5, }, }, variants: { variant: { default: { background: vars.colors.surface.default, color: vars.colors.text.primary, ':hover': { background: vars.colors.surface.hover }, ':active': { background: vars.colors.surface.active }, }, ghost: { background: 'transparent', color: vars.colors.text.primary, ':hover': { background: vars.colors.surface.hover }, }, filled: { background: vars.colors.accent.primary, color: vars.colors.text.primary, ':hover': { background: vars.colors.accent.secondary }, }, }, size: { sm: { height: '20px', padding: `0 ${vars.spacing.sm}`, fontSize: vars.typography.fontSize.xs, }, md: { height: '24px', padding: `0 ${vars.spacing.md}`, fontSize: vars.typography.fontSize.sm, }, lg: { height: '32px', padding: `0 ${vars.spacing.xl}`, fontSize: vars.typography.fontSize.md, }, }, fullWidth: { true: { width: '100%' }, }, }, defaultVariants: { variant: 'default', size: 'md', }, }); ``` ```tsx // Button.tsx import { buttonRecipe } from './Button.css'; import { cx } from '@/utils/cx'; export const Button = ({ variant, size, fullWidth, className, ...rest }) => ( <button className={cx(buttonRecipe({ variant, size, fullWidth }), className)} {...rest} /> ); ``` Extract variant types with `RecipeVariants`: ```typescript import type { RecipeVariants } from '@vanilla-extract/recipes'; export type ButtonVariants = RecipeVariants<typeof buttonRecipe>; // => { variant?: 'default' | 'ghost' | 'filled'; size?: 'sm' | 'md' | 'lg'; fullWidth?: boolean } ``` ## Dynamic CSS Variables For values computed at runtime (percentages, user input), use `createVar()` and `assignInlineVars()`: ```typescript // Slider.css.ts import { createVar, style } from '@vanilla-extract/css'; import { vars } from '@/theme'; export const fillPercentageVar = createVar(); export const thumbPositionVar = createVar(); export const trackFill = style({ position: 'absolute', left: 0, height: '100%', width: fillPercentageVar, background: vars.colors.accent.primary, borderRadius: vars.borderRadius.sm, }); export const thumb = style({ position: 'absolute', left: thumbPositionVar, transform: 'translate(-50%, -50%)', width: '12px', height: '12px', borderRadius: '50%', background: vars.colors.text.primary, boxShadow: vars.shadows.thumb, }); ``` ```tsx // Slider.tsx import { assignInlineVars } from '@vanilla-extract/dynamic'; import { fillPercentageVar, thumbPositionVar, trackFill, thumb, } from './Slider.css'; export const Slider = ({ value, min, max }) => { const pct = ((value - min) / (max - min)) * 100; return ( <div className={track}> <div className={trackFill} style={assignInlineVars({ [fillPercentageVar]: `${pct}%`, })} /> <div className={thumb} style={assignInlineVars({ [thumbPositionVar]: `${pct}%`, })} /> </div> ); }; ``` ## Keyframe Animations ```typescript import { keyframes, style } from '@vanilla-extract/css'; const spin = keyframes({ to: { transform: 'rotate(360deg)' }, }); export const spinnerStyle = style({ animation: `${spin} 1s linear infinite`, }); ``` ## Global Styles and Media Queries For responsive overrides, use `globalStyle()`: ```typescript import { globalStyle } from '@vanilla-extract/css'; export const flexSelector = 'etui-flex'; globalStyle(`${flexSelector}[data-sm-dir="row"]`, { '@media': { '(min-width: 576px)': { flexDirection: 'row' }, }, }); ``` Inline media queries within `style()`: ```typescript const responsiveCard = style({ padding: vars.spacing.md, '@media': { '(min-width: 768px)': { padding: vars.spacing.xl, }, }, }); ``` ## Utilities ### `cx()` — Class Name Combiner Joins class names, filtering out falsy values: ```typescript import { cx } from '@/utils/cx'; function cx(...classes: (string | false | null | undefined)[]): string; // Usage <div className={cx( baseStyle, // always applied isActive && activeStyle, // conditional className // user override (last = highest priority) )} /> ``` Always place the recipe output first and user `className` last so consumer styles can override: ```tsx <button className={cx(buttonRecipe({ variant, size }), className)} /> ``` ### `cn()` — Alternative Class Combiner Alias for `cx()` available for import preference: ```tsx import { cn } from 'entangle-ui'; ``` ## Responsive Design ### Breakpoints | Name | Width | Data Attribute | | ---- | ------ | -------------- | | `sm` | 576px | `data-sm-dir` | | `md` | 768px | `data-md-dir` | | `lg` | 992px | `data-lg-dir` | | `xl` | 1200px | `data-xl-dir` | ### Responsive Direction (Stack / Flex) Layout components accept responsive direction props: ```tsx <Stack direction="column" md="row" spacing={2}> <Sidebar /> <Content /> </Stack> ``` This renders as a column on mobile and switches to a row at `768px`. ### Spacing Multiplier `Stack` and `Flex` use a 4px base unit for the `spacing` prop: | `spacing` value | Computed gap | | --------------- | ------------ | | `0` | 0px | | `1` | 4px | | `2` | 8px | | `3` | 12px | | `4` | 16px | | `5` | 20px | | `6` | 24px | | `7` | 28px | | `8` | 32px | Use `customGap` for arbitrary values: ```tsx <Stack customGap="6px">{/* ... */}</Stack> ``` ## Conventions ### Theme Tokens Over Hardcoded Values ```typescript // Good background: vars.colors.surface.default, padding: vars.spacing.md, // Bad background: '#2d2d2d', padding: '8px', ``` ### Path Aliases Always use `@/` imports for cross-directory references: ```typescript // Good import { vars } from '@/theme'; import type { Prettify } from '@/types/utilities'; // Bad import { vars } from '../../theme'; ``` ### Recipe Naming Recipe exports follow the pattern `{componentName}Recipe`: ```typescript export const buttonRecipe = recipe({ ... }); export const textRecipe = recipe({ ... }); export const sliderTrackRecipe = recipe({ ... }); ``` Standalone styles use `{name}Style`: ```typescript export const iconWrapperStyle = style({ ... }); export const separatorStyle = style({ ... }); ``` ## Build Integration ### For Library Consumers Entangle UI ships pre-compiled CSS. No Vanilla Extract build plugin is needed in consumer projects — just import and use: ```tsx import { Button } from 'entangle-ui'; <Button variant="filled">Works out of the box</Button>; ``` ### For Library Contributors The build uses Rollup with the `@vanilla-extract/rollup-plugin` to compile `.css.ts` files. Vitest uses `@vanilla-extract/vite-plugin` to compile styles for the jsdom test environment. | Task | Command | | --------------- | -------------------- | | Dev (docs site) | `npm run dev` | | Build | `npm run build` | | Type check | `npm run type-check` | ### Tree-Shaking The library is fully tree-shakeable: - `sideEffects: false` in `package.json` - `preserveModules` in Rollup output - `/*#__PURE__*/` annotations where needed Unused components and their styles are eliminated by the consumer's bundler. --- # Theming > Learn how to customize and extend the Entangle UI theme system — colors, spacing, typography, shadows, and more. _Source: /guides/theming_ Entangle UI ships with a dark-first theme designed for professional editor interfaces. Every visual value — colors, spacing, typography, shadows — is defined as a **theme token** and exposed as a CSS custom property prefixed with `--etui-`. The library also ships a maintained **light theme preset** for use in agent UIs, dashboards, and customer-facing surfaces — opt in via `createLightTheme()`. ## Architecture Overview The theme system is built on CSS custom properties: ``` CSS Custom Properties (--etui-*) Applied at :root by default ┌───────────────────────────────────┐ │ contract.css.ts (property names) │ │ darkTheme.css.ts (values) │ └──────────────┬────────────────────┘ │ ┌────────────────────┼────────────────────┐ │ │ Vanilla Extract CSS Override (compile-time) (any selector) import { vars } --etui-*: value ``` - **Vanilla Extract** — tokens compile to CSS custom properties at build time. Zero runtime cost. Access via `vars` object. - **CSS Override** — any `--etui-*` custom property can be overridden with plain CSS on any selector. ## Default Theme The dark theme is applied globally on `:root` at build time. No provider is needed for the default look: ```tsx import { Button } from 'entangle-ui'; // Works out of the box — dark theme is already active <Button variant="filled">Save</Button>; ``` ## Light Theme The light theme is shipped as a build-time preset. It is **not** applied globally — consumers opt in by generating a class with `createLightTheme()` and applying it to a subtree (typically the entire app, but it can also scope to a single dialog or panel). ### Quick start The light-theme class is pre-generated at the package's build time, so it's a plain string you can import from anywhere — no consumer-side `.css.ts` file required: ```tsx // app/Root.tsx import { VanillaThemeProvider, lightThemeClass } from 'entangle-ui'; export function Root() { return ( <VanillaThemeProvider className={lightThemeClass}> <App /> </VanillaThemeProvider> ); } ``` If you prefer a function-style API, `createLightTheme()` returns the same string and is interchangeable: ```tsx import { VanillaThemeProvider, createLightTheme } from 'entangle-ui'; const lightThemeClass = createLightTheme(); <VanillaThemeProvider className={lightThemeClass}> <App /> </VanillaThemeProvider>; ``` The class flips every `--etui-*` custom property under the wrapped subtree to the light preset. ### What flips and what stays The light theme only changes **visual** tokens (colors, shadows). Structural tokens — spacing, typography, border-radius, transitions, z-index — are identical between the two themes, so layout and rhythm don't drift when a user switches modes. A few notes worth flagging: - `colors.surface.whiteOverlay` keeps its name (the contract is part of the public API), but its value flips polarity in light mode — it becomes a subtle dark overlay (`rgba(0, 0, 0, 0.06)`) so it still reads as "subtle tint over the underlying surface". - `colors.backdrop` uses lower opacity in light mode (`0.4` vs dark's `0.6`) — a heavier dim feels harsh against bright underlying content. - Shadows use lower opacity and softer spread to remain visible on white surfaces without looking heavy. - `shell.statusBar` keeps a solid accent background in both themes — it's a deliberately punchy element regardless of mode. ### Common components ### Property panels ### Alerts ### Scoped overrides — light dialog inside a dark app `VanillaThemeProvider` is just a `<div>` wrapper for a className, so themes can be scoped to any subtree. A common pattern is a dark editor with a single light-themed settings dialog (for legibility while a long form is being filled out): ```tsx import { VanillaThemeProvider, lightThemeClass } from 'entangle-ui'; <VanillaThemeProvider className={lightThemeClass}> <SettingsDialog /> </VanillaThemeProvider>; ``` The same pattern works in reverse — embed a dark preview inside a light dashboard by wrapping just that subtree with the dark theme class generated via `createCustomTheme`. ## Token Reference ### Colors #### Background | Token | CSS Property | Value | | ----------------------------- | --------------------------- | -------------------- | | `colors.background.primary` | `--etui-color-bg-primary` | `#1a1a1a` | | `colors.background.secondary` | `--etui-color-bg-secondary` | `#2d2d2d` | | `colors.background.tertiary` | `--etui-color-bg-tertiary` | `#3a3a3a` | | `colors.background.elevated` | `--etui-color-bg-elevated` | `#404040` | | `colors.background.inset` | `--etui-color-bg-inset` | `rgba(0, 0, 0, 0.2)` | > `background.inset` is a sunken/recessed surface — used by `Code` for inline code, by `ChatMarkdownRenderer` for blockquotes, and recommended for any "preview window" / textarea-style background. #### Surface (Interactive Elements) | Token | CSS Property | Value | | ----------------------------- | ------------------------------------ | --------------------------- | | `colors.surface.default` | `--etui-color-surface-default` | `#2d2d2d` | | `colors.surface.hover` | `--etui-color-surface-hover` | `#363636` | | `colors.surface.active` | `--etui-color-surface-active` | `#404040` | | `colors.surface.disabled` | `--etui-color-surface-disabled` | `#1f1f1f` | | `colors.surface.whiteOverlay` | `--etui-color-surface-white-overlay` | `rgba(255,255,255,0.1)` | | `colors.surface.row` | `--etui-color-surface-row` | `transparent` | | `colors.surface.rowHover` | `--etui-color-surface-row-hover` | `rgba(255, 255, 255, 0.03)` | > `surface.row` and `surface.rowHover` are intentionally lighter than `surface.hover` — use them for list and table rows, where the hover state should hint at interactivity without competing with adjacent buttons (`ListItem` uses these out of the box). Reserve `surface.hover` for buttons and other interactive controls. #### Border | Token | CSS Property | Value | | ----------------------- | ----------------------------- | --------- | | `colors.border.default` | `--etui-color-border-default` | `#4a4a4a` | | `colors.border.focus` | `--etui-color-border-focus` | `#007acc` | | `colors.border.error` | `--etui-color-border-error` | `#f44336` | | `colors.border.success` | `--etui-color-border-success` | `#4caf50` | #### Text | Token | CSS Property | Value | | ----------------------- | ----------------------------- | --------- | | `colors.text.primary` | `--etui-color-text-primary` | `#ffffff` | | `colors.text.secondary` | `--etui-color-text-secondary` | `#cccccc` | | `colors.text.muted` | `--etui-color-text-muted` | `#888888` | | `colors.text.disabled` | `--etui-color-text-disabled` | `#555555` | #### Accent | Token | CSS Property | Value | | ------------------------- | ------------------------------- | --------- | | `colors.accent.primary` | `--etui-color-accent-primary` | `#007acc` | | `colors.accent.secondary` | `--etui-color-accent-secondary` | `#005a9e` | | `colors.accent.success` | `--etui-color-accent-success` | `#4caf50` | | `colors.accent.warning` | `--etui-color-accent-warning` | `#ff9800` | | `colors.accent.error` | `--etui-color-accent-error` | `#f44336` | ### Spacing All spacing values in pixels. `md` (8px) is the base unit. | Token | CSS Property | Value | | ------ | --------------------- | ----- | | `xs` | `--etui-spacing-xs` | 2px | | `sm` | `--etui-spacing-sm` | 4px | | `md` | `--etui-spacing-md` | 8px | | `lg` | `--etui-spacing-lg` | 12px | | `xl` | `--etui-spacing-xl` | 16px | | `xxl` | `--etui-spacing-xxl` | 24px | | `xxxl` | `--etui-spacing-xxxl` | 32px | ### Typography #### Font Sizes | Token | CSS Property | Value | | ----- | ---------------------- | ----- | | `xxs` | `--etui-font-size-xxs` | 9px | | `xs` | `--etui-font-size-xs` | 10px | | `sm` | `--etui-font-size-sm` | 11px | | `md` | `--etui-font-size-md` | 12px | | `lg` | `--etui-font-size-lg` | 14px | | `xl` | `--etui-font-size-xl` | 16px | #### Font Weights | Token | CSS Property | Value | | ---------- | ----------------------------- | ----- | | `normal` | `--etui-font-weight-normal` | 400 | | `medium` | `--etui-font-weight-medium` | 500 | | `semibold` | `--etui-font-weight-semibold` | 600 | #### Line Heights | Token | CSS Property | Value | | --------- | ---------------------------- | ----- | | `tight` | `--etui-line-height-tight` | 1.2 | | `normal` | `--etui-line-height-normal` | 1.4 | | `relaxed` | `--etui-line-height-relaxed` | 1.6 | #### Font Families | Token | CSS Property | Value | | ------ | ------------------------- | ----------------------------------------------------------------- | | `mono` | `--etui-font-family-mono` | SF Mono, Monaco, Consolas, "Liberation Mono", monospace | | `sans` | `--etui-font-family-sans` | -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif | ### Border Radius | Token | CSS Property | Value | | ------ | -------------------- | ----- | | `none` | `--etui-radius-none` | 0px | | `sm` | `--etui-radius-sm` | 2px | | `md` | `--etui-radius-md` | 4px | | `lg` | `--etui-radius-lg` | 6px | ### Shadows | Token | CSS Property | Value | | ----------------- | ------------------------------- | ------------------------------- | | `sm` | `--etui-shadow-sm` | `0 1px 2px rgba(0,0,0,0.2)` | | `md` | `--etui-shadow-md` | `0 2px 4px rgba(0,0,0,0.3)` | | `lg` | `--etui-shadow-lg` | `0 4px 8px rgba(0,0,0,0.4)` | | `xl` | `--etui-shadow-xl` | `0 8px 16px rgba(0,0,0,0.5)` | | `focus` | `--etui-shadow-focus` | `0 0 0 2px rgba(0,122,204,0.4)` | | `separatorBottom` | `--etui-shadow-separatorBottom` | `0 1px 2px rgba(0,0,0,0.18)` | | `separatorRight` | `--etui-shadow-separatorRight` | `1px 0 2px rgba(0,0,0,0.18)` | | `separatorLeft` | `--etui-shadow-separatorLeft` | `-1px 0 2px rgba(0,0,0,0.18)` | | `thumb` | `--etui-shadow-thumb` | `0 0 2px rgba(0,0,0,0.5)` | ### Transitions | Token | CSS Property | Value | | -------- | -------------------------- | ---------------- | | `fast` | `--etui-transition-fast` | `100ms ease-out` | | `normal` | `--etui-transition-normal` | `200ms ease-out` | | `slow` | `--etui-transition-slow` | `300ms ease-out` | ### Z-Index | Token | CSS Property | Value | | ---------- | ------------------- | ----- | | `base` | `--etui-z-base` | 1 | | `dropdown` | `--etui-z-dropdown` | 1000 | | `popover` | `--etui-z-popover` | 1000 | | `tooltip` | `--etui-z-tooltip` | 1000 | | `modal` | `--etui-z-modal` | 1100 | ### Shell Tokens Specialized tokens for application layout components: | Token | CSS Property | Value | | ------------------------- | -------------------------------- | --------- | | `shell.menuBar.height` | `--etui-shell-menubar-height` | 28px | | `shell.toolbar.height.sm` | `--etui-shell-toolbar-height-sm` | 32px | | `shell.toolbar.height.md` | `--etui-shell-toolbar-height-md` | 40px | | `shell.statusBar.height` | `--etui-shell-statusbar-height` | 22px | | `shell.statusBar.bg` | `--etui-shell-statusbar-bg` | `#007acc` | | `shell.dock.tabHeight` | `--etui-shell-dock-tabHeight` | 28px | | `shell.dock.splitterSize` | `--etui-shell-dock-splitterSize` | 4px | ## Customization ### 1. CSS Override (Simplest) Override any token for a subtree using plain CSS: ```css .my-brand { --etui-color-accent-primary: #ff6600; --etui-color-accent-secondary: #cc5200; } ``` ```tsx <div className="my-brand"> <Button variant="filled">Orange Button</Button> </div> ``` ### 2. createCustomTheme (Build-Time) Create a scoped theme that compiles to CSS at build time. This **must** be called in a `.css.ts` file. Use it for brand palettes or full custom themes — for the standard light preset prefer [`createLightTheme()`](#light-theme). ```typescript // src/themes/brand.css.ts import { createCustomTheme } from 'entangle-ui'; createCustomTheme('[data-theme="brand"]', { colors: { accent: { primary: '#ff6600', secondary: '#cc5200', }, border: { focus: '#ff6600', }, }, }); ``` Apply it: ```tsx <div data-theme="brand"> <Button variant="filled">Brand-themed button</Button> </div> ``` ### 3. VanillaThemeProvider (Scoped Override) Wrap a section with a className that carries theme overrides: ```tsx import { VanillaThemeProvider } from 'entangle-ui'; <VanillaThemeProvider className="light-theme"> <SettingsPanel /> </VanillaThemeProvider>; ``` ## Accessing Tokens in Code ### Vanilla Extract (`.css.ts` files) ```typescript import { vars } from 'entangle-ui'; import { style } from '@vanilla-extract/css'; export const card = style({ background: vars.colors.surface.default, color: vars.colors.text.primary, padding: vars.spacing.lg, borderRadius: vars.borderRadius.md, boxShadow: vars.shadows.md, transition: `all ${vars.transitions.normal}`, ':hover': { background: vars.colors.surface.hover, boxShadow: vars.shadows.lg, }, }); ``` `vars` is fully typed — autocomplete guides you through the entire token tree. ### Plain CSS ```css .custom-panel { background: var(--etui-color-bg-secondary); padding: var(--etui-spacing-lg); border: 1px solid var(--etui-color-border-default); border-radius: var(--etui-radius-md); } ``` ## ThemeProvider Most apps don't need a provider — the dark theme is applied globally to `:root`. Use `ThemeProvider` for opt-in global behaviors. ```tsx import { ThemeProvider } from 'entangle-ui'; <ThemeProvider globalScrollbars> <App /> </ThemeProvider>; ``` ### Global scrollbars (opt-in) Set `globalScrollbars` to apply Entangle's dark-theme scrollbar styling to native scrollbars on overflowing elements throughout the document. The provider toggles a class on `document.body`; styles are scoped under that class so they don't leak when the prop is `false` (the default). | Prop | Type | Default | Description | | ------------------ | --------- | ------- | ----------------------------------------------------------------------------------- | | `globalScrollbars` | `boolean` | `false` | Add a class to `document.body` that themes WebKit and Firefox scrollbars dark-thin. | You can also opt in manually by adding the published class to your own root element: ```tsx import { GLOBAL_SCROLLBARS_CLASS } from 'entangle-ui'; document.body.classList.add(GLOBAL_SCROLLBARS_CLASS); ``` This is helpful when Entangle is embedded inside another app shell that already manages mount/unmount of the provider. ## Token Export Entangle UI ships machine-readable copies of every token next to the published JS bundle, so design tools and non–Vanilla-Extract projects can consume the same values the components compile against. Three artifacts are emitted on each release: | Artifact | Format | Use case | | ------------------ | --------------------------- | -------------------------------------------------------------- | | `tokens.json` | Loosely DTCG-aligned JSON | Figma plugins, Style Dictionary pipelines, doc generators | | `tokens.dark.css` | `:root { --etui-*: ... }` | Drop-in dark stylesheet for projects not using Vanilla Extract | | `tokens.light.css` | `.etui-theme-light { ... }` | Companion light stylesheet — apply the class to a wrapper | ### Plain CSS consumers Import the stylesheet directly in projects that don't use Vanilla Extract. Once it's loaded, every `--etui-*` custom property is available as a normal CSS variable: ```css @import 'entangle-ui/tokens.dark.css'; .my-button { background: var(--etui-color-accent-primary); padding: var(--etui-spacing-md); border-radius: var(--etui-radius-md); transition: var(--etui-transition-normal); } ``` For the light preset, import the matching stylesheet and apply the `etui-theme-light` class to the subtree you want to theme: ```html <link rel="stylesheet" href="node_modules/entangle-ui/dist/tokens/tokens.light.css" /> <body class="etui-theme-light"> ... </body> ``` > `tokens.light.css` uses the documented class name `etui-theme-light`. This > is independent from the hashed runtime class returned by `createLightTheme()` > — the runtime class is for Vanilla Extract consumers, the documented class > is for plain-CSS consumers. ### JSON consumers (Figma plugins, design tools) `tokens.json` follows the W3C Design Tokens Community Group structure where it fits, with `$value` / `$type` leaves grouped under each theme: ```ts import tokens from 'entangle-ui/tokens.json' with { type: 'json' }; console.log(tokens.themes.dark.colors.accent.primary.$value); // → '#007acc' console.log(tokens.themes.light.spacing.md.$type); // → 'dimension' ``` Top-level structure: ```json { "$schema": "https://design-tokens.org/schema.json", "$version": "<package version>", "themes": { "dark": { "colors": { ... }, "spacing": { ... }, "shell": { ... } }, "light": { "colors": { ... }, "spacing": { ... }, "shell": { ... } } } } ``` The `$version` field is read from `package.json` at export time, so each published artifact carries the exact version it shipped with. The export is loosely DTCG-aligned rather than spec-strict — the goal is completeness over conformance, so the proprietary `shell.*` branch ships alongside standard categories. Library-internal tokens under `demo.*` (used only by the documentation site's editor demos) are intentionally excluded. ### Node / SSR Data Consumers Use `entangle-ui/theme-values` when you need the raw theme objects in a Node script, server-only route, PDF/email generator, or any other environment that does not process CSS imports. This entrypoint contains only plain TypeScript data modules and does not import Vanilla Extract CSS files: ```ts import { darkThemeValues, lightThemeValues, themeContractData, } from 'entangle-ui/theme-values'; console.log(lightThemeValues.colors.accent.primary); // -> '#0066cc' console.log(themeContractData.colors.accent.primary); // -> 'etui-color-accent-primary' ``` Use `entangle-ui/theme` for React UI code that needs `ThemeProvider`, `vars`, `createLightTheme()`, or `createCustomTheme()`. That entrypoint intentionally loads the CSS runtime and is meant for bundled app code. ## TypeScript Types ```tsx import type { Tokens, // Raw token definitions ThemeVars, // Vanilla Extract contract type (CSS variable references) DarkThemeValues, // Dark theme value map LightThemeValues, // Light theme value map (matches DarkThemeValues shape) ThemeValues, // Alias for DarkThemeValues (used in createCustomTheme) DeepPartial, // Recursive Partial<T> for theme overrides } from 'entangle-ui'; ``` For CSS-free theme data imports, use: ```ts import type { DarkThemeValues, LightThemeValues, ThemeContractData, ThemeValues, } from 'entangle-ui/theme-values'; ``` ## Design Principles - **Dark-first** — all default values target dark backgrounds suitable for 3D editors, node editors, and creative tools. - **CSS-native** — tokens are CSS custom properties. Override them with CSS, no JavaScript runtime needed. - **Type-safe** — `vars` and `Theme` provide full autocomplete and compile-time checks. - **Scoped overrides** — custom themes can target any CSS selector for sectional theming. - **Stable API** — CSS property names (`--etui-*`) are considered public API. Renaming them requires a major version bump. ========================= # Components — Primitives ========================= --- # Avatar > Identity primitive with image, initials, and icon fallback chain. AvatarGroup handles overlapping clusters with overflow. _Source: /components/primitives/avatar_ Render a person, agent, or any named entity. `Avatar` resolves an image when one is available, falls back to initials derived from the name, and finally to a generic user glyph. `AvatarGroup` overlaps multiple avatars and collapses overflow into a `+N` indicator. Reach for `Avatar` to represent identities — collaborators, comment authors, agent attributions. For decorative imagery (thumbnails, hero photos) use a plain `<img>`; an avatar isn't the right semantic. **Live Preview** ## Import ```tsx import { Avatar, AvatarGroup } from 'entangle-ui'; ``` ## Usage ```tsx <Avatar src="/users/alice.png" name="Alice Wong" /> <Avatar name="Sebastian Kowalski" status="online" /> <Avatar name="Bot" color="primary" shape="rounded" /> ``` When `src` is provided the avatar attempts to load it; the underlying initials remain in place as a backdrop until the image paints (or fails). If it fails, the initials become the visible content. ## Sizes Six fixed sizes from `xs` (toolbar / chip) through `xxl` (profile header). **Sizes** | Size | Diameter | Use case | | ----- | -------- | ------------------------------ | | `xs` | 16px | Inline next to dense rows | | `sm` | 20px | Compact lists / chat bubbles | | `md` | 24px | Default | | `lg` | 32px | Side panels, comments | | `xl` | 40px | Cards, member rows | | `xxl` | 56px | Profile pages / detail headers | ```tsx <Avatar size="xs" name="Alice" /> <Avatar size="xxl" name="Alice" /> ``` ## Shapes **Shapes** | Shape | Border radius | When to use | | --------- | ----------------- | ----------------------------------- | | `circle` | `50%` | Default — people / personal context | | `square` | `0` | Logos, machine identifiers | | `rounded` | `borderRadius.md` | Soft fallback between the two | ```tsx <Avatar shape="circle" /> <Avatar shape="square" /> <Avatar shape="rounded" /> ``` ## Initials Without a `src`, the avatar derives initials from `name`. A single word produces its first two characters; multi-word names produce the first character of the first and last word. With `color="auto"` (default) the background colour is hashed deterministically from the name — same name, same colour, every render. **Initials** ```tsx <Avatar name="Sebastian Kowalski" /> {/* "SK" */} <Avatar name="Alice" /> {/* "AL" */} <Avatar name="Mary Anne Smith" /> {/* "MS" */} <Avatar name="Sebastian" initials="SK" /> {/* explicit override */} ``` To force a specific accent, pass `color="primary"` (or `success`, `warning`, `error`, `info`, `neutral`). Any other string is treated as a raw CSS colour. ```tsx <Avatar name="Bot" color="primary" /> <Avatar name="Bot" color="#9b59b6" /> ``` ## Icon fallback When neither `src` nor `name`/`initials` resolves, a generic user glyph renders. Override it with `fallbackIcon`. **Icon fallback** ```tsx <Avatar /> <Avatar fallbackIcon={<RobotIcon />} /> ``` ## Statuses Add a presence indicator in the bottom-right corner via the `status` prop. **Statuses** | Status | Colour token | | --------- | ----------------------- | | `online` | `colors.accent.success` | | `away` | `colors.accent.warning` | | `busy` | `colors.accent.error` | | `offline` | `colors.text.muted` | ```tsx <Avatar name="Alice" status="online" /> <Avatar name="Bob" status="away" /> <Avatar name="Carol" status="busy" /> <Avatar name="Dave" status="offline" /> ``` ## Fallback chain The component renders both the image and the fallback in the same DOM, with the image positioned over the fallback. As soon as the image loads it paints over the initials; if it errors, the image is hidden via `data-loaded="false"` and the initials become visible without a layout shift. **Broken image** In order: 1. **Image** — `src` provided and loads successfully 2. **Initials** — derived from `name` (or set explicitly via `initials`) 3. **Icon** — `fallbackIcon` if provided, otherwise a generic user glyph ## Image source recommendations - Square aspect ratio. The `<img>` is rendered with `object-fit: cover`, so non-square sources crop to centre. - Render at 2× the avatar's pixel size for crisp results on HiDPI displays — for a 32px avatar, a 64×64 source. - Prefer modern formats (WebP / AVIF). Avatars sit in a container with `overflow: hidden`, so transparency is preserved when the format supports it. - For user-uploaded images, consider running them through a face-aware crop service (Unsplash, Cloudinary, your own pipeline) before passing the URL to `<Avatar>`. ## Clickable Provide `onClick` to make the avatar interactive — focusable, with a hover affordance and Enter/Space activation. **Clickable** ```tsx <Avatar src="/users/alice.png" name="Alice" onClick={() => openProfile('alice')} /> ``` ## AvatarGroup Display multiple avatars as an overlapping cluster. **AvatarGroup** ```tsx <AvatarGroup max={4}> <Avatar src="/users/alice.png" name="Alice" /> <Avatar name="Bob" /> <Avatar name="Carol" /> <Avatar name="Dave" /> <Avatar name="Eve" /> </AvatarGroup> ``` `size` and `bordered` propagate to every child — the group always wins so the cluster reads uniformly. The default `bordered={true}` adds a 2px ring matching the surrounding background; the leftmost avatar stacks on top via descending z-index. ## AvatarGroup overflow When the number of children exceeds `max`, the remainder collapses into a `+N` indicator. Hover it to reveal the names of the hidden avatars. **AvatarGroup overflow** ```tsx <AvatarGroup max={4}>{/* 12 avatars — 4 visible, "+8" overflow */}</AvatarGroup> ``` Disable the tooltip with `showOverflowTooltip={false}` when the names aren't useful in context (e.g. when none of the avatars carry a `name` prop). ### Group sizes **Group sizes** ### Editor example A collaborators row in a project header. **Editor example** ## Accessibility - The accessible name resolves in this order: `aria-label` → `name` → `alt`. Always supply at least one of these. - The image's `alt` attribute defaults to `name` (then the empty string), so when an image fails the underlying initials read with the supplied name. - The status dot has its own `aria-label` (`"Status: Online"` etc.) so a screen reader reaches it independently of the avatar's name. - When `onClick` is provided, the avatar gets `role="button"` and `tabIndex={0}`, and Enter / Space trigger the handler. - The fallback glyph is marked `aria-hidden="true"` once an image successfully loads so it doesn't double-announce. ## API Reference ### Avatar | Prop | Type | Default | Description | | --- | --- | --- | --- | | `src` | `string` | — | Image source URL. Falls back to initials, then to an icon, on error. | | `alt` | `string` | — | Image alt text and accessible name fallback. | | `name` | `string` | — | Display name. Drives initials and the auto color hash. | | `initials` | `string` | — | Manual initials override. Truncated to two characters and uppercased. | | `fallbackIcon` | `ReactNode` | — | Icon shown when neither image nor initials resolve. | | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'xxl'` | `'md'` | Diameter (16 / 20 / 24 / 32 / 40 / 56 px). | | `shape` | `'circle' \| 'square' \| 'rounded'` | `'circle'` | Border radius treatment. | | `color` | `'auto' \| 'neutral' \| 'primary' \| 'success' \| 'warning' \| 'error' \| 'info' \| string` | `'auto'` | Background colour for the initials / icon surface. | | `status` | `'online' \| 'away' \| 'busy' \| 'offline'` | — | Optional presence indicator in the bottom-right corner. | | `onClick` | `(event) => void` | — | When provided, the avatar becomes a focusable button with Enter/Space activation. | | `bordered` | `boolean` | `false` | Render a 2px ring matching the surrounding background. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the avatar root element. | ### AvatarGroup | Prop | Type | Default | Description | | --- | --- | --- | --- | | `max` | `number` | `4` | Maximum number of visible avatars before the +N indicator appears. | | `spacing` | `number \| string` | `-8` | Spacing between avatars. Negative values overlap. Numbers are pixels. | | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'xxl'` | `'md'` | Size applied to every child avatar. | | `bordered` | `boolean` | `true` | Force a border on every child, separating overlapping avatars. | | `showOverflowTooltip` | `boolean` | `true` | Show a tooltip listing hidden avatar names when the +N indicator is visible. | | `children` *(required)* | `ReactNode` | — | Avatar children. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the group root element. | --- # Badge > Inline status indicator and tag primitive with subtle, solid, outline, and dot variants. _Source: /components/primitives/badge_ A small inline indicator for status, counts, or tags. Designed for editor density: short label, semibold weight, optional uppercase. Supports semantic color tokens or arbitrary CSS color strings, an optional leading icon, and a removable variant for chip-like usage. **Live Preview** ## Import ```tsx import { Badge } from 'entangle-ui'; ``` ## Usage ```tsx <Badge color="success">Saved</Badge> <Badge variant="outline" color="warning">Draft</Badge> <Badge variant="dot" color="error">Offline</Badge> ``` ## Variants The `variant` prop controls the visual treatment. ### Subtle Tinted translucent fill with colored text — the default. Reads as a label without competing with surrounding UI. **subtle** ```tsx <Badge variant="subtle" color="success"> Success </Badge> ``` ### Solid Saturated fill with contrasting text. Use sparingly for high-visibility status. **solid** ```tsx <Badge variant="solid" color="primary"> Primary </Badge> ``` ### Outline Transparent background with a colored border. Good for secondary chips that share row space with subtle badges. **outline** ```tsx <Badge variant="outline" color="warning"> Warning </Badge> ``` ### Dot A small filled circle followed by the label. Use for online/offline or health indicators. **dot** ```tsx <Badge variant="dot" color="success"> Online </Badge> ``` ## Sizes | Size | Height | Use case | | ---- | ------ | ------------------------------- | | `xs` | 14px | Inline counters in dense rows | | `sm` | 16px | Default — paired with body text | | `md` | 20px | Standalone status pills | | `lg` | 24px | Prominent labels in toolbars | **Sizes** ```tsx <Badge size="xs">3</Badge> <Badge size="sm">Default</Badge> <Badge size="md">Status</Badge> <Badge size="lg">Pill</Badge> ``` ## Colors Named colors map to theme accent tokens. Any other string is treated as a raw CSS color (hex, `rgb()`, `hsl()`, …). | Name | Maps to | | --------- | ----------------------- | | `neutral` | `colors.text.muted` | | `primary` | `colors.accent.primary` | | `info` | `colors.accent.primary` | | `success` | `colors.accent.success` | | `warning` | `colors.accent.warning` | | `error` | `colors.accent.error` | ### Custom CSS colors Pass any CSS color string — hex, `rgb()`, `hsl()`, CSS variables — and the badge picks it up via a per-instance runtime variable. **Custom colors** ```tsx <Badge color="#ff6600">orange hex</Badge> <Badge color="#9b59b6" variant="solid">purple solid</Badge> <Badge color="rgb(0, 180, 200)" variant="outline">teal rgb</Badge> ``` ## Uppercase Auto-applies `text-transform: uppercase` and a subtle letter-spacing — matches the editor-UI tag convention. **Uppercase** ```tsx <Badge uppercase color="warning"> draft </Badge> ``` ## With Icon The `icon` slot renders before the label. Pass any ReactNode — SVGs or Entangle's `Icon` wrappers are the common choice. **With icon** ```tsx <Badge color="success" icon={<CheckIcon />}>Saved</Badge> <Badge color="warning" icon={<StarIcon />}>Featured</Badge> <Badge variant="solid" color="primary" icon={<StarIcon />}>Pro</Badge> ``` ## Removable The `removable` prop renders a built-in close button. The badge body click is **not** treated as a remove action — use the dedicated button. **Removable** — Click the × on any tag to remove it. ```tsx const [tags, setTags] = useState([...]); {tags.map(tag => ( <Badge key={tag} color="primary" removable onRemove={() => setTags(prev => prev.filter(t => t !== tag))} > {tag} </Badge> ))} ``` The remove button gets `aria-label="Remove"` automatically. Clicks on the remove button stop propagation so your own click handler on the badge body remains separate. ## In a List Row Pair with `ListItem` (or a plain row) for status-tagged lists. **In a list row** ```tsx <ListItem trailing={<Badge color="success">done</Badge>}> Bake meshes </ListItem> <ListItem trailing={<Badge color="warning">running</Badge>}> Normalize UVs </ListItem> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Label content. | | `variant` | `'subtle' \| 'solid' \| 'outline' \| 'dot'` | `'subtle'` | Visual style. | | `size` | `'xs' \| 'sm' \| 'md' \| 'lg'` | `'sm'` | Size scale. | | `color` | `'neutral' \| 'primary' \| 'info' \| 'success' \| 'warning' \| 'error' \| string` | `'neutral'` | Semantic color name (mapped to theme accent) or any CSS color string. | | `uppercase` | `boolean` | `false` | Auto-uppercase the label and apply a small letter-spacing. | | `icon` | `ReactNode` | — | Icon rendered before the label. | | `removable` | `boolean` | `false` | Render a remove (×) button after the label. | | `onRemove` | `(event: MouseEvent) => void` | — | Called when the remove button is clicked. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying span element. | The component also accepts all standard HTML `<span>` attributes. ## Accessibility - Renders a semantic `<span>` (inline) - The remove button has `aria-label="Remove"` and stops click-propagation so the badge body click handler stays separate - Decorative icons are marked `aria-hidden`; provide your own `aria-label` if the badge is the only label for an action --- # Button > Versatile button component for editor interfaces with multiple variants, sizes, and states. _Source: /components/primitives/button_ Versatile button component optimized for professional editor interfaces. Supports multiple variants, sizes, loading states, and icons. **Live Preview** ## Import ```tsx import { Button } from 'entangle-ui'; ``` ## Usage ```tsx <Button variant="default" size="md"> Save </Button> ``` ## Variants The `variant` prop controls the button's visual style. **default** — Transparent with border, fills on hover. Use for secondary actions. ```tsx <Button variant="default">Default</Button> ``` **ghost** — No border, subtle hover state. Use for inline or low-emphasis actions. ```tsx <Button variant="ghost">Ghost</Button> ``` **filled** — Solid background with accent color. Use for primary actions. ```tsx <Button variant="filled">Filled</Button> ``` ## Sizes Sizes are optimized for editor interface density. ```tsx <Button size="sm">Small (20px)</Button> <Button size="md">Medium (24px)</Button> <Button size="lg">Large (32px)</Button> ``` | Size | Height | Use case | | ---- | ------ | ----------------- | | `sm` | 20px | Compact toolbars | | `md` | 24px | Standard panels | | `lg` | 32px | Prominent actions | ## With Icon Pass an icon element via the `icon` prop. Icons should be 16x16px for optimal appearance. ```tsx import { SaveIcon, PlayIcon } from 'entangle-ui'; <Button icon={<SaveIcon />}>Save</Button> <Button icon={<PlayIcon />} variant="filled">Play</Button> ``` ## Loading State The `loading` prop shows a spinner and disables interaction. Use for async operations. ```tsx <Button loading>Saving...</Button> <Button loading variant="filled">Publishing</Button> ``` ## Full Width The `fullWidth` prop makes the button span the full width of its container. Useful for form actions and modal buttons. ```tsx <Button variant="filled" fullWidth> Confirm Action </Button> ``` ## Disabled ```tsx <Button disabled>Disabled</Button> <Button disabled variant="filled">Disabled Filled</Button> ``` ## Combining Props ```tsx <Button icon={<SaveIcon />} variant="filled" size="lg" loading={isSaving} onClick={handleSave} > Save Project </Button> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Button content — text, icons, or other React elements. | | `variant` | `'default' \| 'ghost' \| 'filled'` | `'default'` | Visual variant of the button. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size variant optimized for editor interfaces. | | `disabled` | `boolean` | `false` | Whether the button is disabled. | | `loading` | `boolean` | `false` | Loading state — shows spinner and disables interaction. | | `icon` | `ReactNode` | — | Icon element to display before text. Should be 16x16px. | | `fullWidth` | `boolean` | `false` | Whether the button should take the full width of the container. | | `onClick` | `(event: MouseEvent) => void` | — | Click event handler. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying button element. | The component also accepts all standard HTML `<button>` attributes. ## Accessibility - Renders a native `<button>` element - Supports keyboard navigation (Enter/Space to activate) - `disabled` and `loading` states set `disabled` on the native element - Use meaningful text content or `aria-label` for icon-only buttons --- # Checkbox > Checkbox component for boolean selection with indeterminate state, labels, and group support. _Source: /components/primitives/checkbox_ Checkbox component for boolean selection in settings panels, property inspectors, and form interfaces. Supports controlled and uncontrolled modes, indeterminate state, label positioning, sizes, variants, and error states. **Live Preview** ## Import ```tsx import { Checkbox } from 'entangle-ui'; ``` ## Usage ```tsx <Checkbox label="Enable shadows" /> ``` ## Controlled Use `checked` and `onChange` for controlled mode. The `onChange` callback receives the new boolean value. ```tsx const [checked, setChecked] = useState(false); <Checkbox checked={checked} onChange={setChecked} label="Auto-save" />; ``` **Controlled** ## Sizes **Sizes** ```tsx <Checkbox size="sm" label="Small (14px)" /> <Checkbox size="md" label="Medium (16px)" /> <Checkbox size="lg" label="Large (20px)" /> ``` | Size | Box size | Use case | | ---- | -------- | ------------------ | | `sm` | 14px | Compact, dense UIs | | `md` | 16px | Standard forms | | `lg` | 20px | Prominent settings | ## Variants **Variants** ```tsx <Checkbox variant="default" label="Default variant" /> <Checkbox variant="filled" label="Filled variant" /> ``` | Variant | Description | | --------- | ----------------------------------------------------- | | `default` | Border-only when unchecked, accent fill when checked | | `filled` | Subtle background when unchecked, accent when checked | ## Indeterminate State The `indeterminate` prop shows a dash icon instead of a checkmark. Used for "select all" patterns when some items are checked. Takes visual precedence over the checked state. **Indeterminate** ```tsx <Checkbox indeterminate label="Select all" /> ``` ## Label Position The label can be placed on either side of the checkbox. **Label Position** ```tsx <Checkbox label="Label on right" labelPosition="right" /> <Checkbox label="Label on left" labelPosition="left" /> ``` ## Helper Text and Error Display helper text below the checkbox, or an error message when validation fails. **Helper Text and Error** ```tsx <Checkbox label="Accept terms" helperText="You must accept to continue" /> <Checkbox label="Accept terms" error errorMessage="This field is required" /> ``` ## Required **Required** ```tsx <Checkbox label="I agree to the terms" required /> ``` ## Disabled **Disabled** ```tsx <Checkbox disabled label="Disabled unchecked" /> <Checkbox disabled checked label="Disabled checked" /> ``` ## Form Submission Use the `name` and `value` props for form integration. A hidden input is rendered with the appropriate value. **Form Submission** ```tsx <Checkbox name="features" value="shadows" label="Enable shadows" /> <Checkbox name="features" value="reflections" label="Enable reflections" /> ``` ## Combining Props **Combining Props** ```tsx <Checkbox checked={enableShadows} onChange={setEnableShadows} label="Enable shadows" size="sm" variant="filled" helperText="Adds real-time shadow rendering" /> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `checked` | `boolean` | — | Whether the checkbox is checked (controlled mode). | | `defaultChecked` | `boolean` | `false` | Default checked state (uncontrolled mode). | | `indeterminate` | `boolean` | `false` | Whether the checkbox is in an indeterminate state. Shows a dash icon. | | `label` | `string` | — | Label text displayed next to the checkbox. | | `labelPosition` | `'left' \| 'right'` | `'right'` | Position of the label relative to the checkbox. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Checkbox size. | | `variant` | `'default' \| 'filled'` | `'default'` | Visual variant controlling unchecked/checked appearance. | | `disabled` | `boolean` | `false` | Whether the checkbox is disabled. | | `required` | `boolean` | `false` | Whether the checkbox is required. | | `error` | `boolean` | `false` | Whether the checkbox has an error state. | | `helperText` | `string` | — | Helper text displayed below the checkbox. | | `errorMessage` | `string` | — | Error message displayed when error is true. Replaces helperText. | | `value` | `string` | — | Value attribute for CheckboxGroup integration and form submission. | | `name` | `string` | — | Name attribute for form submission. | | `onChange` | `(checked: boolean) => void` | — | Change event handler receiving the new checked state. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying button element. | ## Accessibility - Uses `role="checkbox"` on a native `<button>` element - `aria-checked` reflects the current state: `true`, `false`, or `mixed` (indeterminate) - `aria-disabled`, `aria-required`, and `aria-invalid` are set based on props - Helper text and error messages are linked via `aria-describedby` - Label is rendered as a `<label>` element wrapping the checkbox for click target expansion - Keyboard accessible: Space to toggle, Tab to navigate --- # Code > Inline code primitive backed by the theme inset background and monospace font. _Source: /components/primitives/code_ A small inline `<code>` primitive that picks up the theme monospace font and the `colors.background.inset` token so inline snippets read as recessed surfaces inside body text. For multi-line or syntax-highlighted blocks use `ChatCodeBlock` (under the editor namespace). **Live Preview** ## Import ```tsx import { Code } from 'entangle-ui'; ``` ## Usage ```tsx <Text> Run <Code>npm install entangle-ui</Code> to add the package. </Text> ``` ## Sizes The font size is relative to the surrounding text — use `size` to fine-tune the scale. **Sizes** ```tsx <Code size="xs">xs</Code> {/* 0.8em */} <Code size="sm">sm</Code> {/* 0.9em — default */} <Code size="md">md</Code> {/* 1em */} ``` ## Inside a Sentence `Code` is inline — it sits on the same baseline as surrounding text and wraps naturally. **Inside a sentence** ```tsx <Text> Install with <Code>npm install entangle-ui</Code> and import from the root barrel: <Code>{"import { Button } from 'entangle-ui'"}</Code>. </Text> ``` ## Inside a Link Inherits text color from its parent, so it plays nicely with colored anchors. **Inside a link** ```tsx <a href="/components/primitives/button"> See the <Code>Button</Code> docs </a> ``` ## Mixed with String Literals **With string literals** ```tsx <Text> The environment variable <Code>NODE_ENV</Code> defaults to{' '} <Code>"development"</Code> when running <Code>npm run dev</Code>. </Text> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Inline code content. | | `size` | `'xs' \| 'sm' \| 'md'` | `'sm'` | Font size scale, relative to surrounding text. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying code element. | ## Theme Tokens | Token | Used for | | ---------------------------- | ---------------------- | | `colors.background.inset` | Sunken background fill | | `colors.text.primary` | Foreground text | | `typography.fontFamily.mono` | Monospace family | | `borderRadius.sm` | Corner radius (2px) | ## Accessibility - Renders a semantic `<code>` element so assistive tech announces it as inline code - No interactive behavior — wrap with a link or button if needed --- # Collapsible > Expandable/collapsible section component for organizing content in panels and settings interfaces. _Source: /components/primitives/collapsible_ Expandable/collapsible section component for organizing content in panels, settings interfaces, and property inspectors. Supports controlled and uncontrolled modes, custom indicators, multiple sizes, and optional content persistence when collapsed. **Live Preview** ## Import ```tsx import { Collapsible } from 'entangle-ui'; ``` ## Usage ```tsx <Collapsible trigger="Section Title"> <p>Collapsible content here</p> </Collapsible> ``` ## Controlled Use `open` and `onChange` for controlled mode. ```tsx const [isOpen, setIsOpen] = useState(false); <Collapsible open={isOpen} onChange={setIsOpen} trigger="Controlled Section"> <p>Content that you control</p> </Collapsible>; ``` ## Default Open Set `defaultOpen` to have the section expanded on first render. ```tsx <Collapsible defaultOpen trigger="Initially Open"> <p>This content is visible by default</p> </Collapsible> ``` ## Sizes ```tsx <Collapsible size="sm" trigger="Small (24px header)"> <p>Small section content</p> </Collapsible> <Collapsible size="md" trigger="Medium (28px header)"> <p>Medium section content</p> </Collapsible> <Collapsible size="lg" trigger="Large (32px header)"> <p>Large section content</p> </Collapsible> ``` | Size | Header height | Use case | | ---- | ------------- | -------------------------- | | `sm` | 24px | Compact property panels | | `md` | 28px | Standard settings sections | | `lg` | 32px | Prominent section headings | ## Custom Indicator By default, a chevron icon rotates to indicate open/closed state. Pass a custom React node to replace it, or `null` to hide the indicator entirely. ```tsx { /* Custom indicator */ } <Collapsible indicator={<PlusIcon />} trigger="Custom Icon"> <p>Uses a custom indicator icon</p> </Collapsible>; { /* No indicator */ } <Collapsible indicator={null} trigger="No Indicator"> <p>No expand/collapse icon shown</p> </Collapsible>; ``` ## Keep Mounted By default, collapsed content is removed from the DOM. Set `keepMounted` to keep it in the DOM (hidden) for preserving state or improving toggle performance. ```tsx <Collapsible keepMounted trigger="Persistent Content"> <ComplexForm /> </Collapsible> ``` ## Disabled When disabled, the collapsible cannot be toggled. ```tsx <Collapsible disabled trigger="Disabled Section"> <p>This section cannot be toggled</p> </Collapsible> <Collapsible disabled defaultOpen trigger="Disabled Open"> <p>This content is permanently visible</p> </Collapsible> ``` ## Nested Collapsibles Collapsibles can be nested for hierarchical content organization. ```tsx <Collapsible trigger="Transform" defaultOpen> <Collapsible trigger="Position" size="sm"> <p>X: 0, Y: 0, Z: 0</p> </Collapsible> <Collapsible trigger="Rotation" size="sm"> <p>X: 0, Y: 0, Z: 0</p> </Collapsible> <Collapsible trigger="Scale" size="sm"> <p>X: 1, Y: 1, Z: 1</p> </Collapsible> </Collapsible> ``` ## Rich Trigger Content The `trigger` prop accepts any React node, so you can include icons or badges in the header. ```tsx <Collapsible trigger={ <span style={{ display: 'flex', alignItems: 'center', gap: 4 }}> <SettingsIcon /> Advanced Settings </span> } > <p>Advanced configuration options</p> </Collapsible> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `trigger` | `ReactNode` | — | Content shown in the collapsible header/trigger. | | `children` | `ReactNode` | — | Content to render when expanded. | | `open` | `boolean` | — | Whether the section is expanded (controlled mode). | | `defaultOpen` | `boolean` | `false` | Default expanded state (uncontrolled mode). | | `size` | `'sm' \| 'md' \| 'lg'` | `'sm'` | Size controlling header height, padding, and font size. | | `indicator` | `ReactNode \| null` | — | Custom indicator icon. Defaults to a chevron. Set to null to hide. | | `disabled` | `boolean` | `false` | Whether the collapsible is disabled. | | `keepMounted` | `boolean` | `false` | Whether to keep content in the DOM when collapsed. | | `onChange` | `(open: boolean) => void` | — | Callback when the open state changes. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the root div element. | ## Accessibility - Trigger button uses `aria-expanded` to indicate current state - Trigger is linked to content via `aria-controls` / `id` - Content region uses `role="region"` with `aria-labelledby` pointing to the trigger - `aria-disabled` is set when the collapsible is disabled - Content is hidden with the `hidden` attribute when collapsed (unless `keepMounted` is false, in which case it is removed from the DOM) - Keyboard accessible: Enter/Space to toggle, Tab to navigate --- # HoverCard > Hover- and focus-driven floating panel for previews, with safe-polygon cursor handover from trigger to content. _Source: /components/primitives/hover-card_ A floating panel anchored to a trigger and shown on hover or keyboard focus. Use `HoverCard` for read-only previews — user avatars, file metadata, linked-node summaries — where the user wants more context without clicking. The cursor can travel from the trigger onto the content through a safe polygon, so the card stays open while the user reads it. **Live Preview** ## When to use - **HoverCard** — read-only preview that's nice to have, not critical. Opens on hover, dismisses when the cursor leaves. Don't put primary actions inside. - **Popover** — interactive content (forms, menus, multi-step pickers) that needs an explicit click to open. - **Tooltip** — single-line label for an icon or truncated text. Smaller surface, faster open. `HoverCard` is the right pick when the preview is informational, the user might want to skim a few in a row, and a click would feel heavy. ## Import ```tsx import { HoverCard } from 'entangle-ui'; ``` The compound members come along automatically: ```tsx <HoverCard> <HoverCard.Trigger>...</HoverCard.Trigger> <HoverCard.Content>...</HoverCard.Content> </HoverCard> ``` ## Usage Wrap any single-element trigger and pair it with `HoverCard.Content`: ```tsx <HoverCard> <HoverCard.Trigger> <Link href="/user/octocat">@octocat</Link> </HoverCard.Trigger> <HoverCard.Content width={280}> <UserPreview user={...} /> </HoverCard.Content> </HoverCard> ``` `HoverCard.Trigger` clones its single child and attaches the hover / focus listeners to it — pass exactly one React element. **Basic** ## Placements `placement` controls which side of the trigger the card opens on. The default is `bottom-start`. Each placement automatically flips when there's not enough room. **Placements** ```tsx <HoverCard placement="top">...</HoverCard> <HoverCard placement="right">...</HoverCard> <HoverCard placement="bottom">...</HoverCard> <HoverCard placement="left">...</HoverCard> ``` The full set of placements (`'top' | 'top-start' | 'top-end' | 'right' | 'right-start' | 'right-end' | 'bottom' | 'bottom-start' | 'bottom-end' | 'left' | 'left-start' | 'left-end'`) is shared with [Popover](/components/primitives/popover). ## Delays `openDelay` and `closeDelay` (both in ms) tune how eager the card is to appear and how forgiving it is once the cursor leaves. **Delays** | Setting | Default | Use when | | ------------------- | ------- | ------------------------------------------------------------ | | `openDelay={0}` | — | Compact rows where users hover many items in succession. | | `openDelay={400}` | default | Standard discovery feel — avoids flickering across labels. | | `openDelay={800}` | — | Heavy content (avatars + stats + actions) on rarely-used UI. | | `closeDelay={150}` | default | Enough cushion to move the cursor onto the card. | | `closeDelay={300+}` | — | Interactive content the user is likely to read for a moment. | ```tsx <HoverCard openDelay={150} closeDelay={300}> ... </HoverCard> ``` ## Controlled Pass `open` and `onOpenChange` to drive the card from external state — useful for tests, automation, or pairing the card with another UI surface. **Controlled** ```tsx const [open, setOpen] = useState(false); <HoverCard open={open} onOpenChange={setOpen}> <HoverCard.Trigger> <Button>Trigger</Button> </HoverCard.Trigger> <HoverCard.Content>...</HoverCard.Content> </HoverCard>; ``` When `open` is controlled, hover and focus still fire `onOpenChange`, so you can intercept or ignore as needed. ## Interactive content The safe polygon means buttons and links inside the card stay reachable — the cursor's path from the trigger keeps the card alive. Reach for `Popover` if the interaction is the point (forms, multi-step pickers). **Interactive content** ```tsx <HoverCard openDelay={150} closeDelay={300}> <HoverCard.Trigger> <Button>Profile preview</Button> </HoverCard.Trigger> <HoverCard.Content width={300}> <Stack gap={3}> <Text weight="bold">Sebastian Reyes</Text> <Text size="sm" color="secondary"> Senior Technical Artist </Text> <Flex gap={2}> <Button size="sm">Follow</Button> <Button size="sm" variant="outlined"> Message </Button> </Flex> </Stack> </HoverCard.Content> </HoverCard> ``` Set `disableSafePolygon` if you want the card to close the moment the cursor leaves the trigger rect — useful when the content has no interactive children and you want a snappier dismiss. ## Disabled `disabled` short-circuits open requests entirely — the card never appears regardless of hover or focus. **Disabled** ```tsx <HoverCard disabled> <HoverCard.Trigger> <Button>Won't preview</Button> </HoverCard.Trigger> <HoverCard.Content>...</HoverCard.Content> </HoverCard> ``` ## Accessibility - Opens on **hover** and on keyboard **focus** — works without a pointing device. - The trigger receives `aria-describedby` pointing at the content while open. - Content renders with `role="tooltip"` so assistive tech announces it as supplementary information. - `prefers-reduced-motion` users get the same transition timing — there's no large motion to suppress, just a small opacity fade. - The card is not focus-trapped; keep primary interactions in surrounding UI (or reach for `Popover`). ## API Reference ### `<HoverCard>` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `open` | `boolean` | — | Controlled open state. | | `defaultOpen` | `boolean` | `false` | Default open state (uncontrolled). | | `placement` | `'top' \| 'top-start' \| 'top-end' \| 'right' \| 'right-start' \| 'right-end' \| 'bottom' \| 'bottom-start' \| 'bottom-end' \| 'left' \| 'left-start' \| 'left-end'` | `'bottom-start'` | Side relative to the trigger. | | `offset` | `number` | `8` | Distance in pixels from the trigger. | | `openDelay` | `number` | `400` | Delay before opening on hover, in ms. | | `closeDelay` | `number` | `150` | Delay before closing once the cursor leaves, in ms. | | `portal` | `boolean` | `true` | Render the content in a portal. | | `disableSafePolygon` | `boolean` | `false` | Disable the safe polygon that lets the cursor move from trigger onto content without closing. | | `disabled` | `boolean` | `false` | Disable the entire HoverCard (never opens). | | `onOpenChange` | `(open: boolean) => void` | — | Callback when the open state changes. | | `children` | `ReactNode` | — | `HoverCard.Trigger` + `HoverCard.Content` children. | ### `<HoverCard.Trigger>` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactElement` | — | Trigger element — must be a single React element. The hover and focus listeners are attached to it. | ### `<HoverCard.Content>` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Content rendered inside the floating panel. | | `width` | `number \| string` | — | Width of the content. Number → px, string → CSS value. | | `maxHeight` | `number \| string` | — | Maximum height with internal scroll. Number → px, string → CSS value. | | `padding` | `'none' \| 'sm' \| 'md' \| 'lg'` | `'md'` | Padding inside the content. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the content element. | --- # Icon > SVG icon wrapper component with standardized sizing and color theming for editor interfaces. _Source: /components/primitives/icon_ SVG icon wrapper component providing standardized sizing and color theming. Wraps icon SVG children in a consistent `<svg>` container with accessible roles and theme-aware coloring. **Live Preview** ## Import ```tsx import { Icon } from 'entangle-ui'; ``` ## Usage ```tsx <Icon size="md" color="primary"> <path d="M12 2L2 7l10 5 10-5-10-5z" fill="currentColor" /> </Icon> ``` Most of the time you will use the pre-built icon components rather than `Icon` directly. `Icon` is the base building block they are built on. ```tsx import { SaveIcon, PlayIcon, SearchIcon } from 'entangle-ui'; <SaveIcon /> <PlayIcon /> <SearchIcon /> ``` ## Sizes The `size` prop controls the icon dimensions. ```tsx <Icon size="sm">{/* 12px icon */}</Icon> <Icon size="md">{/* 16px icon */}</Icon> <Icon size="lg">{/* 20px icon */}</Icon> ``` | Size | Dimensions | Use case | | ---- | ---------- | ----------------------------------- | | `sm` | 12x12px | Compact toolbars, inline indicators | | `md` | 16x16px | Standard buttons and controls | | `lg` | 20x20px | Prominent actions, headers | ## Colors Standard theme colors are applied via CSS class variants. Custom CSS color values are also supported. ```tsx {/* Standard theme colors */} <Icon color="primary">{/* ... */}</Icon> <Icon color="secondary">{/* ... */}</Icon> <Icon color="muted">{/* ... */}</Icon> <Icon color="accent">{/* ... */}</Icon> <Icon color="success">{/* ... */}</Icon> <Icon color="warning">{/* ... */}</Icon> <Icon color="error">{/* ... */}</Icon> {/* Custom CSS color value */} <Icon color="#ff6b00">{/* ... */}</Icon> <Icon color="rgb(100, 200, 50)">{/* ... */}</Icon> ``` ## Accessible Title Add a `title` for screen readers when the icon conveys meaning. ```tsx <Icon title="Save file" color="primary"> <path d="..." fill="currentColor" /> </Icon> ``` ## Decorative Icons Set `decorative` to `true` for icons that are purely visual and should be hidden from assistive technology. ```tsx <Icon decorative> <path d="..." fill="currentColor" /> </Icon> ``` When `decorative` is `true`, the icon renders with `aria-hidden="true"` and `role="presentation"`. The `title` element is also omitted. ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | SVG content (paths, circles, etc.) to render inside the icon. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Icon size controlling width and height. | | `color` | `'primary' \| 'secondary' \| 'muted' \| 'accent' \| 'success' \| 'warning' \| 'error' \| string` | `'primary'` | Icon color. Standard theme tokens or any CSS color value. | | `title` | `string` | — | Accessible title for the icon. Renders a <title> element inside the SVG. | | `decorative` | `boolean` | `false` | Whether the icon is decorative only. Sets aria-hidden and role=presentation. | | `className` | `string` | — | Additional CSS class names. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying SVG element. | ## Accessibility - Non-decorative icons render with `role="img"` and support a `<title>` element for screen readers - Decorative icons set `aria-hidden="true"` and `role="presentation"` to hide from assistive technology - Always provide a `title` when the icon conveys meaning that is not available through surrounding text - When using icons inside buttons, prefer adding `aria-label` on the button rather than `title` on the icon --- # IconButton > Square button component for icon-based actions in toolbars and editor interfaces. _Source: /components/primitives/icon-button_ Square button component designed for icon-based actions in toolbars, quick controls, and icon-driven interactions. Always square and sized appropriately for the contained icon. Supports multiple variants, border radius options, and a pressed/toggle state. **Live Preview** ## Import ```tsx import { IconButton } from 'entangle-ui'; ``` ## Usage ```tsx import { SaveIcon } from 'entangle-ui'; <IconButton aria-label="Save file"> <SaveIcon /> </IconButton>; ``` ## Variants The `variant` prop controls the button's visual style. ```tsx <IconButton variant="ghost" aria-label="Ghost action"> <SettingsIcon /> </IconButton> <IconButton variant="default" aria-label="Default action"> <SettingsIcon /> </IconButton> <IconButton variant="filled" aria-label="Filled action"> <SettingsIcon /> </IconButton> ``` | Variant | Description | Use case | | --------- | --------------------------------------- | ----------------------------- | | `ghost` | No border, subtle hover state | Toolbar icons, inline actions | | `default` | Transparent with border, fills on hover | Secondary actions | | `filled` | Solid background with accent color | Primary actions | ## Sizes The button is always square, sized to match the icon. ```tsx <IconButton size="sm" aria-label="Small"> <SaveIcon /> </IconButton> <IconButton size="md" aria-label="Medium"> <SaveIcon /> </IconButton> <IconButton size="lg" aria-label="Large"> <SaveIcon /> </IconButton> ``` | Size | Dimensions | Icon size | Use case | | ---- | ---------- | --------- | ----------------- | | `sm` | 20x20px | 12px | Compact toolbars | | `md` | 24x24px | 16px | Standard panels | | `lg` | 32x32px | 20px | Prominent actions | ## Border Radius The `radius` prop controls the button's corner rounding. ```tsx <IconButton radius="none" aria-label="Sharp corners"> <SaveIcon /> </IconButton> <IconButton radius="sm" aria-label="Slight rounding"> <SaveIcon /> </IconButton> <IconButton radius="md" aria-label="Moderate rounding"> <SaveIcon /> </IconButton> <IconButton radius="lg" aria-label="More rounding"> <SaveIcon /> </IconButton> <IconButton radius="full" aria-label="Circular"> <SaveIcon /> </IconButton> ``` | Radius | Value | Use case | | ------ | ----- | --------------------- | | `none` | 0px | Sharp toolbar buttons | | `sm` | 2px | Slightly rounded | | `md` | 4px | Standard rounding | | `lg` | 6px | Softer appearance | | `full` | 50% | Circular buttons | ## Pressed / Toggle State Use the `pressed` prop for toggle states and active tool indicators. The button visually shows the pressed state and sets `aria-pressed`. ```tsx const [isVisible, setIsVisible] = useState(true); <IconButton pressed={isVisible} aria-label="Toggle visibility" onClick={() => setIsVisible(!isVisible)} > <EyeIcon /> </IconButton>; ``` ## Loading State The `loading` prop shows a spinner and disables interaction. ```tsx <IconButton loading aria-label="Saving..."> <SaveIcon /> </IconButton> ``` ## Disabled ```tsx <IconButton disabled aria-label="Save (disabled)"> <SaveIcon /> </IconButton> ``` ## Combining Props ```tsx <IconButton variant="filled" size="lg" radius="full" aria-label="Add new item" onClick={handleAdd} > <AddIcon /> </IconButton> <IconButton variant="default" radius="none" pressed={isActive} aria-label="Toggle grid" onClick={toggleGrid} > <GridIcon /> </IconButton> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Icon component to display inside the button. | | `aria-label` | `string (required)` | — | Accessible label for screen readers. Required for proper accessibility. | | `variant` | `'default' \| 'ghost' \| 'filled'` | `'ghost'` | Visual variant of the button. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Button size. The button is always square. | | `radius` | `'none' \| 'sm' \| 'md' \| 'lg' \| 'full'` | `'md'` | Border radius for button shape control. | | `disabled` | `boolean` | `false` | Whether the button is disabled. | | `loading` | `boolean` | `false` | Loading state -- shows spinner and disables interaction. | | `pressed` | `boolean` | `false` | Whether the button appears pressed/active. Sets aria-pressed. | | `onClick` | `(event: MouseEvent) => void` | — | Click event handler. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying button element. | The component also accepts all standard HTML `<button>` attributes (except `children`). ## Accessibility - Renders a native `<button>` element - `aria-label` is required since there is no visible text content - `pressed` state sets `aria-pressed` for toggle button semantics - `disabled` and `loading` states set `disabled` on the native element - Supports keyboard navigation (Enter/Space to activate) --- # Input > Versatile text input component for editor interfaces with labels, icons, error states, and multiple sizes. _Source: /components/primitives/input_ Versatile text input component for editor interfaces. Supports labels, helper text, start/end icons, error states, and multiple sizes optimized for compact editor UIs. **Live Preview** ## Import ```tsx import { Input } from 'entangle-ui'; ``` ## Usage ```tsx <Input placeholder="Enter text..." value={text} onChange={setText} /> ``` ## Sizes Sizes are optimized for editor interface density. ```tsx <Input size="sm" placeholder="Small (20px)" /> <Input size="md" placeholder="Medium (24px)" /> <Input size="lg" placeholder="Large (32px)" /> ``` | Size | Height | Use case | | ---- | ------ | ---------------- | | `sm` | 20px | Compact toolbars | | `md` | 24px | Standard forms | | `lg` | 32px | Prominent inputs | ## With Label The `label` prop adds an accessible label above the input. Combine with `required` to show a red asterisk. ```tsx <Input label="Project Name" placeholder="My Project" /> <Input label="Email" required placeholder="you@example.com" /> ``` ## With Icons Use `startIcon` and `endIcon` to place icons inside the input field. ```tsx import { SearchIcon, CloseIcon } from 'entangle-ui'; <Input startIcon={<SearchIcon />} placeholder="Search..." /> <Input endIcon={<CloseIcon />} placeholder="Clearable input" /> ``` ## Helper Text The `helperText` prop displays descriptive text below the input. ```tsx <Input label="Project Name" placeholder="My Project" helperText="Choose a unique name for your project" /> ``` ## Error State Set `error` to `true` and provide an `errorMessage` to display validation feedback. The error message replaces the helper text when active. ```tsx <Input label="Email" type="email" error={!!emailError} errorMessage={emailError} value={email} onChange={setEmail} /> ``` ## Input Types The `type` prop supports common HTML input types. ```tsx <Input type="text" placeholder="Text input" /> <Input type="email" placeholder="Email address" /> <Input type="password" placeholder="Password" /> <Input type="number" placeholder="Number" /> <Input type="search" placeholder="Search" /> <Input type="url" placeholder="URL" /> <Input type="tel" placeholder="Phone number" /> ``` ## Read Only ```tsx <Input readOnly value="This value cannot be changed" label="Read Only" /> ``` ## Disabled ```tsx <Input disabled placeholder="Disabled input" /> <Input disabled label="Disabled" value="Cannot edit" /> ``` ## Combining Props ```tsx <Input label="Search Files" placeholder="Type to search..." size="sm" startIcon={<SearchIcon />} helperText="Search across all project files" value={query} onChange={setQuery} /> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `value` | `string` | — | Input value (controlled mode). | | `defaultValue` | `string` | — | Default value (uncontrolled mode). | | `placeholder` | `string` | — | Placeholder text displayed when input is empty. | | `type` | `'text' \| 'email' \| 'password' \| 'number' \| 'search' \| 'url' \| 'tel'` | `'text'` | HTML input type. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Input size optimized for editor interfaces. | | `disabled` | `boolean` | `false` | Whether the input is disabled. | | `error` | `boolean` | `false` | Whether the input has an error state. | | `required` | `boolean` | `false` | Whether the input is required. Shows a red asterisk on the label. | | `readOnly` | `boolean` | `false` | Whether the input is read-only. | | `label` | `string` | — | Input label displayed above the field. | | `helperText` | `string` | — | Helper text displayed below the input. | | `errorMessage` | `string` | — | Error message displayed when error is true. Replaces helperText. | | `startIcon` | `ReactNode` | — | Icon displayed at the start of the input. | | `endIcon` | `ReactNode` | — | Icon displayed at the end of the input. | | `onChange` | `(value: string) => void` | — | Change event handler — receives the new string value directly. | | `onFocus` | `(event: FocusEvent) => void` | — | Focus event handler. | | `onBlur` | `(event: FocusEvent) => void` | — | Blur event handler. | | `onKeyDown` | `(event: KeyboardEvent) => void` | — | Key down event handler. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying input element. | The component also accepts all standard HTML `<input>` attributes (except `onChange` and `size`, which are replaced by the custom props above). ## Accessibility - Label is linked to the input via `htmlFor`/`id` for screen readers - `required` adds `required` attribute to the native input and displays a visual asterisk - Error state helper text is associated with the input for assistive technology - Supports standard keyboard navigation (Tab to focus, type to input) - `disabled` and `readOnly` states are reflected on the native input element --- # Kbd > Keyboard shortcut keycaps with platform-aware glyph rendering. _Source: /components/primitives/kbd_ Kbd renders keyboard shortcuts as semantic `<kbd>` keycaps. Use it anywhere a shortcut hint appears: menu rows, tooltip footers, command palettes, help overlays, and keyboard reference panels. `Cmd` is intentionally reinterpreted as `Ctrl` outside macOS so consumers can describe the logical modifier once and get the expected platform display. **Live Preview** ## Import ```tsx import { Kbd } from 'entangle-ui'; ``` ## Usage ```tsx <Kbd>Ctrl+S</Kbd> <Kbd platform="mac" separator={null}>Cmd+S</Kbd> <Kbd glyphs={false}>Ctrl+S</Kbd> ``` ## Literal vs Glyphs Use literal rendering when the shortcut text should stay exactly as written. Use glyph rendering for OS-aware shortcut hints. **Literal** ```tsx <Kbd glyphs={false} platform="windows"> Ctrl+S </Kbd> ``` **macOS glyphs** ```tsx <Kbd platform="mac" separator={null}> Cmd+S </Kbd> ``` ## Sizes **Sizes** | Size | Height | Padding | Font | Min-width | | ---- | ------ | ------- | -------------- | --------- | | `sm` | 16px | 0 4px | xxs (9px) mono | 16px | | `md` | 20px | 0 6px | xs (10px) mono | 20px | | `lg` | 24px | 0 8px | sm (11px) mono | 24px | ```tsx <Kbd size="sm">Ctrl+S</Kbd> <Kbd size="md">Ctrl+S</Kbd> <Kbd size="lg">Ctrl+S</Kbd> ``` ## Variants **Variants** ```tsx <Kbd variant="solid">Ctrl+S</Kbd> <Kbd variant="outline">Ctrl+S</Kbd> <Kbd variant="ghost">Ctrl+S</Kbd> ``` ## Multiple Keys Strings are split on `+`, and arrays render as separate keycaps. **Common shortcuts** **Multiple keys** ```tsx <Kbd>Ctrl+Shift+P</Kbd> <Kbd>{['Ctrl', 'Shift', 'P']}</Kbd> ``` Strings are split on the literal `+` character, so the `+` key cannot be expressed in string form. Use the `Plus` keyword (which renders as `+` after glyph mapping) or pass an array of keys instead: ```tsx <Kbd>Ctrl+Plus</Kbd> <Kbd>{['Ctrl', '+']}</Kbd> ``` ## Custom Separator Use `separator` to change the text between keycaps, or pass `null` to render adjacent keycaps. **Custom separator** ```tsx <Kbd separator="→" glyphs={false}> Ctrl+S </Kbd> ``` **No separator** ```tsx <Kbd platform="mac" separator={null}> Cmd+S </Kbd> ``` ## In Tooltip **In tooltip** ```tsx <Tooltip title={ <span> Save{' '} <Kbd size="sm" variant="ghost" glyphs={false}> Ctrl+S </Kbd> </span> } > <Button>Save</Button> </Tooltip> ``` ## In Menu Item **In menu item** ```tsx <div className="menu-item"> <span>Save</span> <Kbd size="sm" variant="ghost" glyphs={false}> Ctrl+S </Kbd> </div> ``` ## Keyboard Reference **Editor shortcuts** ## Glyph Mapping | Logical | macOS glyph | Other (literal) | | --------- | ----------- | ----------------------------------- | | Cmd | ⌘ | Ctrl ← Cmd reinterpreted on non-mac | | Meta | ⌘ | Win / Super | | Ctrl | ⌃ | Ctrl | | Alt | ⌥ | Alt | | Option | ⌥ | Alt | | Shift | ⇧ | Shift | | Enter | ↩ | Enter | | Return | ↩ | Enter | | Tab | ⇥ | Tab | | Esc | ⎋ | Esc | | Escape | ⎋ | Esc | | Space | ␣ | Space | | Backspace | ⌫ | Backspace | | Delete | ⌦ | Delete | | Up | ↑ | ↑ | | Down | ↓ | ↓ | | Left | ← | ← | | Right | → | → | ## Accessibility - Each key renders as a native `<kbd>` element. - The wrapper is a plain `<span>` and does not create an interactive target. - The separator between keys is marked `aria-hidden` so screen readers announce the keys directly. - Kbd is decorative; the actual keyboard binding, command name, and activation behavior live elsewhere. - Do not add `tabIndex` or button behavior to Kbd. ## SSR and Platform Detection With the default `platform="auto"`, Kbd renders Windows-style keys on the server and switches to the detected platform after the client mounts. This avoids hydration mismatches but means macOS users will briefly see `Ctrl+S` before it resolves to `⌘S`. To suppress the flash, set `platform` explicitly when you can derive it server-side (for example from the request `User-Agent` or a stored user preference). ## API Reference | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` *(required)* | `ReactNode` | — | Shortcut content. Strings split on "+", arrays render as separate keycaps, and other nodes render as one keycap. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Keycap size. | | `variant` | `'solid' \| 'outline' \| 'ghost'` | `'outline'` | Visual variant. | | `glyphs` | `boolean` | `true` | Render OS-specific glyphs when possible. | | `platform` | `'auto' \| 'mac' \| 'windows' \| 'linux'` | `'auto'` | Platform detection override. | | `separator` | `ReactNode` | `'+'` | Separator rendered between multiple keycaps. Use null for adjacent keycaps. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the wrapper span. | ## Utility Helpers | Prop | Type | Default | Description | | --- | --- | --- | --- | | `getPlatform` | `() => Platform` | — | Detects the current platform from navigator and falls back to 'windows' during SSR. | | `getKeyGlyph` | `(key: string, platform: Platform) => string` | — | Maps a logical key name to its display representation. | | `parseShortcut` | `(shortcut: string) => string[]` | — | Splits shortcut strings on "+", trimming whitespace. | ## Migration Note `MenuBar.Item` still accepts a `shortcut` string in v0.8. That prop will eventually be replaced with a `<Kbd>` child component so menu shortcuts share the same rendering path as tooltips, command palettes, and keyboard reference dialogs. --- # Link > Styled anchor primitive with variants, external-link affordances, and polymorphic router integration. _Source: /components/primitives/link_ A styled `<a>` primitive that standardises theme color, hover and focus states, underline behavior, and external-link affordances. Polymorphic via `as` so it can wrap any router's link component. `Link` is **styling-only**, not router-aware. Use the `as` prop to pass `react-router`'s `Link`, TanStack Router's `Link`, or Next.js's `Link` for client-side navigation. Out of the box, it renders a plain anchor. **Live Preview** ## Import ```tsx import { Link } from 'entangle-ui'; ``` ## Usage ```tsx <Link href="/docs">Documentation</Link> <Link href="https://example.com">Auto-detected external</Link> <Link as={RouterLink} to="/profile">Profile</Link> ``` ## Variants **Variants** | Variant | Color | Use case | | --------- | ------------------------------- | --------------------------------------- | | `default` | Accent primary | Standalone calls-to-action | | `subtle` | Secondary text, accent on hover | Status bars, footers, secondary actions | | `inline` | Inherits — always underlined | Links inside prose | ```tsx <Link href="/foo" variant="default">Default</Link> <Link href="/foo" variant="subtle">Subtle</Link> <Link href="/foo" variant="inline">Inline</Link> ``` ## Underline **Underline** ```tsx <Link href="/foo" underline="always">Always</Link> <Link href="/foo" underline="hover">Hover (default)</Link> <Link href="/foo" underline="never">Never</Link> ``` `variant="inline"` forces `underline="always"` since color is no longer doing the work of distinguishing the link. ## Sizes **Sizes** | Size | Font size | Use case | | ---- | ------------------------ | -------------------- | | `sm` | `typography.fontSize.xs` | Toolbars, status bar | | `md` | `typography.fontSize.sm` | Default | | `lg` | `typography.fontSize.md` | Prominent CTA links | ## External links External links are auto-detected when `href` starts with `http://` or `https://`. Detection adds an external-link icon, sets `target="_blank"` and `rel="noopener noreferrer"`, and announces "(opens in new tab)" to assistive tech. Pass `external={false}` to force same-tab navigation on an absolute URL, or `external` to force external behavior on a relative one. **External** ```tsx <Link href="https://example.com">Auto-detected</Link> <Link href="/internal" external>Forced external</Link> <Link href="https://example.com" external={false}>Same-tab override</Link> ``` ## Disabled `disabled` renders as a `<span>` regardless of `as`, strips navigation handlers (`href`, `to`, `onClick`, key/pointer events), sets `aria-disabled`, and turns off pointer events. This means a disabled router link cannot navigate via mouse, keyboard, or programmatic activation — `aria-disabled` plus CSS alone wouldn't be enough to block router-driven triggers, so the wrapper enforces it at render time. When `disabled` is true, the external-link icon and "(opens in new tab)" announcement are also suppressed, since there is no real external navigation to advertise. **Disabled** ```tsx <Link href="/foo" disabled> Disabled </Link> ``` ## Router integration (`as`) Pass any router library's link component via `as` to get client-side navigation while keeping all of `Link`'s styling. **Polymorphic `as`** ```tsx // react-router v6+ import { Link as RouterLink } from 'react-router-dom'; <Link as={RouterLink} to="/profile"> Profile </Link>; ``` ```tsx // TanStack Router import { Link as RouterLink } from '@tanstack/react-router'; <Link as={RouterLink} to="/profile"> Profile </Link>; ``` ```tsx // Next.js (App Router) import NextLink from 'next/link'; <Link as={NextLink} href="/profile"> Profile </Link>; ``` `as` is the only place in the library where polymorphic rendering is supported. The pattern is too useful for `Link` to skip — but it's deliberately not adopted across other components. ## In prose Use `variant="inline"` for links inside running text. They inherit color from the surrounding paragraph and stay underlined so they remain identifiable. **Inline in prose** ```tsx <Text> Read the{' '} <Link href="/guide" variant="inline"> getting-started guide </Link>{' '} first. </Text> ``` ## Editor example A status bar showing two `subtle` `sm` links — typical use inside an editor footer. **Status bar** ## When to use - **`Link`** — any visible navigation in your UI, internal or external - **`<Button variant="ghost">`** — actions that don't navigate (form submit, modal trigger). Don't reach for a styled link to fire a JavaScript handler that has no URL behind it ## Accessibility - External links automatically get a screen-reader-only "(opens in new tab)" announcement, either appended to your existing `aria-label` or rendered as a hidden span next to the icon - The external-link icon itself is `aria-hidden` so it isn't announced twice - `disabled` renders as a `<span>` with `aria-disabled="true"` so screen readers announce the state and browsers don't follow the link - Focus-visible applies the theme focus ring (`shadows.focus`) so keyboard users can see where they are ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` *(required)* | `ReactNode` | — | Link content. | | `href` | `string` | — | Destination URL. Omitted when `disabled` is true. | | `as` | `ElementType` | `'a' (always 'span' when disabled)` | Polymorphic root override — pass a router's link component to get client-side navigation. When the resolved element is generic (e.g. a router link), its props (`to`, `replace`, …) are type-checked. Ignored when `disabled` is true. | | `variant` | `'default' \| 'subtle' \| 'inline'` | `'default'` | Visual style. | | `color` | `'primary' \| 'secondary' \| 'inherit' \| string` | — | Color override. Defaults follow the variant. Any CSS color string is accepted. | | `underline` | `'always' \| 'hover' \| 'never'` | `'hover'` | Underline behavior. Forced to `always` for `variant="inline"`. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Font size. | | `external` | `boolean` | — | Render as external link. Auto-detected from `href` when it starts with `http://` or `https://`. Explicit prop overrides detection. | | `disabled` | `boolean` | `false` | Disable the link. Renders as a `<span>` with no `href`, `aria-disabled`, and `pointer-events: none`. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles applied to the rendered element. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the rendered anchor. | --- # Paper > Surface component providing elevation, nesting hierarchy, and visual depth for editor interface panels and cards. _Source: /components/primitives/paper_ Surface component for creating cards, panels, modals, and content containers with visual depth. Supports elevation shadows, automatic background adjustment for nested components, borders, and flexible spacing. **Live Preview** ## Import ```tsx import { Paper } from 'entangle-ui'; ``` ## Usage ```tsx <Paper elevation={1} padding="md"> <p>Content in a card</p> </Paper> ``` ## Elevation The `elevation` prop controls shadow intensity for visual depth. ```tsx <Paper elevation={0}>No shadow (flat)</Paper> <Paper elevation={1}>Small shadow (subtle depth)</Paper> <Paper elevation={2}>Medium shadow (moderate depth)</Paper> <Paper elevation={3}>Large shadow (high depth)</Paper> ``` | Elevation | Shadow | Use case | | --------- | ------ | -------------------------------- | | `0` | None | Flat containers, inline sections | | `1` | Small | Standard cards and panels | | `2` | Medium | Floating panels, dropdown menus | | `3` | Large | Modals, overlays | ## Nesting The `nestLevel` prop adjusts background brightness for visual hierarchy in nested layouts. Higher levels get progressively lighter backgrounds. ```tsx <Paper nestLevel={0} padding="xl"> <h2>Main Container (darkest)</h2> <Paper nestLevel={1} padding="lg"> <h3>Nested Section</h3> <Paper nestLevel={2} padding="md"> <p>Deeply nested content (lightest)</p> </Paper> </Paper> </Paper> ``` | Nest Level | Background | Use case | | ---------- | ------------------- | --------------------- | | `0` | Primary (darkest) | Root container | | `1` | Secondary | First-level panels | | `2` | Tertiary | Nested sub-panels | | `3` | Elevated (lightest) | Deeply nested content | ## Padding The `padding` prop uses theme spacing tokens for consistent internal spacing. ```tsx <Paper padding="xs">2px padding</Paper> <Paper padding="sm">4px padding</Paper> <Paper padding="md">8px padding</Paper> <Paper padding="lg">12px padding</Paper> <Paper padding="xl">16px padding</Paper> <Paper padding="xxl">24px padding</Paper> <Paper padding="xxxl">32px padding</Paper> ``` ## Bordered The `bordered` prop adds a subtle border around the paper. Useful for flat containers that need visual separation. ```tsx <Paper elevation={0} bordered padding="md"> Outlined container without shadow </Paper> <Paper elevation={1} bordered padding="lg"> Card with both border and shadow </Paper> ``` ## Expand The `expand` prop makes the paper fill the available width and height of its container. ```tsx <Paper expand elevation={1} padding="lg"> <header>Full-size panel</header> <main>Fills available space</main> </Paper> ``` ## Custom Radius Override the default border radius with a custom value. ```tsx <Paper customRadius="12px" elevation={2} padding="lg"> Custom rounded corners </Paper> <Paper customRadius={0} elevation={1} padding="md"> Sharp corners </Paper> <Paper customRadius="50%" elevation={2} padding="xl"> Circular </Paper> ``` ## Combining Props ```tsx <Paper elevation={2} nestLevel={1} bordered padding="xl" customRadius="8px"> <h3>Settings Panel</h3> <Paper nestLevel={2} padding="md" bordered> <p>Nested settings group</p> </Paper> </Paper> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Paper content -- any React elements. | | `elevation` | `0 \| 1 \| 2 \| 3` | `1` | Shadow intensity level for visual depth. | | `bordered` | `boolean` | `false` | Whether to show a subtle border around the paper. | | `padding` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| 'xxl' \| 'xxxl'` | `'md'` | Internal padding using theme spacing tokens. | | `nestLevel` | `0 \| 1 \| 2 \| 3` | `0` | Nesting level for automatic background brightness adjustment. | | `expand` | `boolean` | `false` | Whether the paper should fill available width and height. | | `customRadius` | `string \| number` | — | Custom border radius override. Numbers are treated as pixels. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying div element. | The component also accepts all standard HTML `<div>` attributes. ## Accessibility - Renders a semantic `<div>` element - Use appropriate headings and landmarks within Paper content for screen reader navigation - Elevation is purely visual and does not affect the accessibility tree - Ensure sufficient color contrast between nested Paper levels and their text content --- # Popover > Floating content container anchored to a trigger element with collision detection and focus management. _Source: /components/primitives/popover_ Floating content container for interactive content anchored to a trigger element. Built on `@floating-ui/react` for robust positioning with collision detection, flip/shift behavior, and scroll-aware auto-updating. Uses a compound component pattern with `Popover`, `PopoverTrigger`, `PopoverContent`, and `PopoverClose`. **Live Preview** ## Import ```tsx import { Popover, PopoverTrigger, PopoverContent, PopoverClose, } from 'entangle-ui'; ``` ## Usage ```tsx <Popover> <PopoverTrigger> <Button>Open</Button> </PopoverTrigger> <PopoverContent> <p>Popover content here</p> </PopoverContent> </Popover> ``` ## Placement The `placement` prop controls where the popover appears relative to the trigger. Supports 12 positions with automatic collision detection. ```tsx <Popover placement="bottom-start"> <PopoverTrigger> <Button>Bottom Start</Button> </PopoverTrigger> <PopoverContent> <p>Placed at bottom-start</p> </PopoverContent> </Popover> <Popover placement="right"> <PopoverTrigger> <Button>Right</Button> </PopoverTrigger> <PopoverContent> <p>Placed on the right</p> </PopoverContent> </Popover> ``` | Placement | Description | | -------------------------------------- | -------------------- | | `top`, `top-start`, `top-end` | Above the trigger | | `bottom`, `bottom-start`, `bottom-end` | Below the trigger | | `left`, `left-start`, `left-end` | Left of the trigger | | `right`, `right-start`, `right-end` | Right of the trigger | ## Controlled Use `open` and `onOpenChange` for controlled mode. ```tsx const [isOpen, setIsOpen] = useState(false); <Popover open={isOpen} onOpenChange={setIsOpen}> <PopoverTrigger> <Button>Toggle</Button> </PopoverTrigger> <PopoverContent> <p>Controlled popover</p> <Button onClick={() => setIsOpen(false)}>Close</Button> </PopoverContent> </Popover>; ``` ## With Close Button Use `PopoverClose` for a built-in close button inside the popover. ```tsx <Popover> <PopoverTrigger> <Button>Open</Button> </PopoverTrigger> <PopoverContent> <PopoverClose /> <h3>Settings</h3> <p>Configure your preferences.</p> </PopoverContent> </Popover> ``` ## Content Sizing `PopoverContent` accepts `width`, `maxHeight`, and `padding` for layout control. ```tsx <Popover> <PopoverTrigger> <Button>Open</Button> </PopoverTrigger> <PopoverContent width={300} maxHeight={400} padding="lg"> <p>Fixed width, scrollable content with large padding.</p> </PopoverContent> </Popover> ``` ## Match Trigger Width Set `matchTriggerWidth` to make the popover content width match the trigger element. ```tsx <Popover matchTriggerWidth> <PopoverTrigger> <Button fullWidth>Select Option</Button> </PopoverTrigger> <PopoverContent> <p>Same width as the button above</p> </PopoverContent> </Popover> ``` ## Offset Control the distance between the trigger and popover with the `offset` prop (in pixels). ```tsx <Popover offset={16}> <PopoverTrigger> <Button>More space</Button> </PopoverTrigger> <PopoverContent> <p>16px from the trigger</p> </PopoverContent> </Popover> ``` ## Dismiss Behavior Control how the popover closes. ```tsx { /* Disable close on outside click */ } <Popover closeOnClickOutside={false}> <PopoverTrigger> <Button>Persistent</Button> </PopoverTrigger> <PopoverContent> <p>Only closes via close button or Escape</p> <PopoverClose /> </PopoverContent> </Popover>; { /* Disable close on Escape */ } <Popover closeOnEscape={false}> <PopoverTrigger> <Button>No Escape</Button> </PopoverTrigger> <PopoverContent> <p>Escape key does not close this</p> </PopoverContent> </Popover>; ``` ## Props ### Popover | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | PopoverTrigger and PopoverContent elements. | | `open` | `boolean` | — | Whether the popover is open (controlled mode). | | `defaultOpen` | `boolean` | `false` | Default open state (uncontrolled mode). | | `placement` | `'top' \| 'top-start' \| 'top-end' \| 'bottom' \| 'bottom-start' \| 'bottom-end' \| 'left' \| 'left-start' \| 'left-end' \| 'right' \| 'right-start' \| 'right-end'` | `'bottom-start'` | Popover placement relative to the trigger. | | `offset` | `number` | `8` | Distance in pixels from the trigger element. | | `closeOnClickOutside` | `boolean` | `true` | Whether clicking outside closes the popover. | | `closeOnEscape` | `boolean` | `true` | Whether pressing Escape closes the popover. | | `returnFocus` | `boolean` | `true` | Whether to return focus to the trigger when popover closes. | | `portal` | `boolean` | `true` | Whether to render the popover in a React Portal. | | `matchTriggerWidth` | `boolean` | `false` | Whether the popover content width matches the trigger width. | | `onOpenChange` | `(open: boolean) => void` | — | Callback when the open state changes. | ### PopoverContent | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Popover content -- any React elements. | | `width` | `number \| string` | — | Width of the popover content. Number = pixels, string = CSS value. | | `maxHeight` | `number \| string` | — | Maximum height with scroll. | | `padding` | `'none' \| 'sm' \| 'md' \| 'lg'` | `'md'` | Padding inside the popover using theme spacing tokens. | ### PopoverClose | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Close button content. Defaults to a built-in X icon. | ## Accessibility - Built on `@floating-ui/react` with `useClick`, `useDismiss` interaction hooks - Focus is returned to the trigger when the popover closes (configurable via `returnFocus`) - Escape key dismisses the popover by default - Popover content is rendered in a Portal to avoid clipping and z-index issues - Uses proper ARIA attributes for trigger/content association - Keyboard accessible: Enter/Space to toggle, Escape to close, Tab to navigate within content --- # Radio > Radio and RadioGroup for mutually exclusive selection in forms and property panels. _Source: /components/primitives/radio_ `Radio` and `RadioGroup` provide mutually exclusive selection for property panels, settings forms, and editor toolbars. Radios share a `name` so the browser handles single-value form submission natively. **Live Preview** ## Import ```tsx import { Radio, RadioGroup } from 'entangle-ui'; ``` ## When to use | Component | Use when | | --------------------------- | --------------------------------------------------------------------------- | | `Radio` | 2–5 mutually exclusive options that should all be visible at once. | | `Checkbox` | A boolean toggle, or independent multi-selection. | | `Select` | 6+ options, or a dynamic / large choice space. | | `SegmentedControl` _(soon)_ | Toolbar-density mutually exclusive states without surrounding form context. | ## Standalone Radio A single `Radio` works without a surrounding `RadioGroup` and supports both controlled and uncontrolled modes. ```tsx <Radio value="aa" label="Anti-aliasing" defaultChecked /> ``` ## Sizes ```tsx <Radio size="sm" value="sm" label="Small" /> <Radio size="md" value="md" label="Medium" /> <Radio size="lg" value="lg" label="Large" /> ``` | Size | Outer | Inner dot | | ---- | ----- | --------- | | `sm` | 12px | 6px | | `md` | 14px | 6px | | `lg` | 16px | 8px | ## States ```tsx <Radio value="off" label="Off (unchecked)" /> <Radio value="on" defaultChecked label="On (checked)" /> <Radio value="dis-off" disabled label="Disabled — off" /> <Radio value="dis-on" disabled defaultChecked label="Disabled — on" /> <Radio value="error" error label="Error state" /> ``` ## RadioGroup `RadioGroup` manages exclusive selection across child `Radio` components via React Context. The group's `name`, `size`, `disabled`, and `error` override the corresponding props on individual radios. ### Vertical ```tsx <RadioGroup label="Coordinate space" defaultValue="local"> <Radio value="local" label="Local" /> <Radio value="world" label="World" /> <Radio value="parent" label="Parent" /> <Radio value="screen" label="Screen" /> </RadioGroup> ``` ### Horizontal ```tsx <RadioGroup label="Coordinate space" orientation="horizontal" defaultValue="local" > <Radio value="local" label="Local" /> <Radio value="world" label="World" /> <Radio value="parent" label="Parent" /> </RadioGroup> ``` ### Controlled Pass `value` and `onChange` for controlled mode. The handler receives the new value as a string. ```tsx const [space, setSpace] = useState('local'); <RadioGroup label="Coordinate space" value={space} onChange={setSpace}> <Radio value="local" label="Local" /> <Radio value="world" label="World" /> <Radio value="parent" label="Parent" /> </RadioGroup>; ``` ### Required & Error ```tsx <RadioGroup label="Coordinate space" required error errorMessage="Selection required to continue" > <Radio value="local" label="Local" /> <Radio value="world" label="World" /> <Radio value="parent" label="Parent" /> </RadioGroup> ``` ## In a property panel ```tsx <PropertyRow label="Coordinate space"> <RadioGroup orientation="horizontal" size="sm" value={space} onChange={setSpace} > <Radio value="local" label="Local" /> <Radio value="world" label="World" /> <Radio value="parent" label="Parent" /> </RadioGroup> </PropertyRow> ``` ## Accessibility - `Radio` renders a native `<input type="radio">` with `role="radio"` and `aria-checked`. - `RadioGroup` renders `role="radiogroup"` with `aria-labelledby`, `aria-required`, `aria-invalid`, `aria-disabled`, and `aria-orientation`. - Helper text and error messages are linked to the input via `aria-describedby`. - **Tab** moves focus to the group; **Space** selects the focused radio. - **Arrow keys** (Up / Down / Left / Right) move selection between radios that share a `name`. This is native browser behavior — `RadioGroup` propagates `name` automatically. - The label wraps the input so clicking the label toggles the radio. - Animations honor `prefers-reduced-motion`: the inner dot snaps instead of scaling. ## Radio props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `value` | `string` | — | Value of this radio option. Used by RadioGroup for selection and as the input value attribute. | | `checked` | `boolean` | — | Whether this radio is selected (controlled, standalone). Ignored when inside a RadioGroup. | | `defaultChecked` | `boolean` | `false` | Default checked state (uncontrolled, standalone). Ignored inside a RadioGroup. | | `label` | `string` | — | Label text displayed next to the radio. | | `labelPosition` | `'left' \| 'right'` | `'right'` | Position of the label relative to the radio. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Radio size. Inside a RadioGroup, the group size overrides this prop. | | `disabled` | `boolean` | `false` | Whether the radio is disabled. Inside a RadioGroup, the group value overrides this prop. | | `error` | `boolean` | `false` | Visual error state. Inside a RadioGroup, the group value overrides this prop. | | `helperText` | `string` | — | Helper text displayed below the radio (standalone use only). | | `errorMessage` | `string` | — | Error message displayed when error is true (standalone use only). | | `name` | `string` | — | Form name attribute. Inside a RadioGroup, the group name overrides this prop. | | `onChange` | `(value: string, event: ChangeEvent) => void` | — | Change handler. Standalone Radio fires when toggled on; rarely used inside a RadioGroup. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying input element. | ## RadioGroup props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `value` | `string` | — | Currently selected value (controlled). | | `defaultValue` | `string` | — | Default selected value (uncontrolled). | | `label` | `string` | — | Group label rendered above the radios. | | `helperText` | `string` | — | Helper text displayed below the group. | | `disabled` | `boolean` | `false` | Disables the entire group. | | `error` | `boolean` | `false` | Whether the group has an error state. | | `errorMessage` | `string` | — | Error message displayed when error is true. Replaces helperText. | | `required` | `boolean` | `false` | Whether selection is required. Sets aria-required on the group. | | `orientation` | `'horizontal' \| 'vertical'` | `'vertical'` | Layout direction for the radios. | | `spacing` | `number \| string` | `2` | Spacing between radios. Number maps to the theme spacing scale; strings pass through unchanged. | | `name` | `string` | — | Form name attribute applied to all child radios. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size applied to all child radios. | | `onChange` | `(value: string) => void` | — | Change handler — fires when any radio in the group is selected. | | `children` | `ReactNode` | — | Typically a list of Radio components. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying wrapper element. | --- # Switch > Toggle switch component for boolean on/off states in editor toolbars and settings panels. _Source: /components/primitives/switch_ Toggle switch component for boolean on/off states in editor toolbars, settings panels, and property inspectors. More space-efficient than a Checkbox for toggle options like "Show Grid", "Snap to Grid", or "Auto-Save". **Live Preview** ## Import ```tsx import { Switch } from 'entangle-ui'; ``` ## Usage ```tsx <Switch label="Show Grid" /> ``` ## Controlled Use `checked` and `onChange` for controlled mode. The `onChange` callback receives the new boolean value directly. ```tsx const [enabled, setEnabled] = useState(false); <Switch checked={enabled} onChange={setEnabled} label="Auto-save" />; ``` ## Sizes ```tsx <Switch size="sm" label="Small (28x14px track)" /> <Switch size="md" label="Medium (34x18px track)" /> <Switch size="lg" label="Large (42x22px track)" /> ``` | Size | Track | Thumb | Use case | | ---- | ------- | ----- | ------------------ | | `sm` | 28x14px | 10px | Compact toolbars | | `md` | 34x18px | 14px | Standard panels | | `lg` | 42x22px | 18px | Prominent settings | ## Label Position The label can be placed on either side of the switch. ```tsx <Switch label="Label on right" labelPosition="right" /> <Switch label="Label on left" labelPosition="left" /> ``` ## Helper Text and Error Display helper text below the switch, or an error message when validation fails. ```tsx <Switch label="Auto-save" helperText="Saves your work every 30 seconds" /> <Switch label="Enable feature" error errorMessage="This feature requires a premium plan" /> ``` ## Disabled ```tsx <Switch disabled label="Disabled off" /> <Switch disabled checked label="Disabled on" /> ``` ## Form Submission Use the `name` prop for form integration. A hidden input is rendered with the value `"on"` when checked. ```tsx <Switch name="autoSave" label="Auto-save" /> ``` ## Combining Props ```tsx <Switch checked={showGrid} onChange={setShowGrid} label="Show Grid" size="sm" labelPosition="left" helperText="Display grid overlay in viewport" /> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `checked` | `boolean` | — | Whether the switch is on (controlled mode). | | `defaultChecked` | `boolean` | `false` | Default on/off state (uncontrolled mode). | | `label` | `string` | — | Switch label text. | | `labelPosition` | `'left' \| 'right'` | `'right'` | Position of the label relative to the switch. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Switch size controlling track and thumb dimensions. | | `disabled` | `boolean` | `false` | Whether the switch is disabled. | | `helperText` | `string` | — | Helper text displayed below the switch. | | `error` | `boolean` | `false` | Whether the switch has an error state. | | `errorMessage` | `string` | — | Error message displayed when error is true. Replaces helperText. | | `name` | `string` | — | Name attribute for form submission. | | `onChange` | `(checked: boolean) => void` | — | Change event handler receiving the new on/off state. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying button element. | The component also accepts all standard HTML `<button>` attributes (except `onChange`). ## Accessibility - Uses `role="switch"` on a native `<button>` element - `aria-checked` reflects the current on/off state - `aria-disabled` is set when the switch is disabled - Helper text and error messages are linked via `aria-describedby` - Label is rendered as a `<label>` element wrapping the switch for click target expansion - Keyboard accessible: Space/Enter to toggle, Tab to navigate --- # Text > Versatile typography component with semantic variants, flexible sizing, and text styling for editor interfaces. _Source: /components/primitives/text_ Versatile typography component for consistent text rendering in editor interfaces. Provides semantic variants, flexible sizing, truncation, and comprehensive styling options using theme typography tokens. **Live Preview** ## Import ```tsx import { Text } from 'entangle-ui'; ``` ## Usage ```tsx <Text variant="body">Regular paragraph text</Text> ``` ## Variants The `variant` prop provides semantic presets with predefined size, weight, and styling. ```tsx <Text variant="display">Display — Large titles</Text> <Text variant="heading">Heading — Section headings</Text> <Text variant="subheading">Subheading — Subsection headings</Text> <Text variant="body">Body — Standard body text</Text> <Text variant="caption">Caption — Small descriptive text</Text> <Text variant="code">Code — Monospace inline code</Text> <Text variant="inherit">Inherit — Inherits from parent</Text> ``` | Variant | Size | Weight | Use case | | ------------ | ---- | ------------- | -------------------------- | | `display` | xl | semibold | Main page titles | | `heading` | lg | medium | Section headings | | `subheading` | md | medium | Subsection headings | | `body` | sm | normal | Standard body text | | `caption` | xs | normal | Small helper text, labels | | `code` | sm | normal (mono) | Inline code snippets | | `inherit` | -- | -- | Inherits parent typography | ## Sizes Override the variant's default size with the `size` prop. ```tsx <Text size="xs">Extra Small</Text> <Text size="sm">Small</Text> <Text size="md">Medium</Text> <Text size="lg">Large</Text> <Text size="xl">Extra Large</Text> ``` ## Colors The `color` prop maps to theme color tokens. ```tsx <Text color="primary">Primary text</Text> <Text color="secondary">Secondary text</Text> <Text color="muted">Muted text</Text> <Text color="disabled">Disabled text</Text> <Text color="accent">Accent text</Text> <Text color="success">Success text</Text> <Text color="warning">Warning text</Text> <Text color="error">Error text</Text> ``` ## Weight Override the variant's default weight with the `weight` prop. ```tsx <Text weight="normal">Normal weight</Text> <Text weight="medium">Medium weight</Text> <Text weight="semibold">Semibold weight</Text> ``` ## Polymorphic Rendering Use the `as` prop to render as any semantic HTML element. ```tsx <Text as="h1" variant="display">Page Title</Text> <Text as="h2" variant="heading">Section</Text> <Text as="p" variant="body">Paragraph content</Text> <Text as="label" variant="caption">Form label</Text> <Text as="span" variant="code">inline code</Text> <Text as="strong" weight="semibold">Important</Text> <Text as="small" variant="caption">Fine print</Text> ``` ## Truncation Use `truncate` to clip overflowing text with an ellipsis. Combine with `maxLines` for multi-line truncation. ```tsx { /* Single line truncation */ } <Text truncate> This very long text will be truncated with an ellipsis when it overflows... </Text>; { /* Multi-line truncation */ } <Text truncate maxLines={2}> This text will be truncated after two lines. Any content beyond the second line will be clipped with an ellipsis at the end. </Text>; ``` ## No Wrap Prevent text from wrapping to the next line. ```tsx <Text nowrap>This text will stay on a single line no matter what.</Text> ``` ## Monospace Force monospace font rendering. Automatically enabled for the `code` variant. ```tsx <Text mono>Monospace text</Text> <Text variant="code">Also monospace</Text> ``` ## Line Height Override line height with the `lineHeight` prop. ```tsx <Text lineHeight="tight">Tight line height</Text> <Text lineHeight="normal">Normal line height</Text> <Text lineHeight="relaxed">Relaxed line height</Text> ``` ## Alignment ```tsx <Text align="left">Left aligned</Text> <Text align="center">Center aligned</Text> <Text align="right">Right aligned</Text> <Text align="justify">Justified text alignment</Text> ``` ## Combining Props ```tsx <Text as="h1" variant="display" color="accent" weight="semibold" align="center" > Project Dashboard </Text> <Text as="p" variant="body" color="secondary" truncate maxLines={3} > A long description that will be truncated after three lines... </Text> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Text content. | | `as` | `'h1' \| 'h2' \| 'h3' \| 'h4' \| 'h5' \| 'h6' \| 'p' \| 'span' \| 'div' \| 'label' \| 'strong' \| 'em' \| 'small'` | `'span'` | HTML element to render as. | | `variant` | `'display' \| 'heading' \| 'subheading' \| 'body' \| 'caption' \| 'code' \| 'inherit'` | `'body'` | Semantic variant with predefined styling. | | `size` | `'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl'` | — | Text size using theme typography tokens. Overrides variant size. | | `weight` | `'normal' \| 'medium' \| 'semibold'` | — | Text weight using theme typography tokens. Overrides variant weight. | | `color` | `'primary' \| 'secondary' \| 'muted' \| 'disabled' \| 'accent' \| 'success' \| 'warning' \| 'error'` | `'primary'` | Text color using theme color tokens. | | `lineHeight` | `'tight' \| 'normal' \| 'relaxed'` | — | Line height. Overrides variant line height. | | `align` | `'left' \| 'center' \| 'right' \| 'justify'` | — | Text alignment. | | `truncate` | `boolean` | `false` | Whether to truncate text with ellipsis on overflow. | | `maxLines` | `number` | — | Maximum lines before truncating. Requires truncate=true. | | `nowrap` | `boolean` | `false` | Whether text should not wrap to the next line. | | `mono` | `boolean` | `false` | Whether to use monospace font family. Automatically true for code variant. | | `className` | `string` | — | Additional CSS class name. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying HTML element. | The component also accepts all standard HTML attributes for the rendered element. ## Accessibility - Use the `as` prop to render semantically correct HTML elements (e.g., `h1` for page titles, `p` for paragraphs) - Color contrast follows the theme token system for WCAG compliance - Truncated text retains its full content in the DOM for screen readers - The `color` prop maps to semantic theme tokens, not raw CSS values --- # TextArea > Multi-line text input with optional auto-resize, character count, monospace mode, and Input-parity styling. _Source: /components/primitives/text-area_ Multi-line text input with the same border, focus ring, and error treatment as `Input`. Supports optional auto-resize between `minRows` and `maxRows`, character counters with `maxLength`, and monospace mode for code-style content. **Live Preview** ## Import ```tsx import { TextArea } from 'entangle-ui'; ``` ## Usage ```tsx const [value, setValue] = useState(''); <TextArea label="Description" placeholder="Brief description..." value={value} onChange={setValue} />; ``` ## Controlled vs Uncontrolled `onChange` receives the string value directly, not a synthetic event — it works well with plain setter functions. **Controlled** — Typing updates parent state, which drives both the field and the character counter below it. ```tsx // Controlled <TextArea value={value} onChange={setValue} /> // Uncontrolled <TextArea defaultValue="initial text" onChange={value => console.log(value)} /> ``` ## Sizes Sizes match `Input` for visual parity in mixed forms. **Sizes** ```tsx <TextArea size="sm" /> <TextArea size="md" /> <TextArea size="lg" /> ``` | Size | Min Height | | ---- | ---------- | | `sm` | 48px | | `md` | 64px | | `lg` | 88px | ## Auto-resize Set `minRows`, `maxRows`, or both to enable auto-resize. The textarea grows with content and scrolls past `maxRows`. The native resize handle is automatically disabled in this mode. **Auto-resize** — Press Enter a few times — the textarea grows until it hits 6 rows, then scrolls. ```tsx <TextArea minRows={2} maxRows={6} value={value} onChange={setValue} /> ``` ## Character Count Pair `showCount` with `maxLength` to render a `123/500` counter aligned to the right. **Character count** ```tsx <TextArea label="Bio" maxLength={140} showCount /> ``` ## Monospace Switch to the theme monospace family for code-style content. **Monospace** ```tsx <TextArea monospace rows={6} defaultValue="const x = 1;" /> ``` ## Error State `errorMessage` overrides `helperText` when `error` is true; the textarea gets `aria-invalid`. **Error state** ```tsx <TextArea label="Description" error errorMessage="This field is required" /> ``` ## Disabled and Read-only **Disabled and read-only** ```tsx <TextArea disabled defaultValue="Cannot edit this field." /> <TextArea readOnly defaultValue="Select me, but don't type." /> ``` ## Resize Direction ```tsx <TextArea resize="none" /> <TextArea resize="vertical" /> {/* default */} <TextArea resize="horizontal" /> <TextArea resize="both" /> ``` When auto-resize is active (`minRows` or `maxRows` set), `resize` is forced to `'none'`. ## With Label and Helper Text `TextArea` integrates with `FormLabel` and `FormHelperText` automatically when you pass `label`, `helperText`, or `errorMessage`. ```tsx <TextArea label="Description" helperText="Markdown is supported" required /> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `value` | `string` | — | Controlled value. | | `defaultValue` | `string` | — | Uncontrolled initial value. | | `placeholder` | `string` | — | Placeholder text. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size scale. | | `disabled` | `boolean` | `false` | Whether the textarea is disabled. | | `error` | `boolean` | `false` | Whether to render in error state. | | `required` | `boolean` | `false` | Whether the textarea is required. | | `readOnly` | `boolean` | `false` | Whether the textarea is read-only. | | `label` | `string` | — | Label rendered above the textarea (uses FormLabel). | | `helperText` | `string` | — | Helper text below the textarea. | | `errorMessage` | `string` | — | Error message when error is true; overrides helperText. | | `resize` | `'none' \| 'vertical' \| 'horizontal' \| 'both'` | `'vertical'` | User resize direction. Forced to "none" when auto-resize is active. | | `rows` | `number` | `3` | Initial number of rows when not auto-sizing. | | `minRows` | `number` | — | Minimum rows when auto-sizing. Setting this enables auto-resize. | | `maxRows` | `number` | — | Maximum rows when auto-sizing. Overflow scrolls above this. | | `monospace` | `boolean` | `false` | Render in monospace font. | | `maxLength` | `number` | — | Maximum allowed character count (HTML attribute). | | `showCount` | `boolean` | `false` | Show a character counter below the textarea. | | `onChange` | `(value: string) => void` | — | Called with the string value on each change. | | `onFocus` | `(event: FocusEvent) => void` | — | Focus event handler. | | `onBlur` | `(event: FocusEvent) => void` | — | Blur event handler. | | `onKeyDown` | `(event: KeyboardEvent) => void` | — | Keydown event handler. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying textarea element. | ## Accessibility - Renders a native `<textarea>` element - `label` is rendered through `FormLabel` with a matching `htmlFor`/`id` pair (auto-generated when `id` is not provided) - `error` sets `aria-invalid` on the textarea - `errorMessage` is rendered with `FormHelperText error` so screen readers announce it - `required` shows a `*` next to the label and sets the native `required` attribute --- # Tooltip > Contextual information tooltip with flexible positioning, collision handling, and animation support. _Source: /components/primitives/tooltip_ Tooltip component that displays contextual information on hover. Built on `@base-ui/react` for robust accessibility and positioning. Provides an intuitive placement API with collision handling and animation support. **Live Preview** ## Import ```tsx import { Tooltip } from 'entangle-ui'; ``` ## Usage ```tsx <Tooltip title="Save your work"> <Button>Save</Button> </Tooltip> ``` ## Placement The `placement` prop controls where the tooltip appears relative to the trigger. Supports 12 positions. ```tsx <Tooltip placement="top" title="Top"> <Button>Top</Button> </Tooltip> <Tooltip placement="bottom-start" title="Bottom start"> <Button>Bottom Start</Button> </Tooltip> <Tooltip placement="right" title="Right"> <Button>Right</Button> </Tooltip> ``` | Placement | Description | | -------------------------------------- | -------------------- | | `top`, `top-start`, `top-end` | Above the trigger | | `bottom`, `bottom-start`, `bottom-end` | Below the trigger | | `left`, `left-start`, `left-end` | Left of the trigger | | `right`, `right-start`, `right-end` | Right of the trigger | ## Collision Handling The `collision` prop controls how the tooltip reacts when it would overflow the viewport. ```tsx <Tooltip collision="smart" title="Intelligent positioning"> <Button>Smart (default)</Button> </Tooltip> <Tooltip collision="flip" title="Flips to opposite side"> <Button>Flip</Button> </Tooltip> <Tooltip collision="shift" title="Shifts within bounds"> <Button>Shift</Button> </Tooltip> ``` | Strategy | Behavior | | ------------ | ---------------------------------------- | | `smart` | Intelligent fallback with axis switching | | `flip` | Flip to the opposite side | | `shift` | Shift within viewport bounds | | `flip-shift` | Flip first, then shift | | `hide` | Hide when no space available | | `none` | No collision handling | For advanced control, use `collisionConfig` to configure side and alignment collision behavior independently. ```tsx <Tooltip collisionConfig={{ side: 'flip', align: 'shift', fallbackAxisSide: 'start', }} title="Advanced collision" > <Button>Advanced</Button> </Tooltip> ``` ## Delay Control the show and hide delays in milliseconds. ```tsx <Tooltip delay={300} title="Shows faster"> <Button>Fast delay</Button> </Tooltip> <Tooltip delay={1000} closeDelay={200} title="Slow show, delayed hide"> <Button>Custom delay</Button> </Tooltip> ``` ## Arrow The arrow pointing to the trigger is shown by default. Set `arrow={false}` to hide it. ```tsx <Tooltip arrow={false} title="No arrow"> <Button>No arrow</Button> </Tooltip> ``` ## Rich Content The `title` prop accepts React nodes for complex tooltip content. ```tsx <Tooltip title={ <div> <strong>Save</strong> <br /> Ctrl+S </div> } > <Button>Save</Button> </Tooltip> ``` ## Disabled When `disabled` is `true`, the tooltip does not render. The trigger element is returned as-is. ```tsx <Tooltip disabled title="This tooltip is disabled"> <Button>No tooltip</Button> </Tooltip> ``` ## Custom Animation Configure tooltip animation with the `animation` prop. ```tsx <Tooltip animation={{ animated: true, duration: 300, easing: 'ease-in-out', }} title="Custom animation" > <Button>Animated</Button> </Tooltip> <Tooltip animation={{ animated: false }} title="No animation"> <Button>Instant</Button> </Tooltip> ``` ## Advanced Positioning Use the `positioner` prop for fine-grained positioning control. ```tsx <Tooltip positioner={{ offset: 12, padding: 10, sticky: true, }} title="Advanced positioning" > <Button>Custom offset</Button> </Tooltip> ``` ## Raw Props Pass-Through For power users, `rootProps` and `positionerProps` provide direct access to the underlying Base UI components. ```tsx <Tooltip rootProps={{ trackCursorAxis: 'x' }} positionerProps={{ arrowPadding: 20 }} title="Cursor tracking" > <Button>Track cursor</Button> </Tooltip> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactElement` | — | The trigger element that shows the tooltip on hover. | | `title` | `ReactNode` | — | Content displayed in the tooltip. Text or React elements. | | `placement` | `'top' \| 'top-start' \| 'top-end' \| 'bottom' \| 'bottom-start' \| 'bottom-end' \| 'left' \| 'left-start' \| 'left-end' \| 'right' \| 'right-start' \| 'right-end'` | `'top'` | Tooltip placement relative to the trigger. | | `collision` | `'flip' \| 'shift' \| 'hide' \| 'flip-shift' \| 'smart' \| 'none'` | `'smart'` | Collision handling strategy when tooltip overflows viewport. | | `collisionConfig` | `{ side?: string; align?: string; fallbackAxisSide?: string }` | — | Advanced collision configuration. Overrides the collision strategy. | | `positioner` | `{ offset?: number; padding?: number; sticky?: boolean; boundary?: string \| HTMLElement; trackCursor?: string }` | — | Advanced positioning configuration. | | `animation` | `{ animated?: boolean; duration?: number; easing?: string }` | — | Animation configuration for show/hide transitions. | | `delay` | `number` | `600` | Delay in milliseconds before showing the tooltip. | | `closeDelay` | `number` | `0` | Delay in milliseconds before hiding the tooltip. | | `arrow` | `boolean` | `true` | Whether to show the arrow pointing to the trigger. | | `disabled` | `boolean` | `false` | Whether the tooltip is disabled. | | `rootProps` | `Partial` | — | Direct props for the Base UI Root component. | | `positionerProps` | `Partial` | — | Direct props for the Base UI Positioner component. | | `className` | `string` | — | Additional CSS class names for the tooltip popup. | | `style` | `CSSProperties` | — | Inline styles for the tooltip popup. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the tooltip popup element. | ## Accessibility - Built on `@base-ui/react` which implements WAI-ARIA tooltip patterns - Tooltip content is associated with the trigger via `aria-describedby` - Tooltip appears on hover and focus, and dismisses on Escape - The trigger is wrapped in a focusable container for keyboard access - Use meaningful text in `title` for screen reader users --- # Viewport > Pan/zoom canvas + HTML container for editor-style surfaces — node graphs, timelines, 2D world editors. Multi-layer rendering, world/screen overlays, marquee selection. _Source: /components/primitives/viewport_ `Viewport` is the foundation primitive for editor-style 2D surfaces. It combines pan/zoom transform, perf-isolated canvas layers, world-anchored HTML, screen-space overlays, and marquee selection into a single composable primitive. Built to be the base for `NodeGraph`, `Timeline`, and similar surfaces. **Live Preview** ## Import ```tsx import { Viewport, ViewportLayer, ViewportWorld, ViewportOverlay, useViewportContext, worldToScreen, screenToWorld, } from 'entangle-ui'; ``` ## Quick start ```tsx <Viewport responsive> <ViewportLayer name="grid" draw={(ctx, { size, transform, theme }) => { // canvas drawing — runs only when transform/size/invalidateOn change }} /> <ViewportWorld> <div style={{ position: 'absolute', left: 100, top: 200 }}> Node at world (100, 200) </div> </ViewportWorld> <ViewportOverlay> <Toolbar /> </ViewportOverlay> </Viewport> ``` ## Transform model The viewport transform is a screen-space translation + uniform scale: ```ts // screenPos = worldPos * zoom + (x, y) type ViewportTransform = { x: number; y: number; zoom: number }; ``` Use `worldToScreen` / `screenToWorld` (re-exported pure functions) to convert between spaces in your own code. ### Controlled vs uncontrolled ```tsx // Controlled const [transform, setTransform] = useState({ x: 0, y: 0, zoom: 1 }); <Viewport transform={transform} onTransformChange={setTransform} /> // Uncontrolled <Viewport defaultTransform={{ x: 0, y: 0, zoom: 1 }} onTransformChange={log} /> ``` Zoom is automatically clamped to `[minZoom, maxZoom]` (defaults `0.1`–`8`). ## Gestures - **Pan** — middle-mouse drag by default. Hold **Space** + left-drag for laptop-friendly pan. - **Zoom** — wheel scroll (zooms toward cursor). Trackpad pinch is supported via the browser's `ctrl+wheel` convention. - **Marquee** — opt-in via `selectionRect={{ enabled: true }}`. Shift-drag for additive selection. ```tsx <Viewport pan={{ button: 'middle', spaceKey: true }} zoom={{ wheel: true, pinch: true, speed: 0.0015 }} selectionRect={{ enabled: true, button: 'left', additiveModifier: 'shift' }} onSelectionChange={info => { // info.rect is in world coordinates, normalized // info.additive is true when the modifier was held // info.inProgress is false on the final pointerup }} /> ``` Set `pan={false}` or `zoom={false}` to disable a gesture family entirely. ## Layers (perf isolation) Each `<ViewportLayer>` is an independent `<canvas>` with its own draw cycle. A layer redraws when: - The viewport transform or size changes, - Any value in its `invalidateOn` array changes, or - `handle.invalidate(layerName)` is called. Stack layers in JSX order — earlier = behind: ```tsx <Viewport> <ViewportLayer name="grid" draw={drawGrid} invalidateOn={[gridStep]} // only redraws when gridStep changes (+ transform/size) /> <ViewportLayer name="content" draw={drawNodes} invalidateOn={[nodes]} // unaffected by gridStep changes /> </Viewport> ``` The `draw` callback receives: ```ts draw(ctx, { size, // { width, height } in CSS px transform, // current transform theme, // resolved theme colors (CanvasThemeColors) worldToScreen, // helper bound to current transform screenToWorld, // inverse helper }); ``` DPR scaling is applied automatically — draw using CSS-pixel coordinates. ## World vs Overlay - `<ViewportWorld>` — HTML children positioned in **world coordinates**. The container is transformed by `translate(x, y) scale(zoom)`, so children with `position: absolute` and world-space `left`/`top` follow pan/zoom for free. - `<ViewportOverlay>` — HTML children in **screen coordinates**, rendered above all canvas layers and world children. Use for toolbars, minimap, status text. Both wrappers default to `pointer-events: none`; child elements opt in with `pointerEvents: 'auto'` so the Viewport's pan/zoom/marquee gestures keep working on the background. ## Imperative API `<Viewport>` accepts a ref to `ViewportHandle`: ```tsx const ref = useRef<ViewportHandle>(null); ref.current?.fitToContent({ x: 0, y: 0, width: 400, height: 300 }, 32); ref.current?.zoomToRect({ x: 100, y: 100, width: 50, height: 50 }); ref.current?.centerOn({ x: 200, y: 150 }, 2); ref.current?.invalidate('grid'); // force redraw of one layer ref.current?.getTransform(); ref.current?.getSize(); ``` ## Reading state from children — `useViewportContext` Any child rendered inside `<Viewport>` can read live state: ```tsx function CursorReadout() { const { transform, size, isPanning } = useViewportContext(); return ( <span> zoom: {transform.zoom.toFixed(2)}x · {size.width}×{size.height} {isPanning ? ' · panning' : ''} </span> ); } ``` Use this in custom Overlay components (minimap, HUD, etc.) instead of prop-drilling transform. --- ## Recipes — building features we intentionally left out of v1 `Viewport` ships **mechanisms, not styles**. The following features were deliberately excluded from the v1 surface because they're either opinionated (inertia, snap) or scene-specific (minimap). Each can be implemented in user code with the primitives `Viewport` does provide. ### Snap-to-zoom Snap the zoom level to discrete stops as the user scrolls. All you need is controlled `transform`: ```tsx const ZOOM_STOPS = [0.25, 0.5, 1, 2, 4]; function snapZoom(z: number) { return ZOOM_STOPS.reduce((a, b) => Math.abs(b - z) < Math.abs(a - z) ? b : a ); } const [t, setT] = useState({ x: 0, y: 0, zoom: 1 }); <Viewport transform={t} onTransformChange={next => setT({ ...next, zoom: snapZoom(next.zoom) })} />; ``` The same shape works for snapping `x`/`y` translation to a grid step. ### Minimap A minimap is just an `<ViewportOverlay>` child that subscribes to viewport state via `useViewportContext` and renders its own small `<canvas>`. `handle.centerOn` makes the minimap clickable: ```tsx function Minimap({ worldBounds }: { worldBounds: WorldRect }) { const { transform, size, handle } = useViewportContext(); const ref = useRef<HTMLCanvasElement>(null); useEffect(() => { const canvas = ref.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; // Scale world bounds into the minimap's box (e.g. 120x80) const scale = Math.min(120 / worldBounds.width, 80 / worldBounds.height); ctx.clearRect(0, 0, 120, 80); // Draw your scene at this scale (e.g. nodes as rectangles) // drawSceneScaled(ctx, scale) // Draw current viewport rectangle const viewportWorld = { x: -transform.x / transform.zoom, y: -transform.y / transform.zoom, width: size.width / transform.zoom, height: size.height / transform.zoom, }; ctx.strokeStyle = 'cyan'; ctx.strokeRect( (viewportWorld.x - worldBounds.x) * scale, (viewportWorld.y - worldBounds.y) * scale, viewportWorld.width * scale, viewportWorld.height * scale ); }, [transform, size, worldBounds]); return ( <canvas ref={ref} width={120} height={80} style={{ pointerEvents: 'auto', cursor: 'crosshair' }} onPointerDown={e => { const rect = e.currentTarget.getBoundingClientRect(); const localX = e.clientX - rect.left; const localY = e.clientY - rect.top; const scale = Math.min( 120 / worldBounds.width, 80 / worldBounds.height ); handle.centerOn({ x: worldBounds.x + localX / scale, y: worldBounds.y + localY / scale, }); }} /> ); } <Viewport> {/* layers + world... */} <ViewportOverlay> <div style={{ position: 'absolute', bottom: 8, right: 8, pointerEvents: 'auto', }} > <Minimap worldBounds={{ x: 0, y: 0, width: 1000, height: 800 }} /> </div> </ViewportOverlay> </Viewport>; ``` ### Inertia (momentum scrolling) `onPanEnd` reports the gesture's end velocity in screen px/ms. Animate a decaying translation on top of the controlled transform: ```tsx function useInertia(handleRef: React.RefObject<ViewportHandle>) { const rafRef = useRef(0); const onPanStart = () => cancelAnimationFrame(rafRef.current); const onPanEnd = ({ velocity }: ViewportPanEndInfo) => { cancelAnimationFrame(rafRef.current); let { x: vx, y: vy } = velocity; let last = performance.now(); const step = (): void => { const now = performance.now(); const dt = now - last; last = now; vx *= 0.92; vy *= 0.92; if (Math.hypot(vx, vy) < 0.01) return; const t = handleRef.current?.getTransform(); if (!t) return; handleRef.current!.centerOn( { x: -(t.x + vx * dt) / t.zoom + handleRef.current!.getSize().width / 2 / t.zoom, y: -(t.y + vy * dt) / t.zoom + handleRef.current!.getSize().height / 2 / t.zoom, }, t.zoom ); rafRef.current = requestAnimationFrame(step); }; rafRef.current = requestAnimationFrame(step); }; return { onPanStart, onPanEnd }; } const ref = useRef<ViewportHandle>(null); const inertia = useInertia(ref); <Viewport ref={ref} {...inertia} />; ``` Replace the `0.92` constant with whatever deceleration curve fits your editor feel. --- ## Why these aren't built in | Feature | Why it lives in user code | | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Snap** | Snap rules vary per editor (frame-rate stops in Timeline vs zoom presets in NodeGraph). One built-in rule would be opinionated; the recipe above is a few lines. | | **Minimap** | A minimap needs to know **what to draw at small scale** — that's scene-specific. The `Overlay` slot + `useViewportContext` give you everything else. | | **Inertia** | Deceleration feel is a per-product decision (Figma vs Blender vs custom). `onPanEnd` exposes the velocity so any curve works. | If you find yourself copy-pasting the same recipe across three components, that's the signal to ask for it as a first-class prop. ## API reference ### `<Viewport>` props ```ts interface ViewportProps { transform?: ViewportTransform; defaultTransform?: ViewportTransform; onTransformChange?: (t: ViewportTransform) => void; minZoom?: number; // default 0.1 maxZoom?: number; // default 8 pan?: ViewportPanConfig | false; zoom?: ViewportZoomConfig | false; selectionRect?: ViewportSelectionConfig; onSelectionChange?: (info: ViewportSelectionEvent) => void; onPanStart?: () => void; onPanEnd?: (info: ViewportPanEndInfo) => void; onZoomStart?: () => void; onZoomEnd?: () => void; responsive?: boolean; // default false height?: number; // default 300, ignored when responsive disabled?: boolean; role?: string; ariaLabel?: string; ariaRoledescription?: string; ref?: React.Ref<ViewportHandle>; } ``` ### `<ViewportLayer>` props ```ts interface ViewportLayerProps { name: string; draw: (ctx: CanvasRenderingContext2D, info: ViewportLayerDrawInfo) => void; invalidateOn?: ReadonlyArray<unknown>; paused?: boolean; className?: string; } ``` ### `ViewportHandle` ```ts interface ViewportHandle { fitToContent(bounds: WorldRect, padding?: number): void; zoomToRect(rect: WorldRect, padding?: number): void; centerOn(point: Point2D, zoom?: number): void; getTransform(): ViewportTransform; getSize(): ViewportSize; invalidate(layerName?: string): void; } ``` --- # VisuallyHidden > Hide content visually while keeping it announced by screen readers. _Source: /components/primitives/visually-hidden_ Hides its children visually while keeping them in the accessibility tree. This is the canonical `sr-only` / `visually-hidden` pattern used by Tailwind, Bootstrap, and Radix — same CSS, just packaged as a primitive so the rule lives in one place. Reach for it to label icon-only buttons, surface extra context next to inputs, or build a skip-to-content link. Don't use it to hide content from sighted users for layout reasons — that's `display: none`. This component keeps content audible on purpose. **Live Preview** ## Import ```tsx import { VisuallyHidden } from 'entangle-ui'; ``` ## Usage ```tsx <VisuallyHidden>Search the catalog</VisuallyHidden> ``` ## Basic Hidden Content Children render into the DOM but are clipped to a single transparent pixel. Inspect the element to confirm the text is still there. **Basic** ```tsx <Text> This sentence has{' '} <VisuallyHidden>extra context only screen readers hear</VisuallyHidden>{' '} invisible content embedded in it. </Text> ``` ## With a Visible Sibling Pair a visible label with a hidden description for a fuller screen-reader experience without cluttering the layout. **With visible sibling** ```tsx <label> <span>Email</span> <VisuallyHidden> We will only use your email to send password reset links. </VisuallyHidden> <input type="email" /> </label> ``` ## Skip-to-Content Link Set `focusable` to make the wrapper appear when it (or a descendant) gains focus. This is the standard skip-link pattern — wrap an anchor inside and let `:focus-within` reveal it. **Focusable (skip-to-content)** ```tsx <VisuallyHidden focusable> <a href="#main-content">Skip to main content</a> </VisuallyHidden> ``` ## Hiding a Label for an Icon-Only Button Pair a visible icon with a hidden label so assistive tech can announce the action. **Icon-only button** ```tsx <button> <SearchIcon /> <VisuallyHidden>Search the catalog</VisuallyHidden> </button> ``` For most cases, prefer `aria-label="Search"` on the button itself — that's terser. Use `VisuallyHidden` when you need rich children (e.g. interpolated values, formatted markup) that `aria-label` cannot express. ## Common Pitfalls - **Don't use it to hide things from everyone.** `VisuallyHidden` keeps content audible. Use `display: none` or `hidden` if the content should not exist for any user. - **Don't put interactive content inside it without `focusable`.** A button hidden with the static styles still receives focus but stays clipped — sighted keyboard users cannot tell where they are. - **Don't add `aria-hidden="true"`.** That removes the content from the accessibility tree, defeating the purpose. ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` *(required)* | `ReactNode` | — | Content rendered inside the hidden region. | | `as` | `'span' \| 'div' \| 'label' \| 'p'` | `'span'` | Element to render. Use `div` for block content or `label`/`p` when the semantics fit. | | `focusable` | `boolean` | `false` | When true, becomes visible when the element (or a descendant) is focused. Used for skip links. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles applied to the rendered element. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the rendered element. | ## Accessibility - Children remain in the DOM and the accessibility tree — screen readers, browser find-in-page, and translation tools all still see them - Implements the canonical SR-only style (`position: absolute`, `clip: rect(0,0,0,0)`, `width/height: 1px`) so the element occupies no visual space but is not removed from layout flow - `focusable` reveals the element under `:focus` and `:focus-within`, which is exactly what skip links need ========================= # Components — Layout ========================= --- # Accordion > Collapsible sections component using a compound pattern for property inspectors, settings panels, and grouped content. _Source: /components/layout/accordion_ Collapsible sections component for organizing content into expandable/collapsible groups. Uses a compound component pattern with `Accordion`, `AccordionItem`, `AccordionTrigger`, and `AccordionContent`. Designed for property inspectors, settings panels, and any UI that groups related content under toggleable headers. **Live Preview** ## Import ```tsx import { Accordion, AccordionItem, AccordionTrigger, AccordionContent, } from 'entangle-ui'; ``` ## Usage ```tsx <Accordion defaultValue="transform"> <AccordionItem value="transform"> <AccordionTrigger>Transform</AccordionTrigger> <AccordionContent>Position, rotation, and scale fields...</AccordionContent> </AccordionItem> <AccordionItem value="material"> <AccordionTrigger>Material</AccordionTrigger> <AccordionContent>Color, texture, and shader settings...</AccordionContent> </AccordionItem> </Accordion> ``` ## Width The accordion defaults to `width: 100%`, filling its parent so the surface keeps a stable width regardless of which item is expanded. Override with the `width` prop when you need a fixed footprint — pass a number for pixels or any CSS length as a string. ```tsx <Accordion width={320}>...</Accordion> <Accordion width="24rem">...</Accordion> ``` **Width** ## Single vs Multiple By default, only one item can be expanded at a time. Opening a new item closes the previously open one. Set `multiple` to allow multiple items to be open simultaneously. **Single vs Multiple** ```tsx { /* Single mode (default) -- one item open at a time */ } <Accordion defaultValue="section-1"> <AccordionItem value="section-1"> <AccordionTrigger>Section 1</AccordionTrigger> <AccordionContent>Content 1</AccordionContent> </AccordionItem> <AccordionItem value="section-2"> <AccordionTrigger>Section 2</AccordionTrigger> <AccordionContent>Content 2</AccordionContent> </AccordionItem> </Accordion>; { /* Multiple mode -- any number of items open */ } <Accordion multiple defaultValue={['section-1', 'section-2']}> <AccordionItem value="section-1"> <AccordionTrigger>Section 1</AccordionTrigger> <AccordionContent>Content 1</AccordionContent> </AccordionItem> <AccordionItem value="section-2"> <AccordionTrigger>Section 2</AccordionTrigger> <AccordionContent>Content 2</AccordionContent> </AccordionItem> </Accordion>; ``` ## Collapsible In single mode, by default one item must always remain open. Set `collapsible` to allow all items to be closed. **Collapsible** ```tsx <Accordion collapsible> <AccordionItem value="optional"> <AccordionTrigger>Optional Section</AccordionTrigger> <AccordionContent>Can be fully closed</AccordionContent> </AccordionItem> </Accordion> ``` ## Variants The `variant` prop controls the visual style of accordion headers. **Variants** ```tsx { /* Default: subtle background on header with clean separator lines */ } <Accordion variant="default"> <AccordionItem value="item"> <AccordionTrigger>Default Variant</AccordionTrigger> <AccordionContent>Content</AccordionContent> </AccordionItem> </Accordion>; { /* Ghost: no background, minimal separators */ } <Accordion variant="ghost"> <AccordionItem value="item"> <AccordionTrigger>Ghost Variant</AccordionTrigger> <AccordionContent>Content</AccordionContent> </AccordionItem> </Accordion>; { /* Filled: darker background on header area */ } <Accordion variant="filled"> <AccordionItem value="item"> <AccordionTrigger>Filled Variant</AccordionTrigger> <AccordionContent>Content</AccordionContent> </AccordionItem> </Accordion>; ``` | Variant | Description | | --------- | -------------------------------------------- | | `default` | Subtle background with clean separator lines | | `ghost` | No background, minimal separators | | `filled` | Darker background on the header area | ## Sizes The `size` prop controls the density of headers and content padding. **Sizes** ```tsx <Accordion size="sm">...</Accordion> <Accordion size="md">...</Accordion> <Accordion size="lg">...</Accordion> ``` | Size | Use case | | ---- | ------------------------- | | `sm` | Compact property panels | | `md` | Standard panels (default) | | `lg` | Spacious settings pages | ## Gap Add spacing between accordion items using the `gap` prop (in pixels). **Gap** ```tsx <Accordion gap={4}> <AccordionItem value="a"> <AccordionTrigger>Section A</AccordionTrigger> <AccordionContent>Content A</AccordionContent> </AccordionItem> <AccordionItem value="b"> <AccordionTrigger>Section B</AccordionTrigger> <AccordionContent>Content B</AccordionContent> </AccordionItem> </Accordion> ``` ## Trigger with Icon and Actions `AccordionTrigger` supports an `icon` slot for a leading icon and an `actions` slot for interactive elements on the right side. Clicking actions does not toggle the accordion. **Trigger with Icon and Actions** ```tsx <Accordion defaultValue="scene"> <AccordionItem value="scene"> <AccordionTrigger icon={<SceneIcon />} actions={ <IconButton icon={<SettingsIcon />} size="sm" onClick={() => openSettings()} /> } > Scene </AccordionTrigger> <AccordionContent>Scene properties...</AccordionContent> </AccordionItem> </Accordion> ``` ## Custom Indicator Replace the default chevron indicator or hide it entirely by passing `null`. **Custom Indicator** ```tsx { /* Custom indicator */ } <AccordionTrigger indicator={<PlusMinusIcon />}> Custom Indicator </AccordionTrigger>; { /* No indicator */ } <AccordionTrigger indicator={null}>No Chevron</AccordionTrigger>; ``` ## Keep Content Mounted By default, collapsed content is unmounted from the DOM. Set `keepMounted` to keep it rendered but hidden, preserving internal state. **Keep Content Mounted** ```tsx <AccordionContent keepMounted> <FormWithState /> </AccordionContent> ``` ## Disabled Items Disable individual accordion items to prevent expansion. **Disabled Items** ```tsx <AccordionItem value="locked" disabled> <AccordionTrigger>Locked Section</AccordionTrigger> <AccordionContent>This cannot be opened</AccordionContent> </AccordionItem> ``` ## Controlled Mode Pass `value` and `onChange` to control the expanded state externally. **Controlled Mode** ```tsx const [expanded, setExpanded] = useState<string>('section-1'); <Accordion value={expanded} onChange={val => setExpanded(val as string)}> <AccordionItem value="section-1"> <AccordionTrigger>Section 1</AccordionTrigger> <AccordionContent>Content 1</AccordionContent> </AccordionItem> <AccordionItem value="section-2"> <AccordionTrigger>Section 2</AccordionTrigger> <AccordionContent>Content 2</AccordionContent> </AccordionItem> </Accordion>; ``` For multiple mode, `value` and `onChange` work with string arrays. ```tsx const [expanded, setExpanded] = useState<string[]>(['a', 'b']); <Accordion multiple value={expanded} onChange={val => setExpanded(val as string[])} > ... </Accordion>; ``` ## Props ### Accordion | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | AccordionItem children. Required. | | `value` | `string \| string[]` | — | Controlled expanded item(s). String for single mode, string array for multiple mode. | | `defaultValue` | `string \| string[]` | — | Default expanded item(s) for uncontrolled mode. | | `multiple` | `boolean` | `false` | Whether multiple items can be expanded simultaneously. | | `collapsible` | `boolean` | `false` | Whether all items can be collapsed in single mode. When false, one item must remain open. | | `variant` | `'default' \| 'ghost' \| 'filled'` | `'default'` | Visual variant applied to all accordion headers. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size controlling header and content density. | | `gap` | `number` | `0` | Gap between accordion items in pixels. | | `width` | `number \| string` | `'100%'` | Width of the accordion. Number → px, string → CSS value. Defaults to filling the parent so layout stays stable regardless of which item is expanded. | | `onChange` | `(value: string \| string[]) => void` | — | Callback when expanded items change. Returns a string in single mode, string array in multiple mode. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the root div element. | ### AccordionItem | Prop | Type | Default | Description | | --- | --- | --- | --- | | `value` | `string` | — | Unique identifier for this item. Must match the value used in Accordion's value/defaultValue. Required. | | `disabled` | `boolean` | `false` | Whether this item is disabled (cannot be expanded or collapsed). | | `children` | `ReactNode` | — | AccordionTrigger and AccordionContent children. Required. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the item div element. | ### AccordionTrigger | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Header content, typically a text label. Required. | | `icon` | `ReactNode` | — | Icon element displayed before the label. | | `actions` | `ReactNode` | — | Action elements rendered on the right side of the header. Clicks on actions do not toggle the accordion. | | `indicator` | `ReactNode \| null` | — | Custom chevron indicator element. Pass null to hide the indicator entirely. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the trigger button element. | ### AccordionContent | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Collapsible content. Required. | | `keepMounted` | `boolean` | `false` | Whether to keep content in the DOM when collapsed. Useful for preserving form state. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the content body div element. | ## Accessibility - Trigger renders as a native `<button>` element with `aria-expanded` and `aria-controls` attributes - Content region has `role="region"` and `aria-labelledby` linking back to its trigger - Disabled items set `aria-disabled` and the native `disabled` attribute on the trigger button - Collapsed content uses the `hidden` attribute when not expanded - Keyboard navigation: triggers are focusable and activate on Enter or Space - Each trigger/content pair shares generated ARIA IDs for correct association --- # Card > Bounded surface for a single semantic unit, with outlined, filled, and elevated variants and a compound API for header / media / body / footer. _Source: /components/layout/card_ A bounded surface representing a single semantic unit — an asset, a summary, a preview, a list row. `Card` is distinct from `Paper` (a generic surface): it ships with a header / media / body / footer convention and can be made interactive by passing `onClick`, giving the whole card a `button` role with keyboard support. **Live Preview** ## When to use - **Card** — one self-contained item (an asset, a preset, a result). Has a fixed slot grammar (header / media / body / footer) and can become a clickable target. - **Paper** — a generic raised / bordered surface with no semantics. Use when you just need "a box". - **PanelSurface** — a structural panel inside an editor shell (header + scrollable body + footer). Anchored, not a discrete item. Reach for `Card` when you're rendering a collection of discrete units — an asset grid, a list of presets, a feed of results. Reach for `Paper` when you just need a styled container. ## Import ```tsx import { Card } from 'entangle-ui'; ``` The compound members are attached to `Card`: ```tsx <Card> <Card.Header>...</Card.Header> <Card.Media>...</Card.Media> <Card.Body>...</Card.Body> <Card.Footer>...</Card.Footer> </Card> ``` ## Usage ```tsx <Card variant="elevated"> <Card.Header title="Asphalt 02" subtitle="Updated 2 hours ago" /> <Card.Media src="/preview.png" alt="Preview" aspectRatio={16 / 9} /> <Card.Body>4K PBR material with roughness and normal maps.</Card.Body> <Card.Footer align="space-between"> <span>12.4 MB</span> <Button variant="filled">Open</Button> </Card.Footer> </Card> ``` All compound members are optional and can appear in any order — though the canonical order (header → media → body → footer) reads best in most layouts. ## Variants `variant` chooses the surface treatment. `outlined` is the default. **Variants** | Variant | Treatment | Use when | | ---------- | ------------------------------------ | ------------------------------------------------ | | `outlined` | Transparent background, 1px border | Default. Quiet card that sits on any surface. | | `filled` | Surface-tinted background, no border | Cards on the canvas background; gives mild lift. | | `elevated` | Surface background plus drop shadow | Interactive cards (asset grids, result lists). | ```tsx <Card variant="outlined">...</Card> <Card variant="filled">...</Card> <Card variant="elevated">...</Card> ``` ## Interactive Pass `onClick` to turn the whole card into a button: `role="button"`, `tabIndex=0`, Enter and Space activation, and a focus ring on `:focus-visible`. Combine with `selected` for grid-style selection. **Interactive** ```tsx const [picked, setPicked] = useState<string | null>(null); <Card variant="elevated" onClick={() => setPicked(item.id)} selected={picked === item.id} > <Card.Header title={item.title} subtitle={item.subtitle} /> </Card>; ``` `selected` cards receive the accent border and a tinted background; the component also sets `aria-pressed="true"` when interactive and selected. ## With media `Card.Media` renders edge-to-edge inside the card and respects its own aspect ratio. Pass `src` for the common `<img>` case, or drop arbitrary children (video, canvas, `<picture>`) for everything else. **With media** ```tsx <Card variant="elevated"> <Card.Media src="/preview.png" alt="Preview" aspectRatio={16 / 9} /> <Card.Header title="Lobby render" subtitle="Saved 5 min ago" /> <Card.Body>Description...</Card.Body> </Card> ``` The default aspect ratio is `16 / 9`. Pass any number to override (e.g. `aspectRatio={1}` for a square thumbnail, `aspectRatio={4 / 3}` for legacy footage). Numeric `aspectRatio` is interpreted as `width / height`. ## List-item layout For dense lists (file rows, search results, asset rows) use `outlined` cards with `Card.Header` only — `leading` carries the icon and `trailing` carries the affordance. **List rows** ```tsx <Card variant="outlined" onClick={open}> <Card.Header leading={<FolderIcon />} title="Concrete 04" subtitle="PBR material — 8.2 MB" trailing={ <Text size="xs" color="muted"> Open </Text> } /> </Card> ``` ## Disabled `disabled` dims the card, drops `pointer-events`, and sets `aria-disabled="true"`. It also short-circuits `onClick`, so a disabled card never becomes interactive. **Disabled** ```tsx <Card variant="elevated" disabled onClick={open}> <Card.Header title="Locked preset" subtitle="Pointer events off" /> <Card.Body>Body text is dimmed.</Card.Body> </Card> ``` ## Footer alignment `Card.Footer` is a flex row with a top border. `align` controls horizontal placement. **Footer alignment** ```tsx <Card.Footer align="left">...</Card.Footer> <Card.Footer align="center">...</Card.Footer> <Card.Footer align="right">...</Card.Footer> <Card.Footer align="space-between">...</Card.Footer> ``` `right` is the default — fits the "Cancel + Confirm" button pattern most cards end with. ## Custom header content When `children` is passed to `Card.Header`, the default title/subtitle layout is bypassed and your nodes are rendered in the text column instead. `leading` and `trailing` still render in their respective columns. **Custom header** ```tsx <Card.Header> <Flex justify="space-between" align="center"> <Text weight="semibold">Custom header layout</Text> <Text size="xs" color="muted"> Free-form children </Text> </Flex> </Card.Header> ``` ## Compound API `Card` is a compound component. The members below are attached to `Card` (`Card.Header`, `Card.Media`, `Card.Body`, `Card.Footer`) and are all optional — pick the ones you need. | Member | Role | | ------------- | --------------------------------------------------------------------------- | | `Card` | Root surface. Owns variant, selection, disabled state, and interactivity. | | `Card.Header` | Title / subtitle row with optional `leading` and `trailing` slots. | | `Card.Media` | Edge-to-edge image (via `src`) or arbitrary children at a fixed aspect. | | `Card.Body` | Descriptive body copy. Flex-grows so the footer stays pinned to the bottom. | | `Card.Footer` | Action row with a top border and configurable alignment. | All members accept their own ARIA / data attributes (passed through to the underlying `div`), so you can tag any slot for testing or screen-reader hints without wrapping. ## Accessibility - Static cards render as a `<div>` with no role; they participate in the document outline through their children. - When `onClick` is set and the card is not `disabled`, the root receives `role="button"`, `tabIndex=0`, and a Space / Enter keyboard handler. `aria-pressed` reflects `selected`. - When `disabled` is set, the card sets `aria-disabled="true"` and short-circuits `onClick`; the keyboard handler is not attached. - Focus state uses `:focus-visible` so keyboard users see the focus ring without it appearing on mouse clicks. - `Card.Header` renders its `title` as `<h3>` and its `subtitle` as `<p>` — pick custom children when the heading level needs to differ. - `Card.Media` requires `alt` whenever `src` is set; pass an empty string for purely decorative imagery. ## API Reference ### `<Card>` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `variant` | `'outlined' \| 'filled' \| 'elevated'` | `'outlined'` | Visual variant. | | `onClick` | `(event: MouseEvent) => void` | — | Click handler. When set, the whole card becomes interactive (button role, keyboard activation, focus ring). | | `selected` | `boolean` | `false` | Selected state — applies the accent border and tint. Combine with `onClick` for grid-style selection. | | `disabled` | `boolean` | `false` | Disabled state. Dims the card, removes pointer events, and short-circuits `onClick`. | | `children` | `ReactNode` | — | Card content — typically `Card.Header`, `Card.Media`, `Card.Body`, and/or `Card.Footer`. | ### `<Card.Header>` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `title` | `ReactNode` | — | Title rendered as `<h3>`. Ignored when `children` is provided. | | `subtitle` | `ReactNode` | — | Subtitle rendered as `<p>` below the title. Ignored when `children` is provided. | | `leading` | `ReactNode` | — | Leading visual (icon, Avatar) rendered to the left of the text column. | | `trailing` | `ReactNode` | — | Trailing slot (IconButton, Menu) rendered to the right of the text column. | | `children` | `ReactNode` | — | Custom content for the text column. Overrides the default title/subtitle layout when provided. | ### `<Card.Media>` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `src` | `string` | — | Image source — shortcut for rendering an `<img>` inside the media slot. | | `alt` | `string` | — | Alt text for the image. Defaults to an empty string when omitted; pass an empty string explicitly for decorative imagery. | | `aspectRatio` | `number` | `16 / 9` | Aspect ratio of the media slot (width / height). Use `1` for square thumbnails, `4 / 3` for legacy footage. | | `children` | `ReactNode` | — | Arbitrary children rendered inside the media slot. Used when `src` is not set — drop in a `<video>`, `<canvas>`, or `<picture>`. | ### `<Card.Body>` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Body content. Flex-grows so the footer stays pinned to the bottom of the card. | ### `<Card.Footer>` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `align` | `'left' \| 'center' \| 'right' \| 'space-between'` | `'right'` | Horizontal alignment of footer content. | | `children` | `ReactNode` | — | Footer content — typically action buttons. | --- # Divider > Thin horizontal or vertical rule for separating content, with optional centered label. _Source: /components/layout/divider_ Thin horizontal or vertical rule for separating content. Supports `solid` / `dashed` / `dotted` line styles, an optional centered label (horizontal only), and a numeric `spacing` prop that maps onto the theme spacing scale. **Live Preview** ## Import ```tsx import { Divider } from 'entangle-ui'; ``` ## Usage ```tsx <Stack> <SectionA /> <Divider /> <SectionB /> </Stack> ``` ## Orientation ### Horizontal The default — a full-width rule between stacked content. **Horizontal** ```tsx <Divider /> ``` ### Vertical A vertical divider needs a parent that gives it a non-zero cross-axis size — typically a flex row with a fixed height. **Vertical** ```tsx <Flex align="center" gap={2} style={{ height: 32 }}> <span>File</span> <Divider orientation="vertical" /> <span>Edit</span> <Divider orientation="vertical" /> <span>View</span> </Flex> ``` ## Variants Three line styles — switch freely without changing layout. **Variants** ```tsx <Divider variant="solid" /> {/* default */} <Divider variant="dashed" /> <Divider variant="dotted" /> ``` ## Labeled Divider Pass a `label` to render an inline title centered between two flanking lines (horizontal only). **Labeled divider** ```tsx <Divider label="Advanced" /> <Divider label="Danger zone" variant="dashed" /> ``` The label is rendered with the theme's `xxs` font size, `text.muted` color, and an uppercase letter-spacing — matches editor section dividers. ## Spacing `spacing` accepts a number index into the theme spacing scale or an arbitrary CSS string. The spacing is applied along the divider's axis. | Number | Token | Pixel value | | ------ | ------ | ----------- | | `0` | none | `0` | | `1` | `xs` | 2px | | `2` | `sm` | 4px | | `3` | `md` | 8px | | `4` | `lg` | 12px | | `5` | `xl` | 16px | | `6` | `xxl` | 24px | | `7` | `xxxl` | 32px | **Spacing scale** ```tsx <Divider spacing={3} /> {/* 8px top/bottom */} <Divider spacing="1.5rem" /> {/* arbitrary CSS */} ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Divider axis. | | `variant` | `'solid' \| 'dashed' \| 'dotted'` | `'solid'` | Line style. | | `spacing` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| string` | `0` | Margin along the axis. Numbers index into the theme spacing scale; strings are passed through verbatim. | | `label` | `ReactNode` | — | Optional centered label. Only honored for horizontal dividers. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying div element. | ## Accessibility - `role="separator"` is set on the root element - `aria-orientation` reflects the `orientation` prop - Labeled dividers keep `role="separator"` on the wrapper; the label text is exposed to assistive tech as a child --- # Flex > Comprehensive flexbox layout component with full control over direction, alignment, wrapping, spacing, and responsive breakpoints. _Source: /components/layout/flex_ Comprehensive flexbox layout component providing full control over flex properties. More powerful than Stack for complex layouts requiring precise flexbox control. Supports all flexbox properties, responsive direction changes, and flexible sizing. Ideal for navigation bars, form layouts, and sophisticated arrangements. **Live Preview** ## Import ```tsx import { Flex } from 'entangle-ui'; ``` ## Usage ```tsx <Flex justify="space-between" align="center"> <div>Left content</div> <div>Right content</div> </Flex> ``` ## Direction The `direction` prop controls the main axis orientation of flex children. ```tsx <Flex direction="row"> <div>A</div> <div>B</div> <div>C</div> </Flex> <Flex direction="column" gap={2}> <div>First</div> <div>Second</div> <div>Third</div> </Flex> <Flex direction="row-reverse"> <div>Appears last</div> <div>Appears first</div> </Flex> ``` | Direction | Behavior | | ---------------- | ----------------------- | | `row` | Left to right (default) | | `row-reverse` | Right to left | | `column` | Top to bottom | | `column-reverse` | Bottom to top | ## Responsive Direction Override the flex direction at different breakpoints using the `sm`, `md`, `lg`, and `xl` props. This lets you create layouts that adapt to screen size without media query boilerplate. ```tsx <Flex direction="column" md="row" justify="space-between" align="center" gap={2} > <Logo /> <Navigation /> <UserMenu /> </Flex> ``` | Breakpoint | Prop | Min Width | | ----------- | ---- | --------- | | Small | `sm` | 576px | | Medium | `md` | 768px | | Large | `lg` | 992px | | Extra Large | `xl` | 1200px | ## Justify and Align Use `justify` to distribute space along the main axis and `align` to position items on the cross axis. ```tsx <Flex justify="space-between" align="center"> <span>Title</span> <Button>Action</Button> </Flex> <Flex justify="center" align="center" fullHeight> <div>Centered content</div> </Flex> ``` ## Gap (Spacing) The `gap` prop accepts a multiplier from 0 to 8, based on a 4px base spacing unit. Use `customGap` for arbitrary CSS values. ```tsx <Flex gap={2}> <div>8px gap between items</div> <div>Next item</div> </Flex> <Flex customGap="1.5rem"> <div>Custom gap</div> <div>Next item</div> </Flex> ``` | Gap Value | Computed Size | | --------- | ------------- | | `0` | 0px | | `1` | 4px | | `2` | 8px | | `3` | 12px | | `4` | 16px | | `5` | 20px | | `6` | 24px | | `7` | 28px | | `8` | 32px | ## Wrapping Control how flex items wrap when they overflow the container. ```tsx <Flex wrap="wrap" gap={3} justify="center"> <Card style={{ flexBasis: '300px' }}>Card 1</Card> <Card style={{ flexBasis: '300px' }}>Card 2</Card> <Card style={{ flexBasis: '300px' }}>Card 3</Card> </Flex> ``` Use `alignContent` to control spacing between wrapped rows. ```tsx <Flex wrap="wrap" alignContent="space-between" style={{ height: '400px' }}> <div>Row 1</div> <div>Row 2</div> <div>Row 3</div> </Flex> ``` ## Flex Item Properties Each `Flex` can also behave as a flex item using `grow`, `shrink`, and `basis`. ```tsx <Flex direction="column" fullHeight> <Header /> <Flex grow={1}> <Flex basis="200px" shrink={0}> <Sidebar /> </Flex> <Flex grow={1}> <MainContent /> </Flex> </Flex> <Footer /> </Flex> ``` ## Full Width and Height Use `fullWidth` and `fullHeight` to make the container fill its parent. ```tsx <Flex fullWidth justify="space-between"> <span>Left</span> <span>Right</span> </Flex> <Flex fullHeight direction="column" justify="center" align="center"> <div>Vertically centered</div> </Flex> ``` ## Size Constraints Apply `minHeight` and `maxWidth` to constrain the flex container. ```tsx <Flex direction="column" gap={2} maxWidth="400px"> <Input label="Email" /> <Input label="Password" /> <Flex justify="space-between" gap={2}> <Button fullWidth variant="ghost"> Cancel </Button> <Button fullWidth variant="filled"> Login </Button> </Flex> </Flex> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Flex content -- any React elements. | | `direction` | `'row' \| 'row-reverse' \| 'column' \| 'column-reverse'` | `'row'` | Flex direction controlling main axis orientation. | | `sm` | `'row' \| 'row-reverse' \| 'column' \| 'column-reverse'` | — | Flex direction override at the small breakpoint (576px). | | `md` | `'row' \| 'row-reverse' \| 'column' \| 'column-reverse'` | — | Flex direction override at the medium breakpoint (768px). | | `lg` | `'row' \| 'row-reverse' \| 'column' \| 'column-reverse'` | — | Flex direction override at the large breakpoint (992px). | | `xl` | `'row' \| 'row-reverse' \| 'column' \| 'column-reverse'` | — | Flex direction override at the extra-large breakpoint (1200px). | | `wrap` | `'nowrap' \| 'wrap' \| 'wrap-reverse'` | `'nowrap'` | Flex wrap behavior for overflowing items. | | `justify` | `'flex-start' \| 'flex-end' \| 'center' \| 'space-between' \| 'space-around' \| 'space-evenly'` | `'flex-start'` | Distributes space along the main axis. | | `align` | `'flex-start' \| 'flex-end' \| 'center' \| 'stretch' \| 'baseline'` | `'stretch'` | Aligns items along the cross axis. | | `alignContent` | `'flex-start' \| 'flex-end' \| 'center' \| 'stretch' \| 'space-between' \| 'space-around'` | `'stretch'` | Aligns wrapped lines. Only applies when wrap is enabled with multiple lines. | | `gap` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8` | `0` | Gap between flex items as a multiplier of the 4px base spacing unit. | | `customGap` | `string \| number` | — | Custom gap override as a CSS value. Overrides the gap prop when provided. | | `grow` | `number` | `0` | Flex grow factor controlling how much the item should grow relative to siblings. | | `shrink` | `number` | `1` | Flex shrink factor controlling how much the item should shrink relative to siblings. | | `basis` | `string \| number` | `'auto'` | Flex basis defining the initial size before free space is distributed. | | `fullWidth` | `boolean` | `false` | Whether the flex container should fill available width (100%). | | `fullHeight` | `boolean` | `false` | Whether the flex container should fill available height (100%). | | `minHeight` | `string \| number` | — | Minimum height constraint for the flex container. | | `maxWidth` | `string \| number` | — | Maximum width constraint for the flex container. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying div element. | The component also accepts all standard HTML `<div>` attributes. ## Accessibility - Renders a semantic `<div>` element with `display: flex` - No special ARIA attributes are needed for layout containers - Ensure content within the flex container maintains a logical reading order, especially when using `row-reverse` or `column-reverse` --- # Grid > A responsive 12-column grid system built on CSS Grid for creating structured layouts with flexible column spans. _Source: /components/layout/grid_ A flexible 12-column grid system for creating responsive layouts. Supports both container and item modes -- container grids establish the grid layout while item grids define how many columns each child spans. Built on CSS Grid for modern, gap-aware layouts. **Live Preview** ## Import ```tsx import { Grid } from 'entangle-ui'; ``` ## Usage ```tsx <Grid container spacing={2}> <Grid size={6}>Left half</Grid> <Grid size={6}>Right half</Grid> </Grid> ``` ## Container and Item Modes A `Grid` with `container` creates the grid context. Child `Grid` elements without `container` act as grid items that span a number of columns. ```tsx <Grid container spacing={3}> <Grid size={4}>One third</Grid> <Grid size={4}>One third</Grid> <Grid size={4}>One third</Grid> </Grid> ``` The total number of columns defaults to 12. Items that exceed 12 columns will wrap to the next row. ```tsx <Grid container columns={12} spacing={2}> <Grid size={8}>Main content (8/12)</Grid> <Grid size={4}>Sidebar (4/12)</Grid> </Grid> ``` ## Auto-sizing Items Use `size="auto"` to let a grid item size based on its content. ```tsx <Grid container spacing={2}> <Grid size="auto">Fits content</Grid> <Grid size={6}>Fixed 6 columns</Grid> </Grid> ``` ## Responsive Sizes Override column spans at different breakpoints using the `xs`, `sm`, `md`, `lg`, and `xl` props. Items start at the smallest specified breakpoint and scale up. ```tsx <Grid container spacing={2}> <Grid xs={12} sm={6} md={4}> Full on mobile, half on tablet, third on desktop </Grid> <Grid xs={12} sm={6} md={4}> Full on mobile, half on tablet, third on desktop </Grid> <Grid xs={12} sm={12} md={4}> Full on mobile, full on tablet, third on desktop </Grid> </Grid> ``` | Breakpoint | Prop | Min Width | | ----------- | ---- | --------- | | Extra Small | `xs` | 0px | | Small | `sm` | 576px | | Medium | `md` | 768px | | Large | `lg` | 992px | | Extra Large | `xl` | 1200px | ## Spacing The `spacing` prop controls the gap between grid items. Values are multipliers of a 4px base unit. Use the `gap` prop for arbitrary CSS values. ```tsx <Grid container spacing={4}> <Grid size={6}>16px gap</Grid> <Grid size={6}>between items</Grid> </Grid> <Grid container gap="2rem"> <Grid size={4}>Custom gap</Grid> <Grid size={4}>between items</Grid> <Grid size={4}>everywhere</Grid> </Grid> ``` | Spacing Value | Computed Size | | ------------- | ------------- | | `0` | 0px | | `1` | 4px | | `2` | 8px (default) | | `3` | 12px | | `4` | 16px | | `5` | 20px | | `6` | 24px | | `7` | 28px | | `8` | 32px | ## Custom Column Count Change the total number of columns with the `columns` prop. ```tsx <Grid container columns={6} spacing={2}> <Grid size={2}>2 of 6</Grid> <Grid size={4}>4 of 6</Grid> </Grid> ``` ## Nested Grids Grids can be nested by placing a container Grid inside a Grid item. ```tsx <Grid container spacing={3}> <Grid size={8}> <Grid container spacing={2}> <Grid size={6}>Nested left</Grid> <Grid size={6}>Nested right</Grid> </Grid> </Grid> <Grid size={4}>Sidebar</Grid> </Grid> ``` ## Props ### Grid (Container) Props that apply when `container` is `true`. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Grid content -- Grid items or any React elements. Required. | | `container` | `boolean` | `false` | Whether this Grid acts as a container enabling CSS Grid layout. | | `columns` | `number` | `12` | Total number of columns in the grid. Only applies to containers. | | `spacing` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8` | `2` | Gap between grid items as a multiplier of the 4px base unit. Only applies to containers. | | `gap` | `string \| number` | — | Custom gap override as a CSS value. Overrides the spacing prop when provided. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying div element. | ### Grid (Item) Props that apply when `container` is `false` (the default). | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Item content. Required. | | `size` | `1-12 \| 'auto'` | — | Number of columns to span (out of the total column count). | | `xs` | `1-12 \| 'auto'` | — | Column span at extra-small breakpoint (0px+). | | `sm` | `1-12 \| 'auto'` | — | Column span at small breakpoint (576px+). | | `md` | `1-12 \| 'auto'` | — | Column span at medium breakpoint (768px+). | | `lg` | `1-12 \| 'auto'` | — | Column span at large breakpoint (992px+). | | `xl` | `1-12 \| 'auto'` | — | Column span at extra-large breakpoint (1200px+). | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying div element. | Both container and item modes accept all standard HTML `<div>` attributes. ## Accessibility - Renders semantic `<div>` elements with CSS Grid - No special ARIA attributes required for layout containers - Grid items maintain their natural DOM order for screen readers regardless of visual placement --- # ListItem > Reusable list row primitive with leading and trailing slots, selected/active/hover states, and built-in keyboard activation. _Source: /components/layout/list-item_ A reusable list row primitive. Encapsulates the common pattern of `leading | content | trailing` with hover, selected, active, and disabled states. When `onClick` is provided, the item becomes a keyboard-activatable button (Enter/Space). Backed by the `colors.surface.row` and `colors.surface.rowHover` tokens so list backgrounds stay subtle without competing with button hover state. **Live Preview** ## Import ```tsx import { ListItem } from 'entangle-ui'; ``` ## Usage ```tsx <ListItem leading={<FileIcon />} trailing={<Badge color="success">done</Badge>} selected={id === activeId} onClick={() => select(id)} > scene.blend </ListItem> ``` ## States The component composes four orthogonal states. Multiple can be set at once. **States** ```tsx <ListItem onClick={fn}>Default</ListItem> <ListItem onClick={fn} selected>Selected</ListItem> <ListItem onClick={fn} active>Active</ListItem> <ListItem disabled>Disabled</ListItem> ``` | Prop | Effect | | ---------- | ---------------------------------------------------------------- | | `selected` | Persistent selection — accent-tinted background | | `active` | Pressed / open — stronger accent tint, typically transient | | `disabled` | Greyed out, pointer-events disabled, keyboard activation skipped | | _(hover)_ | Automatic via the `surface.rowHover` token | ## Clickable Behavior When `onClick` is set, the item renders as `role="button"` with `tabIndex={0}` and activates on Enter or Space. Try keyboard navigation in the demo below. **Clickable** — Tab to the rows, then press Enter or Space. ```tsx <ListItem onClick={handleSelect}>Keyboard-activatable</ListItem> ``` For navigation lists, wrap children in your own `<a>` element instead of using `onClick`, so middle-click / right-click open in new tabs work natively. ## Leading and Trailing Slots `leading` and `trailing` are both inline-flex containers. They flank the main content area and don't shrink — long content is truncated with ellipsis. **Slots** ```tsx <ListItem leading={<FileIcon />}>Leading only</ListItem> <ListItem trailing={<ChevronIcon />}>Trailing only</ListItem> <ListItem leading={<FileIcon />} trailing={<Badge color="success">12</Badge>}> Both slots </ListItem> ``` ## Density **Density** ```tsx <ListItem density="comfortable">32px row, default</ListItem> <ListItem density="compact">24px row, dense lists</ListItem> ``` ## In a List Pair with `Stack` (with `gap={1}`) for a clean selectable list. **Selectable list** — Click a row to select it — the selection persists with data-selected styling. ```tsx <Stack gap={1}> {files.map(file => ( <ListItem key={file.id} leading={<FileIcon type={file.type} />} trailing={<Badge color={statusColor(file)}>{file.status}</Badge>} selected={file.id === activeId} onClick={() => setActive(file.id)} > {file.name} </ListItem> ))} </Stack> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Primary content (typically a title + optional description). | | `leading` | `ReactNode` | — | Leading content (icon, checkbox, avatar). | | `trailing` | `ReactNode` | — | Trailing content (actions, badge, chevron). | | `onClick` | `(event: MouseEvent) => void` | — | Click handler — when set, the item becomes keyboard-activatable. | | `selected` | `boolean` | `false` | Persistent selection state. | | `active` | `boolean` | `false` | Pressed / opened state — typically transient. | | `disabled` | `boolean` | `false` | Disables hover and pointer events; skips keyboard activation. | | `density` | `'compact' \| 'comfortable'` | `'comfortable'` | Row density — compact (24px) or comfortable (32px). | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying div element. | ## Theme Tokens | Token | Used for | | ------------------------- | ------------------------------------ | | `colors.surface.row` | Default row background (transparent) | | `colors.surface.rowHover` | Hover background (subtle) | | `colors.accent.primary` | Selected and active tint | | `colors.text.primary` | Foreground content | | `colors.text.secondary` | Leading content (icon) | | `colors.text.muted` | Trailing content | ## Accessibility - `role="button"` and `tabIndex={0}` are added when `onClick` is provided - `aria-disabled` is set when `disabled` is true - Selected/active state is exposed via `data-selected` / `data-active` attributes for styling, plus the visible color treatment - Keyboard: Enter and Space activate the click handler when clickable - Disabled rows are skipped during keyboard activation --- # PageHeader > Structural page or view header with optional icon, subtitle, breadcrumbs, and right-aligned actions. _Source: /components/layout/page-header_ A semantic `<header>` element that arranges an optional leading icon, the title block (breadcrumbs + title + subtitle), and a right-aligned actions slot. Built for top-of-view headers inside a panel or page container, not for the application chrome — for that, use the components under `Shell`. **Live Preview** ## Import ```tsx import { PageHeader } from 'entangle-ui'; ``` ## Usage **Basic** ```tsx <PageHeader title="Project Assets" /> ``` ## Anatomy ``` [ icon ] [ breadcrumbs ] [ actions ] [ title ] [ subtitle ] ``` The title block grows to fill available space; actions sit flush to the right. ## With Actions **With actions** ```tsx <PageHeader icon={<FolderIcon />} title="Project Assets" subtitle="124 items" actions={ <> <Button>Import</Button> <Button variant="filled">Upload</Button> </> } /> ``` ## With Breadcrumbs You can pass any `ReactNode` as `breadcrumbs` — typically your own breadcrumb component or a series of `<a>` tags. **With breadcrumbs** ```tsx <PageHeader breadcrumbs={<span>Workspace / Project / Assets</span>} title="Textures" subtitle="Baked PBR materials" icon={<FolderIcon />} actions={<Button variant="filled">New</Button>} /> ``` ## Sizes The `size` prop scales the title font size and outer padding. **Sizes** ```tsx <PageHeader size="sm" title="Small" /> <PageHeader size="md" title="Medium" /> <PageHeader size="lg" title="Prominent" /> ``` | Size | Title font size | Padding | | ---- | --------------- | --------------------- | | `sm` | `md` (12px) | `sm` × `md` | | `md` | `lg` (14px) | `md` × `lg` (default) | | `lg` | `xl` (16px) | `lg` × `xl` | ## Borderless The bottom border is on by default. Pass `bordered={false}` when the header sits inside another bordered container. **Inside a bordered container** — The header ```tsx <div style={{ border: '1px solid var(--etui-color-border-default)' }}> <PageHeader title="Settings" bordered={false} /> <div>panel body...</div> </div> ``` ## Inside a PanelSurface ```tsx <PanelSurface> <PageHeader title="Inspector" actions={<IconButton icon={<MoreIcon />} />} /> <PanelSurface.Body scroll>{/* content */}</PanelSurface.Body> </PanelSurface> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `title` *(required)* | `ReactNode` | — | Title — plain string or custom ReactNode. | | `icon` | `ReactNode` | — | Icon rendered before the title. | | `subtitle` | `ReactNode` | — | Optional subtitle below the title. | | `actions` | `ReactNode` | — | Actions rendered on the right (buttons, menus, IconButtons). | | `breadcrumbs` | `ReactNode` | — | Breadcrumb slot rendered above the title. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size scale. | | `bordered` | `boolean` | `true` | Render a bottom border separating the header from content. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying header element. | ## Accessibility - Renders a semantic `<header>` element so assistive tech and search engines treat it as page-level metadata - Title is rendered as `<h1>` inside the title block — pair with `aria-labelledby` on the surrounding section if needed - Actions container is a plain `<div>` — wrap individual actions in `<Button>` or `<IconButton>` for proper button semantics and `aria-label` --- # PanelSurface > Structured panel container with compound Header, Body, and Footer sub-components for editor panels and tool windows. _Source: /components/layout/panel-surface_ A structured panel container using a compound component pattern with `PanelSurface`, `PanelSurface.Header`, `PanelSurface.Body`, and `PanelSurface.Footer`. Provides consistent chrome (border, background, header/footer sizing) for editor panels, tool windows, property inspectors, and sidebar sections. **Live Preview** ## Import ```tsx import { PanelSurface } from 'entangle-ui'; ``` ## Usage ```tsx <PanelSurface> <PanelSurface.Header>Properties</PanelSurface.Header> <PanelSurface.Body padding={8}>Panel content goes here</PanelSurface.Body> </PanelSurface> ``` ## Panel Structure A typical panel consists of a Header, Body, and optionally a Footer. All sub-components are accessed as static properties of `PanelSurface`. ```tsx <PanelSurface> <PanelSurface.Header>Panel Title</PanelSurface.Header> <PanelSurface.Body padding={12}> <p>Main content area</p> </PanelSurface.Body> <PanelSurface.Footer> <Button size="sm">Apply</Button> </PanelSurface.Footer> </PanelSurface> ``` ## Sizes The `size` prop on the root `PanelSurface` controls the visual density of the header and footer chrome. It is shared via context to all sub-components. ```tsx <PanelSurface size="sm"> <PanelSurface.Header>Compact Panel</PanelSurface.Header> <PanelSurface.Body>Content</PanelSurface.Body> </PanelSurface> <PanelSurface size="md"> <PanelSurface.Header>Standard Panel</PanelSurface.Header> <PanelSurface.Body>Content</PanelSurface.Body> </PanelSurface> <PanelSurface size="lg"> <PanelSurface.Header>Spacious Panel</PanelSurface.Header> <PanelSurface.Body>Content</PanelSurface.Body> </PanelSurface> ``` | Size | Use case | | ---- | ------------------------------------------ | | `sm` | Compact tool windows and inline panels | | `md` | Standard property panels (default) | | `lg` | Prominent settings or configuration panels | ## Border The `bordered` prop controls whether the panel has a visible border. ```tsx { /* With border (default) */ } <PanelSurface bordered> <PanelSurface.Header>Bordered Panel</PanelSurface.Header> <PanelSurface.Body>Content</PanelSurface.Body> </PanelSurface>; { /* Without border */ } <PanelSurface bordered={false}> <PanelSurface.Header>Borderless Panel</PanelSurface.Header> <PanelSurface.Body>Content</PanelSurface.Body> </PanelSurface>; ``` ## Custom Background Override the default panel background with any CSS color or gradient value. ```tsx <PanelSurface background="linear-gradient(180deg, #2f3442 0%, #1b202a 100%)"> <PanelSurface.Header>Gradient Panel</PanelSurface.Header> <PanelSurface.Body>Content with gradient background</PanelSurface.Body> </PanelSurface> ``` ## Header with Actions The `PanelSurface.Header` supports an `actions` slot for interactive elements positioned on the right side of the header. ```tsx <PanelSurface> <PanelSurface.Header actions={ <> <IconButton icon={<SettingsIcon />} size="sm" /> <IconButton icon={<CloseIcon />} size="sm" /> </> } > Inspector </PanelSurface.Header> <PanelSurface.Body padding={8}>Inspector content...</PanelSurface.Body> </PanelSurface> ``` ## Scrollable Body Enable scrolling for the body content when it overflows its container. ```tsx <PanelSurface style={{ height: '400px' }}> <PanelSurface.Header>Scrollable Panel</PanelSurface.Header> <PanelSurface.Body scroll padding={8}> <div style={{ height: '800px' }}> Long content that scrolls within the body... </div> </PanelSurface.Body> </PanelSurface> ``` When `scroll` is `false` (the default), body overflow is hidden. ## Body Padding Control the inner padding of the body area. Accepts numbers (treated as pixels) or CSS strings. ```tsx <PanelSurface.Body padding={16}> Content with 16px padding </PanelSurface.Body> <PanelSurface.Body padding="1rem 0.5rem"> Content with custom padding </PanelSurface.Body> ``` ## Complete Example ```tsx <PanelSurface size="md" bordered> <PanelSurface.Header actions={<IconButton icon={<CloseIcon />} size="sm" variant="ghost" />} > Transform </PanelSurface.Header> <PanelSurface.Body scroll padding={8}> <Stack spacing={2}> <NumberInput label="Position X" value={0} /> <NumberInput label="Position Y" value={0} /> <NumberInput label="Position Z" value={0} /> <NumberInput label="Rotation" value={0} /> <NumberInput label="Scale" value={1} /> </Stack> </PanelSurface.Body> <PanelSurface.Footer> <Button size="sm" variant="ghost"> Reset </Button> </PanelSurface.Footer> </PanelSurface> ``` ## Props ### PanelSurface | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Panel content. Typically PanelSurface.Header, PanelSurface.Body, and optionally PanelSurface.Footer. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Visual density for header and footer chrome. Shared via context to sub-components. | | `bordered` | `boolean` | `true` | Whether to show a border around the panel. | | `background` | `string` | — | Custom background color or gradient. Defaults to the theme secondary background. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the root div element. | ### PanelSurface.Header | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Header content such as a title or label. | | `actions` | `ReactNode` | — | Action elements rendered on the right side of the header (e.g. icon buttons). | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the header div element. | ### PanelSurface.Body | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Main panel content. | | `scroll` | `boolean` | `false` | Enables automatic overflow scrolling for the body area. | | `padding` | `number \| string` | `0` | Inner padding of the body area. Numbers are treated as pixels. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the body div element. | ### PanelSurface.Footer | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Footer content such as action buttons. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the footer div element. | All sub-components also accept standard HTML `<div>` attributes. ## Accessibility - Renders semantic `<div>` elements for structural layout - Header content is wrapped in a `<span>` for text styling; actions are grouped separately - The component provides no implicit ARIA roles -- add `role` and `aria-label` as needed for specific use cases (e.g. `role="complementary"` for a sidebar panel) - Use descriptive header text that clearly identifies the panel purpose for screen reader users --- # ScrollArea > Custom scrollable container with styled scrollbars, drag-to-scroll thumbs, fade masks, and configurable visibility behavior. _Source: /components/layout/scroll-area_ A custom scrollable container providing styled scrollbar overlays with drag-to-scroll support, configurable visibility behavior, and optional fade mask gradients at scroll boundaries. Designed for editor panels, sidebars, and content areas that need polished scrolling behavior beyond native scrollbars. **Live Preview** ## Import ```tsx import { ScrollArea } from 'entangle-ui'; ``` ## Usage ```tsx <ScrollArea maxHeight={300}> <div> {/* Long content that overflows */} {items.map(item => ( <Item key={item.id} {...item} /> ))} </div> </ScrollArea> ``` ## Scroll Direction Control which axes can scroll using the `direction` prop. **Scroll Direction** ```tsx { /* Vertical only (default) */ } <ScrollArea direction="vertical" maxHeight={200}> <LongVerticalContent /> </ScrollArea>; { /* Horizontal only */ } <ScrollArea direction="horizontal" maxWidth={400}> <WideHorizontalContent /> </ScrollArea>; { /* Both axes */ } <ScrollArea direction="both" maxHeight={300} maxWidth={500}> <LargeContent /> </ScrollArea>; ``` ## Scrollbar Visibility The `scrollbarVisibility` prop controls when custom scrollbars appear. **Scrollbar Visibility** ```tsx { /* Auto: show on scroll/hover, hide after delay (default) */ } <ScrollArea scrollbarVisibility="auto" maxHeight={200}> <Content /> </ScrollArea>; { /* Always visible */ } <ScrollArea scrollbarVisibility="always" maxHeight={200}> <Content /> </ScrollArea>; { /* Show only on hover */ } <ScrollArea scrollbarVisibility="hover" maxHeight={200}> <Content /> </ScrollArea>; { /* Never show scrollbars (still scrollable via wheel/touch) */ } <ScrollArea scrollbarVisibility="never" maxHeight={200}> <Content /> </ScrollArea>; ``` | Mode | Behavior | | -------- | ------------------------------------------------------------------------ | | `auto` | Shows on scroll or hover, auto-hides after delay | | `always` | Scrollbars always visible when content overflows | | `hover` | Shows only while the mouse is over the scroll area | | `never` | Scrollbars hidden entirely (scroll still works via wheel/touch/keyboard) | ## Auto-hide Delay When using `scrollbarVisibility="auto"`, control the delay before scrollbars fade out. **Auto-hide Delay** ```tsx <ScrollArea scrollbarVisibility="auto" hideDelay={2000} maxHeight={200}> <Content /> </ScrollArea> ``` ## Fade Masks Enable gradient fade masks at scroll boundaries to visually indicate more content is available. **Fade Masks** ```tsx <ScrollArea fadeMask maxHeight={200}> <Content /> </ScrollArea>; { /* Custom fade mask size */ } <ScrollArea fadeMask fadeMaskHeight={40} maxHeight={200}> <Content /> </ScrollArea>; ``` Fade masks appear at the top/bottom for vertical scrolling and left/right for horizontal scrolling. They automatically hide when the user has scrolled to the edge. ## Scrollbar Customization Fine-tune the scrollbar appearance with width, padding, and minimum thumb length. **Scrollbar Customization** ```tsx <ScrollArea scrollbarWidth={8} scrollbarPadding={3} minThumbLength={40} maxHeight={300} > <Content /> </ScrollArea> ``` ## Auto Fill When placed inside a flex or grid layout, use `autoFill` to make the scroll area fill its parent container. **Auto Fill** ```tsx <Flex direction="column" fullHeight> <Header /> <ScrollArea autoFill> <MainContent /> </ScrollArea> <Footer /> </Flex> ``` ## Scroll Callbacks React to scroll position changes and boundary events. **Scroll Callbacks** ```tsx <ScrollArea maxHeight={300} onScroll={e => console.log('Scrolled:', e.currentTarget.scrollTop)} onScrollTop={() => console.log('Reached top')} onScrollBottom={() => loadMoreItems()} > <Content /> </ScrollArea> ``` The `onScrollBottom` callback is useful for implementing infinite scrolling patterns. ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Scrollable content. Required. | | `direction` | `'vertical' \| 'horizontal' \| 'both'` | `'vertical'` | Which axes can scroll. | | `scrollbarVisibility` | `'auto' \| 'always' \| 'hover' \| 'never'` | `'auto'` | Scrollbar visibility behavior. | | `hideDelay` | `number` | `1000` | Delay in milliseconds before scrollbar auto-hides. Only applies when scrollbarVisibility is "auto". | | `scrollbarWidth` | `number` | `6` | Scrollbar track width in pixels. | | `minThumbLength` | `number` | `30` | Minimum scrollbar thumb length in pixels. | | `scrollbarPadding` | `number` | `2` | Padding between scrollbar track and content edge in pixels. | | `fadeMask` | `boolean` | `false` | Whether to show gradient fade masks at scroll boundaries. | | `fadeMaskHeight` | `number` | `24` | Height of the fade mask gradient in pixels. | | `maxHeight` | `number \| string` | — | Maximum height of the scroll area. Required to enable vertical scrolling. | | `maxWidth` | `number \| string` | — | Maximum width of the scroll area. Required to enable horizontal scrolling. | | `autoFill` | `boolean` | `false` | When true, sets width and height to 100% to fill the parent container. | | `onScroll` | `(event: UIEvent) => void` | — | Callback fired when the scroll position changes. | | `onScrollTop` | `() => void` | — | Callback fired when scroll reaches the top edge. | | `onScrollBottom` | `() => void` | — | Callback fired when scroll reaches the bottom edge. | | `className` | `string` | — | Additional CSS class names applied to the root element. | | `style` | `CSSProperties` | — | Inline styles applied to the root element. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the scrollable viewport element. | The component also accepts all standard HTML `<div>` attributes on the root element. ## Accessibility - The scrollable viewport has `role="region"` and `tabIndex={0}` for keyboard accessibility - Custom scrollbar tracks have `role="scrollbar"` with proper `aria-controls`, `aria-orientation`, `aria-valuenow`, `aria-valuemin`, and `aria-valuemax` attributes - Content remains scrollable via keyboard (arrow keys, Page Up/Down, Home/End) regardless of scrollbar visibility setting - Scrollbar thumb supports pointer drag interaction for precise scroll positioning --- # Spacer > A flexible spacer component that expands to fill available space or provides fixed spacing between elements. _Source: /components/layout/spacer_ A utility component that creates space between elements in flex layouts. By default, it grows to fill all available space, pushing siblings apart. It can also be given a fixed size for precise spacing control. Works with Stack, Flex, and any flex container. **Live Preview** ## Import ```tsx import { Spacer } from 'entangle-ui'; ``` ## Usage ```tsx <Stack direction="row"> <Button>Left</Button> <Spacer /> <Button>Right</Button> </Stack> ``` ## Auto-expanding Spacer Without a `size` prop, the Spacer uses `flex-grow: 1` to consume all available space. This is the most common usage for pushing elements to opposite sides of a container. ```tsx <Flex direction="row" align="center"> <Logo /> <Spacer /> <Navigation /> <Spacer /> <UserMenu /> </Flex> ``` Multiple Spacer components distribute space evenly between them, creating equal spacing between groups of elements. ## Fixed Size Spacer Pass a `size` value to create a spacer with fixed dimensions. Accepts CSS strings or numbers (treated as pixels). ```tsx <Stack direction="column"> <Title>Header Body content Label ``` In fixed mode, both width and height are set to the same value. The parent flex container's direction determines which dimension takes effect -- the cross-axis dimension is typically overridden by `align-items`. ## Common Patterns ### Toolbar with actions pushed apart ```tsx } /> } /> ``` ### Vertical spacing between sections ```tsx ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `size` | `string \| number` | — | Fixed size for the spacer. When omitted, the spacer expands to fill available space. Numbers are treated as pixels. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying div element. | The component also accepts all standard HTML `
` attributes. ## Accessibility - Renders an empty `
` element used purely for layout purposes - Has no semantic meaning and is invisible to assistive technologies - The Spacer component is memoized for performance --- # SplitPane > Resizable split-pane layout with draggable dividers, collapsible panels, and keyboard-accessible resizing for editor interfaces. _Source: /components/layout/split-pane_ A resizable split-pane layout component that divides space between two or more child panels with draggable dividers. Supports horizontal and vertical directions, controlled and uncontrolled modes, collapsible panels, min/max size constraints, and full keyboard accessibility. Designed for editor interfaces such as code editors, property inspectors, and multi-panel workspaces. **Live Preview** ## Import ```tsx import { SplitPane, SplitPanePanel } from 'entangle-ui'; ``` ## Usage ```tsx Left panel Right panel ``` ## Direction Use `direction` to control whether panels are laid out side by side (horizontal) or stacked vertically. ```tsx { /* Side by side (default) */ } Left Right ; { /* Stacked */ } Top Bottom ; ``` | Direction | Layout | Divider Orientation | | ------------ | ------------------- | ------------------- | | `horizontal` | Panels side by side | Vertical bar | | `vertical` | Panels stacked | Horizontal bar | ## Panel Configuration The `panels` prop accepts an array of `PanelConfig` objects that define sizing constraints for each panel. The array indices must match the order of `SplitPanePanel` children. ```tsx Sidebar Main content ``` ### Default Sizes Panel default sizes can be specified as pixel numbers or percentage strings. Panels without a `defaultSize` split the remaining space equally. ```tsx Fixed sidebar Main area Properties ``` ### Min and Max Constraints Prevent panels from being resized beyond specific boundaries. ```tsx Constrained panel Main content ``` ## Collapsible Panels Panels with `collapsible: true` snap to zero width/height when dragged below a threshold. The threshold defaults to half the `minSize`, or you can set `collapseThreshold` explicitly. ```tsx { console.log(`Panel ${panelIndex} ${collapsed ? 'collapsed' : 'expanded'}`); }} > Collapsible sidebar Main content ``` ### Custom Collapse Threshold ```tsx Panel Content ``` ## Multiple Panels SplitPane supports more than two panels. Each adjacent pair gets a draggable divider. ```tsx File explorer Editor Properties ``` ## Controlled Mode Pass the `sizes` prop (an array of pixel values) to control panel sizes externally. Use `onResize` to update state during drag and `onResizeEnd` to persist the final layout. ```tsx const [sizes, setSizes] = useState([300, 500]); saveLayout(finalSizes)} > Panel A Panel B ; ``` ## Divider Size Control the visual size (width or height) of the draggable divider. ```tsx Left Right ``` ## Resize Callbacks ```tsx { // Fires continuously during drag console.log('Resizing:', sizes); }} onResizeEnd={sizes => { // Fires once when drag ends -- use to persist layout localStorage.setItem('layout', JSON.stringify(sizes)); }} > Left Right ``` ## Props ### SplitPane | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | SplitPanePanel children (minimum 2). Required. | | `direction` | `'horizontal' \| 'vertical'` | `'horizontal'` | Layout direction. Horizontal places panels side by side, vertical stacks them. | | `panels` | `PanelConfig[]` | `[]` | Array of panel configuration objects defining default size, min/max constraints, and collapse behavior. | | `sizes` | `number[]` | — | Controlled panel sizes in pixels. When provided, the component operates in controlled mode. | | `dividerSize` | `number` | `4` | Divider width (horizontal) or height (vertical) in pixels. | | `onResize` | `(sizes: number[]) => void` | — | Callback fired continuously during drag with the current panel sizes. | | `onResizeEnd` | `(sizes: number[]) => void` | — | Callback fired when a drag ends. Useful for persisting layout to storage. | | `onCollapseChange` | `(panelIndex: number, collapsed: boolean) => void` | — | Callback fired when a collapsible panel collapses or expands. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the container div element. | ### PanelConfig Configuration object for each panel in the `panels` array. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `defaultSize` | `number \| string` | — | Initial panel size in pixels (number) or as a CSS value (e.g. "30%"). | | `minSize` | `number` | — | Minimum panel size in pixels. | | `maxSize` | `number` | — | Maximum panel size in pixels. | | `collapsible` | `boolean` | `false` | Whether this panel can collapse to zero size. | | `collapseThreshold` | `number` | — | Size in pixels below which the panel snaps to collapsed. Defaults to minSize / 2. | ### SplitPanePanel | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Panel content. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the panel div element. | Both SplitPane and SplitPanePanel accept all standard HTML `
` attributes. ## Filling the panel with a child `SplitPanePanel` is sized as `width: 100%; height: 100%` with `min-width: 0; min-height: 0; box-sizing: border-box`. Children that themselves use `height: 100%` (such as `PanelSurface` with header/body/footer, `ScrollArea`, or a fill-textarea) lay out correctly inside it without extra wrappers. ```tsx Inspector {/* content */} Apply {/* main view */} ``` ## Accessibility - Dividers have `role="separator"` with `aria-orientation` matching the split direction - Each divider has `aria-valuenow` (current size), `aria-valuemin` (min size), and `aria-valuemax` (max size) attributes - Dividers are focusable (`tabIndex={0}`) and support keyboard resizing: - **Arrow keys**: Move divider by 10px - **Shift + Arrow keys**: Move divider by 50px - **Enter**: Toggle collapse on collapsible panels - Panel sizes snap to whole pixels to avoid sub-pixel rendering gaps - The component uses a `ResizeObserver` to proportionally redistribute space when the container is resized --- # Stack > Simple stacking component for arranging elements vertically or horizontally with consistent spacing. _Source: /components/layout/stack_ A flexible stacking component for arranging elements vertically or horizontally with consistent spacing. Built on flexbox, Stack is the simplest layout primitive for common stacking patterns. For more complex layouts requiring precise flexbox control, see Flex or Grid. **Live Preview** ## Import ```tsx import { Stack } from 'entangle-ui'; ``` ## Usage ```tsx Item 1 Item 2 Item 3 ``` ## Direction Stack defaults to `column` (vertical stacking). Use `direction="row"` for horizontal layouts. ```tsx
Top
Middle
Bottom
``` | Direction | Behavior | | --------- | ----------------------- | | `column` | Top to bottom (default) | | `row` | Left to right | ## Responsive Direction Override the stack direction at different breakpoints. This is useful for layouts that should be vertical on mobile and horizontal on larger screens. ```tsx ``` | Breakpoint | Prop | Min Width | | ----------- | ---- | --------- | | Small | `sm` | 576px | | Medium | `md` | 768px | | Large | `lg` | 992px | | Extra Large | `xl` | 1200px | ## Spacing The `spacing` prop controls the gap between stacked items. Values are multipliers of a 4px base unit. Use `customGap` for arbitrary CSS values. ```tsx
4px gap
between items
16px gap
between items
Custom gap
between items
``` | Spacing Value | Computed Size | | ------------- | ------------- | | `0` | 0px | | `1` | 4px | | `2` | 8px | | `3` | 12px | | `4` | 16px | | `5` | 20px | | `6` | 24px | | `7` | 28px | | `8` | 32px | ## Expand The `expand` prop makes the stack fill available space in the direction of its layout. For `row` direction it takes 100% width; for `column` direction it takes 100% height. ```tsx ``` ## Justify and Align Use `justify` for main-axis distribution and `align` for cross-axis alignment. ```tsx Welcome ``` ```tsx Label ``` ## Wrapping Allow items to wrap to new lines when they overflow the container. ```tsx React TypeScript CSS Vanilla Extract ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Stack content -- any React elements. Required. | | `direction` | `'row' \| 'column'` | `'column'` | Stack direction controlling main axis orientation. | | `sm` | `'row' \| 'column'` | — | Direction override at the small breakpoint (576px). | | `md` | `'row' \| 'column'` | — | Direction override at the medium breakpoint (768px). | | `lg` | `'row' \| 'column'` | — | Direction override at the large breakpoint (992px). | | `xl` | `'row' \| 'column'` | — | Direction override at the extra-large breakpoint (1200px). | | `wrap` | `'nowrap' \| 'wrap' \| 'wrap-reverse'` | `'nowrap'` | Flex wrap behavior when items overflow. | | `expand` | `boolean` | `false` | Whether the stack fills available space (100% width for row, 100% height for column). | | `spacing` | `0 \| 1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 7 \| 8` | `0` | Gap between stack items as a multiplier of the 4px base spacing unit. | | `customGap` | `string \| number` | — | Custom gap override as a CSS value. Overrides the spacing prop when provided. | | `justify` | `'flex-start' \| 'flex-end' \| 'center' \| 'space-between' \| 'space-around' \| 'space-evenly'` | `'flex-start'` | Distributes space along the main axis. | | `align` | `'flex-start' \| 'flex-end' \| 'center' \| 'stretch' \| 'baseline'` | `'flex-start'` | Aligns items along the cross axis. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying div element. | The component also accepts all standard HTML `
` attributes. ## Accessibility - Renders a semantic `
` element with `display: flex` - No special ARIA attributes are needed for layout containers - Content maintains its natural DOM order for assistive technologies ========================= # Components — Controls ========================= --- # CartesianPicker > 2D point picker for selecting coordinates on a cartesian grid with crosshair, snap-to-grid, and custom rendering. _Source: /components/controls/cartesian-picker_ Interactive 2D point picker for selecting X/Y coordinates on a cartesian grid. Features a draggable marker, crosshair display, origin axes, grid lines with labels, snap-to-grid, and custom background rendering. Commonly used for panning controls, UV coordinate selection, and spatial parameter adjustment. **Live Preview** ## Import ```tsx import { CartesianPicker } from 'entangle-ui'; ``` ## Usage ```tsx const [point, setPoint] = useState({ x: 0, y: 0 }); ; ``` ## Controlled vs Uncontrolled **Controlled** ```tsx // Controlled // Uncontrolled with default ``` ## Domain Range Set custom domain ranges for each axis. The default is -1 to 1 for both axes. **Domain Range** ```tsx // Normalized 0-1 range // Wider range ``` ## Grid and Labels **Grid and Labels** ```tsx ``` ## Crosshair The crosshair shows lines from the marker to the edges. It can be solid or dashed, and can be disabled. **Crosshair** ```tsx ``` ## Snap to Grid Enable grid snapping during drag. Hold Ctrl to toggle snapping behavior. You can also provide a discrete `step` for per-axis snapping. **Snap to Grid** ```tsx // Snap to grid subdivisions // Discrete step snapping // Per-axis step ``` ## Clamping By default, the point is clamped within domain bounds. Disable this to allow values outside the visible range. **Clamping** ```tsx ``` ## Responsive Mode When `responsive` is true, the picker fills its parent container using a ResizeObserver. **Responsive Mode** ```tsx
``` ## Custom Background Use `renderBackground` to draw behind the grid, such as a heatmap or gradient. **Custom Background** ```tsx { // Draw a radial gradient const cx = info.width / 2; const cy = info.height / 2; const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, info.width / 2); grad.addColorStop(0, 'rgba(0, 122, 204, 0.3)'); grad.addColorStop(1, 'rgba(0, 0, 0, 0)'); ctx.fillStyle = grad; ctx.fillRect(0, 0, info.width, info.height); }} /> ``` ## Custom Bottom Bar **Custom Bottom Bar** ```tsx (
X: {point.x.toFixed(2)}, Y: {point.y.toFixed(2)} {isDragging && ' (dragging)'}
)} /> ``` ## Change Complete Use `onChangeComplete` for undo integration. It fires on pointer up after a drag. **Change Complete** ```tsx { undoStack.push(finalPoint); }} /> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `value` | `Point2D` | — | Current point value (controlled). Object with x and y properties. | | `defaultValue` | `Point2D` | `{ x: 0, y: 0 }` | Default point value (uncontrolled). | | `domainX` | `[number, number]` | `[-1, 1]` | Domain range for the X axis. | | `domainY` | `[number, number]` | `[-1, 1]` | Domain range for the Y axis. | | `showGrid` | `boolean` | `true` | Whether to show grid lines. | | `gridSubdivisions` | `number` | `4` | Number of grid subdivision lines. | | `showAxisLabels` | `boolean` | `true` | Whether to show X/Y axis value labels. | | `showOriginAxes` | `boolean` | `true` | Whether to draw emphasized origin axes (X=0, Y=0). | | `showCrosshair` | `boolean` | `true` | Whether to draw crosshair lines from the point to edges. | | `crosshairStyle` | `'solid' \| 'dashed'` | `'dashed'` | Crosshair line style. | | `labelX` | `string` | — | X axis name label. | | `labelY` | `string` | — | Y axis name label. | | `markerRadius` | `number` | `6` | Point marker radius in pixels. | | `snapToGrid` | `boolean` | `false` | Snap to grid subdivisions while dragging (Ctrl toggles). | | `step` | `number \| [number, number]` | — | Discrete step for value snapping. Single value or [stepX, stepY]. | | `clampToRange` | `boolean` | `true` | Whether to clamp the point within domain bounds. | | `precision` | `number` | `2` | Number format precision for displayed values. | | `width` | `number` | `200` | Width of the picker in pixels. | | `height` | `number` | `200` | Height of the picker in pixels. | | `responsive` | `boolean` | `false` | Whether the picker fills its parent container via ResizeObserver. | | `disabled` | `boolean` | `false` | Whether the picker is disabled. | | `readOnly` | `boolean` | `false` | Whether the picker is read-only. | | `onChange` | `(point: Point2D) => void` | — | Callback fired continuously during drag. | | `onChangeComplete` | `(point: Point2D) => void` | — | Callback fired when editing is committed (pointer up). | | `renderBackground` | `(ctx: CanvasRenderingContext2D, info: CanvasBackgroundInfo) => void` | — | Custom background renderer for the canvas. | | `renderBottomBar` | `(info: CartesianPickerInfo) => ReactNode` | — | Render prop for custom content below the canvas. | | `className` | `string` | — | Additional CSS class names. | | `testId` | `string` | — | Test identifier for automated testing. | ## Accessibility - Canvas has `aria-label="Cartesian point picker"` and `aria-roledescription` with the current coordinates - Live region announces coordinate changes during interaction - Keyboard: Arrow keys move the point by step increments - The picker is focusable and supports pointer and touch interactions - `disabled` state prevents all interaction --- # ColorPicker > Full-featured color picker with saturation/value area, hue slider, alpha channel, input modes, presets, and built-in palettes. _Source: /components/controls/color-picker_ Full-featured color picker for editor interfaces. Includes a saturation/value color area, hue slider, optional alpha slider, multiple input modes (HEX, RGB, HSL), color presets, built-in palettes, and optional EyeDropper support. Opens as a popover by default, or can be rendered inline. **Live Preview** ## Import ```tsx import { ColorPicker } from 'entangle-ui'; ``` ## Usage ```tsx const [color, setColor] = useState('#007acc'); ; ``` ## Inline Mode By default, the ColorPicker opens as a popover from a swatch trigger. Set `inline` to render the picker panel directly. ```tsx ``` ## Alpha Channel Enable the alpha slider with `showAlpha`. ```tsx ``` ## Output Format The `format` prop controls the string format passed to `onChange`. ```tsx // HEX output (default) // RGB output // HSL output ``` ## Input Modes Control which input fields are available in the picker with `inputModes` and set the default with `defaultInputMode`. ```tsx ``` ## Presets Provide a set of preset colors for quick selection. ```tsx ``` ## Built-in Palettes Use the `palette` prop with a built-in palette name to show a full color palette in the picker. ```tsx // Material Design palette // Tailwind CSS palette ``` Available built-in palettes: | Palette | Description | | ------------ | -------------------------------------------- | | `material` | Material Design colors (19 hues x 10 shades) | | `tailwind` | Tailwind CSS colors (22 hues x 11 shades) | | `pastel` | Soft pastel colors | | `earth` | Natural earth tones | | `neon` | High-saturation neon colors | | `monochrome` | Neutral, warm, and cool grays | | `skin-tones` | Portrait/illustration skin palette | | `vintage` | Desaturated retro film colors | You can also pass a custom `Palette` object or an array of `PaletteColor[]`. ## Swatch Trigger When not inline, the picker opens from a color swatch. Customize its size and shape. ```tsx ``` ## EyeDropper Enable the native EyeDropper API (Chromium browsers only) with `showEyeDropper`. The button is automatically hidden if the browser does not support the API. ```tsx ``` ## Change Complete Use `onChangeComplete` for actions that should only run when the user finishes a drag interaction (e.g., undo history). ```tsx { addToUndoStack(finalColor); }} /> ``` ## Sizes ```tsx ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `value` | `string` | — | Current color value (controlled). Accepts any valid CSS color string. | | `defaultValue` | `string` | `'#007acc'` | Default color value (uncontrolled). | | `format` | `'hex' \| 'rgb' \| 'hsl'` | `'hex'` | Output format for the onChange callback. | | `showAlpha` | `boolean` | `false` | Whether to show the alpha channel slider. | | `inputModes` | `('hex' \| 'rgb' \| 'hsl')[]` | `['hex', 'rgb', 'hsl']` | Available input mode tabs in the picker. | | `defaultInputMode` | `'hex' \| 'rgb' \| 'hsl'` | `'hex'` | Default active input mode. | | `presets` | `ColorPreset[]` | — | Array of preset colors for quick selection. Each preset has color and optional label. | | `palette` | `string \| Palette \| PaletteColor[]` | — | Built-in palette name, custom Palette object, or array of PaletteColor. | | `showEyeDropper` | `boolean` | `false` | Whether to show the EyeDropper button (Chromium-only). | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Size of the swatch trigger. | | `swatchShape` | `'square' \| 'circle'` | `'square'` | Shape of the swatch trigger. | | `label` | `string` | — | Label displayed next to the swatch trigger. | | `disabled` | `boolean` | `false` | Whether the picker is disabled. | | `inline` | `boolean` | `false` | Whether to render the picker inline (no popover). | | `pickerWidth` | `number` | `240` | Width of the picker panel in pixels. | | `onChange` | `(color: string) => void` | — | Callback fired continuously as the color changes. | | `onChangeComplete` | `(color: string) => void` | — | Callback fired when a color change is committed (drag end). | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | ## Accessibility - The swatch trigger opens the picker in a Popover with proper focus management - Color area and sliders support mouse and touch drag interactions - Input fields allow direct typed entry of color values - Preset and palette colors are clickable for quick selection - EyeDropper button uses the native browser API when available --- # Combobox > Single-value select with an editable input, built-in fuzzy filtering, and optional free-solo or creatable modes. _Source: /components/controls/combobox_ A single-value select with an editable input. The option list filters as the user types — by default with the built-in fuzzy matcher shared with [CommandPalette](/components/feedback/command-palette), or with a custom `filterFn`. Reach for `Combobox` when the option set is too large for a plain `Select`, when the user might type a value that isn't there (`freeSolo`), or when they should be able to add new entries on the fly (`creatable`). **Live Preview** ## When to use - **Combobox** — long lists where typing is faster than scrolling, or any flow where the user might type a value not in the list. - **Select** — short, fixed lists where the value must come from a known set. Lighter weight. - **MultiSelect** — same shape as `Combobox` but the user picks several values at once. ## Import ```tsx import { Combobox } from 'entangle-ui'; import type { ComboboxOption } from 'entangle-ui'; ``` ## Usage ```tsx const options: ComboboxOption[] = [ { value: 'react', label: 'React' }, { value: 'vue', label: 'Vue' }, { value: 'svelte', label: 'Svelte' }, ]; ; ``` **Basic** ## Controlled vs uncontrolled Both modes are supported. `onChange` receives `null` when the value is cleared. ```tsx // Controlled const [value, setValue] = useState(null); // Uncontrolled ``` ## Sizes **Sizes** ```tsx ``` ## Variants **Variants** ```tsx ``` ## Free-solo mode `freeSolo` lets the user commit an arbitrary string that isn't in the options list. The `onChange` handler fires with the typed value on `Enter` or blur. **Free-solo** ```tsx ``` ## Creatable `creatable` surfaces a `Create ""` row when the query has no exact match. Pressing Enter (or clicking the row) invokes `onCreate` with the typed string — the consumer is responsible for adding the new option to the list. Implies `freeSolo`. **Creatable** ```tsx const [options, setOptions] = useState(initialOptions); const [value, setValue] = useState(null); { const created = { value: input, label: input }; setOptions(prev => [...prev, created]); setValue(input); }} />; ``` Customise the row's label via `createLabel`: ```tsx ( <> Add new tag: {q} )} options={options} onCreate={handleCreate} /> ``` ## Loading Async option sources hand the component a `loading` flag — the dropdown shows a spinner row instead of options. Pair with `onInputChange` to debounce a remote search and re-feed `options` once the response lands. **Loading** ```tsx debouncedSearch(query)} /> ``` ## Open on focus By default the dropdown opens once the user starts typing. `openOnFocus` opens it the moment the input receives focus — useful for short curated lists where the input is more of a "filterable picker". **Open on focus** ```tsx ``` ## Custom filter Replace the built-in fuzzy matcher with your own logic — `filterFn` can return a boolean (keep / drop) or a numeric score (higher = ranked higher). ```tsx option.value.startsWith(query.toLowerCase()) ? 2 : 0 } /> ``` ## Error state **Error** ```tsx ``` ## Form integration Use `name` to render a hidden `` with the selected value for native form submission. ```tsx ``` ## Accessibility - Input renders with `role="combobox"`, `aria-haspopup="listbox"`, and `aria-expanded` reflecting the dropdown state. - Dropdown is a `role="listbox"` with `role="option"` children; the active option is reflected via `aria-activedescendant`. - Keyboard: **Arrow Up / Down** to navigate, **Enter** to select (or create in `creatable` mode), **Escape** to close, **Home / End** for first / last option, **Backspace** on an empty input clears a selected value when `clearable`. - `aria-invalid` is set in the error state; `aria-required` when `required` is set. - `aria-describedby` links to the helper / error text. - Clear button (when shown) has `aria-label="Clear selection"`. ## API Reference ### `` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `value` | `T \| null` | — | Controlled selected value. | | `defaultValue` | `T` | — | Default selected value (uncontrolled). | | `options` | `ComboboxOption[]` | — | Available options. | | `placeholder` | `string` | — | Placeholder rendered when the input is empty. | | `filterFn` | `(option: ComboboxOption, query: string) => boolean \| number` | — | Custom filter. Boolean keeps / drops; number is a score (higher = ranked higher). Defaults to the built-in fuzzy matcher. | | `emptyMessage` | `string` | `'No results found'` | Message shown when filtering yields no results. | | `loading` | `boolean` | `false` | Loading indicator — replaces options with a spinner row. | | `loadingMessage` | `string` | `'Loading...'` | Message shown while loading. | | `freeSolo` | `boolean` | `false` | Allow values not present in `options`. `onChange` fires with the typed string on blur / Enter. | | `creatable` | `boolean` | `false` | Surface a "Create " row when the query has no exact match. Implies `freeSolo`. | | `createLabel` | `(query: string) => ReactNode` | — | Render the create row label. Defaults to `Create ""`. | | `openOnFocus` | `boolean` | `false` | Open the dropdown automatically when the input gains focus, even with no query. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Component size. | | `variant` | `'default' \| 'ghost' \| 'filled'` | `'default'` | Visual variant. | | `label` | `string` | — | Label rendered above the input. | | `helperText` | `string` | — | Helper text rendered below. | | `error` | `boolean` | `false` | Error state. | | `errorMessage` | `string` | — | Error message — overrides `helperText` when `error` is true. | | `disabled` | `boolean` | `false` | Disable the input. | | `required` | `boolean` | `false` | Mark the field as required. | | `readOnly` | `boolean` | `false` | Read-only — input value is fixed. | | `clearable` | `boolean` | `false` | Show a clear button when a value is selected. | | `maxDropdownHeight` | `number` | `240` | Maximum dropdown height in pixels. | | `minDropdownWidth` | `number` | — | Minimum dropdown width in pixels. | | `name` | `string` | — | Hidden form-input name. | | `onChange` | `(value: T \| null) => void` | — | Selection change handler. `null` is passed when cleared. | | `onInputChange` | `(input: string) => void` | — | Free-text change handler — fires every keystroke regardless of selection. | | `onCreate` | `(input: string) => void` | — | Called when the user creates a new option (only with `creatable`). Falls back to `onChange` when omitted. | | `onOpenChange` | `(open: boolean) => void` | — | Open state change handler. | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the underlying input element. | ### `ComboboxOption` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `value` *(required)* | `T` | — | Unique value used for selection. | | `label` | `string` | — | Display label. Falls back to `value` when omitted. | | `description` | `string` | — | Optional description rendered next to the label. | | `icon` | `ReactNode` | — | Optional icon rendered before the label. | | `disabled` | `boolean` | — | Whether this option is disabled. | --- # CurveEditor > Interactive bezier curve editor for animation timing, color grading, and value remapping with keyframes, tangent modes, and presets. _Source: /components/controls/curve-editor_ Interactive bezier curve editor for professional tools. Supports keyframe editing with multiple tangent modes, built-in and custom presets, grid and axis labels, snap-to-grid, and custom render props for backgrounds and bottom bars. Used for animation curves, color grading, easing functions, and value remapping. **Live Preview** ## Import ```tsx import { CurveEditor } from 'entangle-ui'; ``` ## Usage ```tsx const [curve, setCurve] = useState(undefined); ; ``` The component defaults to an ease-in-out curve when no value is provided. ## Controlled vs Uncontrolled **Controlled** ```tsx // Controlled // Uncontrolled with default ``` ## Keyframe Interaction - **Double-click** on the curve to add a new keyframe - **Click** a keyframe to select it (Shift+Click for multi-select, box select by dragging on empty space) - **Drag** a keyframe to move it - **Delete/Backspace** to remove selected keyframes - **Double-click** a keyframe to cycle tangent modes - **Keyboard shortcuts 1-6** set tangent modes on selected keyframes ## Tangent Modes Each keyframe has a tangent mode controlling how the curve passes through it. | Mode | Description | | ---------- | ------------------------------------------------------ | | `free` | Each handle moves independently | | `aligned` | Handles stay co-linear but can differ in length | | `mirrored` | Handles are symmetric (same angle and length) | | `auto` | Smooth catmull-rom style, auto-computed from neighbors | | `linear` | No handles, straight line segments | | `step` | Constant value until next keyframe (step function) | The toolbar provides buttons for switching tangent modes on selected keyframes. **Tangent Modes** ## Lock Tangents When `lockTangents` is true, tangent handle editing is hidden and disabled. The curve still renders normally using each keyframe's existing tangent mode. **Lock Tangents** ```tsx ``` ## Presets Built-in presets include ease-in, ease-out, ease-in-out, linear, and more. You can add custom presets that appear alongside them in the toolbar. **Presets** ```tsx ``` ## Grid and Labels **Grid and Labels** ```tsx ``` ## Snap to Grid Hold Ctrl to toggle snapping, or enable it as the default. **Snap to Grid** ```tsx ``` ## Constraints **Constraints** ```tsx ``` ## Responsive Mode When `responsive` is true, the editor fills its parent container using a ResizeObserver, ignoring the `width` prop. **Responsive Mode** ```tsx
``` ## Custom Background Use `renderBackground` to draw behind the curve, such as a histogram or gradient (like Photoshop/Lightroom curves). **Custom Background** ```tsx { // Draw a gradient behind the curve const grad = ctx.createLinearGradient(0, info.height, 0, 0); grad.addColorStop(0, 'rgba(0, 0, 0, 0.3)'); grad.addColorStop(1, 'rgba(255, 255, 255, 0.1)'); ctx.fillStyle = grad; ctx.fillRect(0, 0, info.width, info.height); }} /> ``` ## Custom Bottom Bar Use `renderBottomBar` for custom content below the canvas, such as coordinate readouts or channel selectors. **Custom Bottom Bar** ```tsx (
{selectedKeyframes.length > 0 && ( Selected: ({selectedKeyframes[0].x.toFixed(2)}, {selectedKeyframes[0].y.toFixed(2)}) )}
)} /> ``` ## Change Complete Use `onChangeComplete` for undo system integration. It fires on drag end, keyframe add, and keyframe delete. **Change Complete** ```tsx { undoStack.push(finalCurve); }} /> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `value` | `CurveData` | — | Curve data (controlled). | | `defaultValue` | `CurveData` | `ease-in-out preset` | Default curve data (uncontrolled). | | `width` | `number` | `320` | Width of the editor in pixels. | | `height` | `number` | `200` | Height of the editor in pixels. | | `responsive` | `boolean` | `false` | Whether the editor fills its parent container via ResizeObserver. | | `showToolbar` | `boolean` | `true` | Whether to show the toolbar with presets and tangent mode buttons. | | `showGrid` | `boolean` | `true` | Whether to show grid lines. | | `gridSubdivisions` | `number` | `4` | Number of grid subdivision lines between domain bounds. | | `showAxisLabels` | `boolean` | `true` | Whether to display X/Y axis value labels on the grid. | | `allowAdd` | `boolean` | `true` | Whether double-clicking the curve adds a new keyframe. | | `allowDelete` | `boolean` | `true` | Whether Delete/Backspace removes selected keyframes. | | `maxKeyframes` | `number` | `Infinity` | Maximum number of keyframes allowed. | | `lockEndpoints` | `boolean` | `true` | Whether first/last keyframe X positions are locked. | | `minKeyframeDistance` | `number` | `0.001` | Minimum distance between keyframes on the X axis. | | `clampY` | `boolean` | `true` | Whether Y values are clamped to domain bounds. | | `snapToGrid` | `boolean` | `false` | Snap to grid while dragging (Ctrl toggles). | | `precision` | `number` | `3` | Number format precision for displayed values. | | `labelX` | `string` | — | X axis label (e.g., "Time", "Input"). | | `labelY` | `string` | — | Y axis label (e.g., "Value", "Output"). | | `presets` | `CurvePreset[]` | — | Custom preset curves, merged with built-in presets. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Component size affecting toolbar and label sizing. | | `disabled` | `boolean` | `false` | Whether the editor is disabled. | | `readOnly` | `boolean` | `false` | Whether the editor is read-only (viewable but not editable). | | `curveColor` | `string` | `theme accent color` | CSS color for the curve line. | | `curveWidth` | `number` | `2` | Curve line width in pixels. | | `lockTangents` | `boolean` | `false` | Hides tangent handles and disables tangent editing UI. | | `onChange` | `(curve: CurveData) => void` | — | Callback fired continuously during drag. | | `onChangeComplete` | `(curve: CurveData) => void` | — | Callback fired when editing is committed (drag end, add, delete). | | `onSelectionChange` | `(selectedIds: string[]) => void` | — | Callback when keyframe selection changes. | | `renderBackground` | `(ctx: CanvasRenderingContext2D, info: CurveBackgroundInfo) => void` | — | Custom background renderer for the canvas. | | `renderBottomBar` | `(info: CurveBottomBarInfo) => ReactNode` | — | Render prop for custom content below the canvas. | | `className` | `string` | — | Additional CSS class names. | | `testId` | `string` | — | Test identifier for automated testing. | ### CurveData | Property | Type | Description | | -------------- | ------------------------------------------------- | --------------------------------------- | | `keyframes` | `CurveKeyframe[]` | Ordered array of keyframes sorted by x. | | `domainX` | `[number, number]` | Domain bounds for the X axis. | | `domainY` | `[number, number]` | Domain bounds for the Y axis. | | `preInfinity` | `'constant' \| 'linear' \| 'cycle' \| 'pingpong'` | Behavior before the first keyframe. | | `postInfinity` | `'constant' \| 'linear' \| 'cycle' \| 'pingpong'` | Behavior after the last keyframe. | ### CurveKeyframe | Property | Type | Description | | ------------- | -------------------------- | ------------------------------------------- | | `x` | `number` | X position in domain space. | | `y` | `number` | Y value at this position. | | `handleIn` | `{ x: number; y: number }` | Left tangent handle offset (relative). | | `handleOut` | `{ x: number; y: number }` | Right tangent handle offset (relative). | | `tangentMode` | `TangentMode` | Tangent mode for this keyframe. | | `id` | `string` | Unique ID (auto-generated if not provided). | ## Accessibility - Canvas-based rendering with keyboard support for keyframe manipulation - Arrow keys move selected keyframes - Delete/Backspace removes selected keyframes - Number keys 1-6 set tangent modes on selected keyframes - Box select via Shift+drag on empty canvas area - Multi-select via Shift+Click on keyframes --- # FileTree > File-system-flavored TreeView specialization with automatic file-type icons and drag-and-drop import of OS files. _Source: /components/controls/file-tree_ A file-system-flavored specialization of [`TreeView`](/components/controls/tree-view). It maps a file/folder model onto the tree, auto-assigns **file-type icons** by extension (plus open/closed folder glyphs), and adds **drag-and-drop import** of OS files onto a folder or the root. Expansion, selection, keyboard navigation, and sizing are inherited from `TreeView` unchanged. **Live Preview** ## Import ```tsx import { FileTree } from 'entangle-ui'; import type { FileTreeNode } from 'entangle-ui'; ``` ## Usage `FileTree` takes a nested array of `FileTreeNode`s. Each node has a `kind` of `'file'` or `'folder'`; folders carry `children`. File-type icons are derived from the file `name` (or an explicit `ext`), so you don't assign them yourself. ```tsx const nodes: FileTreeNode[] = [ { id: 'src', name: 'src', kind: 'folder', children: [ { id: 'btn', name: 'Button.tsx', kind: 'file' }, { id: 'logo', name: 'logo.svg', kind: 'file' }, ], }, { id: 'readme', name: 'README.md', kind: 'file' }, ]; ; ``` ## Expanding folders Clicking anywhere on a folder row toggles it open/closed — not just the chevron (`expandOnClick`, on by default). Files are unaffected. Set `expandOnClick={false}` to require a chevron click instead. ```tsx // Whole-row toggle (default) // Require the chevron ``` ## Drag-and-drop import Provide `onImport` to enable the import drop zone. Dropping OS files onto a folder targets that folder; dropping a file row targets its parent folder; dropping on empty space targets the root (`targetFolder: null`). The active target folder is highlighted while dragging. `FileTree` only reports intent — apply the change to your data and pass back fresh `nodes`. **Drag-and-drop import** ```tsx { uploadInto(targetFolder?.id ?? 'root', files); }} /> ``` ## File-type icons Icons are resolved from the file extension into a small set of buckets (image, media, code, archive, text) backed by the library icon set, with open/closed folder glyphs. Pass `resolveIcon` to override per node — return `undefined` to fall back to the built-in icon. **File-type icons** ```tsx node.name.endsWith('.json') ? : undefined } /> ``` The `classifyExtension` / `getFileIconKind` helpers used internally are also exported, in case you want to drive your own UI from the same mapping. ### Coloring icons Built-in icons follow the theme (folders use `text.secondary`, files `text.muted`), so a theme change re-colors them. For per-type or per-node colors, return a colored icon from `resolveIcon` — every library icon takes a `color` prop (a theme key like `accent` / `success` / `warning`, or any CSS color). **Colored icons by type** ```tsx { if (node.kind === 'folder') { return expanded ? ( ) : ( ); } if (node.name.endsWith('.png')) return ; return undefined; // fall back to the built-in icon }} /> ``` ## Selection Selection reuses `TreeView`'s controlled/uncontrolled model verbatim — `single` (default), `multiple` (Ctrl / Shift click, Ctrl+A), or `none`. **Selection** ```tsx ``` ## Sizes **Sizes** ```tsx ``` ## Relationship to TreeView `FileTree` renders a `TreeView` internally and feeds it derived nodes — it does **not** reimplement expansion, selection, or keyboard navigation. The same controlled/uncontrolled props (`expandedIds` / `defaultExpandedIds`, `selectedIds` / `defaultSelectedIds`) and the same keyboard behaviour apply. Reach for `TreeView` directly when you need a generic hierarchy; reach for `FileTree` when the data is files and folders. The drag-and-drop import is built on top of `TreeView`'s generic drop-target props (`dropTargetId`, `onNodeDragOver` / `onNodeDragLeave` / `onNodeDrop`), which are available for building your own drop interactions on a plain `TreeView` as well. ## Styling FileTree is themed entirely through the `--etui-*` theme contract — no colors, spacings, or fonts are hard-coded. There are three ways to restyle it. ### Render & content overrides - `className` / `style` are applied to the **root container** (`style` is also the place to set `--etui-*` overrides — they cascade to every row). - `resolveIcon` swaps a node's icon; `renderNode` replaces a row's whole inner content (icon + label); `renderActions` adds trailing controls. ### Targeting hooks The internal class names are compiled (hashed) by Vanilla Extract, so target the stable structural hooks instead: | Selector | Matches | | -------------------------------------------- | ---------------------------------------------------- | | `[role="tree"]` | The tree region. | | `[role="treeitem"]` | Every row. | | `[role="treeitem"][aria-selected="true"]` | Selected rows. | | `[role="treeitem"][data-drop-target="true"]` | The folder currently highlighted as a drop target. | | `[data-root-active="true"]` | The container while files are dragged over the root. | | `#treenode-` | A specific row by node id. | ### Theme tokens consumed Override these on any ancestor to re-skin FileTree (or use `createCustomTheme(...)` for a whole-app palette): | Token | Used for | | -------------------------------------------- | -------------------------------------------------------------------- | | `--etui-color-accent-primary` | Selected-row tint and the active drop-target highlight (row + root). | | `--etui-color-surface-hover` | Row hover background. | | `--etui-color-border-focus` | Focused-row ring. | | `--etui-color-border-default` | Guide lines. | | `--etui-color-text-primary` | File / folder names; chevron hover. | | `--etui-color-text-secondary` | Folder icons. | | `--etui-color-text-muted` | File icons, chevrons, and empty-state text. | | `--etui-radius-sm` | Container corner rounding. | | `--etui-spacing-xs`, `--etui-spacing-md` | Icon gap / row padding; empty-state padding. | | `--etui-font-size-md`, `--etui-font-size-lg` | Size-scaled label text. | | `--etui-line-height-normal` | Label line height. | | `--etui-transition-fast` | Chevron rotation and hover transitions. | **Re-skinned via token overrides** ```tsx ``` ## Internationalization FileTree renders no copy of its own — file and folder names come from your data, and the file-type icons are decorative. The only built-in strings are the tree's accessible name and the empty-state text, both overridable through the `labels` prop (a `Partial`, so anything you omit keeps its English default). An explicit `aria-label` / `emptyContent` still wins over the matching label. **Localized labels (Polish)** ```tsx ``` The English defaults are exported as `DEFAULT_FILE_TREE_LABELS` (spread and tweak it when you only need to change one key). | Key | Type | Default | | ------------ | -------- | ------------- | | `treeLabel` | `string` | `"File tree"` | | `emptyLabel` | `string` | `"No files"` | ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `nodes` | `FileTreeNode[]` | — | Tree data — array of root-level files/folders. | | `expandedIds` | `string[]` | — | Expanded node IDs (controlled). | | `defaultExpandedIds` | `string[]` | — | Default expanded node IDs (uncontrolled). | | `onExpandedChange` | `(expandedIds: string[]) => void` | — | Fired when expanded nodes change. | | `selectedIds` | `string[]` | — | Selected node IDs (controlled). | | `defaultSelectedIds` | `string[]` | — | Default selected node IDs (uncontrolled). | | `selectionMode` | `'single' \| 'multiple' \| 'none'` | `'single'` | Selection mode (reused from TreeView). | | `onSelectionChange` | `(selectedIds: string[]) => void` | — | Fired when selected nodes change. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Row size. | | `indent` | `number` | `16` | Indentation per depth level in pixels. | | `showChevrons` | `boolean` | `true` | Whether to show expand/collapse chevrons for folders. | | `showGuideLines` | `boolean` | `false` | Whether to show connecting guide lines. | | `expandOnClick` | `boolean` | `true` | Toggle a folder open/closed when its whole row is clicked (not just the chevron). | | `maxHeight` | `number \| string` | — | Maximum height before the tree scrolls. | | `resolveIcon` | `(node: FileTreeNode, state: { expanded: boolean }) => ReactNode` | — | Override a node icon. Return undefined to fall back to the built-in extension map. | | `renderNode` | `(node: FileTreeNode, state: FileTreeNodeState) => ReactNode` | — | Fully replace a node's inner content (icon + label). | | `renderActions` | `(node: FileTreeNode, state: FileTreeNodeState) => ReactNode` | — | Render trailing actions on the right of a row. | | `emptyContent` | `ReactNode` | — | Content shown when nodes is empty. | | `onImport` | `(payload: { files: File[]; targetFolder: FileTreeNode \| null }) => void` | — | Fired when OS files are dropped onto a folder or the root. Presence enables the import drop zone. | | `onNodeClick` | `(node: FileTreeNode, event: MouseEvent) => void` | — | Fired when a node is clicked. | | `onNodeDoubleClick` | `(node: FileTreeNode, event: MouseEvent) => void` | — | Fired when a node is double-clicked. | | `onNodeContextMenu` | `(node: FileTreeNode, event: MouseEvent) => void` | — | Fired when a node is right-clicked. | | `labels` | `Partial` | — | Override built-in strings (tree accessible name, empty-state text). Merged onto the defaults. | | `aria-label` | `string` | — | Accessible name for the tree. Wins over labels.treeLabel; lands on the role="tree" element. | | `className` | `string` | — | Additional CSS class names (on the root container). | | `testId` | `string` | — | Test identifier for automated testing. | ### FileTreeNode | Property | Type | Description | | ---------- | ------------------------- | -------------------------------------------------- | | `id` | `string` | Stable unique id within the tree. | | `name` | `string` | Display name, e.g. `"Button.tsx"`. | | `kind` | `'file' \| 'folder'` | File vs. folder. Folders accept dropped files. | | `ext` | `string` | Extension override (no dot). Inferred from `name`. | | `path` | `string` | Optional path (informational; surfaced unchanged). | | `children` | `FileTreeNode[]` | Child entries (folders). | | `disabled` | `boolean` | Dim + non-interactive. | | `data` | `Record` | Arbitrary consumer payload. | ## Accessibility Accessibility is inherited from `TreeView`: - The container is `role="tree"`; each row is `role="treeitem"` with `aria-expanded` / `aria-selected` and `aria-level`. - `aria-activedescendant` tracks the focused node; focus is scrolled into view. - Full keyboard navigation (Arrow keys, Home/End, Enter, Space, Ctrl+A, Shift+Arrow range selection). File-type icons are decorative (`aria-hidden`), so the file name is the accessible label. --- # FileUploader > Drag-and-drop file uploader with type and size validation, per-row status, and an animated progress bar. _Source: /components/controls/file-uploader_ A drag-and-drop dropzone with a click-to-browse fallback. Enforces MIME / extension matching, file size bounds, and per-file count caps, surfaces a list of files with `pending` / `uploading` / `done` / `error` status and an animated progress bar. The component itself is presentational — the consumer drives the actual upload (fetch, XHR, SDK) and reflects progress back through controlled `value`. **Live Preview** ## How it works 1. The user drops or selects files. `FileUploader` validates each one against `accept`, `maxSize`, `minSize`, `maxFiles`, and the optional `validate` callback. 2. Accepted files are appended to the items list with `status: 'pending'` and `onFilesAdd` fires with the raw `File` objects. 3. The consumer kicks off the upload externally and updates each item's `status` / `progress` by passing a controlled `value`. 4. Rejected files are reported via `onReject` along with a reason — never silently dropped. ## Import ```tsx import { FileUploader } from 'entangle-ui'; import type { FileUploaderItem } from 'entangle-ui'; ``` ## Usage The simplest usage takes no value at all — the component manages the list internally. Useful for forms where you only need the final list at submit time. ```tsx ``` For real uploads, control the list: ```tsx const [items, setItems] = useState([]); uploadAll(files, setItems)} accept="image/*,.pdf" maxSize={5 * 1024 * 1024} />; ``` ## Single file mode `multiple={false}` switches to single-file mode — selecting a new file replaces the previous one. **Single file** ```tsx ``` ## Sizes **Sizes** ```tsx ``` ## With simulated upload A realistic end-to-end demo: drop a file, watch its row progress to `done`. Rejections surface below the dropzone. **Simulated upload** ```tsx const [items, setItems] = useState([]); toast.error(`${file.name}: ${reason}`)} onFilesAdd={files => { // Kick off the upload for each just-added file files.forEach(file => startUpload(file, setItems)); }} />; ``` The per-row UI shows the file name, formatted size, status badge, and animated progress bar — all driven from the `FileUploaderItem` shape: ```ts interface FileUploaderItem { id: string; file: File; status: 'pending' | 'uploading' | 'done' | 'error'; progress?: number; // 0–100 errorMessage?: string; } ``` ## Validation Built-in checks for type, size, and count are available via the matching props. For anything beyond that, pass a synchronous `validate` callback — return `false` to reject silently, or a string to use as the rejection reason. **Validation** ```tsx { if (file.name.startsWith('.')) return 'no-dotfiles'; return true; }} onReject={(file, reason) => console.warn(file.name, reason)} /> ``` | Reason | When | | -------------- | ------------------------------------------------------------------ | | `'wrong-type'` | File MIME or extension does not match `accept`. | | `'too-large'` | `file.size > maxSize`. | | `'too-small'` | `file.size < minSize`. | | `'max-files'` | The list already holds `maxFiles` items. | | `'invalid'` | The `validate` callback returned `false`. | | _custom_ | The string returned from a `validate` callback is forwarded as-is. | ## Error state When you need to surface a form-level error (e.g. the server rejected the batch), use the `error` / `errorMessage` props. **Error** ```tsx ``` ## Disabled **Disabled** ```tsx ``` ## Accessibility - Dropzone is keyboard-reachable as `role="button"` with `tabIndex={0}`; **Enter** and **Space** open the native file picker. - Drag-over state is reflected visually and via `aria-busy="true"`. - `aria-invalid` is set in the error state; `aria-required` when `required` is set. - `aria-describedby` links to the helper / error text. - Remove buttons on each row have a labelled `aria-label="Remove "`. - The hidden `` carries the `name` attribute when supplied, so native form submission still works for simple use cases. ## API Reference ### `` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `value` | `FileUploaderItem[]` | — | Controlled list of items. | | `defaultValue` | `FileUploaderItem[]` | — | Default list (uncontrolled). | | `onChange` | `(items: FileUploaderItem[]) => void` | — | Called whenever the items list changes. | | `onFilesAdd` | `(files: File[]) => void` | — | Called with the raw `File` objects that were just accepted (after validation, before they appear as `pending`). Kick off the upload here. | | `onFileRemove` | `(item: FileUploaderItem) => void` | — | Called when a single item is removed. | | `onReject` | `(file: File, reason: FileUploaderRejectReason) => void` | — | Called for files rejected by validation. | | `accept` | `string` | — | Comma-separated list of accepted MIME types and/or extensions (e.g. `image/*,.pdf`). All types when omitted. | | `multiple` | `boolean` | `true` | Allow selecting more than one file at a time. | | `maxFiles` | `number` | — | Maximum number of files the uploader will hold. Extra files are rejected as `max-files`. | | `maxSize` | `number` | — | Maximum file size in bytes. Files above are rejected as `too-large`. | | `minSize` | `number` | — | Minimum file size in bytes. Files below are rejected as `too-small`. | | `validate` | `(file: File) => boolean \| string` | — | Synchronous validator. `false` rejects as `invalid`, a string is forwarded as the rejection reason. | | `renderItem` | `(state: FileUploaderRenderItemState) => ReactNode` | — | Custom row renderer. When omitted the built-in row is used. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Component size. | | `label` | `string` | — | Label rendered above the dropzone. | | `helperText` | `string` | — | Helper text rendered below. | | `error` | `boolean` | `false` | Error state. | | `errorMessage` | `string` | — | Error message — overrides `helperText` when `error` is true. | | `disabled` | `boolean` | `false` | Disable interaction with the uploader. | | `required` | `boolean` | `false` | Mark the field as required. | | `dropZoneText` | `ReactNode` | `'Drag files here or click to browse'` | Primary text shown inside the dropzone. | | `dropZoneHint` | `ReactNode` | — | Secondary text shown inside the dropzone. | | `name` | `string` | — | Hidden form-input name attribute (for native form submission). | | `className` | `string` | — | Additional CSS class names. | | `style` | `CSSProperties` | — | Inline styles. | | `testId` | `string` | — | Test identifier for automated testing. | | `ref` | `Ref` | — | Ref to the root element. | ### `FileUploaderItem` | Prop | Type | Default | Description | | --- | --- | --- | --- | | `id` *(required)* | `string` | — | Stable identifier — generated for new items, preserved across renders. | | `file` *(required)* | `File` | — | Underlying `File` object. | | `status` *(required)* | `'pending' \| 'uploading' \| 'done' \| 'error'` | — | Current status of the upload pipeline. | | `progress` | `number` | — | Upload progress, 0–100. Optional in `pending` / `done`. | | `errorMessage` | `string` | — | Error message displayed when `status === 'error'`. | ### `FileUploaderRejectReason` `'too-large' | 'too-small' | 'wrong-type' | 'max-files' | 'invalid' | string` Built-in reasons plus any string returned from a `validate` callback. --- # MultiSelect > Multi-value select that renders chosen options as inline chips, with a "+N more" overflow badge and optional search. _Source: /components/controls/multi-select_ A dropdown that lets the user pick several values from a flat or grouped list. Selected values surface as inline chips inside the trigger; once the count exceeds `maxInlineChips`, the rest collapses into a `+N more` badge so the trigger never grows out of its slot. Supports optional in-dropdown search, a `max` cap, and a clear-all button. **Live Preview** ## When to use - **MultiSelect** — picking several values from a known set; the chips show the current selection at a glance. - **Combobox** — single value, with typing-as-search. - **TagInput** — multi-value where the entries are free text, not options from a list. ## Import ```tsx import { MultiSelect } from 'entangle-ui'; import type { MultiSelectOption } from 'entangle-ui'; ``` ## Usage ```tsx const options: MultiSelectOption[] = [ { value: 'react', label: 'React' }, { value: 'vue', label: 'Vue' }, { value: 'svelte', label: 'Svelte' }, ]; ; ``` **Basic** ## Controlled vs uncontrolled ```tsx // Controlled const [value, setValue] = useState([]); // Uncontrolled ``` ## Sizes **Sizes** ```tsx ``` ## Variants **Variants** ```tsx ``` ## Searchable Pass `searchable` to render a filter input at the top of the dropdown. A custom `filterFn` overrides the default substring match. **Searchable** ```tsx ``` ```tsx // Custom filter option.value.startsWith(query.toLowerCase())} options={options} /> ``` ## Grouped options Options can be organised under non-selectable group headers. **Grouped** ```tsx ``` ## Max selection `max` caps the number of selections. Once reached, additional options become un-selectable until the user removes one. **Max** ```tsx ``` ## Inline chips overflow `maxInlineChips` controls how many selected chips render in the trigger before the rest collapse into a `+N more` badge. Set to `Infinity` to render every chip; `0` shows only the count. **Many chips** ```tsx ``` ## Close on select By default the dropdown stays open across selections — useful when the user is picking several values in a row. Set `closeOnSelect` to dismiss after each pick. ```tsx ``` ## Error state **Error** ```tsx ``` ## Form integration Use `name` to render a hidden input with the selected values joined by `,`. ```tsx ``` ## Accessibility - Trigger is a ` )} /> ``` The `state` object provides: `selected`, `expanded`, `focused`, `depth`, `isLeaf`, `isFirst`, `isLast`. ## Guide Lines Show connecting guide lines between parent and child nodes. **Guide Lines** ```tsx ``` ## Indentation Control the indentation per depth level. Default is 16px. **Indentation** ```tsx ``` ## Expand on Select Automatically expand parent nodes when they are selected. **Expand on Select** ```tsx ``` ## Max Height Set a maximum height before the tree scrolls. **Max Height** ```tsx ``` ## Empty State Customize the content shown when the tree is empty. **Empty State** ```tsx No items in scene
} /> ``` ## Sizes **Sizes** ```tsx ``` ## Event Handlers **Event Handlers** ```tsx console.log('Clicked:', node.id)} onNodeDoubleClick={(node, event) => console.log('Double-clicked:', node.id)} onNodeContextMenu={(node, event) => showContextMenu(node, event)} onSelectionChange={ids => setSelected(ids)} onExpandedChange={ids => setExpanded(ids)} /> ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `nodes` | `TreeNodeData[]` | — | Tree data — array of root-level nodes. | | `expandedIds` | `string[]` | — | Expanded node IDs (controlled). | | `defaultExpandedIds` | `string[]` | — | Default expanded node IDs (uncontrolled). | | `selectedIds` | `string[]` | — | Selected node IDs (controlled). | | `defaultSelectedIds` | `string[]` | — | Default selected node IDs (uncontrolled). | | `selectionMode` | `'single' \| 'multiple' \| 'none'` | `'single'` | Selection mode for the tree. | | `renamable` | `boolean` | `false` | Whether to allow inline renaming via double-click. | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Tree node size. | | `indent` | `number` | `16` | Indentation per depth level in pixels. | | `showChevrons` | `boolean` | `true` | Whether to show expand/collapse chevrons for parent nodes. | | `showGuideLines` | `boolean` | `false` | Whether to show connecting guide lines. | | `expandOnSelect` | `boolean` | `false` | Whether to expand parent nodes when selected. | | `expandOnClick` | `boolean` | `false` | Toggle a parent node open/closed when its whole row is clicked (not just the chevron). | | `maxHeight` | `number \| string` | — | Maximum height before scrolling. | | `renderNode` | `(node: TreeNodeData, state: TreeNodeState) => ReactNode` | — | Custom render function for node content. | | `renderActions` | `(node: TreeNodeData, state: TreeNodeState) => ReactNode` | — | Custom render function for trailing actions (right side). | | `emptyContent` | `ReactNode` | — | Content shown when nodes array is empty. | | `onExpandedChange` | `(expandedIds: string[]) => void` | — | Fired when expanded nodes change. | | `onSelectionChange` | `(selectedIds: string[]) => void` | — | Fired when selected nodes change. | | `onNodeClick` | `(node: TreeNodeData, event: MouseEvent) => void` | — | Fired when a node is clicked. | | `onNodeDoubleClick` | `(node: TreeNodeData, event: MouseEvent) => void` | — | Fired when a node is double-clicked. | | `onNodeContextMenu` | `(node: TreeNodeData, event: MouseEvent) => void` | — | Fired when a node is right-clicked. | | `onNodeRename` | `(nodeId: string, newLabel: string) => void` | — | Fired when a node is renamed. | | `className` | `string` | — | Additional CSS class names. | | `testId` | `string` | — | Test identifier for automated testing. | ### TreeNodeData | Property | Type | Description | | ----------- | ------------------------- | ---------------------------------------- | | `id` | `string` | Unique identifier for this node. | | `label` | `string` | Display label. | | `icon` | `ReactNode` | Optional icon rendered before the label. | | `children` | `TreeNodeData[]` | Child nodes (undefined for leaf nodes). | | `disabled` | `boolean` | Whether this node is disabled. | | `draggable` | `boolean` | Whether this node can be dragged. | | `droppable` | `boolean` | Whether this node accepts drop targets. | | `renamable` | `boolean` | Whether this node can be renamed inline. | | `data` | `Record` | Additional custom data. | ## Accessibility - The tree container uses `role="tree"` with `aria-multiselectable` when in multiple mode - Each node uses `role="treeitem"` with proper `aria-expanded` and `aria-selected` attributes - `aria-activedescendant` tracks the focused node - Full keyboard navigation: - **Arrow Down/Up**: Move focus between visible nodes - **Arrow Right**: Expand collapsed node, or move to first child - **Arrow Left**: Collapse expanded node, or move to parent - **Home/End**: Move to first/last visible node - **Enter**: Select focused node - **Space**: Toggle selection (multi-select mode) - **Ctrl+A**: Select all nodes (multi-select mode) - **Shift+Arrow**: Range selection (multi-select mode) - Focused nodes are scrolled into view automatically --- # VectorInput > Grouped numeric input for Vec2, Vec3, and Vec4 vectors with per-axis labels, color coding, and linked proportional editing. _Source: /components/controls/vector-input_ Grouped numeric input for Vec2, Vec3, and Vec4 vectors. Composes NumberInput with per-axis labels, color-coded axis indicators, and optional linked proportional editing. Used for position, rotation, scale, color channels, UV coordinates, and bounding box extents. **Live Preview** ## Import ```tsx import { VectorInput } from 'entangle-ui'; ``` ## Usage ```tsx const [position, setPosition] = useState([0, 0, 0]); ; ``` ## Dimensions The `dimension` prop controls how many axis inputs are shown: 2 (Vec2), 3 (Vec3), or 4 (Vec4). ```tsx // Vec2 // Vec3 (default) // Vec4 ``` ## Label Presets Axis labels are configured via presets that match common use cases. ```tsx // XYZ (default) — Position, rotation, scale // RGBA — Color channels // UVW — Texture coordinates // Custom labels ``` ## Color Presets Axis labels can be color-coded to follow standard conventions. ```tsx // Spatial (default): X=red, Y=green, Z=blue, W=purple // Color: R=red, G=green, B=blue, A=white // No colors // Custom colors ``` ## Linked Proportional Editing Enable the link toggle to scale all axes proportionally when one changes. When linked, changing one axis value adjusts the others by the same ratio (or delta when the original value is zero). ```tsx ``` The linked state can also be controlled externally. ```tsx ``` ## Units and Steps All NumberInput options for units, steps, and precision apply to every axis. ```tsx ``` ## Min / Max ```tsx ``` ## Layout Direction Arrange axis inputs horizontally (row) or vertically (column). ```tsx // Row layout (default) // Column layout ``` ## Gap Control the spacing between axis inputs. ```tsx ``` ## Labels and Helper Text ```tsx ``` ## Error State ```tsx ``` ## Sizes ```tsx ``` ## Props | Prop | Type | Default | Description | | --- | --- | --- | --- | | `value` | `number[]` | — | Current vector value (controlled). Array length must match dimension. | | `defaultValue` | `number[]` | — | Default vector value (uncontrolled). | | `dimension` | `2 \| 3 \| 4` | `3` | Number of vector components. | | `labelPreset` | `'xyz' \| 'rgba' \| 'uvw' \| 'custom'` | `'xyz'` | Axis label preset configuration. | | `axisLabels` | `string[]` | — | Custom axis labels (when labelPreset is "custom"). | | `colorPreset` | `'spatial' \| 'color' \| 'none' \| 'custom'` | `'spatial'` | Color coding preset for axis labels. | | `axisColors` | `string[]` | — | Custom axis colors (when colorPreset is "custom"). CSS color strings. | | `min` | `number` | — | Minimum allowed value (applies to all axes). | | `max` | `number` | — | Maximum allowed value (applies to all axes). | | `step` | `number` | `1` | Step size for value increments. | | `precisionStep` | `number` | — | Step size when Shift is held (precision mode). | | `largeStep` | `number` | — | Step size when Ctrl is held (large steps). | | `precision` | `number` | `2` | Number of decimal places. | | `unit` | `string` | — | Unit suffix displayed in each input (e.g., "px", "deg"). | | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Component size. | | `label` | `string` | — | Label displayed above the vector input. | | `helperText` | `string` | — | Helper text displayed below. | | `error` | `boolean` | `false` | Error state. | | `errorMessage` | `string` | — | Error message. | | `disabled` | `boolean` | `false` | Whether the vector input is disabled. | | `showLink` | `boolean` | `false` | Whether to show the link/unlink proportions toggle. | | `defaultLinked` | `boolean` | `false` | Whether axes are linked by default (uncontrolled). | | `linked` | `boolean` | — | Whether axes are linked (controlled). | | `onLinkedChange` | `(linked: boolean) => void` | — | Callback when the linked state changes. | | `direction` | `'row' \| 'column'` | `'row'` | Layout direction of axis inputs. | | `gap` | `number` | `2` | Gap between axis inputs in pixels. | | `onChange` | `(value: number[], axisIndex: number) => void` | — | Callback when any axis value changes. | | `onChangeComplete` | `(value: number[]) => void` | — | Callback when editing is committed (blur, Enter). | | `className` | `string` | — | Additional CSS class names. | | `testId` | `string` | — | Test identifier for automated testing. | ## Accessibility - The container uses `role="group"` with `aria-label` set to the label or "Vector input" - Each axis input has `aria-label` indicating the axis (e.g., "X axis") - The link toggle button uses `aria-pressed` to indicate linked state and `aria-label` for screen readers - All NumberInput accessibility features (keyboard navigation, expression support) apply to each axis - Disabled state propagates to all child inputs and the link toggle ========================= # Components — Navigation ========================= --- # Breadcrumbs > Hierarchical navigation trail for pages, editor paths, and nested resources. _Source: /components/navigation/breadcrumbs_ Breadcrumbs show where the current view sits in a hierarchy: file paths, nested categories, project folders, or parent routes. Use Tabs for sibling views at the same level; use Breadcrumbs when each segment is a parent of the next one. **Live Preview** ## Import ```tsx import { BreadcrumbEllipsis, BreadcrumbItem, Breadcrumbs, BreadcrumbSeparator, } from 'entangle-ui'; ``` ## Usage **Basic** ```tsx Home Components Button ``` Pass `isCurrent` explicitly on the current page. Breadcrumbs does not infer the current segment from missing links. ## With Icons **With icons** ```tsx }> Home ``` ## Custom Separator **String separator** ```tsx Home Button ``` **Icon separator** ```tsx }> {/* items */} ``` ## Sizes **Sizes** | Size | Font token | Separator gap | | ---- | ---------- | ------------- | | `sm` | `xs` | `xs` | | `md` | `sm` | `sm` | | `lg` | `md` | `sm` | ```tsx ``` ## Collapsed **Collapsed** ```tsx {/* long trail */} ``` When `maxItems` is greater than `0` and the item count exceeds it, the middle of the trail collapses into an ellipsis. By default, the first item and the last two items remain visible. ## Expandable **Expandable** ```tsx {/* clicking the ellipsis expands the trail */} ``` `expandable` defaults to `true`, so the ellipsis is a button with `aria-label="Show all breadcrumbs"`. ## Truncation **Long labels** ```tsx Photogrammetry capture workspace ``` Only string labels are truncated. Icons remain visible before the shortened label, and the full label is exposed through Tooltip. ## Editor Paths **File path** ```tsx project src components Button.tsx ``` ## Integration with PageHeader **In PageHeader** ```tsx Workspace Project Assets } title="Textures" /> ``` ## Compound API If you pass only `BreadcrumbItem` children, separators are inserted automatically. Use the `separator` prop to change the default separator for the whole trail. ```tsx Home Button ``` For full control, place `BreadcrumbSeparator` manually between items. When explicit separators are present, Breadcrumbs renders your children as provided. ```tsx Home / Button ``` ## When to use Use Breadcrumbs for hierarchical paths: files, folders, nested categories, scene hierarchy depth, or parent route trails. Use a Stepper for linear numbered flows when that component ships. Do not use Breadcrumbs as primary navigation; they are supporting context and a way back up the hierarchy. ## Accessibility - The root renders `