-
Notifications
You must be signed in to change notification settings - Fork 1
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.
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)
| 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 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
}| Interface | Purpose | Documentation |
|---|---|---|
IRequireAssets |
Declare script/CSS dependencies | Asset Management |
IHandleMetadata |
Configure SEO metadata | Server Integration |
| 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") }
}}| 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/OnInputevent binding -
Disabled,ReadOnly,Requiredstates - Theme integration via
IInputTheme
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
| 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" }
}
}| 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
|
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
new Table<User> {
Data = users,
Columns = new[] {
new TableColumn { Header = "Name", Field = "Name" },
new TableColumn { Header = "Email", Field = "Email", Sortable = true }
}
}| 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 |
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 }
}}
}}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| 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." }
}}| 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 = { ... } }
}}| 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...
}}
}}| 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
|
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 |
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>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 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()Component themes inject an IColorTheme interface that maps semantic colors to concrete classes, facilitating global color palette customization.
Inputs support state-based variants for validation feedback:
-
Success- Valid/successful input -
Warning- Warning state -
Destructive- Error state -
Ghost- Minimal style
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",
_ => ""
};
}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
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-errorclass on wrapper
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" } } }
}
}- Use compound components for complex UIs (Card, Tabs, Accordion)
- Use controlled mode for real-time validation, uncontrolled for simple forms
- Wrap inputs in FormField for consistent validation UX
- Add loading states to all async action buttons
- Add Data/ARIA attributes for testing and accessibility
| 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 |
- Asset Management - Component asset dependencies
- Styling - Theme system and styling patterns
- Server Integration - SSR, SEO, and server configuration
- Architecture - Overall framework architecture
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
#appframe has EXACT viewport height (height: 100dvh, grid, childrenmin-height: 0): an APP page (rootHeight = 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
ScrollViewwithout an explicitHeightdefaults toheight: 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.
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/SetColWidthare 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/CommitEditlive 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
SheetSurfacethat 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
Draggablegrip (Stack+Positioned,Follows = false). Moves preview straight into the document (SetColWidth/SetRowHeight); the release rewinds the preview and lands the whole gesture as ONE undoableController.Resize. Floors (MinColWidth24,MinRowHeight14) keep a sliver grabbable — clamped in the component, not in the node'sMin/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 focusablerole="grid"div withuser-select: none(a selection drag paints the band, not the browser's blue sweep), keydown → the transpiledSheetKeymap, 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.
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.