-
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.
Since 0.2.0-preview.15
InFlow(Dialog("Delete this?", "It cannot be undone.", [new DialogAction("Delete")]))
InFlow(Drawer(content)) // no `open: true` needed — see below
InFlow(BottomSheet(content))
InFlow(Popover(trigger, panel))A dialog, a drawer, a sheet and a popover only exist once something opens them, and when they do they take the whole viewport: a scrim over the page, the surface centred or pinned in front of it, an Escape binding, an enter animation. A page that wants to SHOW one — documentation, a design review, a visual-regression suite — wants the surface and none of that. Five components rendered blank.
InFlow states the intent; each overlay answers it by building its panel and nothing else, because
the component is the only thing that knows what its surface is.
In the flow there is no open or closed: the panel is placed, therefore it is there. Open
governs whether the LAYER exists, and there is no layer. A drawer hugs its content rather than
filling the viewport (full height belongs to the edge it is pinned to, and it has no edge); a
popover draws its panel without its trigger; a PullToRefresh rests with its indicator uncovered,
since nothing can be pulled.
It is not a preview mode: the surface is real, with real actions. What is dropped is the presentation. And the intent ends with the node — a dialog opened for real beside a previewed one still takes the viewport.
Since 0.2.0-preview.15
Simulated(SimulatedState.Hovered, Button("Save"))
Simulated(SimulatedState.Pressed | SimulatedState.Focused, IconButton(Icons.Check))A pressed button cannot be handed to a constructor: pressed is something the host observes, not something a tree states. So a documentation gallery, a design review and a visual-regression suite could only ever show a control's rest state — the three states a reader most wants to compare were the three no page could draw.
One node rather than a property on every interactive component: it composes, it covers the components that do not exist yet, and it works for a control nested three levels inside a card being previewed. Nested previews COMBINE — a hovered card holding a pressed button is two nodes, and the inner one does not turn the outer's hover off.
It changes only what is DRAWN. Nothing is invoked, no state is entered, no handler runs, and the state ends with the node: the control beside it behaves normally.
On the web, :hover and :focus-visible cannot be forced from CSS — nothing can, which is the
point of a pseudo-class — so the diffs those rules carry are folded into the base rule instead.
Same declarations, so the preview is the real thing rather than an approximation of it. A simulated
press reuses the very selector body a real press uses, which is what keeps the two from drifting.
- 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