Photon Design SystemO design system por trás dos componentes write-once: uma especificação completa de tokens/componentes (preservada no repositório em docs/design/Photon-Design-System.dc.html, a fonte da verdade) implementada como tokens C# tipados em eQuantic.UI.Primitives e consumida por todos os alvos de realização. Camadas de tokens (§01–§09)
•
Cores: toda cor é um ColorToken par claro/escuro; componentes nunca guardam cores cruas.
•
Cores de dados: com que um gráfico desenha os dados também vive no tema — IAppTheme.Data, uma DataPalette de oito posições de série fixas, uma rampa sequencial, um par divergente, os degraus de estado e um cinza de desênfase, auditada por PaletteAudit (veja Styling). Variantes interativas resolvem cinco sub-tokens (Base, OnBase, Pressed, que é um token real e não uma sobreposição, Subtle, OnSubtle). Desabilitado não é uma cor: é um grupo com 38% de opacidade. •
Escala tipográfica: dirigida por papéis (Display … BodyM, Caption, Label) com clamps de Dynamic Type por papel, mais a ponta densa onde todo chrome de desktop vive: TitleSmall (15/700, o degrau entre Label 13 e Title 20), LabelSmall (11, para trilhos de status, leituras de inspetor, legendas de espécime) e Overline (10/800, com tracking, a sobrancelha em maiúsculas acima de um grupo). Uma escala que para em 12dp é uma escala de toque; uma sidebar, uma toolbar e uma barra de status vivem todas abaixo dela, e sem esses degraus cada uma sai um quinto grande demais. Um estilo também carrega sua face: TypeStyle.Mono seleciona a monoespaçada (a do próprio sistema, SF Mono num Mac), e TypeStyle.Italic o corte inclinado (desde a 0.2.0-preview.28 é um EIXO, então negrito itálico e código itálico existem os dois), que o medidor, o rasterizador e o cache de raster honram porque já chaveiam pelo estilo.
•
Caixas de linha seguem o tamanho: TypeStyle.WithSize(size) escala a altura de linha na mesma proporção (o line-height sem unidade do CSS), e TypeStyle.OfSize(size, weight) deriva uma no 1.25× tipográfico. Remendar só o tamanho deixa um glifo maior na caixa antiga, e é assim que uma descendente termina fora da linha dela.
•
Um tamanho pode seguir a janela (desde 0.2.0-preview.61): TypeStyle.WithFluidSize(min, percentOfWindow, max) é o clamp(34px, 4.2vw, 54px) do CSS, um título que cresce com a janela entre um piso e um teto em vez de saltar a cada classe de janela. A web escreve o clamp e uma altura de linha sem unidade, o Photon resolve o tamanho pela janela contra a qual faz o layout (TypeStyle.AtWindow), e um alvo que não conhece janela (um e-mail) usa o teto, que é o que Size guarda. Um tamanho dado com WithSize volta a ser um tamanho em dp. Vale para um parágrafo ou um papel: um TextRun só leva o tamanho do seu override, então uma run fluida fica no teto, e uma run que deve crescer com a janela cresce com o seu parágrafo.
var display = TypeStyle.OfSize(40, FontWeight.Bold).WithFluidSize(34, 4.2f, 54);
Text("Escolha o seu distrito.", styleOverride: display, headingLevel: 1);
•
Espaçamento (Space.S1–S16, base 4dp, pertencente ao gap, margem não existe), raio (Radius.Xs–Full, clampado pelo engine), tamanhos de ícone (whitelist §07 16/20/24/32; tamanhos arbitrários lançam), toque (contrato de alvo ≥48dp sob um dedo, um piso de 24dp sob um ponteiro), elevação (uma sombra analítica por nó), movimento (durações + curvas + mola).
Densidade: o mesmo botão, 32dp sob um polegar e 26dp sob um mouse
Density (Comfortable | Compact) é uma propriedade do alvo, nunca do local da chamada: um app nunca pede um botão menor no Mac; o Mac é que pede um app mais denso. É a densidade do Material e o control size da Apple, e é o que mantém UMA árvore honesta num telefone e num desktop.
Sizing.Height, PaddingX, LabelSize e HitTarget resolvem por ela (Small 32→26, Medium 40→32, Large 48→40, XLarge 56→48), e a escada de seleção também (SwitchWidth/SwitchHeight/ SwitchThumb/SwitchTravel, SelectionBox, RadioDot). Sob um ponteiro o alvo de toque para de inflar: o mínimo do §08 é um contrato de dedo, e numa toolbar de botões de 26dp aquelas margens invisíveis se sobreporiam umas às outras. Ele guarda um piso no lugar, Touch.MinPointerTarget (24dp), o mínimo que o WCAG 2.2 SC 2.5.8 pede de um alvo: os degraus Compact já passam dele e não crescem, e uma Checkbox ou um Radio sem rótulo, uma caixa de 20dp, é um alvo de 24 × 24. No web a folga fica por baixo do conteúdo do próprio controle, então um botão mantém o seu hover e um controle dentro de um Pressable mantém os seus próprios hits, também quando o filho do controle não desenha caixa própria (um InView, um Adaptive, uma Image clara e escura): as primeiras coisas abaixo dele que desenham uma são erguidas acima da folga (desde 0.2.0-preview.61).
Ela chega aos componentes por ComponentContext.Density, a mesma porta pela qual o tema chega, então nenhuma assinatura de Build mudou. Quem decide é o alvo: o shell do macOS diz Compact; o boot do web lê (pointer: fine).
O servidor renderiza na densidade do browser
Desde 0.2.0-preview.61
O servidor não vê o ponteiro, então o browser conta a ele: o runtime deixa a densidade que o seu ponteiro pede num cookie de sessão (eq-density), e todo pedido seguinte da sessão é montado nela, desde o primeiro byte. A página diz em que densidade foi montada, e a hidratação baixa nessa, então adota a marcação servida como ela é. No primeiro pedido de uma sessão o servidor ainda não sabe e monta Comfortable; sob um mouse a página inteira passa então a Compact de uma vez, logo depois da hidratação. Antes ela guardava a densidade servida até cada componente renderizar de novo, e uma página sob um mouse mostrava uma mistura das duas.
O cookie guarda um fato de exibição sobre o aparelho e some quando o browser fecha; não há nada a configurar.
A ESCADA VIVE NA CAMADA DE TOKENS, SEMPRE Um componente nunca guarda uma cópia privada da própria tabela de tamanhos, porque uma cópia não consegue seguir a densidade que o alvo pede. Sizing.Avatar(size) e os degraus de seleção existem para que quem chama leia o mesmo número que o componente desenha.
Fidelidade à spec é testada, não desejada
•
Valores de token são pinados, e o contraste é recalculado nos testes para cada par afirmado, duas vezes: a razão do WCAG 2 e o contraste de luminosidade do APCA (Lc 75 para texto de corpo, 60 para texto atenuado, links e rótulos, 15 para a borda de um campo), nos dois temas. O APCA é a razão de os preenchimentos da paleta escura serem mais claros, com texto escuro por cima.
•
As próprias páginas do design system são mantidas contra o SDK: um número impresso numa página é comparado com o seu token, o estado de cada bloco vem da API pública, e todo token público está publicado em docs/design/tokens.json ou nomeado como isento.
•
Métricas de componente vêm das tabelas da spec (ex.: a tabela de tamanhos do Button: alturas 32/40/48/56) e são afirmadas em todos os eixos: pins do realizer web em C#, imagens golden nativas (claro + escuro), execução de fixture transpilada no vitest.
•
As regras do resolvedor de estilo vivem em C# neutro de alvo: as cores de cada variante, as derivadas Outline/Ghost/Link incluídas, vêm de IAppTheme.Colors(variant), e o Button escrito uma vez só deriva delas os preenchimentos de pressed e de hover (uma variante preenchida troca Base por Pressed na mesma forma, Outline e Ghost ficam com SurfaceSubtle).
A regra "gerado, nunca escrito à mão"
Artefatos client-side que carregam valores do design system são gerados da fonte única em C# e pinados byte a byte na CI (um desvio falha o build; fluxos por variável de ambiente regeneram):
Artefato
Gerador
Regenerar com
Stylesheet normativo (custom properties, .eq-type-*, .eq-elevation-*, vars de motion)
PhotonCssGenerator
(função pura, testada por valor)
design-system.generated.ts (tokens, objeto de tema, tabela de tamanhos do Button)
DesignSystemTsGenerator
EQ_UPDATE_DESIGN_TS=1
icons.generated.ts (dados de path dos glifos)
IconTsGenerator
EQ_UPDATE_ICONS_TS=1
Módulos de componente transpilados embarcados
saída viva do eqc
EQ_UPDATE_TRANSPILED=1
A mesma regra sustenta a hidratação: o SSR (realizer C#) e o lowering do cliente (TS) são mantidos byte-idênticos por literais cross-pinadas nas duas suítes de teste.
Pipeline de ícones (spec A10)
Um enum Icons curado (23 glifos e crescendo: Menu, o gatilho de navegação do shell compacto, chegou na 0.2.0-preview.26) com os dados de path vivendo uma vez no IconRegistry em C# (máscaras alpha de path única 24×24): o web emite <svg fill="currentColor"> inline (o tom monta no token de cor exatamente como o texto faz), o lado TS consome o módulo gerado, e o futuro atlas de glifos nativo rasteriza do mesmo registro. Outline ↔ preenchido são glifos distintos; ícones ignoram Dynamic Type.
Estilo sem CSS: o engine atômico
A autoria é 100% C# tipado (BoxStyle, StyleDiff, tokens), e não existe um plano CSS. Três leis governam a realização web:
1.
Uma semântica: o mesmo vocabulário de estilo dirige valores dp no Photon e CSS no web.
2.
Sem teto de expressividade: Wrap, Grid, AspectRatio, AlignSelf, Opacity de grupo, Transform2D estático, diffs de estado Hover/Focus, adaptatividade por classe de tamanho de janela (AdaptiveNode), Pinned, Layer, scroll nos dois eixos.
3.
Sem reescrita por construção: toda declaração regular desce para UMA classe atômica deduplicada (eq- + hash FNV-1a de prop:valor); cores de tema referenciam var(--eq-color-*); o SSR junta as regras da página num único <style id="eq-atomic"> que o cliente adota por identidade (hidratação = igualdade de classe, garantida de ponta a ponta pelo teste e2e de identidade). ~118 regras / 4,8 KB cobriram o showroom inteiro.
Pseudo-estados descem para regras atômicas de pseudo-variante (:hover/:focus-visible, zero JS); classes de tamanho para classes fixas com media gate. O StyleAtomizer em C# e o style-atomizer.ts em TS são gêmeos byte-idênticos (fixture cross-pinada).
Tema = fornecer um IAppTheme
Uma interface (cores como pares ColorToken claro/escuro, papéis de tipo, escala de forma, specs de elevação, opacidade de desabilitado), selecionada via AddUI(...).UseTheme(...) e transportada do SSR para o cliente.
Trocar claro/escuro em tempo de execução é um SERVIÇO: IThemeController (Mode + Apply) é tomado por construtor como qualquer capacidade, e o componente nunca aprende qual alvo respondeu. O runner nativo repinta a janela contra a outra paleta; o browser vira uma declaração color-scheme (a paleta web inteira é autorada como pares light-dark(), então a troca não custa re-render). ITextClipboard é oferecido do mesmo jeito para o botão de copiar de uma página. Duas implementações acompanham: PhotonTheme (o padrão) e MaterialTheme (Material 3 de verdade: baseline MaterialTheme.Instance ou cor dinâmica FromSeed(cor) via um port CAM16/HCT validado contra os valores do Google). A forma é dirigida pelo tema; a mesma árvore de componentes se rebrandiza com uma linha.
Desde 0.2.0-preview.51
Um tema nomeia as faces em que compõe o texto. Antes disto o vocabulário não tinha face nenhuma e todos os alvos desenhavam a do sistema, portanto um design feito sobre um tipo de letra de marca não era construível — nada falhava e nada avisava, a aplicação simplesmente saía noutro tipo de letra.
Duas propriedades, porque respondem a perguntas diferentes. TypeStyle.Family é por PAPEL, logo um tema pode compor títulos numa face e corpo noutra. IAppTheme.MonoFamily é a face de CÓDIGO, e tem de ser separada: Mono é uma propriedade por NÓ — um nome de branch é monoespaçado e a palavra ao lado não é, ambos TypeRole.Caption — por isso um papel nunca consegue dizer que texto queria a outra face.
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";
// …o resto delega para inner
Um Text também pode nomear a sua, e a especificidade ganha como ganha em todo o resto daqui: um estilo que NOMEIA uma família fica com ela, e só um estilo monoespaçado sem nome recebe a face de código do tema.
Text("git log", TypeRole.Caption, mono: true) // recebe MonoFamily
Text("git log", TypeRole.Caption) { StyleOverride = style with { Family = "Fira Code", Mono = true } }
Uma face que não existe é REPORTADA
Todo motor de texto substitui uma família desconhecida em vez de recusar — CoreText, DirectWrite e Android, todos — portanto uma face de marca que a máquina não tem seria uma diferença silenciosa entre o design e a aplicação a correr. O Photon nomeia-a na linha que o resumo do frame já usa, nos quatro shells:
[photon] screenshot: out.png — FACE NOT FOUND: IBM Plex Sans
A web não precisa de relatório e não o tem: o lowering cita a face nomeada à frente da pilha que a página usaria de qualquer forma, logo uma família indisponível cai no próprio tipo de letra do documento e não no palpite do browser. Nomear PARA ONDE cair é a única coisa que o CSS faz melhor aqui.
Isto não torna uma face disponível. Registar um tipo de letra que o sistema nunca viu é uma peça à parte e ainda não existe; até lá, uma família só resolve se estiver instalada.
Com o que se pode escrever uma família
O nome de uma família é o único texto livre no pipeline de estilo, e aterra dentro de um elemento <style> e dentro de JSON num elemento <script>. Aspas não são defesa nenhuma aí — nem uma string CSS nem uma string JSON neutralizam </style> ou </script> para o PARSER de HTML — por isso uma família é VERIFICADA em vez de escapada: letras e dígitos Unicode, espaço, e -, _, ., +. Uma família CJK ou cirílica passa; o que significa algo para CSS, JSON ou HTML não passa, e um nome recusado é reportado como uma face que esta execução pediu e não obteve, em vez de desaparecer em silêncio.