Die Styling-Architektur von eQuantic.UIeQuantic.UI bietet eine von Flutter inspirierte Entwicklungserfahrung (DX) und nutzt dabei die volle Kraft jedes Render-Ziels. Die Styling-Architektur steht auf drei Säulen:
1.
Abstraktion: Komponenten schreiben, _was_ gestylt wird, als typisierte C#-Werte, nie als CSS-Strings.
2.
Eine Semantik, zwei Ziele: Dasselbe Style-Vokabular treibt dp-Werte auf Native (Photon) und CSS im Web.
3.
Performance: Klassenerzeugung und Deduplizierung zur Build-Zeit, ohne den Mehraufwand einer CSS-in-JS-Runtime.
1. Komponenten stylen: typisiertes C#, keine CSS-Ebene
Das Styling von Komponenten wird vollständig in typisiertem C# geschrieben (BoxStyle, StyleDiff, Design-Tokens) und von der atomaren Style-Engine abgesenkt: Jede gewöhnliche Deklaration wird zu EINER deduplizierten atomaren Klasse, byte-identisch zwischen SSR (C#) und Hydration (TS); Theme-Farben verweisen auf var(--eq-color-*)-Custom-Properties. Hover- und Focus-Zustände, die Adaptivität nach Fenstergrößenklasse, klebende Positionierung und Transforms sind alle Teil des Vokabulars, ohne jedes JavaScript.
Hover ist abgeleitet, und er gilt nur für den Zeiger
Seit 0.2.0-preview.24
Die Variantenpalette bleibt bei fünf Sub-Tokens; der Hover löst sich durch ABLEITUNG auf: VariantColors.Hover ist die Kanalmitte von Base → Pressed (ColorToken.MidpointWith, dieselbe Farbe, auf der color-mix(in srgb, Base 50%, Pressed) landet), und stille Varianten hovern auf SurfaceSubtle. Komponenten schreiben ihn als gewöhnlichen BoxStyle.Hover-Diff, beide Realisierer wussten also schon, wie man ihn malt: das Web als atomare :hover-Regel, Photon über die Zeiger-Pipeline.
Zwei Regeln reiten in der Ausgabe selbst mit. Jede atomare :hover-Regel wird in @media (hover: hover) gehüllt, sodass der klebende, emulierte Hover eines Touch-Browsers keine Regel findet, und das Gate hüllt die Regel, nie den Hash, sodass SSR und Hydration weiterhin identische Klassenstrings vergleichen. Und auf Photon wird der Hover nach Layout-PFAD als KETTE alles dessen verfolgt, was unter dem Zeiger liegt, denn CSS' :hover trifft jeden Vorfahren: Den Auslöser eines Tooltips zu hovern hovert auch den Tooltip, und ein Komponenten-Neuaufbau, der jede Knoteninstanz ersetzt, verliert den Hover, den er neu gemalt hat, nicht mehr.
Eine Farbe, die eine Komponente ableitet, läuft im Browser
Seit 0.2.0-preview.60
Der Browser hält eine Color als reine Daten, { r, g, b, a }, weil die Styles der Runtime, das generierte Design System und ein Hydration-Payload alle eine ohne Konstruktor erzeugen. Der browserseitige Code einer Komponente schreibt sie weiterhin als C#: Brand.WithOpacity(0.8f), a.MidpointWith(b), new Color(…), default(Color), $"{color}" und eine Methodengruppe wie Func<float, Color> fade = Brand.WithOpacity. eqc senkt jede davon auf die Color der Runtime ab, und der Text ist der Record-Text, den .NET schreibt, Color { R = 248, G = 113, B = 113, A = 204 }.
Vor 0.2.0-preview.60 rendeten diese auf dem Server und warfen im Browser, sobald die Seite hydrierte (withOpacity is not a function), und ein eigener Typ der Anwendung mit dem Namen Color wurde als der des Vokabulars gebaut.
Eine Kurve, die eine Komponente baut, läuft im Browser
Seit 0.2.0-preview.61
Eine Curve ist im Browser ebenfalls reine Daten, { x1, y1, x2, y2 }, wo auch immer der Browser einer begegnet: ein Preset, die Kurve einer Bewegungsrolle, das Easing einer Transition, eine Kurve, die eine Komponente baut, eine Kurve im Zustand einer Seite. Der browserseitige Code einer Komponente liest und baut eine Kurve also so, wie C# es tut: Curve.Standard.X1, Motion.Press.Curve, new Curve(0.2f, 0.9f, 0.3f, 1.25f) als Easing eines TransitionSpec, ==, with, eine Dekonstruktion und $"{curve}", was Curve { X1 = 0.2, Y1 = 0, X2 = 0, Y2 = 1 } druckt, wie .NET es tut.
var easing = new Curve(0.2f, 0.9f, 0.3f, 1.25f);
Transition = new TransitionSpec(StyleChannels.Colors, 150) { Easing = easing },
Der Evaluator bleibt auf dem Server und in den nativen Shells. Eine Web-Transition ist eine CSS-Timing-Funktion, der Browser wertet die Kurve also selbst aus, und eine Komponente, die curve.Ease(t) oder CurveEvaluator.Ease(curve, t) aufruft, lässt den Build mit EQ2010 scheitern. Geben Sie die Kurve einem TransitionSpec, oder rufen Sie den Evaluator aus Servercode auf.
Vor 0.2.0-preview.61 war ein Preset im Browser ein Array und die Kurve einer Bewegungsrolle ein Name, also las ein Punkt undefined, new Curve(…) warf "is not a constructor", sobald die Seite hydrierte, und curve.Ease(t) baute und warf.
Ein Zustand ändert jedes Mitglied des Styles
Seit 0.2.0-preview.60
Ein StyleDiff ist, wie eine Box aussieht, während ein Zustand aktiv ist: BoxStyle.Hover unter dem Zeiger und BoxStyle.Focus und BoxStyle.Pressed, während ihr Control fokussiert oder gedrückt ist (siehe unten). Jedes Mitglied, das er setzt, gilt, im Web und auf Photon, und jedes Mitglied, das er ungesetzt lässt, behält den Wert der Basis. Bis zu dieser Version wandte ein Hover auf Photon nur seinen Hintergrund und seinen Rand an, und im Web konnte er eine Box nicht bewegen und seine eigenen Schatten nicht ändern.
Background = theme.Surface,
Shadows = [new ShadowSpec(12, 30, -12, glow)],
Transition = new TransitionSpec(StyleChannels.Transform | StyleChannels.Shadow),
Transform = Transform2D.Translate(0, -2),
Shadows = [new ShadowSpec(16, 36, -12, glow)],
Diese Karte hebt sich unter dem Zeiger um 2dp und vertieft ihr Glühen, und gleitet in beide Richtungen. Was jedes Mitglied tut, während der Zustand aktiv ist:
•
Background, BorderColor, BorderWidth, Opacity und BackdropBlur ersetzen die der Basis.
•
Transform ersetzt das Transform der Basis, so wie eine CSS-Regel transform ersetzt: Eine Karte, die im Ruhezustand gedreht ist und beim Hover angehoben wird, nennt beide in ihrem Hover, Transform2D.Rotate(2).WithTranslate(0, -2), und Transform2D.Scale(1) macht das des Ruhezustands rückgängig.
•
Elevation und Shadows ändern ihren Teil EINER Schattenliste, und der Rest der Liste bleibt: der Schatten der Elevation, dann die eigenen Schatten (die des Zustands ersetzen sowohl Shadow als auch Shadows der Basis), dann das innere Highlight. Ein Zustand, der nichts zu zeichnen lässt, schreibt eine Liste, die nichts zeichnet.
•
Gradient ersetzt den Gradienten der Basis und behält das Glühen und das Gittermuster der Basis darunter.
Ein Schatten ohne Versatz, Weichzeichnung oder Spreizung zeichnet nichts, er bleibt also auf beiden Zielen aus der Liste. Unter einer Transition gleiten beide Ziele die Deckkraft, das Transform, die Farben und jeden Schatten, die eigenen eingeschlossen, und ein Schatten, den ein Zustand hinzufügt oder wegnimmt, blendet ein oder aus (Photon seit 0.2.0-preview.61). Eine angehobene Box nimmt im Web auch ihre Trefferfläche mit, wie jedes CSS-Transform, sodass ein Zeiger, der auf dem Rand ruht, von dem sich die Box wegbewegt, sie anheben und wieder fallen lassen kann. Photon testet eine transformierte Box noch dort auf Treffer, wo sie ausgelegt wurde, ihr ruhendes Transform eingeschlossen (#513). Halten Sie eine Hebung klein, oder legen Sie den Hover auf eine Box, die sich nicht bewegt. StyleDiff ist seit dieser Version eine Klasse (es war ein Struct): Hover!.Value.Background wird Hover!.Background, und eine Box, die keinen Zustand deklariert, trägt an seiner Stelle ein null.
Druck und Fokus eines Controls
Seit 0.2.0-preview.61
Ein Hover ist der eigene Zustand der Box: Der Zeiger ist über ihr. Ein Druck und ein Tastaturfokus geschehen einem CONTROL, dem Pressable, also gelten Pressed und Focus für jede Box innerhalb des Controls, während es gedrückt oder fokussiert ist: die Fläche und alles darin, was sagt, wie es dann aussieht.
Pressable(Box(new BoxStyle
Background = theme.Surface,
Transition = new TransitionSpec(StyleChannels.Transform, Motion.BaseMs),
Pressed = new StyleDiff { Transform = Transform2D.Scale(0.985f) },
Focus = new StyleDiff { BorderColor = theme.FocusRing, BorderWidth = 2 },
}, label), onPressed: save)
Dieses Control drückt auf 98,5% ein und zurück und nimmt von der Tastatur einen Fokusrand, auf beiden Zielen. Die Regeln:
•
Wenn Zustände zusammentreffen, schlägt ein Druck einen Fokus und ein Fokus einen Hover, Mitglied für Mitglied, in der Reihenfolge, die §10 des Handoffs gibt. Pressable.PressedBackground bleibt die stärkste Füllung.
•
Der Fokusring wird neben den Schatten der Box gezeichnet: Ein angehobenes oder glühendes Control behält seine Elevation und sein Glühen, während es den Fokus hat. Im Web ersetzte der Ring sie.
•
Ein Simulated-Druck oder -Fokus zeichnet, was ein echter zeichnet, den Ring eingeschlossen.
•
Ein Control ohne PressedBackground behält seine eigene Füllung, während es gedrückt ist. Im Web wurde es durchsichtig.
•
Eine Transition, die die Fläche deklariert, ist ihre eigene: Das Einblenden der gedrückten Füllung ist eine Voreinstellung, die eine Transition an der Box überschreibt, sodass ein Druck, der skaliert, gleitet. Im Web ersetzte das Einblenden sie.
•
Ein deaktiviertes Control zeigt keinen Druck und keinen Fokus, auf beiden Zielen: nicht, wenn es deaktiviert wurde, während es gehalten war, nicht in einem Simulated-Bild und nicht, wenn ein aktiviertes Control um es herum gedrückt ist.
Wo die Ziele sich unterscheiden: Photon hat keine gehaltene Taste, eine Taste aktiviert ihr Control beim Hinuntergehen, ein Pressed-Diff zeigt sich für eine gehaltene Taste also nur im Web. Und bei einem Control innerhalb eines Controls drückt das Web auch das äußere, während das innere gedrückt ist, so wie CSS' :active durch die Vorfahren aufsteigt, während Photon nur das Control drückt, das der Zeiger getroffen hat.
Focus hat in dieser Version seine Bedeutung geändert, vom eigenen :focus-visible der Box, das eine Box nie hat, zum Fokus ihres Controls: Ein früher geschriebenes Focus galt nirgends im Web und nirgends auf Photon, und gilt nun.
Seit 0.2.0-preview.61
ScrolledStyle = new StyleDiff
Background = theme.Surface,
Elevation = 2, // Flutters scrolledUnderElevation
BorderColor = theme.Border, // die Haarlinie entlang der Unterkante des Headers
Transition = new TransitionSpec(StyleChannels.Colors | StyleChannels.Shadow),
Jedes Mitglied von ScrolledStyle gilt, im Web und auf Photon, solange die Fläche, an die sich der Header heftet, über Pinned.ScrolledThreshold (8dp) hinaus gescrollt ist. Der Diff wird über Pinned.ScrolledBase gelegt: Ein Header zeichnet nichts Eigenes, und sein Rand ist die Haarlinie entlang seiner Unterkante, der Kante, unter der der Inhalt scrollt. Unter der Transition des Headers gleitet er auf beiden Zielen ein und aus.
Die Fläche ist die, an die sich position: sticky heftet: die nächste ScrollView um den Header, oder die Seite, wenn er in keiner ist. Ein Header in einem scrollenden Panel folgt dem Panel, nicht dem Fenster. Auf Photon scrollt die Seite selbst nie, ein Header außerhalb jeder ScrollView ist dort also nie gescrollt.
Davor schrieb das Web vier von sieben Mitgliedern und einen Rand allein, also taten eine Elevation, ein Gradient, ein Transform und die Shadows nichts, und Photon zeichnete davon nichts.
Seit 0.2.0-preview.23
Photon malt in KINDREIHENFOLGE: Ein späteres Geschwister liegt obenauf, und es muss nichts gesagt werden. CSS hört in dem Moment auf, so zu arbeiten, in dem Boxen Stapelkontexte erwerben, also buchstabiert der Web-Realisierer aus, was Native umsonst gibt, in den eigenen Worten des Design Systems, nie in denen von CSS.
Elevation entscheidet, was darüber liegt, nicht nur, wie tief der Schatten ist. Sie zeichnete einen Schatten und sonst nichts, und das ist ein halber Satz: Eine erhobene Fläche, die alles überdeckt, was nach ihr gemalt wird, ist nicht erhoben. Die Stufen 1 bis 5 sind die INHALTSEBENE, und sie sind die einzigen Zahlen, die eine Autorin schreibt.
Chrome ist eine Ebene, keine erhobene Karte. Alles Angeheftete (ein Pinned-Header, schwebend oder im Fluss) sitzt über jeder Inhaltselevation. Früher saß es eine Stufe über nichts, und das ist eine Zahl, die mit anderen Zahlen konkurriert: Geben Sie der Elevation den Bereich 1 bis 5, und eine bloß erhobene Karte würde den Header im Stapel überbieten und geradewegs darüber scrollen.
Darum hat Pinned kein Layer und wird nie eines haben. Ein z-index ist CSS-Vokabular, und von einer Autorin, die eines wählen muss, wird verlangt zu wissen, welche Zahlen alles andere gewählt hat. Die Elevation sagt wie hoch, der Realisierer entscheidet wie das geschrieben wird, und Chrome liegt nicht auf derselben Skala wie der Inhalt.
Eine Ausnahme von eine Deklaration, eine Klasse, und es ist überhaupt keine, sobald sie benannt ist: Ein Herstellerpräfix ist keine eigene Deklaration, es ist dieselbe, für eine andere Engine geschrieben. Das Paar teilt eine Klasse, deren Regel beide nennt, die Standardeigenschaft zuletzt. Allein in einer Regel wird -webkit-backdrop-filter ganz von einer Engine verworfen, die nur den Standardnamen nimmt (insertRule lässt in Chromium eine LEERE Regel zurück, gemessen), also wurde die Klasse berechnet, gehasht, ausgegeben, auf das Element gesetzt, und tat nichts. Seit 0.2.0-preview.21.
Eine atomare Klasse ist der schwächste Selektor, den es gibt. Eine Klasse ist Spezifität (0,1,0), also überbietet jede Regel des generierten Stylesheets, die ein Element nennt (.eq-spinner rect ist (0,1,1)), die Klasse, die ein Knoten trägt. Das macht die Einschränkung einseitig: Das generierte Stylesheet darf nie eine Eigenschaft deklarieren, die ein Knoten atomar tragen kann, und eine Kurzschreibweise zählt, weil sie jede Langform schreibt, die sie auslässt.
Der Spinner ist der gemessene Fall. Seine acht Balken tragen je ihr eigenes negatives animation-delay, und diese Phase IST die Drehung; das Einblenden daneben war als animation: eq-spinner-fade 800ms linear infinite geschrieben, was stillschweigend animation-delay: 0s über alle acht schrieb. Serverseitig gerendert pulsierte der Ring an seinem Platz, statt sich zu drehen, während der Client sich aus dem falschen Grund korrekt drehte, ein Inline-Style, der jeden Selektor überbot. Diese Regel steht jetzt in Langformen, und eine Wache behauptet, dass nichts außer Reduce Motion den Verzug eines Balkens deklariert. Dieselbe Form wie das Herstellerpräfix oben: Die Klasse wurde berechnet, gehasht, ausgegeben, auf das Element gesetzt, und etwas anderes gewann.
Seit 0.2.0-preview.7
Ein nicht proportionaler Lauf (ein Text mit Mono, ein reicher Lauf, ein CodeBlock) zeichnet in einem Stapel, den das Framework besitzt, und er landet auf einer atomaren Klasse, die eine body { font-family }-Regel schlägt und deren Name ein Inhaltshash ist, der sich zwischen Builds bewegt. Es gab also keinen Ort, an dem man seine eigene Code-Schrift aufhängen konnte. Der Haken ist eine Variable:
:root { --eq-font-mono: 'JetBrains Mono', ui-monospace, monospace; }
Jeder Mono-Lauf folgt ihr, mit dem Plattformstapel als Rückfall, wenn nichts sie setzt.
Der Messer liest dieselbe Variable, und das ist kein Implementierungsdetail, das Sie ignorieren können, wenn Sie einen Code-Editor ausliefern: CodeEditor setzt seinen Caret aus einem gemessenen Spaltenvorschub, in einer Schrift zu messen und in einer anderen zu zeichnen setzt den Caret also neben das Zeichen, auf dem er steht, statt darunter.
Die vollständige Engine (Gesetze, Pseudozustände, Größenklassen, das generierte Stylesheet) ist in DesignSystem dokumentiert. Das abstrakte Vokabular selbst (Box, Row/Column, Grid, Stack, …) ist in Write-once-Komponenten dokumentiert. Theming = ein IAppTheme bereitstellen
Themes sind ebenfalls typisiertes C# (Farben als Hell-/Dunkel-Paare von ColorToken, Typo-Rollen, Formskala, Elevation), in einer Zeile gewählt und von SSR zum Client gebrückt:
builder.Services.AddUI(options =>
options.ScanAssembly(typeof(Program).Assembly)
.UseTheme(PhotonTheme.Instance); // oder MaterialTheme.FromSeed(color)
Zwei Implementierungen werden ausgeliefert: PhotonTheme (die Voreinstellung) und MaterialTheme (echtes Material 3, dynamische Farbe aus einem Saatwert eingeschlossen). Tauschen Sie das Theme, und jede Komponente trägt die neue Marke, Server und Client, Web und Native. Das Umschalten zwischen hell und dunkel zur Laufzeit ist ein Dienst (IThemeController); siehe DesignSystem. Ein Theme beantwortet auch, womit ein Diagramm Daten zeichnet: IAppTheme.Data ist eine DataPalette: acht Serienfarben in fester Reihenfolge (Identität), eine sequenzielle Rampe eines einzigen Farbtons (Größe), ein divergierendes Paar auf neutraler Mitte (Polarität), vier reservierte Statusstufen (Zustand) und ein Grau zum Zurücknehmen. Sie ist auf eine validierte Referenzpalette voreingestellt, sodass ein Theme, das nie an Diagramme gedacht hat, dennoch eine korrekt bereitstellt; ein Theme mit Markenrampen überschreibt Data und hält das Ergebnis an dasselbe Audit, PaletteAudit (OKLab-Helligkeit und -Chroma, Trennung für Farbsehen, Kontrast), und das ist, was eine Palette sicher änderbar macht. Eine neunte Serie wird mit Namen abgelehnt, nie mit einem generierten Farbton versehen. Der Plan dahinter ist docs/CHARTS-PLAN.md im Repository.
Und wann man NICHT nach einem Token greift
Ein Token INVERTIERT mit dem Modus, und das ist sein ganzer Sinn, und darum sollte fast alles eines sein. Die Ausnahme ist also alles, dessen Bedeutung nicht invertiert.
Der Fall, der das gefunden hat: ein Glanzlicht auf einer Blase, mit TextInverse gemalt, weil das im hellen Modus weiß ist und ein Glanzlicht weiß ist. Im dunklen Modus ist dasselbe Token marineblau, also zeichnete die Anwendung genau dort einen dunklen Fleck, wo eine Spiegelung hingehört. Licht ist ein ausdrückliches Weiß, nie ein Token: Eine Spiegelung ist Licht und nicht Vordergrund, und Licht wird nicht dunkel, wenn die Oberfläche es wird. Dieselbe Begründung deckt einen Schatten, der dunkel bleiben muss, und eine Markenfarbe, die ein Unternehmen nach Sonnenuntergang nicht umstylt.
Ein Rand an einigen Kanten
Seit 0.2.0-preview.13
BorderColor = theme.Border,
BorderSides = BorderSides.Top, // eine Linie über einem Abschnitt
BorderColor = theme.Colors(Variant.Primary).Base,
BorderSides = BorderSides.Start, // ein Akzentbalken an einem Hinweiskasten
BorderSides.All ist die Voreinstellung, also rendert jede Box, die davor geschrieben wurde, Byte für Byte gleich: Ein voller Rand gibt weiterhin die Ein-Deklarations-Kurzform und ihre eine atomare Klasse aus.
Ein Rand liegt innerhalb der Box, und das Kind auch: Die Box hält ihr Kind innerhalb ihres Paddings und ihres Randes an den Seiten, an denen der Rand gezeichnet wird, und eine Box, die ihr Kind umschließt, zählt beides in ihre Größe, auf beiden Zielen (Photon legte das Kind bis 0.2.0-preview.61 über den Rand).
Der Rand eines Zustands folgt denselben Kanten: Ein Hover mit BorderWidth = 2 an einer Box, die ihre Unterkante zeichnet, verdickt nur diese Kante, in der Farbe der Basis, wenn der Zustand keine nennt. Das Web zeichnete ihn an allen vier Kanten und schrieb für eine Breite allein nichts (seit 0.2.0-preview.61).
Keines davon ist ein Divider. Ein Divider ist ein Geschwister zwischen zwei Dingen; diese gehören der Box selbst, und die einzige Art, einen zu zeichnen, war eine Row, die eine Ein-dp-Box umhüllt, und das ist eine Layout-Lüge über das, was das Design meinte.
Start und End statt Left und Right, wie bei EdgeInsets: Sie spiegeln in einer Rechts-nach-links-Lesung, und ein Akzentbalken, der links bleibt, wenn der Text in die andere Richtung fließt, ist auf der falschen Seite davon.
ZAUN Mit einem Eckenradius unterscheidet sich ein teilweiser Rand zwischen den Zielen leicht an der Ecke, an der eine vorhandene Kante eine fehlende trifft: Das Web stößt sie auf Gehrung, Photon stößt sie rechtwinklig. Bei Radius 0 (eine Linie, ein Akzentbalken, eine Tabellenzelle, und dafür sind teilweise Ränder da) sind die beiden identisch. Nutzen Sie BorderSides.All für eine gerundete Umrandung.
Die Web-Schicht auf niedriger Ebene (eQuantic.UI.Web) spiegelt das DOM eins zu eins. Jedes HtmlElement legt die Attribute offen, die der Browser hat:
public abstract class HtmlElement : IComponent {
/// <summary>Raw CSS classes (space-separated).</summary>
public string? ClassName { get; set; }
/// <summary>Inline styles for dynamic values (e.g., coordinates, colors from DB).</summary>
public HtmlStyle? Style { get; set; }
Dieser Kontrakt bedeutet, dass die Web-Schicht kein CSS-Framework erzwingt, sie rendert HTML-Attribute. DynamicElement geht weiter und rendert jedes Tag mit jedem Attribut. Gewöhnliche CSS-Klassen aus jedem Stylesheet funktionieren nativ:
new DynamicElement("aside") { ClassName = "my-sidebar" }
3. Externes CSS mitbringen
Das Framework liefert genau eine Style-Engine aus, die typisierte C#-Pipeline von oben. Es gibt kein mitgeliefertes CSS-Framework und keine Adapterschicht: Eine Seite, die handgeschriebenes Markup braucht, nutzt die Notluke (HtmlElement/DynamicElement + ClassName), und jedes Stylesheet, das die Anwendung mitbringt (ihre eigene CSS-Datei, ein Utility-Framework, das sie selbst baut), wird referenziert wie in jedem Webprojekt:
builder.Services.AddUI(options =>
options.ConfigureHtmlShell(shell => shell.AddStylesheet("/css/app.css"));
Das Framework erzeugt dieses CSS nicht und verarbeitet es nicht; es ist Sache des eigenen Builds der Anwendung.
eQuantic.UI.Primitives
Typisierte Styles, Design-Tokens, das abstrakte Vokabular: die Quelle der Wahrheit für das Styling.
eQuantic.UI.Web
Der Web-Realisierer + die atomare Style-Engine (StyleAtomizer) + das generierte Stylesheet, und die DOM-spiegelnde Notluke: HtmlElement, ClassName, HtmlStyle, DynamicElement.