Repository navigation
Components de
🌐 Diese Seite auf: English · Português · Español · Français · Deutsch
Die Komponentenbibliothek (eQuantic.UI.Components) ist write-once: Jede Komponente wird einmal
in C# gegen das abstrakte Vokabular in eQuantic.UI.Primitives geschrieben und je Ziel realisiert,
DOM + CSS im Web, GPU-Pixel über Photon auf Native. Nicht "zwei ähnliche APIs", sondern
buchstäblich dieselbe Klasse. Die Architektur (Schichten, Realisierer, Hydration-Parität) ist in
Write-once-Komponenten dokumentiert.
| Typ | Beschreibung |
|---|---|
StatelessComponent |
Hängt nur von seinen Eigenschaften ab. Überschreiben Sie Build(ComponentContext), um den Baum zurückzugeben. |
StatefulComponent |
Hält dauerhaften internen Zustand; SetState löst einen Neuaufbau aus. |
ComponentContext ist die moduslose Tür, durch die alles Zielabhängige kommt: das Theme, die
Dichte, die Textmessung (MeasureText/MonoAdvance) und die per Konstruktor
genommenen Fähigkeitsdienste (IThemeController, ITextClipboard, Gerätefähigkeiten). Komponenten
schreiben Tokens, nie aufgelöste Farben, sodass ein gebauter Baum sich hell oder dunkel
realisiert.
Events sind typisierte Eigenschaften (OnPressed, OnChanged, OnSelectionChanged, …), die
C#-Delegates tragen; derselbe Handler läuft auf beiden Zielen.
Die Web-Schicht auf niedriger Ebene (
HtmlElement,ClassName,DynamicElement) existiert weiterhin für reine Web-Notluken. Siehe Styling.
Die Basis einer Komponente muss nicht direkt StatelessComponent oder StatefulComponent sein. Eine
eigene Basis (die einen gemeinsamen Rahmen, gemeinsames Padding, einen Prefetch hält, den mehrere
Seiten wiederholen) ist etwas Gewöhnliches zu schreiben, und alles, was die Unterklasse deklariert,
wandert in ihren Zwilling: Konstruktoren, Felder, Eigenschaften, Instanzmethoden, statische Methoden
und [ServerAction]-Stubs.
public abstract class CardBase : StatelessComponent
{
protected VisualNode Frame(VisualNode child) =>
Box(new BoxStyle { Padding = EdgeInsets.All(Space.S4) }, child);
}
public sealed class Tile : CardBase
{
public Tile(string caption) => Caption = caption;
public string Caption { get; init; }
public override VisualNode Build(ComponentContext context) => Frame(Text(Caption));
}Zwei Build-Fälle, und eine Leserin trifft beide sofort:
-
Das Kind überschreibt
Build. Sein Zwilling trägt diesesbuildund jedes Mitglied, das der Körper erreicht, die geerbten eingeschlossen. -
Das Kind tut es nicht und erbt das der Basis. Sein Zwilling trägt überhaupt kein
build, und die Prototypenkette antwortet: Das der Basis ist das, was läuft.
Eine abstrakte Basis, die Mitglieder und kein Build deklariert, wird mit diesen Mitgliedern
ausgegeben, sodass ein Kind, das in sie hineinruft, sie dort findet.
Was eine Basis NICHT tragen darf, ist eine [ServerAction]. Der Server registriert eine Aktion unter
dem Namen der KONKRETEN Komponente (er überspringt abstrakte Typen und geht geerbte Methoden ab),
also würde eine auf der Basis deklarierte Aktion unter einer Id aufgerufen, die nichts bedient, und
die Seite bekäme "action not found", ohne Fehler auf einer der beiden Seiten. EQ2012 verweigert die
Form: Deklarieren Sie die Aktion auf der konkreten Komponente. Auf der Komponente selbst ist eine
[ServerAction] in Ordnung, ob stateful oder stateless.
Aktionen und Eingaben: Button, IconButton, TextInput, SearchField, Checkbox, Switch,
RadioGroup, Select, Slider, Stepper, SegmentedControl.
Flächen und Darstellung: Card, Divider, Badge, Chip, Avatar, Banner, ProgressBar,
EmptyState, Skeleton.
Navigation: Tabs, AppBar, BottomNavigation, NavigationRail, Breadcrumb, Pagination,
PageIndicator, Menu, Drawer. Die Leiste und die Schiene sind DIESELBEN Ziele für zwei
Fensterbreiten: Geben Sie eine NavItem-Liste beiden innerhalb eines AdaptiveNode, und die Shell
folgt dem Fenster, Leiste unter einem Telefon und Schiene am Anfangsrand eines Tablets, ohne Listener
und ohne zweiten Zustand. Die Leiste nimmt 3 bis 5 Ziele, die Schiene 3 bis 7 (sie ist höher, als
eine Leiste breit ist); darüber hinaus ist die Antwort bei beiden ein Drawer.
Seit 0.2.0-preview.29
Die Leiste und die Schiene teilen das Ziel-MODELL und nicht ihre Metriken: Das Handoff gibt jeder die eigenen (B4: eine Pille 56×26, eine Md-24-Glyphe, ein Label 11/700; C16: eine Pille 52×30 in einer Zelle 80×56, Dense 20, ein Caption 12, das auf 700 geht, wenn es ausgewählt ist, und eine 1dp-Linie am Endrand). Die Schiene war aus der Leiste gebaut worden, war also die Leiste auf die Seite gestellt, und drei Shells zeichneten den Rand, den sie nun selbst malt.
Seit 0.2.0-preview.31
Die Ziele der Schiene sitzen dort, wo Alignment es sagt (standardmäßig oben, wie bei Material,
zentriert unter einem Header). Sie nimmt das MainAlign des Vokabulars selbst statt eines
schienenförmigen Enums, und sie wartete auf eine Scheibe des Transpilers, nicht auf eine des
Designs: Eine Komponente, die ihre Enum-Eigenschaft in einen Platz des Vokabulars weiterleitet,
brauchte, dass diese Eigenschaft den Zwilling als UNION erreicht und nicht als nackten String.
Seit 0.2.0-preview.31
ListDetail ist die dritte Form und die erste Komponente im Katalog, die selbst ADAPTIV ist: eine
Liste neben einem Detail auf einem breiten Fenster, ein Bereich auf einmal auf einem Telefon.
Seit 0.2.0-preview.31
Die Anwendung besitzt die DATEN: einen Listenknoten und einen Detailknoten, sobald etwas gewählt ist. Die Komponente besitzt die REGEL, und das ist der Teil, den jede Anwendung gerade erneut schreiben wollte und zweimal geschrieben hätte, weil die zwei Breiten wie zwei verschiedene Bildschirme aussehen und keine sind:
new ListDetail(
list: Inbox.List(_selected, i => SetState(() => _selected = i)),
detail: _selected is { } chosen ? Inbox.Detail(chosen) : null,
onBack: () => SetState(() => _selected = null))
{
ListTitle = "Inbox",
Title = _selected is { } open ? Inbox.Items[open].Title : null,
Placeholder = EmptyState(Icons.Mail, "Pick a message"),
}Ein Feld antwortet auf beide Formen. Auf einem Telefon ist ein Detail von null ein ORT: Die Liste
ist der Bildschirm, und zurück wählt ab; jenseits von TwoPaneFrom (840dp, die Expanded-Klasse der
Spezifikation) ist es nur eine Abwesenheit, die Placeholder füllt. Die Zurück-Affordanz wird NUR
auf dem kompakten Bereich gezeichnet: In der Breite sind beide Bereiche sichtbar, und ein Control,
das Sie zu etwas zurückbringt, das Sie sehen können, ist die Art, wie ein adaptives Layout sich
kaputt liest. dotnet new equantic-native --shell list-detail erzeugt genau den Aufruf von oben.
Das kompakte Detail antwortet auf ZURÜCK: Es bindet Escape an OnBack, die Taste, mit der ein
Desktop eine Ebene schließt, und die, an die Photons Android-Shell ihre Zurück-Gestik schickt
(Photon), also verlässt zurück auf einem Telefon das Detail für die
Liste, und in einem schmalen Browserfenster tut Escape dasselbe. Nichts sonst bindet sie: In der
Breite sind beide Bereiche auf dem Bildschirm, und mit der Liste auf dem Bildschirm gehört zurück dem
System, das die Anwendung verlässt.
Ein ehrlicher Zaun, zum Tausch des kompakten Bereichs, der eine Navigation und nicht nur ein neues Layout ist: Der Tastaturfokus bleibt, wo er war, statt in den angekommenen Bereich zu wandern (das Vokabular hat noch kein zielneutrales "fokussiere diesen Teilbaum").
Overlays: Dialog, Toast, BottomSheet (ziehen zum Verwerfen), Popover, Tooltip (durch
Hover enthüllt, nur Zeiger, null JS).
Listen und Daten: List/ListItem, ListView (recycelnd, siehe unten), Accordion, Table,
DataTable, Spreadsheet (siehe unten).
Eine Reihe ist 52 / 68 / 88 hoch für eine, zwei und drei Zeilen (SubtitleLines = 2 bittet um die
dritte, die existiert, damit ein Untertitel einer Einstellung einen zweiten Satz nehmen kann), und
die List rückt jeden Trenner bis dorthin ein, wo die Reihe DARÜBER ihren Text beginnt,
16 + Leading + Gap, sodass eine Haarlinie nie quer über ein Icon schneidet. Sie leitet das aus dem
Platz ab: Ein Icon trägt seine Größe und ein Avatar seine Stufe, und nur ein Platz, den die
Anwendung selbst gebaut hat, muss eine LeadingWidth nennen.
Seit 0.2.0-preview.31
Ein Zaun, benannt, weil er eine Lücke und keine Vorliebe ist: Eine AUSWÄHLBARE Liste ist weiterhin
eine Reihe einzelner Tab-Stopps, nicht ein Stopp mit ↑/↓ und Leertaste. ListItem.Selected nennt
deshalb aria-current="page" (ein Navigationsziel) und nicht aria-selected auf einer listbox:
Der wandernde Stopp gehört dem Container, und bis List ihn komponiert, würde eine option-Reihe
die Tab-Reihenfolge verlassen, ohne dass etwas sie zurückbringt.
Berührungsinteraktion: PullToRefresh, SwipeableRow. Eine Reihe, die offen ruht, behält das
Transform, das ihre eigene Box zeichnet, das ihres Hovers eingeschlossen: Der Versatz ist das
einzelne translate, das der Browser zusammen mit transform anwendet, so wie Photon die beiden
komponiert, und ein Wischen, das vor dem Öffnen endet, gleitet zurück (seit 0.2.0-preview.61).
Code: CodeBlock (nur lesen) und CodeEditor (Caret, Auswahl, Tastatur). Das vollständige Modell
und beide Flächen sind auf der Seite Code-Editor dokumentiert.
Jede Komponente wird mit Pins des Web-Realisierers, nativen Golden-Bildern (hell + dunkel), gepinnten
transpilierten Fixtures, die in vitest laufen, und dem lebenden Showroom ausgeliefert (/ und
/shared im DefaultUIDashboard: SSR + Hydration-Identität + Interaktion, Ende zu Ende von der
Playwright-Suite verifiziert). Systeme, die neben der Bibliothek ausgeliefert werden: das
Bewegungssystem für Zustandsübergänge (Presence Ein-/Ausgang), die Zeiger-Pipeline (Hover, Ziehen zum
Verwerfen mit Slop/Cancel/Glide), der Scroll-Compositor (echtes Anheften mit Pinned), verankerte
Overlays und die modale Fokusverwaltung (siehe unten).
Der Interaktionskontrakt des Design Systems (§10 der Spezifikation) wird von den Realisierern umgesetzt, also erbt ihn jede Komponente, statt dass jede improvisiert. Was folgt, ist das ausgelieferte Verhalten.
Seit 0.2.0-preview.24
Es gibt keinen sechsten Farbplatz: Die Hover-Füllung eines gefüllten Controls ist abgeleitet (die
Kanalmitte von Base → Pressed, als VariantColors.Hover offengelegt), und die stillen Varianten
(Outline, Ghost, IconButton Standard) hovern auf SurfaceSubtle, der Füllung, die ihr
Pressed-Zustand schon nutzt. Button und IconButton schreiben sie; im Web lebt jede :hover-Regel,
die die atomare Engine ausgibt, hinter @media (hover: hover), sodass der klebende Tap-Hover eines
Touch-Browsers überhaupt keine Regel findet, und auf Photon überspringt eine als Berührung
etikettierte Bewegung den Hover ganz. Gedrückt schlägt immer Hover, und Hover hinein und hinaus
nehmen dieselbe 100-ms-Stufe wie die Druckrückmeldung.
Seit 0.2.0-preview.24
RadioGroup und SegmentedControl folgen dem nativen Kontrakt von <input type=radio>: Die Gruppe
ist ein einzelnes fokussierbares role="radiogroup", das den Namen der Gruppe trägt, die Pfeiltasten
bewegen die Auswahl und laufen an den Enden um (Ab ist das Nächste in einer Radiogroup:
Leserichtung, nicht Slider-Wert), und jede Option ist ein role="radio", das aria-checked nennt,
außerhalb der Tab-Reihenfolge, per Zeiger weiterhin drückbar. Das Vokabular trägt dies als
PressableRole.Radio, und die Form ist seither über die Komposita übernommen worden: Die Elemente von
Tabs sind role="tab" und nennen aria-selected (ein Tab wird gewählt, nicht angehakt) unter dem
einzelnen Stopp der Leiste, und die Reihen von Menu/Select sind menuitem/option (siehe unten).
Seit 0.2.0-preview.24
Ein offenes, modales Overlay (worauf Dialog, Drawer und BottomSheet aufbauen) ist im Web ein
Dialog: role="dialog", aria-modal="true" (die Seite dahinter ist für assistive Technik untätig)
und tabindex="-1", damit die Ebene selbst der Rückfall für den Anfangsfokus ist (ein Sheet ohne
etwas Fokussierbares fokussiert das Sheet, wie die Spezifikation sagt). Während es offen ist,
kreisen Tab und Umschalt+Tab darin, und eine focusin-Wache holt alles zurück, was auf anderem Weg
entkommt; wenn es schließt (entfernt, über Overlay.Motion geschlossen und montiert geblieben, oder
mitten in der Ausgangsanimation) kehrt der Fokus zu dem Element zurück, das es geöffnet hat, auf
jedem Schließweg. Dialoge stapeln sich zuletzt hinein, zuerst hinaus. Toast fängt nie (es ist die
nicht modale Ebene), und Popover wird absichtlich nicht gefangen: Ein Popover ist eine nicht modale
Fläche, also läuft Tab aus ihm hinaus.
Seit 0.2.0-preview.29
Die Ebene nennt auch ihren NAMEN (Overlay.Label, da der Dialog seinen Titel als aria-label
weitergibt), eine destruktive Bestätigung ist ein role="alertdialog", bestimmt angekündigt
(Overlay.Alert), und der Anfangsfokus bevorzugt die SICHERE Aktion: Der Dialog markiert seinen
Ghost-Button (Button.InitialFocus), sodass ein aus Reflex gedrücktes Enter abbricht statt zu
zerstören.
Seit 0.2.0-preview.59
Ein Shortcut antwortet, wo auch immer die Tastatur ist, solange sein Teilbaum montiert ist, und der
zuletzt montierte gewinnt einen Akkord: Was das ⌘K einer Palette und das Escape eines Dialogs
brauchen. Die eigenen Tasten einer Komponente sind die andere Sorte, und FocusScoped ist für sie:
Der Akkord antwortet nur, solange der Fokus innerhalb des Teilbaums ist, und eine Taste, die kein
Bereich hält, wird in Ruhe gelassen, im Web dem Browser.
new Shortcut(form, KeyChord.Command("s"), Save) { FocusScoped = true }Das ⌘F des Code-Editors und das F7 des Diffs werden so deklariert. Mit zwei Editoren auf einer Seite
öffnete ⌘F die Suche des zuletzt montierten, wo auch immer die Tastatur war. Es öffnet die Suche
dessen, in den jetzt getippt wird, und mit der Tastatur in keinem von beiden behält der Browser seine
eigene Suche. Flutters Shortcuts antworten immer für den Fokus: Dieser ist von jener Sorte, neben
dem seitenweiten.
Seit 0.2.0-preview.60
Ein Akkord, der nur gilt, solange etwas auf dem Bildschirm ist, bleibt im Baum und sagt mit Enabled,
ob er lebt: Ist er es nicht, bindet er nichts, also geht die Taste weiter zu dem, was sie sonst
nehmen würde, so wie eine Action, die nicht aktiviert ist, sie in Flutter durchlässt. Stattdessen um
seinen Teilbaum herum montiert und abmontiert, würde er diesen Teilbaum im Baum verschieben.
new Shortcut(layers, KeyChord.Escape, CloseFind) { FocusScoped = true, Enabled = findOpen }Die Suchleiste des Code-Editors schließt so: Escape schließt die Leiste des Editors, in dem die
Tastatur ist, die Leiste oder den Code, und während die Leiste geschlossen ist, behält ein Dialog um
den Editor sein eigenes Escape. Select, Menu, TimePicker und DatePicker halten die Tasten
ihres Panels genauso, also verschiebt das Öffnen eines Panels nichts im Baum, und der Tastaturfokus
bleibt, wo er war.
Von zwei Shortcuts eines Akkords, bei denen einer den anderen umhüllt, antwortet der innere, im Web und auf Photon, so wie in Flutter die Shortcuts dem Fokus am nächsten zuerst antworten: In einem Dialog schließt das Escape der Suchleiste die Leiste, und das nächste Escape schließt den Dialog.
Seit 0.2.0-preview.24
TextInput gibt sein Label an die Eingabe als zugänglichen Namen weiter, seinen Hilfe- oder
Fehlertext als zugängliche Beschreibung (über aria-describedby verknüpft, höflich angekündigt,
wenn sie auf einen Fehler umschlägt), und die Fehlerhaftigkeit als aria-invalid. Was ein
Screenreader hört, ist genau die Zeichenkette, die die sehende Person liest. SearchField und die
Suchleiste des Code-Editors erheben ihren Platzhalter zu einem echten Namen: Ein Platzhalter
verschwindet unter dem Text, er tritt also nie an die Stelle eines Namens.
Seit 0.2.0-preview.29
Checkbox ist ein role="checkbox" und Switch ein role="switch", jedes nennt seinen Zustand als
aria-checked, mit "mixed" für die unbestimmte Checkbox, und nur dort, ARIAs eigene Regel. Der
zugängliche NAME sagt, wofür das Control da ist, und er ändert sich nie, wenn der Zustand sich
ändert (ein Name, der umschaltet, liest sich wie ein anderes Control, das auftaucht). Nativ trägt der
Semantikbaum dieselbe Antwort (SemanticCheck Off/On/Mixed), die macOS auf den numerischen Wert von
AXCheckBox abbildet, sodass VoiceOver den Zustand in der Sprache des Nutzers ankündigt, und
darum haben die alten "Checked"/"On"-Zeichenketten das SDK ganz verlassen.
Seit 0.2.0-preview.55
Ein Slider nennt seinen WERT, nicht nur seinen Namen: aria-valuenow, aria-valuemin und
aria-valuemax im Web und den Wert des semantischen Knotens auf Photon, sodass VoiceOver und
TalkBack dieselbe Zeichenkette ankündigen. ARIA verlangt, dass role="slider" aria-valuenow
trägt, und jeder Slider vor dieser Version kündigte sein Label und nichts weiter an. Wenn die Zahl
nicht das ist, was ein Mensch sagen würde, sagt valueText es in Worten, und ein Reader kündigt
diese Worte ANSTELLE der Zahl an:
Slider(budget, onBudget, valueText: "R$ 400");Im Vokabular ist es Adjustable.Value, ein RangeValue(Now, Min, Max) mit einem optionalen Text,
und die Rolle folgt dem Wert: Ein Adjustable in seiner Standardrolle ist ein Slider, wenn es einen
hat, und eine group, wenn es keinen hat, und genau das ist ein fokussierbarer Container, den die
Pfeiltasten verstellen. Ein Wert auf Tablist oder Radiogroup, Rollen, die keinen tragen, wird
nicht ausgegeben.
Seit 0.2.0-preview.56
Manche Änderungen haben keinen Fokus, der sie trägt: Ein Toast erscheint, ein Banner tauscht seinen
Text, ein Filter lässt drei Ergebnisse, wo es vierzig waren. Ein Screenreader sagt zu all dem
nichts, es sei denn, die Region, die sich geändert hat, kündigt sich selbst an. LiveRegion umhüllt
den Teil, der sich ändert, und beide Realisierer sprechen sie: aria-live im Web (polite, oder
assertive für LiveRegionUrgency.Assertive) und der Ankündigungsplatz der nativen Semantik auf
Photon, und das ist, was VoiceOver und TalkBack lesen.
LiveRegion(results, label: "Results");Banner und Toast tragen schon eine, also wird, was sie sagen, angekündigt, ohne dass der Aufrufer
darum bittet. Die Ankündigung wird EINMAL JE ÄNDERUNG berechnet, aus dem Frame, in dem der Inhalt
gezeichnet wird, und nicht aus dem Semantik-Gang: Dieser Gang antwortet einer Plattformbrücke, wann
immer assistive Technik neugierig ist, was nie, einmal oder dreimal im selben Frame sein kann, und
eine Ankündigung, die daran hängt, würde sich eine Anzahl von Malen wiederholen, die niemand gewählt
hat.
Seit 0.2.0-preview.31
Eine Leiste kündigte ihre drei Ziele identisch an, "Activity, button", "Search, button", "Profile, button", weil das aktive eine Pille und ein Farbton war, was für einen Screenreader überhaupt nichts ist. Es ist das eine Stück Zustand, für dessen Tragen eine Navigation existiert, und keine der drei Schreibweisen, die das Vokabular hatte, konnte es sagen: Ein Ziel ist nicht GEDRÜCKT (es schaltet nicht um), nicht ANGEHAKT (es wird nichts gewählt) und nicht AUSGEWÄHLT (ein Tab wechselt ein Panel; ein Ziel wechselt, wo Sie SIND).
Also trat PressableRole.Destination dem Satz bei, und Link.Current daneben für das eigene
Navigationsidiom des Webs. BottomNavigation, NavigationRail und ein als Selected markiertes
ListItem nennen es für Sie; eine handgebaute Seitenleiste aus Links sagt Current = true an
jenem, der zur Route passt.
-
Web:
aria-current="page", und nur am aktuellen.aria-current="false"ist erlaubt, nutzlos und wird vorgelesen, also sind neun Links, die "nicht aktuell" ankündigen, schlimmer als Schweigen. -
Native: Der Semantikbaum trägt das Bit, und jede Brücke meldet es mit dem nächstliegenden Wort,
das ihre Plattform hat: das
Selected-Trait auf iOS,accessibilitySelectedauf macOS,Selectedin Androids Node-Info. - Tab-Reihenfolge: Ein Ziel BEHÄLT seinen Tab-Stopp, anders als ein Tab oder ein Radio. Eine Navigation ist eine Liste von Links, kein Kompositum mit einem Einstiegspunkt, und wer die Tastatur nutzt und eine Leiste erreicht, erwartet, sie abzugehen.
Seit 0.2.0-preview.29
Tastaturfokus enthüllt einen Tooltip genau wie Hover (:has(:focus-visible), focus-visible und
nicht focus-within, weil ein Tooltip, der bei jedem Mausklick aufspringt, im Weg ist), die Pille ist
eine echte role="tooltip", auf die der Auslöser mit aria-describedby zeigt (ein Screenreader hört
den Hinweis nach dem Namen des Controls, ohne dass etwas enthüllt wird), und Escape verbirgt einen
enthüllten Tooltip, OHNE Zeiger oder Fokus zu bewegen, und das ist die Verwerfbarkeitsklausel von
WCAG 1.4.13. Den Auslöser zu verlassen hebt die Unterdrückung auf, der nächste Besuch enthüllt also
wieder: Esc ist ein "nicht jetzt", nie ein "nie wieder".
Seit 0.2.0-preview.29
Ein gedrehter Chevron ist Farbe, und Farbe sagt assistiver Technik nichts. Pressable.Expanded nennt
die Aufklappung: aria-expanded im Web, das Aufgeklappt-Bit des nativen Semantikknotens (VoiceOvers
"expanded"/"collapsed" auf macOS). Und null zieht dieselbe Linie, die Selected zieht: "steuert
überhaupt keine Aufklappung" ist eine andere Antwort als "steuert eine, derzeit geschlossen". Die
Header des Accordion, das Feld des Select und der Auslöser des Menu nennen es alle.
Seit 0.2.0-preview.29
Ein offenes Menu ist ein role="menu" aus menuitem-Reihen (deaktivierte Befehle eingeschlossen,
abgeblendet angekündigt statt verborgen), sein Auslöser trägt aria-haspopup/aria-controls. Ein
offenes Select ist das vollständige Combobox-Muster: Das FELD ist die role="combobox", das Panel
eine role="listbox" aus numerierten option-Reihen, die aria-selected nennen. Und in beiden wird
die Tastaturhervorhebung, die die Pfeile bewegen und die sonst nur Farbe wäre, als
aria-activedescendant am Auslöser genannt, sodass ein Screenreader, der der Tastatur folgt, jeden
Schritt hört. Der Fokus selbst verlässt den Auslöser nie, und das ist die eigene Anordnung des
Combobox-Musters.
Seit 0.2.0-preview.24
Keine Komponente verdrahtet eine an den Nutzer gerichtete Zeichenkette fest: die Verwerfen-Affordanzen,
der Suchplatzhalter, die Labels der Suchleiste lesen alle SdkStrings, die resx-gestützte Naht, die
die Lokalisierungsspur je Kultur füllt. Der Satz SCHRUMPFTE in preview.29: Die
alten Umschaltankündigungen ("Checked", "On") gingen, als der Zustand einer Checkbox in
aria-checked wanderte, weil die Plattform den Zustand in der Sprache des Nutzers ankündigt, was kein
resx von uns erreichen könnte.
Eine Liste, die nur die Reihen BAUT, die Sie sehen können: Geben Sie ihr eine Anzahl, eine feste Ausdehnung je Element und einen Builder je Index, und sie materialisiert das sichtbare Fenster plus einen Overscan-Rand. Der Rest der Liste sind zwei Abstandshalter, sodass das Layout (und die Scrollleiste) die wahre Inhaltshöhe sehen, während zehntausend Reihen kosten, was ein Bildschirm kostet. Write-once: Dasselbe C# läuft auf Photon und im Web.
new ListView(count: 10_000, itemExtent: 44, itemBuilder: i => RowFor(i))
{
Width = SizeValue.Fill,
Height = SizeValue.Fill, // die Liste ist ein FENSTER, geben Sie ihr eines
}Das Fenster konvergiert statt zu blockieren: Der erste Frame baut gegen eine Viewport-Vermutung, der
realisierte Frame meldet den wahren Viewport und Versatz über die Ausgangskanäle der ScrollView
(OnScrolled / OnViewportChanged, auf beiden Zielen verdrahtet), und der nächste Frame baut das
korrigierte Fenster. Pixel scrollen ohne Neuaufbau; der Zustand ändert sich nur, wenn eine Reihe den
Overscan-Rand überschreitet. Umfang (v1): vertikale Listen mit festem ItemExtent; variable
Ausdehnungen sind nicht enthalten.
Layout-Regeln, die Fenster überall funktionieren lassen (SDK-Invarianten, keine Pflichten der Anwendung):
- Der
#app-Rahmen der Anwendungs-Shell hat EXAKTE Viewport-Höhe (height: 100dvh, grid, Kinder mitmin-height: 0): Eine APP-Seite (Wurzel mitHeight = Fill) löst sich zu genau einem Viewport auf und scrollt intern; eine DOKUMENT-Seite (Wurzel mit automatischer Höhe) läuft über den Rahmen hinaus, und der Body scrollt, wie ein Dokument es immer tut. - Eine
ScrollViewohne ausdrücklicheHeightist im Web standardmäßigheight: 100%, weil eine Scroll-Ansicht das Fenster IST, das ihr Elternteil ihr gibt, nie ihr Inhalt (native Parität: Der Realisierer gibt ihr Layout-Grenzen und beschneidet immer). - Die Hydration übernimmt die
data-eq-*-Marker des Frameworks aus dem Client-Baum, weil SSR clientseitige Identitäten nicht kennen kann, und die späteren Durchgänge finden ihre Elemente daran.
Eine Tabellenkalkulation mit Excel-Bedienbarkeit, auf demselben Prinzip gebaut wie der Code-Editor: der Controller trägt alles, die Pixel sind Arithmetik. Ein C#-Modell + eine C#-Komponente rendern auf Photon und im DOM; der Browser-Build transpiliert genau dieselben Quellen.
Das Modell (eQuantic.UI.Primitives/Sheet, wörtlich geteilt):
-
SheetDocument: spärliche Zellen (leer ist abwesend), Größen je Zeile und Spalte, logische Ausdehnung.SetCell/SetRowHeight/SetColWidthsind der Lade- und Vorschauweg, absichtlich nicht rückgängig machbar. -
CellRef/SheetRange: A1-Adressierung; Anker-/Fokus-Auswahl in 2D. Zellen werden über einen gepackten int (row × 16384 + col) geschlüsselt, weil ein Record-Struct ein nutzloser Schlüssel für eine JS-Map ist. -
SheetController: Excels Semantik als Methoden. Pfeile/Umschalt erweitern, Strg+Pfeil springt an die Datenkante, Tab/Enter gehen (und laufen um) innerhalb einer Auswahl, während das Rechteck still steht (aktive Zelle ≠ Auswahlfokus, absichtlich), Auswahlen über Kopfbänder, Zeilen und Spalten einfügen/löschen/skalieren mit spärlichem, auf Inversen gebautem Undo/Redo, TSV-Kopieren/Einfügen, das Excels Quoting spricht, und der Entwurf fürs Editieren in der Zelle:BeginEdit/TypeIntoDraft/CommitEditleben im Controller, sodass beide Ziele identisch editieren und ein bestätigter Entwurf als ein Schritt rückgängig geht. -
SheetKeymap: DIE eine Tastatur. Tippen ersetzt (Excels Schnelleingabe), F2 editiert an Ort und Stelle, Enter/Tab bestätigen und rücken weiter, Escape verwirft, Pfeile bestätigen und bewegen dann, ⌘A/⌘Z. Der native Host und das Web-Lowering delegieren beide hierher; der Dialekt kann nicht abzweigen.
Die Komponente (eQuantic.UI.Components/Spreadsheet.cs):
- Spaltenköpfe oben fest; Zeilenköpfe scrollen MIT den Zeilen; das sichtbare Fenster der Zellen wird
in eine
SheetSurfacegehüllt, die INNERHALB des Scrolls lebt, sodass Marken sich mit dem Inhalt verschieben und ein Klick auf ein gescrolltes Gitter die Zelle auswählt, die tatsächlich unter dem Zeiger liegt, von Bauart. - Zeilen virtualisieren nach Art der ListView (Fenster + Abstandshalter); das Auswahlband, der Ring der aktiven Zelle und der Editierentwurf mit Caret werden AUF die Zellen gemalt, in der Komponente, sodass beide Ziele optisch identisch sind, weil es realisiererseitig nichts gibt, was abdriften könnte.
- Auswahl im Excel-Kern: Kopfklicks wählen ihre ganze Zeile oder Spalte (die Ecke wählt alles, und Köpfe schattieren, wenn das Band der Auswahl sie kreuzt); Umschalt+Klick streckt vom Anker, ohne die aktive Zelle zu bewegen; ein einfaches Ziehen streicht einen Bereich auf beiden Zielen.
-
Der Füllgriff, Excels kleines Quadrat an der unteren rechten Ecke der Auswahl, zieht eine
Vorschau entlang der DOMINANTEN Achse und gießt den Quellblock beim Loslassen darüber, phasenweise
mit Umlauf in alle vier Richtungen gefliest, als EIN Undo-Schritt. ⌘D/⌘R gießen nach unten und nach
rechts durch dieselbe Maschine in der gemeinsamen Keymap. Die Semantik der Gestik
(
BeginFill/UpdateFill/CommitFill,Fill,FillDown,FillRight,SelectTo) lebt im Controller, sodass beide Ziele identisch füllen. -
Skalieren durch Ziehen: Der Endrand jedes Kopfes trägt einen unsichtbaren 6dp-
Draggable-Griff (Stack+Positioned,Follows = false). Bewegungen zeigen direkt im Dokument eine Vorschau (SetColWidth/SetRowHeight); das Loslassen spult die Vorschau zurück und setzt die ganze Gestik als EIN rückgängig machbaresController.Resize. Die Böden (MinColWidth24,MinRowHeight14) halten einen Splitter greifbar, in der Komponente begrenzt und nicht imMin/Maxdes Knotens (die bauen mitten im Ziehen neu). Innerhalb der ScrollView gewinnt der Griff über das Scrollen: Eine Ziehfläche ist die spezifischere Gestik (Host-Regel,PressDown). - Verdrahtung der Interaktion: Der native Host leitet Klicks, Auswahlziehen, Tasten,
TSV-Zwischenablage und IME-sicheren Text an den Controller; das Web-Lowering (
lowerSheetSurface) ist ein fokussierbaresrole="grid"-div mituser-select: none(ein Auswahlziehen malt das Band, nicht den blauen Strich des Browsers), keydown → die transpilierteSheetKeymap, Doppelklick editiert, und Kopieren/Ausschneiden/Einfügen reiten auf den eigenen Zwischenablage-Events des Browsers in TSV. -
Zeigercursor sind Vokabular (
BoxStyle.Cursor, der Spiegel des CSS-cursor): Das Web gibt die Deklaration aus, Photon registriert eineCursorRegion, die dasCursorAtdes Hosts von oben nach unten beantwortet, und die macOS-Shell bildet aufNSCursorab. Die Skaliergriffe sagen col-resize/row-resize und der Füllgriff sagt crosshair, auf beiden Zielen, aus demselben Komponentencode.
Umfang (v1): keine Formel-Engine, keine Formatierung je Zelle, keine Verbünde, keine fixierten Bereiche. Die Virtualisierung ist nur vertikal (Spalten materialisieren), und es gibt kein 2D-Scrollen, also werden Spalten jenseits der Breite des Bereichs beschnitten. Die Suiten für Controller, Editieren und Komponente treiben beide Achsen des Skalierens durch den Host; der transpilierte Zwilling spielt dieselben Züge unter vitest, dazu Web-Flächen-Spezifikationen mit echten DOM-Events.
Positioned ist ein Kontrakt mit dem ELTERNTEIL: wie ein Flex-Gewicht bedeutet es sonst nirgends
etwas. Es muss Kind eines Stack sein, und bis 0.2.0-preview.18 hieß das direktes Kind: Eine
Komponente, deren Build eines zurückgab, wurde nicht erkannt, fiel auf ihr Kind zurück und trat dem
Fluss bei. Ein Eckenbutton rendete über der Platte, zu der er gehörte, hart am linken Rand, auf beiden
Zielen und stillschweigend.
Beide Realisierer lösen nun durch die Komponente auf, also funktioniert das hier:
Stack(children: [
CodeSlab(code),
CopyAction(code), // sein Build gibt Positioned(top: 0, end: 0) zurück
])Ein positioniertes Kind nimmt weiterhin nicht an der INHALTSGRÖSSE des Stacks teil, sonst würde ein Eckenabzeichen die Platte wachsen lassen, auf der es sitzt.
Seit 0.2.0-preview.61
Ein Rand ist ein Punktversatz, ein BRUCHTEIL des Stacks, oder beides addiert; dann verschieben
ShiftX und ShiftY das Kind um Bruchteile seiner EIGENEN Größe (Flutters
FractionalTranslation). So wird etwas auf einer Box platziert, deren Größe die Anwendung nie
erfährt: ein Tooltip, 16 über dem Punkt bei 50% / 25% einer Karte zentriert, welche Breite die Karte
am Ende auch hat.
Positioned(tooltip, top: -16, startFraction: 0.5f, topFraction: 0.25f, shiftX: -0.5f, shiftY: -1f)Das Web schreibt left: 50%; top: calc(25% - 16px); transform: translate(-50%, -100%). Photon
platziert mit derselben Arithmetik in seinem Layout-Durchgang, also liegt die Trefferregion dort, wo
das Kind gezeichnet wird, so wie ein CSS-Transform es im Browser lässt.
Seit 0.2.0-preview.61
GridTrack.AutoFill(min) ist so viele Spalten, wie Spuren von mindestens min Breite passen, der
Gap des Gitters zwischen ihnen, und sie teilen sich, was übrig bleibt: das Kartengitter, das jedes
Handoff als repeat(auto-fill, minmax(210px, 1fr)) zeichnet, was das Web als
minmax(min(210px, 100%), 1fr) schreibt, damit ein Gitter, das schmaler als eine Spur ist, eine
Spalte eigener Breite ist statt eines Überlaufs, so wie Photon es zeichnet. Das Minimum ist
mindestens 1dp und das Gewicht mindestens 1.
Grid([GridTrack.AutoFill(210)], gap: 10, width: SizeValue.Fill, children: [.. cards])Es ist die ganze Spaltenliste des Gitters: Neben einer anderen Spur bräuchte es einen Löser, um die Breite zu teilen, und der Konstruktor verweigert sie mit dem Grund. Auf einer Breite, die das Gitter aus seinem eigenen Inhalt bemisst, gibt es nichts zu teilen, es ist also eine Spalte an ihrem Minimum, was CSS dort auch antwortet.
Seit 0.2.0-preview.20
InView(Heading(section.Title), visible => SetState(() => _active = visible ? section.Id : _active))Die Frage, die ein Inhaltsverzeichnis wirklich stellt, ist welche Überschrift die Leserin ansieht,
und die einzige Art, sie zu beantworten, war die Scrollposition der Seite, die auf einer ScrollView
lebt, der Artikel musste also in eine gehüllt werden. Das ändert das Scrollmodell der ganzen Seite:
Der klebende Header klebt nicht mehr am Fenster, die eigene Scroll-Wiederherstellung des Browsers
funktioniert nicht mehr, und die Adressleiste versteckt sich auf einem Telefon nicht mehr.
Ein roher Versatz hätte auch nicht viel geholfen. Er ist eine Zahl in den Koordinaten der Seite, und "ist diese Überschrift darüber hinaus" zu beantworten braucht die Position jeder Überschrift, eine Messung, die eine Write-once-Komponente nichts angeht und die sie auf beiden Zielen nicht gleich machen kann. Anwesenheit ist die Frage, also ist Anwesenheit, was dies meldet.
- Feuert nur auf den Übergängen: true, wenn das Kind ins Blickfeld kommt, false, wenn es es verlässt. Nie einmal je Frame, während es dort sitzt.
-
Threshold(0 bis 1) verlangt mehr als einen Splitter: eine Überschrift, halb oben heraus, ist nicht die, auf der die Leserin ist. - Nicht sichtbar ist der angenommene Zustand, eine Komponente, die mit ihrem Kind außerhalb des Bildschirms montiert, hört also nichts. Dreißig Überschriften kündigen keine dreißig Abwesenheiten an.
- Layout-durchlässig: Es nimmt keinen Platz und zeichnet nichts.
Im Web ist es ein IntersectionObserver: kein Scroll-Listener, also keine Arbeit in den Frames, in
denen nichts überschreitet, und die Messung passiert außerhalb des Haupt-Threads. Photon vergleicht
Grenzen mit der Fläche einmal je Frame, auf einem Gang, den es schon macht.
Beide beschneiden. Eine Reihe, die aus einer ScrollView oder aus einer beschneidenden Box
herausgescrollt ist, ist nicht auf dem Bildschirm. Auf Photon verengt sich, was als sichtbar gilt,
durch jeden Schnitt, so wie Malen und Eingabe sich schon verengen; im Web beschneidet der Observer
selbst an jedem Vorfahren. So lädt eine gefensterte Liste nur, was die Leserin sehen kann, und das
ist die Arbeit, die dies billig macht. Das Beschneiden verengt sich auf Photon seit
0.2.0-preview.21.
Zwei Dinge mussten im Web wahr sein, bevor irgendetwas davon etwas meldete, und beide waren falsch, als dies ausgeliefert wurde (behoben in 0.2.0-preview.21):
-
Der Marker gehört auf das KIND, nie auf die layout-durchlässige Hülle.
display: contentserzeugt keine Box, also beobachtet ein auf die Hülle gerichteter Observer ein 0×0-Rechteck am Ursprung. Er meldet treu, über einen Ort, an dem die Überschrift nie ist. - Die Observer hängen sich an, nachdem der Durchgang GESCHRIEBEN wurde. Ein Durchgang erzeugt einen Baum; der Render-Manager schreibt ihn, sobald der Durchgang zurückkehrt. Das Festschreiben innerhalb des Durchgangs suchte Elemente, die noch nicht existierten, fand keine und löschte die Deklarationen, sodass eine frisch hydrierte Seite jeden markierten Knoten ohne Observer trug und erst ein spätes Neurendern sie anhängte. Auf einer Seite, auf der sich sonst nie etwas ändert, ist das nie.
Seit 0.2.0-preview.23
Link("/docs/Storage", Text("Storage")) { KeepsPosition = true }Eine Navigation beginnt die neue Seite oben, was für einen Link richtig ist, der Sie woandershin bringt, und falsch für einen, der ein Panel neben einer Liste tauscht, in der Sie halb unten waren. Eine Dokumentations-Seitenleiste ist der Fall, der ihm den Namen gibt: Die Shell wird bewahrt, und nur ein paar hundert Knoten werden geflickt, und dann springt alles nach oben, was sich für die Person, die hinsieht, genau so liest, als hätte die Seite neu geladen.
Chrome, das sein EIGENES Scrollen behält, hatte das Problem nie und braucht dies nicht. Es ist für die Layouts, in denen Liste und Inhalt ein Scrollen teilen.
Seit 0.2.0-preview.16
InFlow(Dialog("Delete this?", "It cannot be undone.", [new DialogAction("Delete")]))
InFlow(Drawer(content)) // kein `open: true` nötig, siehe unten
InFlow(BottomSheet(content))
InFlow(Popover(trigger, panel))Ein Dialog, ein Drawer, ein Sheet und ein Popover existieren erst, sobald etwas sie öffnet, und wenn es das tut, nehmen sie den ganzen Viewport: ein Scrim über der Seite, die Fläche davor zentriert oder angeheftet, eine Escape-Bindung, eine Eingangsanimation. Eine Seite, die eines ZEIGEN will (Dokumentation, eine Design-Durchsicht, eine Suite visueller Regression), will die Fläche und nichts davon. Fünf Komponenten rendeten leer.
InFlow nennt die Absicht; jedes Overlay antwortet darauf, indem es sein Panel baut und nichts
weiter, weil die Komponente das Einzige ist, das weiß, was ihre Fläche ist.
Im Fluss gibt es kein offen oder geschlossen: Das Panel ist platziert, also ist es da. Open
bestimmt, ob die EBENE existiert, und es gibt keine Ebene. Ein Drawer umschließt seinen Inhalt, statt
den Viewport zu füllen (die volle Höhe gehört dem Rand, an dem er angeheftet ist, und er hat keinen
Rand); ein Popover zeichnet sein Panel ohne seinen Auslöser; ein PullToRefresh ruht mit
unbedecktem Indikator, da nichts gezogen werden kann.
Es ist kein Vorschaumodus: Die Fläche ist echt, mit echten Aktionen. Was wegfällt, ist die Darstellung. Und die Absicht endet mit dem Knoten: Ein wirklich geöffneter Dialog neben einem in der Vorschau nimmt weiterhin den Viewport.
Seit 0.2.0-preview.15
Simulated(SimulatedState.Hovered, Button("Save"))
Simulated(SimulatedState.Pressed | SimulatedState.Focused, IconButton(Icons.Check))Ein gedrückter Button kann einem Konstruktor nicht übergeben werden: gedrückt ist etwas, das der Host beobachtet, nicht etwas, das ein Baum nennt. Also konnten eine Dokumentationsgalerie, eine Design-Durchsicht und eine Suite visueller Regression immer nur den Ruhezustand eines Controls zeigen, und die drei Zustände, die eine Leserin am meisten vergleichen will, waren die drei, die keine Seite zeichnen konnte.
Ein Knoten statt einer Eigenschaft an jeder interaktiven Komponente: Er komponiert, er deckt die Komponenten ab, die noch nicht existieren, und er funktioniert für ein Control, das drei Ebenen tief in einer Karte in der Vorschau steckt. Geschachtelte Vorschauen KOMBINIEREN sich: eine gehoverte Karte, die einen gedrückten Button hält, sind zwei Knoten, und der innere schaltet den Hover des äußeren nicht ab.
Es ändert nur, was GEZEICHNET wird. Nichts wird aufgerufen, kein Zustand wird betreten, kein Handler läuft, und der Zustand endet mit dem Knoten: Das Control daneben verhält sich normal.
Im Web können :hover und :focus-visible nicht aus CSS erzwungen werden (nichts kann es, und das
ist der Sinn einer Pseudoklasse), die Diffs, die jene Regeln tragen, werden also in die Basisregel
eingefaltet. Dieselben Deklarationen, die Vorschau ist also die echte Sache und nicht eine Annäherung
an sie. Ein simulierter Druck nutzt genau denselben Selektorkörper, den ein echter Druck nutzt, und
das ist, was die beiden am Abdriften hindert.
- Write-once-Komponenten: die gemeinsame Architektur, auf der diese Komponenten reiten
- Design System: Tokens, Dichte, Theming, die atomare Style-Engine
-
Code-Editor: das Editor-Modell,
CodeBlockundCodeEditor - Server-Integration: SSR, SEO und Serverkonfiguration
🏁 Hier starten
🏗️ Architektur
- Architekturüberblick
- Write-once-Komponenten
- Deklarative Oberfläche
- Paketarchitektur
- Komponenten
- Styling
- Lokalisierung
- Analytics und GTM
📱 Write-once
- Photon-Engine
- Design System
- Fähigkeiten
- Speicher
- Formulare
- Code-Editor
- Markdown
- Mermaid
- E-Mail-Rendering
⚙️ Kompilierung
⚡ Runtime
🔌 Server
🎨 Ökosystem
🚀 Entwicklung