Skip to content

Components

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

Components

The component library (eQuantic.UI.Components) is write-once: every component is authored once in C# against the abstract vocabulary in eQuantic.UI.Primitives, and realized per target — DOM + CSS on the web, GPU pixels through Photon on native. Not "two similar APIs" — literally the same class. The architecture (layers, realizers, hydration parity) is documented in Write-Once Components.

The component model

Type Description
StatelessComponent Depends only on its properties. Override Build(ComponentContext) to return the tree.
StatefulComponent Holds persistent internal state; SetState triggers a rebuild.

ComponentContext is the mode-free door everything target-dependent comes through: the theme, Density, text measurement (MeasureText/MonoAdvance), and capability services taken by constructor (IThemeController, ITextClipboard, device capabilities). Components author tokens, never resolved colors — one built tree realizes in light or dark.

Events are typed properties (OnPressed, OnChanged, OnSelectionChanged, …) carrying C# delegates; the same handler runs on both targets.

The low-level web layer (HtmlElement, ClassName, DynamicElement) still exists for web-only escape hatches — see Styling.

Catalog

Actions & inputsButton, IconButton, TextInput, SearchField, Checkbox, Switch, RadioGroup, Select, Slider, Stepper, SegmentedControl.

Surfaces & displayCard, Divider, Badge, Chip, Avatar, Banner, ProgressBar, EmptyState, Skeleton.

NavigationTabs, AppBar, BottomNavigation, Breadcrumb, Pagination, PageIndicator, Menu, Drawer.

OverlaysDialog, Toast, BottomSheet (drag-to-dismiss), Popover, Tooltip (hover-revealed, zero JS).

Lists & dataList/ListItem, ListView (recycling — below), Accordion, Table, DataTable, Spreadsheet (below).

Touch interactionPullToRefresh, SwipeableRow.

CodeCodeBlock (read-only) and CodeEditor (caret, selection, keyboard) — the full model and both surfaces are documented on the Code Editor page.

Each component ships with web realizer pins, native golden images (light + dark), pinned transpiled fixtures executed in vitest, and the live showroom (/ and /shared in DefaultUIDashboard — SSR + hydration identity + interaction verified end-to-end by the Playwright suite). Systems shipping alongside the library: the state-transition motion system (Presence enter/exit), the pointer pipeline (hover, drag-to-dismiss with slop/cancel/glide), the scroll compositor (real Sticky pinning) and anchored overlays.

ListView — the recycling list

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. Scope (v1): vertical lists with a fixed ItemExtent — variable extents are not included.

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 a document always does.
  • 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

An Excel-usability spreadsheet, built on the same principle as the Code Editor: 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.
  • Excel-core selection: 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.
  • 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.
  • 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: the 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.
  • Pointer cursors are 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.

Scope (v1): no formula engine, no per-cell formatting, no merges, no frozen panes. Virtualization is vertical only (columns materialize), and there is no 2D scroll — columns past the pane's width are clipped. Controller, editing and component suites drive both axes of resize through the host; the transpiled twin runs the same moves under vitest, plus web-surface specs with real DOM events.


See Also

Clone this wiki locally