Skip to content

Components

Edgar Mesquita edited this page Aug 8, 2026 · 11 revisions

Components

Warning

Outdated: the legacy web component library documented below was EXCISED on 2026-07-31 — the write-once library (WriteOnceComponents, 28 components) is the only architecture. This page is kept for history.

Note

This catalog covers the legacy web component library (eQuantic.UI.Components). The framework is migrating to a write-once library (eQuantic.UI.Components.Shared) authored against the Photon vocabulary and realized on web AND native — 8 components migrated so far, mixing is safe in both directions. See Write-Once Components.

Complete reference of the eQuantic.UI component system — architecture, catalog, and production patterns.

Component Architecture

HtmlNode (Virtual DOM)

HtmlNode is the basic unit of the Virtual DOM — a lightweight representation of a real DOM element.

  • Tag: HTML tag name (e.g., div, button)
  • Key: Unique identifier for reconciliation optimization
  • Attributes: Dictionary of HTML attributes (class, id, style, etc.)
  • Events: C# delegates converted into event listeners
  • Children: List of child HtmlNodes
  • TextContent: Used for text nodes (#text)

Component Types

Type Description Use Case
StatelessComponent Functional components depending only on props. Override Build() to return the component tree. Visual components: Text, Heading, Badge
StatefulComponent Components with persistent internal state. SetState triggers re-render of the component and children. Interactive pages, forms, dashboards
HtmlElement Low-level primitives mapping directly to HTML tags. Box, Flex, Grid, Container
InputComponent<TValue> Base for input components with value binding, events, and theme integration. TextInput, Select, Checkbox

Events

Events follow the On{EventName} convention. The compiler removes the On prefix and converts to lowercase in TypeScript:

new Button {
    Text = "Clear",
    Variant = Variant.Secondary,
    OnClick = HandleClear
}

Component Interfaces

Interface Purpose Documentation
IRequireAssets Declare script/CSS dependencies Asset Management
IHandleMetadata Configure SEO metadata Server Integration

Component Catalog

Layout

Component Description Key Props
Container Responsive max-width container MaxWidth, Fluid
Box Generic block container (div) Children
Flex Flexbox container Direction, Justify, Align, Gap, Wrap
Row Horizontal flex container Inherits from Flex
Column Vertical flex container Inherits from Flex
Stack Stack layout with spacing Direction, Gap
Grid CSS Grid layout Columns, Gap
GridItem Grid child with placement ColSpan, RowSpan
new Grid { Columns = 3, Gap = "4", Children = {
    new GridItem { ColSpan = 2, Content = new Text("Wide") },
    new GridItem { Content = new Text("Narrow") }
}}

Inputs

Component Description Key Props
TextInput Text input field Type, Placeholder, Value, OnChange, Size, Disabled
TextArea Multi-line text input Rows, Value, OnChange, Disabled
Checkbox Checkbox with optional rich UI Checked, OnChange, IsNative
Radio Radio button Checked, OnChange, Name, Label
RadioGroup Radio button group Value, OnChange, Options, Direction
RadioOption Data class for radio options Value, Label, Disabled
Select Dropdown select Value, OnChange, Multiple, Options, IsNative
SelectOption Data class for select options Value, Label, Disabled
Switch Toggle switch (inherits Checkbox) Value, OnChange, Label
Slider Range slider Value, Min, Max, Step, OnChange

All inputs inherit from InputComponent<TValue>, which provides:

  • Value / OnChange / OnInput event binding
  • Disabled, ReadOnly, Required states
  • Theme integration via IInputTheme

Controlled vs Uncontrolled

All input components support both modes:

// Controlled — external state management, real-time validation
new TextInput {
    Value = email,
    OnChange = (value) => SetState(() => email = value)
}

// Uncontrolled — internal state, simple forms
new TextInput {
    DefaultValue = "user@example.com",
    Name = "email"
}

When to use each:

  • Controlled: Real-time validation, dependent fields, complex forms
  • Uncontrolled: Simple forms, server-side validation, minimal state

Forms

Component Description Key Props
FormField Label + input + error wrapper Label, Error, Required, HelperText, For
InputGroup Input with prefix/suffix addons Children
// FormField with validation
new FormField {
    Label = "Email Address",
    Required = true,
    Error = validationError,
    HelperText = "We'll never share your email",
    For = "email-input",
    Children = {
        new TextInput { Id = "email-input", Type = "email", Name = "email" }
    }
}

// InputGroup with addons
new InputGroup {
    Children = {
        new InputAddon { Text = "https://" },
        new TextInput { Placeholder = "example.com" },
        new Button { Text = "Go" }
    }
}

Display

Component Description Key Props
Badge Status/category badge Text, Variant
Avatar User avatar (image or initials) ImageUrl, AltText, Initials, Size
Heading H1-H6 heading Content, Level (1-6)
Text Text span/paragraph Content, Variant, Paragraph
Icon Unified icon component Name (e.g., "lucide:search"), Size
CodeBlock Syntax-highlighted code Code, Language, Collapsible
List Ordered/unordered list Ordered, Unstyled, Children
ListItem List item Active, Disabled, OnClick
Table<T> Data table Data, Columns, Striped, OnRowClick

CodeBlock

Renders syntax-highlighted code using Prism.js with automatic dark/light theme switching:

new CodeBlock("Console.WriteLine(\"Hello\");", "csharp") { Collapsible = true }

Features:

  • Prism.js loaded automatically via IRequireAssets
  • Copy to clipboard button
  • Collapsible/expandable code blocks
  • Dark/light theme auto-switches with the page theme

Table<T>

new Table<User> {
    Data = users,
    Columns = new[] {
        new TableColumn { Header = "Name", Field = "Name" },
        new TableColumn { Header = "Email", Field = "Email", Sortable = true }
    }
}

Surfaces

Component Description Key Props
Card Card container Variant, Shadow
CardHeader Card header section Children
CardTitle Card title Text
CardDescription Card description Text
CardBody Card body content Children
CardFooter Card footer Children

Compound Pattern

new Card { Variant = CardVariant.Elevated, Shadow = Shadow.Large, Children = {
    new CardHeader { Children = {
        new CardTitle { Text = "Q1 2026 Roadmap" },
        new CardDescription { Text = "Key initiatives" }
    }},
    new CardBody { Children = {
        new Text("Launch mobile app"),
        new Text("Implement AI features")
    }},
    new CardFooter { Children = {
        new Button { Text = "View Details", Variant = Variant.Outline },
        new Button { Text = "Share", Variant = Variant.Ghost }
    }}
}}

Card Variants

CardVariant.Default    // Filled card with background
CardVariant.Outline    // Border only, transparent background
CardVariant.Elevated   // Prominent shadow for emphasis
CardVariant.Subtle     // Minimal visual weight
CardVariant.Ghost      // No background or border

Feedback

Component Description Key Props
Alert Alert message Variant
AlertTitle Alert title Text
AlertDescription Alert description Text
Toast Toast notification Variant, DelayMs, Title, Message
ToastContainer Toast positioning container Position (TopRight, BottomLeft, etc.)
Spinner Loading indicator Variant (Border/Grow), Size (Small/Normal)
new Alert { Variant = Variant.Warning, Children = {
    new AlertTitle { Text = "Warning" },
    new AlertDescription { Text = "This action cannot be undone." }
}}

Navigation

Component Description Key Props
Tabs Tab navigation DefaultValue, Value, OnValueChange
TabsList Tab header list Children
TabsTrigger Tab trigger button Value, Text
TabsContent Tab content panel Value, Children
Accordion Collapsible sections Type (Single/Multiple), Collapsible
AccordionItem Accordion section Value, Children
AccordionTrigger Accordion trigger button Text
AccordionContent Accordion content panel Children
Breadcrumb Breadcrumb navigation Items, Separator
Navbar Top navigation bar Brand, Items, Dark, Fixed
Sidebar Side navigation panel Items, Width, Collapsed
NavigationMenu Complex nav menu Children (compound pattern)
new Tabs { DefaultValue = "tab1", Children = {
    new TabsList { Children = {
        new TabsTrigger { Value = "tab1", Text = "General" },
        new TabsTrigger { Value = "tab2", Text = "Advanced" }
    }},
    new TabsContent { Value = "tab1", Children = { ... } },
    new TabsContent { Value = "tab2", Children = { ... } }
}}

Overlays

Component Description Key Props
Modal Modal dialog IsOpen, Title, OnClose
Drawer Slide-out panel IsOpen, Side, OnClose
DrawerContent Drawer body Width, Children
DrawerTrigger Drawer open trigger Children
ContextMenu Right-click menu Children (compound pattern)
ContextMenuItem Menu item Disabled, Shortcut
new Drawer { Side = DrawerSide.Right, IsOpen = isOpen, Children = {
    new DrawerContent { Children = {
        new DrawerTitle { Text = "Settings" },
        // content...
    }}
}}

Utility

Component Description
DynamicElement Generic HTML element (any tag, attributes, events)
NullComponent Empty/null component (renders nothing)
Link Anchor tag with Href, Target
Button Button with Variant, Size, Loading, Disabled

Common Props

All components inherit from IComponent and share:

Prop Type Description
Id string? HTML id attribute
ClassName string? CSS class(es)
Style HtmlStyle? Inline styles
DataAttributes Dictionary<string, string>? data-* attributes
AriaAttributes Dictionary<string, string>? aria-* attributes
Children List<IComponent> Child components

Data & ARIA Attributes

new Button {
    Text = "Submit",
    DataAttributes = new Dictionary<string, string> {
        ["testid"] = "submit-button",
        ["analytics"] = "checkout-submit"
    },
    AriaAttributes = new Dictionary<string, string> {
        ["label"] = "Submit order",
        ["describedby"] = "submit-help"
    }
}
// Renders: <button data-testid="submit-button" aria-label="Submit order">Submit</button>

Theme & Styling

Variants & Sizes

Most components support theme-driven styling:

  • Variant: Primary, Secondary, Destructive, Outline, Ghost, Link, Success, Warning, Info
  • Size: Small, Medium, Large, XLarge

Themes are provided by IAppTheme (default eQuantic theme or Tailwind via AddTailwind()).

StyleBuilder

StyleBuilder is a utility inspired by CVA (Class Variance Authority) for managing conditional CSS classes:

["class"] = StyleBuilder.Create(theme?.Base)
                .Add(theme?.GetVariant(Variant))
                .Add(theme?.GetSize(Size))
                .Add(ClassName)
                .Build()

IColorTheme

Component themes inject an IColorTheme interface that maps semantic colors to concrete classes, facilitating global color palette customization.

Input Variants

Inputs support state-based variants for validation feedback:

  • Success - Valid/successful input
  • Warning - Warning state
  • Destructive - Error state
  • Ghost - Minimal style

Custom Themes

public class MyCardTheme : ICardTheme
{
    public string Container => "my-custom-card";
    public string Title => "my-custom-title";
    public string GetVariant(CardVariant variant) => variant switch
    {
        CardVariant.Outline => "my-outline-variant",
        _ => ""
    };
}

Production Patterns

Loading States

new Button {
    Text = "Submit Order",
    Loading = isSubmitting,  // Automatic spinner + disabled state
    OnClick = HandleSubmit
}

Features:

  • Automatic spinner rendering
  • Button disabled during loading
  • data-loading="true" attribute for CSS targeting
  • Accessible with aria-hidden="true" on spinner

Error States & Validation

new FormField {
    Label = "Email Address",
    Required = true,
    Error = validationError,
    HelperText = "We'll never share your email",
    Children = {
        new TextInput { Type = "email", Name = "email" }
    }
}

Features:

  • Label with automatic required indicator (*)
  • Error messages styled with role="alert" for accessibility
  • Helper text displayed when no errors present
  • Automatic .eq-error class on wrapper

Compound Components

For complex UIs, use the compound pattern for semantic structure:

// Recommended — semantic and flexible
new Card {
    Variant = CardVariant.Elevated,
    Children = {
        new CardHeader { Children = { new CardTitle { Text = "Title" } } },
        new CardBody { Children = { new Text("Content") } },
        new CardFooter { Children = { new Button { Text = "Save" } } }
    }
}

Best Practices

  1. Use compound components for complex UIs (Card, Tabs, Accordion)
  2. Use controlled mode for real-time validation, uncontrolled for simple forms
  3. Wrap inputs in FormField for consistent validation UX
  4. Add loading states to all async action buttons
  5. Add Data/ARIA attributes for testing and accessibility

Comparison with shadcn/ui

Feature eQuantic.UI shadcn/ui Advantage
Compound Components Full support Full support Parity
Variants 5+ per component 5+ per component Parity
Controlled/Uncontrolled TextInput, Select, Checkbox Full support Parity
Loading States Button + inputs Button + inputs Parity
Error States FormField component Form component Parity
Input Groups Full support Full support Parity
Data/ARIA Attributes Native support Native support Parity
Documentation XML (IntelliSense) JSDoc eQuantic
Type Safety C# Compiler TypeScript eQuantic
Theming System DI + Fallback CSS vars eQuantic
Performance Compile-time Runtime eQuantic

See Also

ListView — the recycling list (M3)

A list that only BUILDS the rows you can see: give it a count, a fixed per-item extent and a builder by index, and it materializes the visible window plus an overscan margin — the rest of the list is two spacers, so layout (and the scrollbar) see the true content height while ten thousand rows cost what a screenful costs. Write-once: the same C# runs on Photon and on the web.

new ListView(count: 10_000, itemExtent: 44, itemBuilder: i => RowFor(i))
{
    Width = SizeValue.Fill,
    Height = SizeValue.Fill,   // the list is a WINDOW — give it one
}

The window converges rather than blocks: the first frame builds against a viewport guess, the realized frame reports the true viewport and offset through the ScrollView's out-channels (OnScrolled / OnViewportChanged — wired on both targets), and the next frame builds the corrected window. Pixels scroll without a rebuild; state only changes when a row crosses the overscan margin. v1 fence: vertical, fixed ItemExtent (variable extents need incremental measurement and join later).

Layout rules that make windows work everywhere (SDK invariants, not app chores):

  • The app shell's #app frame has EXACT viewport height (height: 100dvh, grid, children min-height: 0): an APP page (root Height = Fill) resolves to exactly one viewport and scrolls internally; a DOCUMENT page (auto-height root) overflows the frame and the body scrolls as it always did.
  • A ScrollView without an explicit Height defaults to height: 100% on web — a scroll view IS the window its parent gives it, never its content (native parity: the realizer hands it layout bounds and clips always).
  • Hydration adopts data-eq-* framework markers from the client tree — SSR cannot know client-side identities, and the after-pass sweeps find their elements by them.

Spreadsheet — the editable grid (write-once)

An Excel-usability spreadsheet, built the way the code editor was: the controller carries everything, the pixels are arithmetic. One C# model + one C# component render on Photon and the DOM; the browser build transpiles the very same sources.

The model (eQuantic.UI.Primitives/Sheet, shared verbatim):

  • SheetDocument — sparse cells (empty is absent), per-row/column sizes, logical extent. SetCell/SetRowHeight/SetColWidth are the load/preview path, deliberately not undoable.
  • CellRef/SheetRange — A1 addressing; anchor/focus selection in 2D. Cells key by a packed int (row × 16384 + col) because a record struct is a useless JS Map key.
  • SheetController — Excel's semantics as methods: arrows/Shift extend, Ctrl+arrow data-edge jumps, Tab/Enter walking (and wrapping) inside a selection while the rectangle stands still (active cell ≠ selection focus, deliberately), header band selections, row/column insert/delete/resize with sparse inverse-based undo/redo, TSV copy/paste speaking Excel's quoting, and the in-cell editing draft: BeginEdit/TypeIntoDraft/CommitEdit live in the controller, so both targets edit identically and a committed draft undoes as one step.
  • SheetKeymap — the ONE keyboard: typing replaces (Excel's quick-entry), F2 edits in place, Enter/Tab commit-and-step, Escape discards, arrows commit-then-move, ⌘A/⌘Z. The native host and the web lowering both delegate here; the dialect cannot fork.

The component (eQuantic.UI.Components/Spreadsheet.cs):

  • Column headers fixed on top; row headers scroll WITH the rows; the visible window of cells wraps in a SheetSurface that lives INSIDE the scroll — marks translate with the content, so a click on a scrolled grid selects the cell actually under the pointer, by construction.
  • Rows virtualize the ListView way (window + spacers); selection band, active-cell ring and the editing draft+caret paint ON the cells, in the component — both targets are visually identical because there is nothing realizer-side to drift.
  • Resize by drag: every header's trailing edge carries an invisible 6dp Draggable grip (Stack + Positioned, Follows = false). Moves preview straight into the document (SetColWidth/SetRowHeight); the release rewinds the preview and lands the whole gesture as ONE undoable Controller.Resize. Floors (MinColWidth 24, MinRowHeight 14) keep a sliver grabbable — clamped in the component, not in the node's Min/Max (those rebuild mid-drag). Inside the ScrollView the grip wins over the scroll: a drag surface is the more specific gesture (host rule, PressDown).
  • Interaction wiring: native host routes clicks/drag-select/keys/TSV clipboard/IME-safe text to the controller; the web lowering (lowerSheetSurface) is a focusable role="grid" div with user-select: none (a selection drag paints the band, not the browser's blue sweep), keydown → the transpiled SheetKeymap, dblclick edits, copy/cut/paste ride the browser's own clipboard events in TSV.

The gallery in eQuantic Studio (macOS) carries a Spreadsheet section — the same component the web serves at /sheet, its controller held in the section state so edits and undo survive rebuilds. The gallery walk drove the new page and flagged the real fence: columns past the pane's width are clipped, unreachable content until wide sheets get 2D scroll.

Fences v1, stated: no formula engine, no per-cell formatting, no merges, no frozen panes; vertical virtualization only (columns materialize). Suites: 15 controller + 7 editing + 8 component xunit tests drive both axes of resize through the host; the transpiled twin runs the same moves under vitest, plus 4 web-surface specs with real DOM events.

Excel-core interaction (2026-08-08): header clicks select their whole row/column (the corner selects all — headers shade when the selection's band crosses them); shift+click stretches from the anchor without moving the active cell; a plain drag sweeps a range on both targets; and the fill handle — Excel's little square on the selection's bottom-right corner — drags a preview along the DOMINANT axis and pours the source block across it on release, tiling wrap-around in phase in all four directions, as ONE undo step. ⌘D/⌘R pour down/right through the same engine in the shared keymap. The gesture's semantics (BeginFill/UpdateFill/CommitFill, Fill, FillDown, FillRight, SelectTo) live in the controller, so both targets fill identically.

Pointer cursors are now vocabulary (BoxStyle.Cursor, the CSS cursor mirror): web emits the declaration, Photon registers a CursorRegion the host's CursorAt answers topmost-first, and the macOS shell maps to NSCursor. The resize grips say col-resize/row-resize and the fill handle says crosshair — on both targets, from the same component code.

A compiler bug this slice caught (fixed in ObjectCreationStrategy): named constructor arguments that SKIP earlier optional parameters (new Positioned(grip, bottom: 0, start: 0) { ZIndex = 1 }) already emit the skipped defaults in the reordered argument list — the trailing config object must fill from the slots actually emitted, not count the call site again, or it lands past the constructor's arity and dies silently (zIndex was being dropped on the web). NamedArgumentEmissionTests pins both shapes.

Frame lifetime & the node pool (2026-08-08)

RealizeResult now carries an explicit ownership story — the missing piece since the double-buffer revert. PhotonHost.RecycleFrames (opt-in, default off) declares the host OWNS every frame it replaces: the replaced tree feeds a LayoutNodePool and the next frame is built from it. The production shells (macOS/iOS/Android) turn it on — nothing retains their frames; tests and tooling that hold RealizeResults leave it off and keep immutable trees. Steady-state allocation on the dense harness scene: 106 → 67.5 KB/frame; the perf harness pins both profiles (default ceiling 152 KB, pooled ceiling 72 KB). What remains is the frame's region lists — the pool's next candidates.

Clone this wiki locally