-
Notifications
You must be signed in to change notification settings - Fork 1
DeclarativeSurface
A screen is plain C# expressions — no markup language, no builder ceremony, and no 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)),
]),
]);Every name there is a factory method named exactly like the type it returns. There is no import to write: the SDK puts the framework's surface in scope in every file of your project, and your own components join it automatically (see below).
Because styles are typed values rather than CSS strings, the compiler checks the whole interface — layout and styling included — and the same class renders on the web and natively through Photon.
- A factory is named exactly like its type.
Column,Text,Button. - It mirrors a constructor parameter for parameter — same names, same order, same defaults — so
named arguments carry between
new X(…)andX(…)unchanged. - Container nodes take a trailing
childrenparameter, written as a collection expression. - There are no overloads. The surface transpiles to a JavaScript twin, and JS methods cannot overload, so each type has ONE canonical factory.
Rarer init properties keep the constructor + initializer form; the factories are sugar over the
same types, never a second API:
Box(new BoxStyle { Padding = EdgeInsets.All(Space.S4), Background = theme.Surface },
Text("Still the same Box", TypeRole.BodyM))Value records (GridTrack, DialogAction, NavItem) deliberately have no factories — they are
data, and target-typed new(…) already reads well.
Define a component; the build generates its factory. Nothing to register, nothing to import:
// 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 — no using, no new
Row(gap: Space.S3, children: [
StatTile("Count", $"{_count}"),
StatTile("Doubled", $"{_count * 2}"),
])A source generator reads the compilation, finds the components, and writes a static AppUI class
plus the global using static that puts it in scope. It stays in step with your components because
it is generated from them.
Pages get no factory. A [Page] is reached by its route, never composed by hand.
The widest — the same rule the transpiler applies when it collapses constructor overloads, so the factory and the emitted constructor never disagree. When the widest is not the one you want offered, elect another:
public sealed class Badge : StatelessComponent
{
[UiFactory]
public Badge(string label) { … } // ← this one gets the factory
public Badge(string label, int count, bool dot) { … }
}| Code | Severity | Meaning |
|---|---|---|
EQ3101 |
Error | Two constructors of one component are marked [UiFactory]. An election needs a single winner. |
EQ3102 |
Warning | Two components share a name, so only one can own that factory. Rename one, or build the other with new. |
<PropertyGroup>
<EQuanticGenerateFactorySurface>false</EQuanticGenerateFactorySurface>
</PropertyGroup>Your components then compose with new, exactly as before.
A method named like a type shadows that type in any file where the surface is in scope. It bites
only on types that arrive through a using — which means the framework's, not yours:
Spacer.Fixed(34) // ✗ CS0119 — `Spacer` binds to the factory method
Panel.Empty("x") // ✓ your own type, declared in your own namespace, winsC# resolves names declared in the current namespace ahead of using static imports, so your own
components are never shadowed by their own factories, and you never need the workaround below.
The framework's own statics that sit behind a factory name get a factory under a name of their own:
| Instead of | Write |
|---|---|
Spacer.Fixed(34) |
Gap(34) |
Badge.AsDot(variant) |
DotBadge(variant) |
new Icon(packGlyph) |
Glyph(packGlyph) |
A conformance test walks every factory, looks for statics on the type it shadows, and fails naming them until each has a named factory — so a fourth one cannot appear unnoticed.
Glyph is there for a slightly different reason than the other two, and it is worth knowing which:
Icon has two constructors — one taking the framework's curated Icons enum, one taking the
IconGlyph an icon package hands out. The mirrored Icon(...) can only be one of them, and there
are no overloads here, so a pack glyph had no way into a file importing this surface at all: it
could only be drawn with new, in the one place the framework promises you never need it.
using eQuantic.UI.MaterialSymbols;
Glyph(MaterialSymbolsIcons.PlayArrowRounded) // any icon package's catalog
Icon(Icons.Check) // the curated setThe generated surface is written to disk (EmitCompilerGeneratedFiles) because eqc reads files,
not the C# compilation: a factory your page calls has to be part of what the transpiler sees, or
the call resolves to nothing and the emitted JavaScript degrades silently. It is then transpiled
into a module like any other class, so AppUI.statTile(…) exists in the bundle beside
UI.column(…).
If you rename or remove a generator, run
dotnet clean. Compiler-generated files are not removed when the generator that wrote them goes away, and eqc would still read the leftovers — a phantom type, or a second copy of a live one.
- Write-Once Components — the architecture the surface sits on
- Components — the catalog every factory corresponds to
- Design System — the typed tokens the arguments are made of