-
Notifications
You must be signed in to change notification settings - Fork 1
WriteOnceComponents pt BR
🌐 Esta página em: English · Português
A promessa central da história multi-alvo do eQuantic.UI: componentes são escritos uma vez, em C#, contra um vocabulário abstrato, e realizados por alvo: DOM + CSS no web, pixels de GPU através do Photon no nativo. Não são "duas APIs parecidas", é literalmente a mesma classe.
Esta página é a visão de arquitetura; o catálogo de componentes vive na página Components, e como se ESCREVE uma árvore (factories em vez de
new, inclusive para os seus próprios componentes) em Declarative Surface.
| Pacote | Papel | Depende de |
|---|---|---|
eQuantic.UI.Primitives |
A fundação compartilhada: cores, tokens de design, estilos tipados, nós abstratos, modelo de componente | nada (zero) |
eQuantic.UI.Components |
A biblioteca de componentes write-once | só Primitives |
eQuantic.UI.Web |
Realizer web: árvore abstrata → HtmlElement/DOM + CSS (SSR) + stylesheet gerado |
Core, Primitives |
eQuantic.UI.Native.Components |
Realizer nativo: árvore abstrata → display list do Photon | Primitives, Engine |
Runtime TS (@equantic/runtime) |
Lowering client-side (hidratação) + módulos da biblioteca embarcados | nada |
Estrutura: Box (+BoxStyle: pintura, opacidade, transform, aspect-ratio, diffs de hover/focus),
Row/Column (flex com gap + Wrap/RunGap), Grid (+GridTrack, o gêmeo do CSS Grid),
Stack/Positioned (ordem z + ZIndex), AdaptiveNode (classes de tamanho de janela, zero
listeners), ScrollView (+Sticky), Overlay (camada de viewport, flag Modal), Anchored
(painéis flutuantes: posicionamentos, scrim de toque-fora, MatchAnchorWidth, OpenOnHover).
Conteúdo: Text (papéis de tipo), TextEntry, Icon (curado + glifo de qualquer pack), Image,
Spinner, Markdown (um documento renderizado como o design system),
Mermaid (diagramas desenhados por ele).
Interação e movimento: Pressable (contrato de alvo ≥48dp), Link (âncoras reais no web, a costura
de navegação do host no nativo), DragDismiss (o gesto da folha), LoopMotion, Presence
(movimento de entrada/saída).
Mais os tipos de valor (EdgeInsets, SizeValue Hug/Fill/Fixed, CornerRadii, ColorToken em
pares claro/escuro, StyleDiff, Transform2D) e o modelo de componente (StatelessComponent,
StatefulComponent + SetState, ComponentContext sem modo).
O estilo não tem um plano CSS. Veja o engine de estilo atômico em DesignSystem: cada declaração de estilo vira uma classe atômica deduplicada, byte-idêntica entre SSR (C#) e hidratação (TS).
A paridade de layout é um contrato: o engine flex em C# (nativo) e o flex do CSS (web) implementam a
mesma spec: Flexible distribuindo a sobra por peso, o contrato de truncamento (o texto encolhe até
a elipse antes de empurrar os irmãos), stretch preenchendo só o que é automático.
Desde 0.2.0-preview.6
Flexible(child, flex, basis, shrink) é o trio flex: grow shrink basis do CSS. A base é o
tamanho contra o qual um pai com wrap quebra linhas, e é o que torna um layout responsivo
expressável:
// `Wrap` é uma propriedade init, então esta é a forma construtor + inicializador:
// as factories cobrem construtores, nunca uma segunda API.
var panes = new Row(gap: Space.S4) { Wrap = true, Width = SizeValue.Fill };
panes.Add(Flexible(EditorPane(), flex: 1, basis: 440));
panes.Add(Flexible(PreviewPane(), flex: 1, basis: 380));Largo o bastante para as duas bases, os painéis dividem a linha e repartem a sobra por peso. Estreito demais, cada um toma uma linha própria e cresce para preenchê-la, em vez de ficar em 440 com o resto da linha vazio. Essa segunda parte é uma passada de verdade, não efeito colateral da quebra de linha: a sobra vai para quem cresce, por peso; uma linha que transborda é retomada de quem encolhe, ponderada pela base (como o CSS faz); e nada cruza o piso de min-content.
Uma base 0, o padrão, é o comportamento histórico: o filho não contribui com nada próprio e é
dimensionado puramente pelo peso. shrink: 0 recusa devolver espaço, então uma linha que não cabe
quebra em vez de espremer.
Nativo: o PhotonHost expande Build() inline durante o layout e o realizer emite comandos de
desenho.
Web, servidor (SSR): o WebRealizer desce a árvore para HtmlElements. Cores viram
light-dark(#l, #d) para o DOM ficar livre de modo (troca de tema = color-scheme). Páginas do Core
podem embutir subárvores write-once pela ponte VisualNodeComponent, e um StatefulComponent de
Primitives com [Page] é uma página inteira (o servidor faz a ponte automaticamente).
Web, cliente: o eqc (o compilador C#→JS) transpila as mesmas fontes; o lowering do runtime
(lowerVisualNode) espelha o WebRealizer regra por regra, e a paridade de hidratação é garantida
por strings de estilo cross-pinadas byte a byte afirmadas nas suítes C# e TS.
Todo alvo expande um componente pela MESMA costura, e essa costura é uma fronteira: se o Build
lançar, a subárvore do componente que falhou é substituída por uma superfície contida e tudo em volta
continua renderizando. Uma página que não tem pai para contê-la carrega a mesma fronteira na própria
costura de render, então uma página quebrada vira um painel, nunca um documento em branco.
| antes | agora | |
|---|---|---|
| Servidor (SSR) | 500 para a requisição inteira | 200, painel no lugar da subárvore |
| Browser | o mount lançou, a raiz nunca escreveu | painel, irmãos interativos |
| Janela (Photon) | o frame nunca chegou | painel, o app segue apresentando |
O painel é construído com o vocabulário, então é a mesma superfície numa página e numa janela. Em
desenvolvimento ele nomeia o componente e cita o que foi lançado; em produção diz apenas que uma
seção não pôde ser exibida, porque uma mensagem de exceção é escrita para quem escreveu o código e
pode citar um id, um caminho ou uma query. De qualquer forma a falha chega ao log do host por
ComponentBoundary.Report: uma fronteira que só engole troca um crash barulhento por um silencioso.
Nada é lembrado. Um componente que para de lançar (uma nova tentativa, novas props, um hot reload) simplesmente constrói de novo na próxima passada.
O CONTRATO do próprio componente não é uma falha de render e não pertence aqui: declare-o onde o erro
é escrito (init => field = value.Count > 3 ? throw … : value), para que o autor receba uma exceção
na linha que errou em vez de um painel vermelho uma vez por frame.
Os módulos transpilados de eQuantic.UI.Components viajam dentro do runtime.js, pinados byte a
byte na CI contra a saída viva do eqc, e os apps os importam de @equantic/runtime. Componentes
write-once escritos pelo usuário não precisam de fiação: vivem no app e fluem pela varredura normal
de páginas (o eqc detecta as formas de componente de Primitives, incluindo stateful com SetState
direto).
Todo valor do design system no cliente é gerado da fonte única em C# e pinado byte a byte:
-
PhotonCssGenerator→ o stylesheet normativo (custom properties, classes de papel de tipo, elevação, movimento). -
DesignSystemTsGenerator→design-system.generated.ts(tokens, tema, tabela de tamanhos do Button). -
IconTsGenerator→icons.generated.ts(dados de path dos glifos, doIconRegistryem C#).
Veja DesignSystem.
Existe exatamente UMA biblioteca de componentes. Botões, cartões, inputs, navegação, overlays,
listas, o ListView com reciclagem, a grade Spreadsheet, o editor de código. O catálogo completo,
agrupado e descrito, está na página Components.
Cada componente vem com pins do realizer web, goldens nativos (claro+escuro), fixtures transpiladas
pinadas executadas no vitest, e o showroom vivo (/ e /shared no DefaultUIDashboard: SSR +
identidade de hidratação + interação verificados de ponta a ponta pela suíte Playwright). Sistemas
que acompanham a biblioteca: o sistema de movimento por transição de estado (entrada/saída de
Presence), o pipeline de ponteiro (hover, arrastar-para-dispensar com slop/cancel/glide), o
compositor de scroll (fixação Sticky real) e os overlays ancorados.
Desde 0.2.0-preview.13
TextRun.Destination, porque um link no meio de um parágrafo não tem outro lugar para morar:
new Text("", TypeRole.BodyM)
{
Spans =
[
new TextRun("Leia o "),
new TextRun("guia inicial") { Destination = "/docs/start", Weight = FontWeight.SemiBold },
new TextRun("."),
],
}Um Link em volta do Text inteiro faz o parágrafo todo virar um link. Uma Row de Texts quebra
entre runs em vez de entre palavras, então uma frase com três spans de código quebra nos spans,
que é justamente por que ênfase mista precisa ser um Text com Spans. Entre esses dois, um link no
meio da frase era inexpressável, e para prosa isso é a maioria dos parágrafos.
Um run também pode ter TAMANHO diferente da prosa em volta: StyleOverride no run, a mesma válvula
de escape que Text.StyleOverride é. Código inline em 13,5 dentro de um parágrafo 16 é o caso, e o
run mantém a caixa de linha do parágrafo: definir a própria altura de linha abriria um vão acima e
abaixo: isso é uma linha, não um run. Desde 0.2.0-preview.15.
A FORMA é universal: um NSAttributedString da Apple carrega um atributo .link num intervalo, um
Spannable do Android carrega um URLSpan. Um link inline é um run com um atributo em todo lugar, e
não pode ser um nó separado, porque um nó separado é o que quebra a linha.
O NOME segue a mesma regra. A Apple diz link, o Android diz URLSpan, o HTML diz href, então
href é a palavra de um alvo, e a camada abstrata não fala a língua de nenhum alvo (Pressable, não
"Button"). É Destination tanto em Link quanto em TextRun: o que o valor É, que é uma rota ou uma
URL, numa palavra que nenhum dos três alvos possui.
Renomeado na 0.2.0-preview.13, de
Href, no mesmo release em que apareceu. Uma quebra tomada enquanto custa uma linha por app, em vez de depois de existirem apps para quebrar.
Spansé realizado no web hoje. O Photon realizaLink(coleta as regiões do link e faz hit-test nelas, entregando o destino à costura de navegação do shell) mas não o destino por RUN, então um parágrafo com links inline desenha lá como o texto dele, sem link. O que falta é hit-testing por run, não significado.
Desde 0.2.0-preview.13
Uma página em /docs/{slug} lê o slug do próprio contexto:
[Page("/docs/{slug}")]
public sealed class DocPage : StatelessComponent, IServerPrefetch
{
private Doc? _doc;
[ServerOnly]
public async Task PrefetchAsync(IServiceProvider services, CancellationToken cancellationToken)
=> _doc = await services.GetRequiredService<IDocs>()
.FindAsync(RouteValues.Current.Param("slug"), cancellationToken);
public override VisualNode Build(ComponentContext context) =>
Text(_doc?.Title ?? "Not found", TypeRole.Heading);
}-
context.Route.Param("slug")noBuild,RouteValues.Current.Param("slug")num prefetch (que roda antes de existir um contexto). -
context.Route.Query("page")para a query string. - Nunca nulo; um nome sem correspondência responde
null.
Antes disso o único caminho era o IHttpContextAccessor. Funciona, e custa à página justamente o que
a torna write-once: um componente que conhece o ASP.NET é um componente que não roda no Photon. Ler
um parâmetro de rota é a coisa mais ordinária que uma página faz, e se o jeito de fazer isso é
específico do web, então "write-once" vale para o visual e não para a página.
A rota é armada antes do prefetch, e essa é a parte que importa: uma página em /docs/{slug}
carrega pelo slug, então uma rota que chega depois do prefetch chega depois da única pergunta que
ela existia para responder.
Por requisição, num AsyncLocal como todo valor por requisição, porque o SSR renderiza requisições
concorrentes sobre instâncias compartilhadas, e um slug guardado num campo entregaria o documento de
um visitante a outro.
Precisa da 0.2.0-preview.14 para ser lido do
Build. Na preview.13 o runtime não exportavaRouteValues, então uma página que o nomeasse fora de[ServerOnly]morria na hidratação com "does not provide an export named 'RouteValues'", enquanto o SSR seguia respondendo 200 com markup correto, que é por que parecia que nada estava errado. Ler de um prefetch[ServerOnly]funciona nas duas.
Desde 0.2.0-preview.13
public sealed class LiveRates : StatefulComponent
{
private IDisposable? _subscription;
private NetworkState _network;
protected override void OnMount() =>
_subscription = _status.Subscribe(state => SetState(() => _network = state));
protected override void OnUnmount() => _subscription?.Dispose();
public override VisualNode Build(ComponentContext context) => …;
}Um construtor não pode ser o lugar de carregar estado guardado, assinar um dispositivo ou iniciar uma
requisição: o reconciliador constrói instâncias novas a cada passada e mantém a retida, então
qualquer coisa que um construtor inicie é iniciada de novo para uma instância que é jogada fora. O
OnMount roda uma vez, na instância que fica.
O OnUnmount veio junto, e não depois, porque sem o segundo todo mount é um vazamento.
Três coisas que vale saber:
-
Entregue quando a passada fecha, não no meio do build. Um hook rodando dentro do reconcile roda
enquanto o próprio pai ainda está construindo, então um
SetStatedali mutaria uma árvore no meio da produção. - O que sai desmonta antes de o que entra montar: a ordem que permite dois componentes compartilharem um recurso exclusivo durante uma troca.
-
Não significa "os pixels existem". O Photon não tem DOM de onde ler geometria, então um hook que
prometesse isso não poderia ser write-once. Significa: este componente está na árvore, o build dele
está prestes a ser mostrado, e um
SetStatedaqui agenda a próxima passada em vez de brigar com a atual.
A RAIZ de uma página não tem pai para montá-la, então a superfície monta: o PhotonHost depois do
primeiro frame, a base web no mount/hydrate.
Um componente é um valor, então afirmar sobre o que ele renderiza custa um assert, não um processo. O realizer web é público e os próprios testes do framework usam exatamente isto:
using eQuantic.UI.Core.Rendering;
using eQuantic.UI.Web;
var html = HtmlRenderer.RenderNode(
WebRealizer.Lower(new PriceTag(1999), PhotonTheme.Instance).Render());
html.Should().Contain("R$ 19,99");Três passos, porque cada um faz uma coisa: Lower(node, theme) realiza a árvore, .Render()
transforma o elemento realizado num HtmlNode, e HtmlRenderer.RenderNode(...) escreve esse nó como
markup. Parar no .Render() te dá o nó, o que é útil quando a afirmação é sobre estrutura (.Tag,
.Attributes) e não sobre texto, que é o que a maioria dos testes do próprio framework quer. Sem um
sink de estilo, os estilos ficam inline, que é o que se quer num teste: a forma de classe atômica é
para a página, e afirmar sobre um hash de conteúdo é afirmar sobre um artefato de build.
Duas coisas que isso compra além de velocidade. Um teste pode renderizar o MESMO nó nos dois realizers e comparar o que cada um fez com ele, que é como o contrato de paridade de layout é mantido. E um componente que lança é pego pela fronteira, então uma subárvore quebrada falha o próprio assert em vez de levar a suíte junto.
🌐 English · Português
🏁 Comece aqui
📱 Write-once
- Componentes write-once
- Superfície declarativa
- Motor Photon
- Design System
- Capacidades
- Armazenamento
- Formulários
- Editor de código
- Markdown
- Mermaid
- Renderização de Email
🏗️ Arquitetura
⚙️ Compilação
- Compilador
- Avaliação em tempo de compilação
- Recursos C# suportados
- Resolução de tipos externos
- Fluxo de build
- Diagnósticos
⚡ Runtime
🔌 Servidor
🎨 Ecossistema
🚀 Desenvolvimento