A superfície declarativa (autoria sem new)Uma tela é C# puro em expressões: sem linguagem de marcação, sem cerimônia de builder, e sem new:
public override VisualNode Build(ComponentContext context) =>
Column(gap: Space.S4, children: [
Text($"Contagem: {_count}", TypeRole.Display, context.Theme.TextPrimary),
Row(gap: Space.S3, children: [
Button("Somar", onPressed: () => SetState(() => _count++)),
Button("Zerar", Variant.Outline, onPressed: () => SetState(() => _count = 0)),
Todo nome ali é um método fábrica chamado exatamente como o tipo que ele retorna. Não há import para escrever: o SDK coloca a superfície do framework em escopo em cada arquivo do seu projeto, e os seus próprios componentes entram nela automaticamente (veja abaixo).
Como os estilos são valores tipados em vez de strings CSS, o compilador confere a interface inteira, layout e estilo incluídos, e a mesma classe renderiza no web e no nativo pelo Photon. •
Uma fábrica é chamada exatamente como o tipo dela. Column, Text, Button.
•
Ela espelha um construtor parâmetro por parâmetro (mesmos nomes, mesma ordem, mesmos padrões), então argumentos nomeados atravessam de new X(…) para X(…) sem mudança.
•
Depois dos parâmetros espelhados, uma factory pode receber uma cauda semântica opcional: parâmetros que correspondem, um a um, a uma propriedade init-only pública do nó (mesmo nome, mesmo tipo), aplicados via object initializer. É assim que uma tela declarativa afirma o que os construtores deliberadamente não carregam: `Pressable(child, onPressed, label:, selected:, disabled:, pressedBackground:, expanded:), Link(destination, child, label:, current:)` e TextEntry(value, onChanged, label:, placeholder:, disabled:, obscure:). A maquinaria de compostos e modais (Role, Mixed, InitialFocus) fica só no initializer de propósito: pertence aos componentes que são donos desses padrões.
•
Nós contêineres recebem um parâmetro children ao final, escrito como expressão de coleção.
•
Não há sobrecargas. A superfície transpila para um gêmeo JavaScript, e métodos JS não podem sobrecarregar, então cada tipo tem UMA fábrica canônica.
Layout é parâmetro, não inicializador
Desde 0.2.0-preview.13
Row(gap: Space.S3, main: MainAlign.SpaceBetween, cross: CrossAlign.Start, children: [ … ])
Column(gap: Space.S2, wrap: true, runGap: Space.S4, children: [ … ])
Text("42", TypeRole.Display, align: TextAlignment.Center, tabular: true)
Semântica também é parâmetro
Since 0.2.0-preview.45
Um botão só-ícone que o VoiceOver sabe nomear, um item de navegação que AFIRMA estar selecionado, o link da página em que o leitor está, tudo alcançável sem sair da forma de factory:
Pressable(Icon(Icons.Search), onPressed: Search, label: "Search", selected: true)
Link("/scan", Icon(Icons.Home), label: "Home", current: true)
TextEntry(_password, OnPassword, label: "Password", obscure: true)
Um controle tocado enquanto o teclado fica onde está, o botão de uma barra de formatação ao lado do texto que ele formata ou uma linha de sugestões debaixo de um campo, diz isso com canRequestFocus: false: o toque roda, e o foco fica onde estava.
Pressable(Icon(Icons.Check), onPressed: Apply, label: "Apply", canRequestFocus: false)
Since 0.2.0-preview.61
Layout também é parâmetro
Um contêiner que preenche o pai (o layout mais comum que existe) já não precisa de new: Column, Row, Grid, Stack, ScrollView e ListView recebem width e height antes de children (um SizeValue; omitir é Hug, exatamente o que um initializer sem eles significa), e Pressable recebe seu role de composto. Contado por um app real depois de migrar a UI inteira: 49 sítios que tinham ficado imperativos por uma largura, 3 por um role.
Row(gap: Space.S2, width: SizeValue.Fill, children: [ … ])
Column(height: SizeValue.Fill, children: [ ScrollView(list, height: SizeValue.Fill) ])
Pressable(Text("Overview"), Select, selected: true, role: PressableRole.Radio)
Como Stack ganhou dois knobs antes de children, o children dele passa a ser nomeado, Stack(Alignment.Center, children: [ … ]), a regra que todo outro contêiner já seguia.
Desde 0.2.0-preview.46
E os controlos de configuração
Mais três do mesmo relato, e a mesma regra. Um Switch anuncia-se e fica inerte enquanto um lote aplica; um Checkbox fica inerte também; e o SegmentedControl não tinha factory nenhuma, então uma faixa de abas ficava imperativa por um knob.
Switch(on: item.Enabled, onChanged: Toggle, label: item.Name, disabled: applying)
Checkbox(includeSystem, onChanged: Flip, label: "Incluir ficheiros de sistema", disabled: scanning)
SegmentedControl(["Apps", "Itens de arranque"], selected, onChanged: Pick, stretch: false)
Uma lista de switches sem nomes é uma lista de controlos idênticos para a tecnologia assistiva, que é porque o label vem primeiro deles.
Desde 0.2.0-preview.46
Um link precisa de um sítio onde chegar
O Bookmark nomeia um lugar da tela que um link consegue alcançar: o Link("#features", …) chega ao nó que diz Bookmark = "features".
Box(EstiloDaSeccao, children: [ … ]) with { Bookmark = "features" }
Deliberadamente não é o Key, e a confusão custou a um site a navegação inteira da landing mais o rail "nesta página" das docs: um Key é identidade de reconciliação, única só entre IRMÃOS, e numa lista é tipicamente "1", "2", "3", e projetar isso num nome de tela inteira colide na primeira lista repetida. Um nó pode ter os dois.
Nada valida o valor além de vazio, de propósito: bookmarks são frequentemente GERADOS (um rail de documentação faz slug dos títulos que encontra), e recusar um duplicado em tempo de build faria uma página falhar porque a fonte que ela renderiza ganhou um título repetido.
Na web vira o id do elemento, e o realizador dá-lhe espaço para aterrar: um navegador rola o alvo até ao topo, então debaixo de um cabeçalho fixo ele chegaria escondido. O offset é medido do sticky que a app já declarou, nunca um número repetido na página: um site tem 60dp de nav na landing e 56dp de topbar nas docs e não declara nenhum. No Photon fica inerte hoje: um bookmark é um sítio para onde rolar, e rolagem programática ainda está à frente.
Desde 0.2.0-preview.46
Row e Column recebem main, cross, wrap, runGap e padding; Text recebe align, mono, tabular e styleOverride. Essas eram propriedades init-only, então definir uma exigia um inicializador de objeto, que exige new, e new é exatamente o que essa superfície remove. Uma linha que precisava centralizar o conteúdo caía fora da superfície inteira e tinha que ser escrita do jeito antigo.
Largura, altura, fundo e raio de canto deliberadamente não são parâmetros de um flex: um flex carregando isso é um Box embrulhando um flex, e as propriedades já dizem isso.
O styleOverride do Text é a saída de uma escala de tipo fechada. Os degraus são o padrão certo, e um design que passa por cima deles em todo lugar deixou de ter escala, mas uma escala fechada sem saída acaba contornada aninhando um HtmlElement cru, o que é pior, porque só funciona num alvo.
QUEBRA NA 0.2.0-PREVIEW.13 children vem ao final (o contrato de contêiner), então os botões ficam entre ele e o gap: Column(Space.S3, [ … ]) vira Column(Space.S3, children: [ … ]). Uma palavra, e já é a forma normal da superfície.
Propriedades init mais raras mantêm a forma construtor + inicializador; as fábricas são açúcar sobre os mesmos tipos, nunca uma segunda API:
Box(new BoxStyle { Padding = EdgeInsets.All(Space.S4), Background = theme.Surface },
Text("Continua o mesmo Box", TypeRole.BodyM))
Records de valor (GridTrack, DialogAction, NavItem) deliberadamente não têm fábricas: eles são dado, e o new(…) com tipo alvo já lê bem.
Os seus componentes entram nela
Defina um componente; o build gera a fábrica dele. Nada para registrar, nada para importar:
// Components/StatTile.cs
public sealed class StatTile : StatelessComponent
public StatTile(string label, string value) { Label = label; Value = value; }
public string Label { get; init; }
public string Value { get; init; }
public override VisualNode Build(ComponentContext context) => /* … */;
// Pages/HomePage.cs, sem using, sem new
Row(gap: Space.S3, children: [
StatTile("Contagem", $"{_count}"),
StatTile("Dobro", $"{_count * 2}"),
Um gerador de código lê a compilação, encontra os componentes, e escreve uma classe estática AppUI mais o global using static que a põe em escopo. Ele fica em passo com os seus componentes porque é gerado a partir deles.
Páginas não ganham fábrica. Uma [Page] é alcançada pela rota dela, nunca composta na mão.
Qual construtor é espelhado
O mais largo, a mesma regra que o transpilador aplica quando colapsa sobrecargas de construtor, para que a fábrica e o construtor emitido nunca discordem. Quando o mais largo não é o que você quer oferecer, eleja outro:
public sealed class Badge : StatelessComponent
public Badge(string label) { … } // ← este ganha a fábrica
public Badge(string label, int count, bool dot) { … }
Código
Severidade
Significado
EQ3101
Erro
Dois construtores de um componente estão marcados com [UiFactory]. Uma eleição precisa de um único vencedor.
EQ3102
Aviso
Dois componentes compartilham um nome, então só um pode ser dono daquela fábrica. Renomeie um, ou construa o outro com new.
<EQuanticGenerateFactorySurface>false</EQuanticGenerateFactorySurface>
Os seus componentes passam a compor com new, exatamente como antes.
A única aresta afiada: uma fábrica sombreia o tipo dela
Um método chamado como um tipo sombreia aquele tipo em qualquer arquivo onde a superfície esteja em escopo. Isso só morde tipos que chegam por um using, ou seja, os do framework, não os seus:
Spacer.Fixed(34) // ✗ CS0119: `Spacer` liga ao método fábrica
Panel.Empty("x") // ✓ seu próprio tipo, declarado no seu namespace, vence
O C# resolve nomes declarados no namespace atual antes dos imports using static, então os seus componentes nunca são sombreados pelas próprias fábricas, e você nunca precisa do contorno abaixo.
Os statics do próprio framework que ficam atrás de um nome de fábrica ganham uma fábrica com nome próprio:
Badge.AsDot(variant)
DotBadge(variant)
new Icon(packGlyph)
Glyph(packGlyph)
Desde 0.2.0-preview.7 (a linha do Glyph; as outras duas desde 0.2.0-preview.2)
Um teste de conformidade percorre cada fábrica, procura statics no tipo que ela sombreia, e falha nomeando-os até que cada um tenha uma fábrica com nome próprio, para que uma quarta não possa aparecer despercebida.
O Glyph está lá por uma razão um pouco diferente das outras duas, e vale saber qual: o Icon tem dois construtores, um recebendo o enum Icons curado do framework, outro recebendo o IconGlyph que um pacote de ícones entrega. O Icon(...) espelhado só pode ser um deles, e não há sobrecargas aqui, então um glifo de pacote não tinha caminho nenhum para dentro de um arquivo que importa essa superfície: só podia ser desenhado com new, no único lugar onde o framework promete que você nunca precisa dele.
using eQuantic.UI.MaterialSymbols;
Glyph(MaterialSymbolsIcons.PlayArrowRounded) // o catálogo de qualquer pacote de ícones
Icon(Icons.Check) // o conjunto curado
Como isso chega ao browser
A superfície gerada é escrita em disco (EmitCompilerGeneratedFiles) porque o eqc lê arquivos, não a compilação C#: uma fábrica que a sua página chama tem que fazer parte do que o transpilador enxerga, ou a chamada resolve para nada e o JavaScript emitido degrada em silêncio. Depois ela é transpilada num módulo como qualquer outra classe, então AppUI.statTile(…) existe no bundle ao lado de UI.column(…).
O QUE UM GERADOR DEIXA VAI COM ELE O compilador não remove arquivo gerado nenhum, então um componente que você apaga, ou um gerador que você renomeia ou remove, deixaria a última saída para o eqc ler: um tipo fantasma, ou uma segunda cópia de um vivo. O SDK remove isso sozinho: depois de uma compilação que roda, um arquivo gerado que ela não escreveu é apagado, e o log do build diz qual. Não é preciso dotnet clean.