Skip to content

DesignSystem de

Edgar Mesquita edited this page Oct 14, 2026 · 1 revision

Photon Designsystem

🌐 Diese Seite auf: English · Português · Español · Français · Deutsch

Das Designsystem hinter den Write-once-Komponenten: eine vollständige Token- und Komponentenspezifikation (im Repository unter docs/design/Photon-Design-System.dc.html erhalten, die Quelle der Wahrheit), implementiert als typisierte C#-Tokens in eQuantic.UI.Primitives und von jedem Realisierungsziel konsumiert.

Token-Schichten (§01–§09)

  • Farben: jede Farbe ist ein gepaartes helles/dunkles ColorToken; Komponenten halten niemals rohe Farben. Interaktive Varianten lösen fünf Untertokens auf (Base, OnBase, Pressed, ein echtes Token und keine Überlagerung, Subtle, OnSubtle). Deaktiviert ist keine Farbe: es ist eine Gruppe mit 38 % Deckkraft.

  • Datenfarben: womit ein Diagramm Daten zeichnet, lebt ebenfalls am Theme, IAppTheme.Data, eine DataPalette aus acht festen Serienplätzen, eine sequenzielle Rampe, ein divergierendes Paar, die Statusstufen und ein Grau zur Abschwächung, geprüft von PaletteAudit (siehe Styling).

  • Typografische Skala: rollengetrieben (Display … BodyM, Caption, Label) mit Dynamic-Type-Grenzen pro Rolle, dazu das dichte Ende, in dem jedes Desktop-Chrom lebt: TitleSmall (15/700, die Sprosse zwischen Label 13 und Title 20), LabelSmall (11, für Statusleisten, Inspektor-Anzeigen, Musterbeschriftungen) und Overline (10/800, gesperrt, die Zeile in Großbuchstaben über einer Gruppe). Eine Skala, die bei 12dp endet, ist eine Touch-Skala; eine Seitenleiste, eine Werkzeugleiste und eine Statusleiste leben alle darunter, und ohne diese Sprossen kommt jede von ihnen ein Fünftel zu groß heraus. Ein Stil trägt auch seinen Schnitt: TypeStyle.Mono wählt den nichtproportionalen (den des Systems selbst, SF Mono auf einem Mac), und TypeStyle.Italic den geneigten Schnitt (seit 0.2.0-preview.28 ist er eine ACHSE, also existieren fette Kursive und kursiver Code beide), was der Vermesser, der Rasterisierer und der Raster-Cache alle beachten, weil sie bereits auf den Stil schlüsseln.

  • Zeilenkästen folgen der Größe: TypeStyle.WithSize(size) skaliert die Zeilenhöhe im selben Verhältnis (das einheitenlose line-height von CSS), und TypeStyle.OfSize(size, weight) leitet eine beim typografischen 1,25× ab. Nur die Größe zu flicken lässt eine größere Glyphe im alten Kasten, und so landet eine Unterlänge außerhalb ihrer Zeile.

  • Eine Größe kann dem Fenster folgen (seit 0.2.0-preview.61): TypeStyle.WithFluidSize(min, percentOfWindow, max) ist das clamp(34px, 4.2vw, 54px) von CSS, eine Überschrift, die mit dem Fenster zwischen einem Boden und einer Decke wächst, statt bei jeder Fensterklasse zu springen. Das Web schreibt den Clamp und eine einheitenlose Zeilenhöhe, Photon löst die Größe aus dem Fenster auf, gegen das es layoutet (TypeStyle.AtWindow), und ein Ziel, das kein Fenster kennt (eine E-Mail), setzt die Decke, die Size hält. Eine mit WithSize angegebene Größe ist wieder eine Größe in dp. Sie ist für einen Absatz oder eine Rolle: ein TextRun nimmt aus seiner Überschreibung nur eine Größe, also wird ein fluider Lauf an seiner Decke gesetzt, und ein Lauf, der mit dem Fenster wachsen soll, wächst mit seinem Absatz.

    var display = TypeStyle.OfSize(40, FontWeight.Bold).WithFluidSize(34, 4.2f, 54);
    Text("Escolha o seu distrito.", styleOverride: display, headingLevel: 1);
  • Abstand (Space.S1–S16, 4dp-Basis, dem Gap gehörend, ein Margin existiert nicht), Radius (Radius.Xs–Full, von der Engine begrenzt), Icon-Größen (§07-Whitelist 16/20/24/32; beliebige Größen werfen), Berührung (Trefferzusage ≥48dp unter einem Finger, ein Boden von 24dp unter einem Zeiger), Elevation (ein analytischer Schatten pro Knoten), Bewegung (Dauern, Kurven und Feder).

