Repository navigation
DeclarativeSurface es
🌐 Esta página en: English · Português · Español · Français · Deutsch
Una pantalla son expresiones C# corrientes: ningún lenguaje de marcado, ninguna ceremonia de
builder, y ningún new:
public override VisualNode Build(ComponentContext context) =>
Column(gap: Space.S4, children: [
Text($"Count: {_count}", TypeRole.Display, context.Theme.TextPrimary),
Row(gap: Space.S3, children: [
Button("Up", onPressed: () => SetState(() => _count++)),
Button("Reset", Variant.Outline, onPressed: () => SetState(() => _count = 0)),
]),
]);Cada nombre de ahí es un método factory llamado exactamente como el tipo que devuelve. No hay ningún import que escribir: el SDK pone la superficie del framework al alcance en todos los archivos de tu proyecto, y tus propios componentes se suman a ella automáticamente (ver más abajo).
Como los estilos son valores tipados y no cadenas de CSS, el compilador verifica toda la interfaz, layout y estilos incluidos, y la misma clase se renderiza en la web y de forma nativa a través de Photon.
- Una factory se llama exactamente como su tipo.
Column,Text,Button. -
Refleja un constructor parámetro por parámetro (mismos nombres, mismo orden, mismos valores
por defecto), así que los argumentos nombrados pasan entre
new X(…)yX(…)sin cambio. - Después de los parámetros reflejados, una factory puede tomar una cola semántica opcional:
parámetros que corresponden cada uno a una propiedad pública init-only del nodo (mismo nombre,
mismo tipo) y que se aplican con un inicializador de objeto. Así es como una pantalla declarativa
declara lo que los constructores deliberadamente no llevan:
Pressable(child, onPressed, label:, selected:, disabled:, pressedBackground:, expanded:),Link(destination, child, label:, current:), yTextEntry(value, onChanged, label:, placeholder:, disabled:, obscure:). La maquinaria de composición y de modales (Role,Mixed,InitialFocus) se queda solo en el inicializador a propósito: pertenece a los componentes dueños de esos patrones. - Los nodos contenedores toman un parámetro
childrenfinal, escrito como una expresión de colección. - No hay sobrecargas. La superficie se transpila a un gemelo en JavaScript, y los métodos de JS no pueden sobrecargarse, así que cada tipo tiene UNA factory canónica.
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)Desde 0.2.0-preview.45
Un botón de solo icono que VoiceOver puede nombrar, un elemento de navegación que DECLARA que está seleccionado, el enlace a la página en la que está el lector, todo alcanzable sin salir de la 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)Un control que se pulsa mientras el teclado se queda donde está, el botón de una barra de formato al
lado del texto que formatea o una fila de sugerencias bajo un campo, lo dice con
canRequestFocus: false: la pulsación corre, y el foco se queda donde estaba.
Pressable(Icon(Icons.Check), onPressed: Apply, label: "Apply", canRequestFocus: false)Desde 0.2.0-preview.61
Un contenedor que llena a su padre (el layout más común que existe) ya no necesita new: Column,
Row, Grid, Stack, ScrollView y ListView toman width y height antes de children (un
SizeValue; dejarlos fuera es Hug, exactamente lo que significa un inicializador sin ellos), y
Pressable toma su role compuesto. Contado por una aplicación real después de migrar toda su UI:
49 sitios que se habían quedado imperativos por un ancho, 3 por un rol.
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)Desde 0.2.0-preview.46
Tres más del mismo informe, y la misma regla. Un Switch se anuncia y se vuelve inerte mientras se
aplica un lote; un Checkbox se vuelve inerte también; y SegmentedControl no tenía factory
ninguna, así que una tira de pestañas se quedaba imperativa por un solo botón.
Switch(on: item.Enabled, onChanged: Toggle, label: item.Name, disabled: applying)
Checkbox(includeSystem, onChanged: Flip, label: "Include system files", disabled: scanning)
SegmentedControl(["Apps", "Login items"], selected, onChanged: Pick, stretch: false)Una lista de switches sin nombres es una lista de controles idénticos para la tecnología de apoyo, y
por eso label es el primero de ellos.
Desde 0.2.0-preview.46
Bookmark nombra un lugar de una pantalla al que un enlace puede llegar: Link("#features", …)
llega al nodo que dice Bookmark = "features".
Box(SectionStyle, children: [ … ]) with { Bookmark = "features" }Deliberadamente no es Key, y la confusión le costó a un sitio toda la navegación de su landing
más el raíl de "En esta página" de sus docs: una Key es identidad para el reconciliador, única solo
entre HERMANOS, y en una lista es típicamente "1", "2", "3", y proyectar eso sobre un nombre de
pantalla entera choca en la primera lista repetida. Un nodo puede llevar ambas.
Nada valida el valor más allá de que no esté vacío, a propósito: los bookmarks se GENERAN con frecuencia (un raíl de docs saca el slug de los encabezados que encuentra), y rechazar un duplicado en tiempo de build haría fallar una página porque la fuente que renderiza ganó un encabezado repetido.
En la web se vuelve el id del elemento, y el realizador le da espacio donde aterrizar: un
navegador desplaza un destino hasta arriba del todo, así que bajo una cabecera fija llegaría
escondido. El desplazamiento se mide desde la cabecera fija que la aplicación ya declaró, nunca un
número repetido en la página: un sitio tiene una nav de 60dp en su landing y una topbar de 56dp en
sus docs y no declara ninguna de las dos. En Photon es inerte hoy: un bookmark es un sitio al que
desplazarse, y el desplazamiento programático todavía está por llegar.
Desde 0.2.0-preview.46
Como Stack ganó dos mandos antes de children, su children pasa a ir nombrado,
Stack(Alignment.Center, children: [ … ]), la regla que todos los demás contenedores ya seguían.
Row y Column toman main, cross, wrap, runGap y padding; Text toma align, mono,
tabular y styleOverride. Estas eran propiedades init-only, así que fijar una significaba un
inicializador de objeto, lo que significa new, y new es exactamente lo que esta superficie
quita. Una fila que tenía que centrar su contenido se caía de la superficie por completo y había que
escribirla a la antigua.
Ancho, alto, fondo y radio de esquina deliberadamente no son parámetros de un flex: un flex que
lleve eso es un Box envolviendo un flex, y las propiedades ya lo dicen.
styleOverride en Text es la salida de una escala tipográfica cerrada. Los peldaños son el
default correcto y un diseño que se sale de ellos en todas partes ha dejado de tener una escala,
pero una escala cerrada sin salida se sortea anidando un HtmlElement crudo, que es peor, porque
solo funciona en un destino.
Rompe en la 0.2.0-preview.13.
childrenva al final (el contrato de los contenedores), así que los mandos se sitúan entre él ygap:Column(Space.S3, [ … ])pasa a serColumn(Space.S3, children: [ … ]). Una palabra, y ya es la forma normal de la superficie.
Las propiedades init más raras conservan la forma constructor + inicializador; las factories son
azúcar sobre los mismos tipos, nunca una segunda API:
Box(new BoxStyle { Padding = EdgeInsets.All(Space.S4), Background = theme.Surface },
Text("Still the same Box", TypeRole.BodyM))Los registros de valor (GridTrack, DialogAction, NavItem) deliberadamente no tienen
factories: son datos, y un new(…) con tipo inferido ya se lee bien.
Define un componente; el build genera su factory. Nada que registrar, nada que 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, sin using, sin new
Row(gap: Space.S3, children: [
StatTile("Count", $"{_count}"),
StatTile("Doubled", $"{_count * 2}"),
])Un generador de código lee la compilación, encuentra los componentes, y escribe una clase estática
AppUI más el global using static que la pone al alcance. Se mantiene al paso de tus componentes
porque se genera a partir de ellos.
Las páginas no reciben factory. Una [Page] se alcanza por su ruta, nunca se compone a mano.
El más ancho, la misma regla que aplica el transpilador cuando colapsa las sobrecargas de constructor, para que la factory y el constructor emitido nunca discrepen. Cuando el más ancho no es el que quieres ofrecer, elige otro:
public sealed class Badge : StatelessComponent
{
[UiFactory]
public Badge(string label) { … } // ← este recibe la factory
public Badge(string label, int count, bool dot) { … }
}| Código | Severidad | Significado |
|---|---|---|
EQ3101 |
Error | Dos constructores de un componente están marcados con [UiFactory]. Una elección necesita un único ganador. |
EQ3102 |
Advertencia | Dos componentes comparten nombre, así que solo uno puede ser dueño de esa factory. Renombra uno, o construye el otro con new. |
<PropertyGroup>
<EQuanticGenerateFactorySurface>false</EQuanticGenerateFactorySurface>
</PropertyGroup>Tus componentes entonces se componen con new, exactamente como antes.
Un método llamado como un tipo ensombrece ese tipo en cualquier archivo donde la superficie esté
al alcance. Solo muerde en los tipos que llegan por un using, lo que significa los del framework,
no los tuyos:
Spacer.Fixed(34) // ✗ CS0119: `Spacer` se liga al método factory
Panel.Empty("x") // ✓ tu propio tipo, declarado en tu propio namespace, ganaC# resuelve los nombres declarados en el namespace actual antes que los imports using static,
así que tus propios componentes nunca quedan ensombrecidos por sus propias factories, y nunca
necesitas el apaño de abajo.
Los estáticos propios del framework que quedan detrás de un nombre de factory reciben una factory con un nombre propio:
| En lugar de | Escribe |
|---|---|
Spacer.Fixed(34) |
Gap(34) |
Badge.AsDot(variant) |
DotBadge(variant) |
new Icon(packGlyph) |
Glyph(packGlyph) |
Desde 0.2.0-preview.7
(la fila de Glyph; las otras dos desde la 0.2.0-preview.2)
Un test de conformidad recorre cada factory, busca estáticos en el tipo que ensombrece, y falla nombrándolos hasta que cada uno tiene una factory con nombre, así que un cuarto no puede aparecer sin que se note.
Glyph está ahí por una razón algo distinta de las otras dos, y vale saber cuál: Icon tiene
dos constructores, uno que toma el enum Icons curado del framework, otro que toma el
IconGlyph que entrega un paquete de iconos. El Icon(...) reflejado solo puede ser uno de ellos, y
aquí no hay sobrecargas, así que un glifo de pack no tenía ninguna entrada a un archivo que importara
esta superficie: solo se podía dibujar con new, en el único sitio donde el framework te promete que
nunca lo necesitas.
using eQuantic.UI.MaterialSymbols;
Glyph(MaterialSymbolsIcons.PlayArrowRounded) // el catálogo de cualquier paquete de iconos
Icon(Icons.Check) // el conjunto curadoLa superficie generada se escribe a disco (EmitCompilerGeneratedFiles) porque eqc lee archivos,
no la compilación de C#: una factory que llama tu página tiene que ser parte de lo que ve el
transpilador, o la llamada se resuelve a nada y el JavaScript emitido se degrada en silencio.
Después se transpila a un módulo como cualquier otra clase, así que AppUI.statTile(…) existe en el
bundle al lado de UI.column(…).
Las sobras de un generador se van con él. El compilador no elimina ningún archivo generado, así que un componente que borres, o un generador que renombres o quites, dejaría su última salida ahí para que eqc la lea: un tipo fantasma, o una segunda copia de uno vivo. El SDK los quita él mismo: después de una compilación que corre, un archivo generado que esa compilación no escribió se borra, y el log del build lo nombra. No hace falta ningún
dotnet clean.
- Componentes write-once: la arquitectura sobre la que se asienta la superficie
- Componentes: el catálogo al que corresponde cada factory
- Design System: los tokens tipados de los que están hechos los argumentos
🏁 Empieza aquí
🏗️ Arquitectura
- Visión general de la arquitectura
- Componentes write-once
- Superficie declarativa
- Arquitectura de paquetes
- Componentes
- Estilos
- Localización
- Analytics y GTM
📱 Write-once
- Motor Photon
- Design System
- Capacidades
- Almacenamiento
- Formularios
- Editor de código
- Markdown
- Mermaid
- Renderizado de email
⚙️ Compilación
⚡ Runtime
🔌 Servidor
🎨 Ecosistema
🚀 Desarrollo