-
Notifications
You must be signed in to change notification settings - Fork 1
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.
| 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.
Actions & inputs — Button, IconButton, TextInput, SearchField, Checkbox, Switch,
RadioGroup, Select, Slider, Stepper, SegmentedControl.
Surfaces & display — Card, Divider, Badge, Chip, Avatar, Banner, ProgressBar,
EmptyState, Skeleton.
Navigation — Tabs, AppBar, BottomNavigation, Breadcrumb, Pagination,
PageIndicator, Menu, Drawer.
Overlays — Dialog, Toast, BottomSheet (drag-to-dismiss), Popover, Tooltip
(hover-revealed, zero JS).
Lists & data — List/ListItem, ListView (recycling — below), Accordion, Table,
DataTable, Spreadsheet (below).
Touch interaction — PullToRefresh, SwipeableRow.
Code — CodeBlock (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.
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
#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 a document always does. - 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 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/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.
- 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
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: the 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. -
Pointer cursors are vocabulary (
BoxStyle.Cursor, the CSScursormirror): web emits the declaration, Photon registers aCursorRegionthe host'sCursorAtanswers topmost-first, and the macOS shell maps toNSCursor. 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.
- Write-Once Components — the shared architecture these components ride on
- Design System — tokens, density, theming, the atomic style engine
-
Code Editor — the editor model,
CodeBlockandCodeEditor - Server Integration — SSR, SEO, and server configuration