Dichte: derselbe Button, 32dp unter einem Daumen und 26dp unter einer Maus

Density (Comfortable | Compact) ist eine Eigenschaft des Ziels, niemals der Aufrufstelle: eine App bittet auf dem Mac nie um einen kleineren Button; der Mac bittet um eine dichtere App. Es ist die Dichte von Material und die Steuerelementgröße von Apple, und es ist das, was EINEN Baum auf einem Telefon und auf einem Desktop ehrlich hält.

Sizing.Height, PaddingX, LabelSize und HitTarget lösen sich danach auf (Small 32→26, Medium 40→32, Large 48→40, XLarge 56→48), und die Auswahlleiter ebenso (SwitchWidth/SwitchHeight/SwitchThumb/SwitchTravel, SelectionBox, RadioDot). Unter einem Zeiger hört das Trefferziel auf zu wachsen: das Minimum aus §08 ist die Zusage eines Fingers, und auf einer Werkzeugleiste aus 26dp-Buttons würden sich diese unsichtbaren Ränder gegenseitig überlappen. Es hält stattdessen einen Boden, Touch.MinPointerTarget (24dp), das Minimum, das SC 2.5.8 von WCAG 2.2 von einem Ziel verlangt: die Compact-Sprossen liegen darüber und wachsen gar nicht, und eine Checkbox oder ein Radio ohne Beschriftung, ein 20dp-Kasten, ist ein Ziel von 24 × 24. Im Web liegt das Spiel unter dem eigenen Inhalt des Steuerelements, also behält ein Button seinen Hover und ein Steuerelement innerhalb eines Pressable behält seine eigenen Treffer, auch wenn das Kind des Steuerelements keinen eigenen Kasten zeichnet (ein InView, ein Adaptive, ein helles und dunkles Image): die ersten Dinge darunter, die einen zeichnen, werden über das Spiel gehoben (seit 0.2.0-preview.61).

Sie erreicht Komponenten über ComponentContext.Density, dieselbe Tür, durch die das Theme kommt, also hat sich keine Build-Signatur geändert. Wer entscheidet, ist das Ziel: die macOS-Shell sagt Compact; der Web-Start liest (pointer: fine).

Der Server rendert in der Dichte des Browsers

Seit 0.2.0-preview.61

Der Server kann den Zeiger nicht sehen, also sagt es ihm der Browser: die Laufzeit hinterlässt die Dichte, um die ihr Zeiger bittet, in einem Sitzungscookie (eq-density), und jede spätere Anfrage der Sitzung wird damit gebaut, von ihrem ersten Byte an. Die Seite sagt, in welcher Dichte sie gebaut wurde, und die Hydration senkt in genau dieser, also übernimmt sie das ausgelieferte Markup so, wie es ist. Bei der ersten Anfrage einer Sitzung weiß der Server es noch nicht und baut Comfortable; unter einer Maus wechselt dann die ganze Seite auf einmal zu Compact, direkt nach der Hydration. Früher behielt sie die ausgelieferte Dichte, bis jede Komponente das nächste Mal neu rendere, und eine Seite unter einer Maus zeigte eine Mischung aus beiden.

Das Cookie hält eine Darstellungstatsache über das Gerät und geht, wenn der Browser schließt; es gibt nichts zu konfigurieren.

Die Leiter lebt immer in der Token-Schicht. Eine Komponente behält niemals eine private Kopie ihrer eigenen Größentabelle, denn eine Kopie kann der Dichte nicht folgen, um die das Ziel bittet. Sizing.Avatar(size) und die Auswahlsprossen existieren, damit ein Aufrufer dieselbe Zahl liest, die die Komponente zeichnet.

Spezifikationstreue wird geprüft, nicht angestrebt

  • Tokenwerte sind festgenagelt, und der Kontrast wird in Tests neu berechnet für jedes behauptete Paar, zweimal: das WCAG-2-Verhältnis und der APCA-Helligkeitskontrast (Lc 75 für Lauftext, 60 für gedämpften Text, Links und Beschriftungen, 15 für die Grenze eines Feldes), in beiden Themes. APCA ist der Grund, warum die Füllungen der dunklen Palette heller sind, mit dunklem Text darauf.
  • Die eigenen Seiten des Designsystems werden am SDK gehalten: eine Zahl, die eine Seite ausgibt, wird mit ihrem Token verglichen, der Status eines Blocks wird aus der öffentlichen API abgeleitet, und jedes öffentliche Token wird in docs/design/tokens.json veröffentlicht oder als ausgenommen benannt.
  • Komponentenmetriken kommen aus den Tabellen der Spezifikation (z. B. die Größentabelle von Button: Höhen 32/40/48/56) und werden auf allen Achsen geprüft: Festlegungen des C#-Web-Realisierers, native Golden-Bilder (hell und dunkel), Ausführung transpilierter Fixtures in vitest.
  • Die Regeln des Stilauflösers leben in zielneutralem C#: die Farben jeder Variante, einschließlich der abgeleiteten Outline/Ghost/Link, kommen aus IAppTheme.Colors(variant), und der Write-once-Button leitet daraus seine Füllungen für gedrückt und Hover ab (eine gefüllte Variante tauscht Base gegen Pressed auf derselben Form, Outline und Ghost nehmen SurfaceSubtle).

Die Regel „generiert, niemals handgeschrieben“

Client-Artefakte, die Werte des Designsystems tragen, werden aus der einzigen C#-Quelle generiert und in CI byteweise festgenagelt (eine Abweichung bricht den Build; Umgebungsvariablen-Flüsse regenerieren):

Artefakt Generator Regenerieren mit
Normatives Stylesheet (benutzerdefinierte Eigenschaften, .eq-type-*, .eq-elevation-*, Bewegungsvariablen) PhotonCssGenerator (reine Funktion, pro Wert getestet)
design-system.generated.ts (Tokens, Theme-Objekt, Größentabelle von Button) DesignSystemTsGenerator EQ_UPDATE_DESIGN_TS=1
icons.generated.ts (Pfaddaten der Glyphen) IconTsGenerator EQ_UPDATE_ICONS_TS=1
Eingebettete transpilierte Komponentenmodule lebende eqc-Ausgabe EQ_UPDATE_TRANSPILED=1

Dieselbe Regel treibt die Hydration: SSR (C#-Realisierer) und Client-Lowering (TS) werden durch gegenseitig festgenagelte Literale in beiden Testsuiten byteidentisch gehalten.

Icon-Pipeline (Spezifikation A10)

Ein kuratiertes Icons-Enum (23 Glyphen und wachsend: Menu, der Navigationsauslöser der kompakten Shell, kam in 0.2.0-preview.26) mit den Pfaddaten, die einmal im C#-IconRegistry leben (24×24-Alphamasken mit einem Pfad): das Web gibt inline <svg fill="currentColor"> aus (die Tönung fährt genau wie Text auf dem Farbtoken mit), die TS-Seite konsumiert das generierte Modul, und der künftige native Glyphen-Atlas rasterisiert aus demselben Register. Kontur und Füllung sind verschiedene Glyphen; Icons ignorieren Dynamic Type.

Styling ohne CSS: die atomare Engine

Geschrieben wird zu 100 % in typisiertem C# (BoxStyle, StyleDiff, Tokens), und es gibt keine CSS-Ebene. Drei Gesetze regieren die Web-Realisierung:

  1. Eine Semantik: dasselbe Stilvokabular treibt dp-Werte auf Photon und CSS im Web.
  2. Keine Ausdrucksdecke: Wrap, Grid, AspectRatio, AlignSelf, Gruppen-Opacity, statisches Transform2D, Hover/Focus-Zustandsdiffs, Anpassung nach Fenstergrößenklasse (AdaptiveNode), Pinned, Layer, Scrollen auf beiden Achsen.
  3. Kein Umschreiben durch Konstruktion: jede reguläre Deklaration senkt sich auf EINE dedupliziert atomare Klasse (eq- + FNV-1a-Hash von prop:value); Theme-Farben verweisen auf var(--eq-color-*); SSR sammelt die Regeln der Seite in ein einziges <style id="eq-atomic">, das der Client nach Identität übernimmt (Hydration = Klassengleichheit, durchgehend vom e2e-Identitätstest erzwungen). ~118 Regeln / 4,8 KB deckten den gesamten Showroom ab.

Pseudozustände senken sich auf atomare Pseudovarianten-Regeln (:hover/:focus-visible, null JS); Größenklassen auf feste Medien-Gate-Klassen. Der C#-StyleAtomizer und der TS-style-atomizer.ts sind byteidentische Zwillinge (gegenseitig festgenagelte Fixture).

Theming = ein IAppTheme bereitstellen

Eine Schnittstelle (Farben als helle/dunkle ColorToken-Paare, Typrollen, Formskala, Elevationsspezifikationen, Deckkraft für deaktiviert), ausgewählt über AddUI(...).UseTheme(...) und von SSR zum Client überbrückt.

Hell/dunkel zur Laufzeit umzuschalten ist ein DIENST: IThemeController (Mode + Apply) wird wie jede Fähigkeit über einen Konstruktor genommen, und die Komponente erfährt niemals, welches Ziel geantwortet hat. Der native Runner übermalt das Fenster gegen die andere Palette, der Browser kippt eine einzige color-scheme-Deklaration (die ganze Web-Palette ist als light-dark()-Paare geschrieben, der Tausch kostet also kein Neurendern). ITextClipboard wird auf dieselbe Weise für den eigenen Kopieren-Button einer Seite angeboten. Zwei Implementierungen werden ausgeliefert: PhotonTheme (die Vorgabe) und MaterialTheme (echtes Material 3: Grundlinie MaterialTheme.Instance oder dynamische Farbe FromSeed(color) über eine CAM16/HCT-Portierung, die gegen Googles Werte validiert ist). Die Form wird vom Theme getrieben; derselbe Komponentenbaum trägt mit einer Zeile eine andere Marke.

Eine Schrift benennen

Seit 0.2.0-preview.51

Ein Theme benennt die Schriften, in denen es Text setzt. Vorher hatte das Vokabular überhaupt keine Schrift und jedes Ziel renderte die des Systems, also konnte ein Design, das gegen eine Markenschrift gezeichnet war, nicht gebaut werden: nichts schlug fehl und nichts warnte, die App kam einfach in einer anderen Schrift heraus.

Zwei Eigenschaften, weil sie verschiedene Fragen beantworten. TypeStyle.Family ist pro ROLLE, ein Theme kann also Überschriften in einer Schrift und den Lauftext in einer anderen setzen. IAppTheme.MonoFamily ist die CODE-Schrift, und sie muss getrennt sein: Mono ist eine Eigenschaft pro KNOTEN, ein Branch-Name ist nichtproportional und das Wort daneben nicht, beide TypeRole.Caption, eine Rolle kann dir also niemals sagen, welcher Text die andere Schrift wollte.

public sealed class BrandTheme(IAppTheme inner) : IAppTheme
{
    public TypeStyle Type(TypeRole role) => inner.Type(role) with { Family = "IBM Plex Sans" };
    public string? MonoFamily => "JetBrains Mono";
    // …der Rest delegiert an inner
}

Ein Text darf auch eine für sich selbst benennen, und die Spezifität gewinnt wie überall sonst hier: ein Stil, der eine Familie BENENNT, behält sie, und nur ein unbenannter nichtproportionaler Stil nimmt die Code-Schrift des Themes.

Text("git log", TypeRole.Caption, mono: true)                     // nimmt MonoFamily
Text("git log", TypeRole.Caption) { StyleOverride = style with { Family = "Fira Code", Mono = true } }

Eine Schrift, die nicht da ist, wird GEMELDET

Jede Text-Engine ersetzt eine unbekannte Familie, statt sich zu weigern, CoreText, DirectWrite und Android tun es alle, eine Markenschrift, die der Maschine fehlt, wäre sonst also ein stiller Unterschied zwischen dem Design und der laufenden App. Photon benennt sie auf der Zeile, die die Bildzusammenfassung schon verwendet, auf allen vier Shells:

[photon] screenshot: out.png — FACE NOT FOUND: IBM Plex Sans

Das Web braucht keine Meldung und bekommt keine: das Lowering nennt die benannte Familie vor dem Stapel, den die Seite ohnehin verwendet hätte, eine nicht verfügbare Familie landet also auf der eigenen Schrift des Dokuments und nicht auf der Vermutung des Browsers. Zu benennen, WORAUF zurückzufallen ist, ist das Einzige, was CSS hier besser macht.

Das macht eine Schrift nicht verfügbar. Eine Schriftart zu registrieren, die das System nie gesehen hat, ist ein eigenes Stück und existiert noch nicht; bis dahin löst sich eine Familie nur auf, wenn sie installiert ist.

Womit eine Familie geschrieben werden darf

Der Name einer Familie ist der einzige freie Text in der Stil-Pipeline, und er landet in einem <style>-Element und in JSON in einem <script>-Element. Anführungszeichen sind dort keine Verteidigung, weder eine CSS- noch eine JSON-Zeichenkette neutralisiert </style> oder </script> für den HTML-Parser, eine Familie wird also GEPRÜFT statt maskiert: Unicode-Buchstaben und -Ziffern, Leerzeichen, und -, _, ., +. Eine CJK- oder kyrillische Familie geht durch; alles, was für CSS, JSON oder HTML etwas bedeutet, nicht, und ein abgelehnter Name wird als Schrift gemeldet, die dieser Lauf angefordert und nicht bekommen hat, statt still verworfen zu werden.

Clone this wiki locally