Skip to content

Upgrading

Edgar Mesquita edited this page Sep 22, 2026 · 14 revisions

Upgrading

🌐 This page in: English · Português

What a release CHANGED under you, version by version — the compile errors you will meet and the one-line answer to each.

Every entry here was met by a real app upgrading, not derived from a changelog. A break that cost somebody an hour and was never written down costs the next person the same hour.

Each entry opens with what got simpler, because a page that only lists breakage never says whether the upgrade was worth taking.


To 0.2.0-preview.57

What got simpler. A component can be written over a base of your own, and a component the page composes can load its own server data. Both were promised and neither held: a subclass of an app base kept only its Build and died in the browser, and an IServerPrefetch below the page was never called. The runtime also carries one committed copy of each transpiled module instead of two.

The native declarations moved to eQuantic.UI.Native.Hosting

using eQuantic.UI.Primitives;       // PhotonEntitlements, AppCategory… are no longer found here
using eQuantic.UI.Native.Hosting;   // add this

PhotonCapabilityAttribute, PhotonEntitlementAttribute, PhotonEntitlements, PhotonBundleKeyAttribute, PhotonBundleValueKind and AppCategory. A Photon app already references the hosting assembly — it is where CreateApp comes from — so this is one using. A web app could never name them usefully and is not affected.

RangeValue carries numbers only

Progress(child, value: new RangeValue(0.45f, 0, 1) { Text = "Estimating" })   // no longer compiles
Progress(child, value: new RangeValue(0.45f, 0, 1), valueText: "Estimating")

The words moved to the node — Progress.ValueText and Adjustable.ValueText — because an indeterminate bar has no number and lost its words along with it. Coming from .55, this is the type's second change: .56 renamed AdjustableValue to RangeValue, and .57 moves Text off it.

SemanticNode deconstructs into thirteen parts

var (role, path, bounds, label, value, disabled, isChecked, expanded,
     current, selected, heading, live) = node;          // no longer compiles
var (role, path, bounds, label, value, disabled, isChecked, expanded,
     current, selected, heading, live, range) = node;

Constructing is untouched: Range is a parameter with a default.

ImageOptimizationState is gone

It was written by UseImageOptimization and read by nothing. Read ImageOptimizationOptions from DI instead, which is what the /_equantic/image endpoint does on every request.

Four overloads became one each, and most calls still compile

  • UI.Glyph(glyph) becomes UI.Icon(glyph). UI.Icon and new Icon take an IconGlyph, with an implicit conversion from Icons, so UI.Icon(Icons.Check) is unchanged.
  • EmailRenderer.Render(component, theme) binds to the VisualNode form, which a component already is, with the same parameters.
  • WebRealizer.Lower(node, theme, scale, styles) is one method with StyleSink? styles = null.
  • CodeWriter.BeginScope(line, opener, closer) is gone; the opener-and-closer form stays.

EQ2012: a [ServerAction] on an abstract component

public abstract class CardBase : StatelessComponent
{
    [ServerAction] public Task<string> Load() =>;   // EQ2012
}

The server registers an action under the CONCRETE component's name, so a stub on the base invoked an id nothing served. Declare the action on the concrete component.

A composed IServerPrefetch now runs

Not a compile error, but a change in what happens: a component implementing IServerPrefetch below the page used to be skipped, and now its PrefetchAsync runs and its fields reach the hydration payload. A component that implemented it expecting nothing will find that something does.


To 0.2.0-preview.56

What got simpler. No member survives to keep an old shape alive. TypeStyle and SemanticNode carried a constructor and a Deconstruct added so an assembly compiled against an older package would still bind, and both are gone with the paragraph that explained them. An extension over the vocabulary lowers like every other extension, so node.Centered() is VisualNodeExtensions.centered(node) and the two mirrors plus the import-cycle seam that exemption needed went with it. That also closes a hole no C# author could see: a component parameter named like a runtime member used to overwrite it and take the page down in the browser with centered is not a function.

AdjustableValue is RangeValue

Value = new AdjustableValue(now, min, max)   // no longer compiles
Value = new RangeValue(now, min, max)

The same three numbers, the same optional Text. The name moved because a second node carries the trio now: Progress.Value is a RangeValue too, and a progress bar is not adjustable.

WebFrame takes its content as one value

new WebFrame { Source = "https://example.com", Title = "Docs" }   // no longer compiles
new WebFrame { Document = "<p>hello</p>" }                        // no longer compiles
WebFrame(WebContent.Url("https://example.com"), "Docs")
WebFrame(WebContent.Document("<p>hello</p>"), "Note")

A frame shows a URL or inline markup and never both, which two init properties could not say. The title is a constructor argument for the reason it exists at all: a frame nobody can name is one a screen reader cannot announce.

Navigable takes its handler first

new Navigable(rows, onMove)   // no longer compiles
new Navigable(onMove, rows)

Children go last, the way every container in the vocabulary takes them.

The chart factories mirror their records

ChartSeries(name: "2026", values: [15, 21])   // no longer compiles
ChartSeries(Name: "2026", Values: [15, 21])

A positional record's parameters ARE its properties, so new ChartSeries(Name: …) and the factory take one spelling between them. Only a NAMED argument breaks, and CategoryAxis and ValueAxis changed the same way.

TypeStyle and SemanticNode deconstruct into their real shape

var (size, line, weight, tracking, maxScale, mono, italic) = style;            // no longer compiles
var (size, line, weight, tracking, maxScale, mono, italic, family) = style;

Both carried a shorter Deconstruct (and TypeStyle a matching constructor) so code compiled against an older package would still bind. Constructing is untouched, since the new parameters have defaults. Only the deconstruction changes, and the compiler names the arity it wants.

LinkRegion and RealizeResult carry more

var (bounds, destination) = region;          // no longer compiles
var (bounds, destination, path) = region;

A link region now knows the layout path it came from, which is what lets Tab reach it and activate it on Photon. Only a host reading the realized regions is affected, and RealizeResult's constructor grew for the same walk.

To 0.2.0-preview.55

What got simpler. Every dispatch over the vocabulary is a visitor now: the layout engine, the Photon paint pass, the web realizer, the server's DOM, the semantics walk and both email alternatives. A node that a realizer forgot is a compile error instead of a default arm, which is how Navigable and WebFrame were found measuring as a silent zero box. The twenty wrappers that hold one child share a base, SingleChildNode, where four hand-kept lists used to disagree in eight places. It shows on a native screen: a Text inside a Pressable, a Link or a Hoverable, in a Row too narrow for it, now truncates like a bare Text instead of running 128dp past the row's edge. And a slider says what it holds, on the web and to VoiceOver and TalkBack.

ButtonStyles is gone

It was a view over Sizing, and the ladder is now the only statement of a control's metrics. Each field of Metrics(size, density) was already produced by the rung it now names, so no number moves:

ButtonStyles.MinWidth                          // → Sizing.ButtonMinWidth
ButtonStyles.Metrics(size, density).Height     // → Sizing.Height(size, density)
ButtonStyles.Metrics(size, density).PadX       // → Sizing.PaddingX(size, density)
ButtonStyles.Metrics(size, density).Gap        // → Sizing.Gap(size)
ButtonStyles.Metrics(size, density).LabelSize  // → Sizing.LabelSize(size, density)
ButtonStyles.Metrics(size, density).IconSize   // → Sizing.Icon(size)
ButtonStyles.Metrics(size, density).Radius     // → Sizing.Radius(size)
ButtonStyles.Metrics(size, density).Hit        // → Sizing.HitTarget(size, density)

FlexNode is host-only

FlexNode container = row;      // EQ2010 in a component

The runtime never exported a twin for FlexNode, so a client component that named it compiled and then died at hydration on "does not provide an export named". eqc now says so at the call site. Name the concrete node, Row or Column, instead. Row(...).With(child) keeps compiling, because the fence asks what a member was reached through. Server code and the native track are unchanged.

EQ2111 where a service lookup has nothing to key on

A GetService call with neither a type argument nor an argument used to emit getService(), which the runtime cannot answer, because its registry is keyed by the interface NAME. Nothing reported it. It is now EQ2111, the error the other path to the same situation already gave. If your build stops here, that call was already returning nothing in the browser on .54: pass the type.

An Adjustable with no value announces group

ARIA requires role="slider" to carry aria-valuenow, and every slider the SDK shipped carried the role without the value. The role is derived from the value now. An Adjustable at its default role is a slider when it has a Value and a group when it has none, which is what a focusable container the arrow keys adjust is. A value on Tablist or Radiogroup, roles that carry none, is not emitted.

Adjustable(track, onAdjust, value: new AdjustableValue(now, 0, 1000) { Text = "R$ 400" });
Slider(budget, onBudget, valueText: "R$ 400");

The shipped Slider hands over the value its thumb is drawn from, and valueText gives words in place of the number, because only the app knows what its numbers mean.

To 0.2.0-preview.54

What got simpler. Geometry has one home. Rect, Point and Size were declared in the native engine and copied four times to reach the places that needed them; they now live in eQuantic.UI.Primitives beside the rest of the vocabulary, so a component, a chart and a realizer all name the same type. A painter takes that box instead of four loose floats. And the node hierarchy is closed, which means a realizer missing a case is a build error rather than a node that silently fails to draw.

Correction to .53 first, because it shipped wrong. .53's entry said a truncated native label "now ends in ". The mark was right and the PLACE was not: we passed CoreText the constant 2 believing it meant truncate-at-end, and 2 is kCTLineTruncationMiddle. So on macOS .53 cut labels in the MIDDLE — "Documents and Settings" became "Docum…ttings". .54 puts the ellipsis at the end, where the entry always claimed it was. Found by a consumer, not by us.

Geometry moved to Primitives

Rect, Point, Size, RRect and Matrix2D moved from eQuantic.UI.Native.Engine to eQuantic.UI.Primitives. A source rebuild is the whole migration — both assemblies ship in the same SDK at the same version — and only a consumer holding a compiled reference relinks. No type forwarders: in preview a break is free, and a forwarder is a second name for the one type we just finished removing four copies of.

The same move, same reason, for SemanticRole, SemanticCheck and SemanticNode, out of eQuantic.UI.Native.Components.

Point's operators are host-only, and so are RRect and Matrix2D

var middle = a + b;          // EQ2010 in a component

JavaScript cannot overload an operator, so a + b on two Points emitted JavaScript's own + and concatenated two objects into "[object Object][object Object]"; a * 2 produced NaN. It did this silently, which is why it is now a build error instead of a page that renders the wrong number. On the server and in the native track they work as before. In a component, write the arithmetic out:

var middle = new Point(a.X + b.X, a.Y + b.Y);

RRect and Matrix2D are fenced whole — they are what a rasterizer consumes, and a page naming Matrix2D was emitting an import for a symbol the runtime does not export.

ICanvasPainter takes a box and a point

void FillRect(Rect box, ColorToken color, float cornerRadius = 0);
void FillCircle(Point center, float radius, ColorToken color);
void Line(Point from, Point to, ColorToken color, float strokeWidth);
Size Size { get; }

Only an author of a CUSTOM painter changes anything. Code that draws on the painter we hand it is untouched. The four floats were always the four a Rect is, spelled out at every call.

The runtime's route surface

routeData() and the RouteData type are gone. RouteValues.from(params, query) and RouteValues replace them, and getCurrentRoute() still exists and answers a RouteValues — one rename and one import. Two behaviours changed with it:

  • param and query answer null where the TypeScript answered undefined, because C#'s Param returns string?. ?? is unaffected; an === undefined comparison is not.
  • A repeated query key answers its FIRST value on both sides. ?tag=a&tag=b reached a page as "a,b" from the server and "a" in the browser; now both say "a".

RenderContext's service surface is one method

Five members went, not one: ServiceProvider, SetGlobalServiceProvider, SetScopedServiceProvider, RegisterService<T> and TryGetService<T>. What remains is GetService<T>(), and it returns T? where it used to return T.

context.RegisterService(new Clock());        // gone — register with the app's own container
var clock = context.TryGetService<IClock>(); // gone
var clock = context.GetService<IClock>();    // now T?, so the null check is yours

TryGetService never had a twin at all — the transpiler emitted context.tryGetService(...), a method the runtime has never had, so any component calling it was already broken in the browser.

Navigator.Go's parameter is destination

Navigator.Go(href: "/reports");   // no longer compiles
Navigator.Go("/reports");         // always fine
Navigator.Go(destination: "/reports");

A positional call is untouched. Only a NAMED argument breaks, which is the kind of break a compiler reports clearly and a reader of release notes never expects.

BarRect carries a Rect

public sealed record BarRect(int Category, int Series, Rect Box, bool Negative, bool DataEnd);

It was eight positional parameters with X, Y, Width and Height spelled out. The generated constructor, the Deconstruct and those four properties went with the change: read bar.Box.X rather than bar.X, and construct with a Rect. Only code that reads the solved chart geometry directly is affected — BarChart itself is untouched.

To 0.2.0-preview.53

What got simpler. A brand code face works for the theme that has one — IAppTheme.MonoFamily shipped in .51 and was inert for exactly the themes it was built for, so if you named the face at every call site to work around that, you can stop. A bookmark link lands in the same place cold and warm. A spreadsheet exists in the server's HTML. And the native layout engine carries no mutable state, which changes nothing you write and a good deal of what anyone reading it has to hold.

Nothing breaks. No API moved, nothing was renamed, and the one source change is inside the native layout track whose only caller is the SDK itself.

A role's face is a default, not a choice

MonoFamily did nothing for the normal way of branding type:

public TypeStyle Type(TypeRole role) => Base.Type(role) with { Family = "IBM Plex Sans" };

Every role names a face, so the theme's code face always found Family already set and stepped aside on the rule that a style which NAMES a family keeps it. Nothing about that family was chosen for the text in question — it came from the ROLE. So the feature worked for a theme that leaves Family null and was inert for one that does not, which is backwards.

What changes for you: if you named the code face at each call site to work around this, those lines are now redundant rather than wrong.

A cold load with a fragment lands where a warm one does

.52 fixed this and the fix did not work. The measurement that explains it is worth keeping:

cold, loadEventEnd 1310 ms  →  correct
warm, loadEventEnd   36 ms  →  wrong

The faster the page, the more reliably it failed. The .52 correction took its one chance at load, and load can fire BEFORE the browser performs its fragment jump. A returning visitor is almost everyone, and was always in the broken column.

It watches frames now, bounded by a budget once the document completes and by a wall clock whatever the page does — and it retires when the reader navigates, so it cannot follow them into another page.

Nothing to change in your code. A scroll nudge you added to work around this is now redundant.

A spreadsheet exists in the server's HTML

SheetSurface had no arm in the web realizer, so SSR wrote an empty <span> where the browser draws a grid. Since the day it shipped: a crawler never saw the data, a reader without scripts saw a hole. The server now writes the grid, its child, role="grid", its name and a tabindex.

CodeSurface is deliberately not included yet — the client appends a caret to every editor, so a server tree with only the child is one element short of what hydration expects, and that shape is a decision still to take.

A cut line ends one way

ITextMeasurer has promised a trailing ellipsis since it was written, and a cut line ended four different ways. CoreText truncates in its own layout now, so the width reported includes the mark and the glyphs drawn are that same line.

How you find it: a truncated label on macOS ends in and measures a little wider than before, because a truncation runs to the character where a wrap stops at the last whole word. If you keep pixel goldens of your own over truncated native text, they move once. DirectWrite still cuts without a mark.

Correction, and it is 0.2.0-preview.53 that is wrong. That release cuts the MIDDLE out of a truncated label on macOS — Replace hardco…with IAppTheme — not the end. We passed CoreText's truncation constant 2 with a comment calling it kCTLineTruncationEnd, and CTLine.h says 2 is kCTLineTruncationMiddle. So the mark is there, in the wrong place, and the web realizer beside it cuts at the end as the design system asks. Fixed after .53, with the assertion that was missing: two strings that share a head must cut to the same width. Only native text is affected — the web was always right.

LayoutConstraints, if you host the layout engine

Native layout only. LayoutContext no longer carries IndeterminateWidth, IndeterminateHeight, StretchWidth or StretchHeight — those are facts about what a parent offered a child, so they travel in the child's constraint. LayoutEngine.Layout gained an optional rootStretch.

Behaviour-neutral and measured that way: the pixel goldens are untouched and allocation is eight bytes per frame LOWER on both paths, because the context object is four fields smaller.

To 0.2.0-preview.52

What got simpler. One thing you can stop working around: a bookmark link lands in the same place whether the reader followed it inside your app or pasted it into a fresh tab. If you had a scroll nudge of your own in front of that, it can go.

Nothing breaks. No API moved, nothing was renamed, and there is no behaviour here you have to go looking for — the one change is a fix you would have noticed as a reader.

A cold load with a fragment lands where a warm one does

Opening a link like /privacy#rights in a fresh tab left the target UNDER the sticky header, while navigating to the same fragment from inside the app landed it correctly. That pairing is the whole diagnosis: scroll-margin-top reading the measured chrome height was right, and only the ORDER was wrong.

The chrome is measured after a render pass, so on a cold load the browser has already jumped with the height still unknown. A correction existed for exactly that and had one chance, which it spent on the MEASUREMENT rather than the correction — and because the browser re-runs its jump as late content settles the layout, that measurement often landed while the target was still far down the page. Nothing to correct, chance gone.

It now spends the chance on the correction, and books a second one on the document's own load, re-measuring the chrome then rather than trusting the number that booked it — a bar that wraps, or grows with a late webfont, is taller at load than at first paint.

How you find it: open a bookmark link into a long page in a fresh tab, with a sticky header on that page. The section heading used to sit behind the header; now it sits below it.

Nothing to change in your code. If you added a scroll nudge to work around this, it is redundant rather than wrong.

To 0.2.0-preview.51

What got simpler. A brand typeface is one property on your theme (TypeStyle.Family per role, IAppTheme.MonoFamily for code) instead of a StyleOverride at every call site, and a face the machine lacks is NAMED on the run line instead of silently substituted. Text.Align works on Photon, so centred text no longer needs a box around it. A laid-out node knows its Parent, so "where am I" stops being a parsed path string. And two mysteries became build errors with remedies: one that only ever appeared at hydration (EQ2010), and one that named a generated file you had never seen (EQ3006, an adopting app that kept its own Main).

Nothing was renamed. One member changed shape. Five behaviours changed under code that still compiles, and a compiler cannot help you with those, so each says how to find it.

LayoutNode.Children is read-only — the only source break

public IReadOnlyList<LayoutNode> Children { get; }   // was List<LayoutNode>

node.Children.Add(child) no longer compiles. Every laid-out node now carries Parent, recursively to the root, and attaching is the engine's job so the link cannot be forgotten. Reading is unchanged, and foreach (var child in node) — iterating the NODE, not the list — allocates nothing on the per-frame paths.

EQ2010 — a host-only symbol named from a component

A component calling FaceName.Usable(...) or reading FaceResolution.Unresolved used to compile, emit, and die at hydration on "does not provide an export named" while SSR kept answering 200. It is a build error now. Move the call to a [ServerAction], a [ServerOnly] class, or a realizer. The fence is per SYMBOL, so FaceName.IsWellFormed still crosses while Usable does not.

A Mac in dark mode opens the app in dark mode

PhotonOptions.Mode = null has documented "follows the system" since it existed, and the macOS shell answered Light unconditionally — the one target of four that did not honour it.

How you find it: open your app on a Mac with dark mode on. If you were relying on the old behaviour, say so with Mode = ThemeMode.Light rather than relying on a bug.

Photon honours Text.Align

All three text services cut their raster tight to the longest line and drew every line at x=0, so Text.Align had nowhere to go and was dropped.

How you find it: any Text that set Align and looked left-aligned on Photon now moves. A Row or a hand-placed box you added to work around it will now double-centre.

A regional culture falls back through its parent

The client's flat string catalogue was folded from the NEUTRAL culture alone, so pt-BR skipped pt: a key translated once in Strings.pt.resx came back in ENGLISH on the client while the server rendered it in Portuguese.

How you find it: a regional culture whose parent carries translations shows more of them now. Keys you copied down into the regional .resx to work around this are now redundant rather than wrong.

Variant.Link carries its ink in Base and Pressed

The theme had both transparent with the ink in OnBase, and the pressed ink did not exist at all, so the pressed-text swap Button defers had nothing to swap to. A custom theme or realizer reading Colors(Variant.Link).Base now gets a colour. Nothing FILLS a Link, so the ink sits there without painting a rectangle — what a Link never has is a fill, and that is the Subtle slot.

Web styling: the role styles the class, the node styles the element

The face, the slant, the mono stack and a code role's white-space moved from inline styles into the .eq-type-* class. The client's lowering cannot read the theme's type scale, so a role property emitted inline by SSR was dropped on the first re-render — a hydration mismatch, not a cosmetic one.

How you find it: hand-written CSS overriding an inline font-family, font-style or white-space on our text now competes with a class instead of an inline style, so it wins where it used to lose. Entry controls take their face from .eq-entry.eq-type-* rather than an inline font-family: inherit.

To 0.2.0-preview.50

Nothing was renamed and nothing was removed. What this release changed is four behaviours under code that still compiles, and one package that is a different thing. A compiler helps you with the last one only, so each of the others says how to find it.

IWorkspace.OpenUrl hands over only the schemes your app opens

Handing a URL to the operating system hands it to whatever claims its scheme: https reaches a browser, file: launches what the path names, and any scheme another app registered runs that app. A link that arrived in content must not get to pick among those, so OpenUrl now consults one policy first. It hands over http, https, and every scheme you already declared with builder.Bundle.UrlScheme(…). Anything else is one line in Program.cs:

builder.Workspace.OpensMail();                          // mailto:
builder.Workspace.OpensPhone().OpensMessages();         // tel:, sms:
builder.Workspace.Opens("x-apple.systempreferences");   // another app's scheme, or the system's

How you find it: the call returns false and the realization logs a warning naming the scheme and the exact line that would open it, so the first click tells you. It does not throw, because the policy exists for URLs your app did not write. CanOpen applies the same policy, so a button gated on it disappears for exactly the links OpenUrl refuses.

file: cannot be opted into. Use OpenFile or Reveal, which take a path, check it exists and never launch a folder. And builder.Bundle.UrlScheme("acme://") now throws at its own line rather than writing a manifest entry that matches nothing.

VisualNode.Key now does what it always said it did

Key has been documented as reconciler identity since the vocabulary existed, and neither realizer read it. Both read it now, which is a fix: a virtualized list that slides its window stops renumbering its children, so the focus ring stops walking off the row it was on.

How you find it: it makes real a contract nothing was checking. Duplicate keys among siblings used to be harmless because nobody read them. If you build a key from your own data, row.Key in a DataTable being the shape to look for, confirm it is unique within its list.

Focus scrolling jumps instead of gliding

Moving focus snaps the target into view. A glide had not arrived on the frame that draws the focus ring, so a keyboard walk could read the ring as off screen. A wheel or a drag still glides.

Two layout fixes that can move something

A stack now measures its children against its own box, where it was offering them the incoming constraints, so a Fill child in a fixed-height stack measured zero under a scroll view. If you pinned a height to work around that, the pin can come out. And a canvas with handlers keeps the pointer, where a Stack's layers were switching pointer-events off and every hover fell through.

eQuantic.UI.Charts is a different package

The only compile error here, and the one nobody is likely to meet: none of the three apps that track this SDK referenced that package. It used to be a small shared assembly for the two web-only wrappers; it is now the write-once chart library, and the three types moved into the wrappers that use them.

If you used It is now
eQuantic.UI.Charts.IChart with the Chart.js wrapper eQuantic.UI.Charts.ChartJs.IChart — change the using
eQuantic.UI.Charts.IChart with the ApexCharts wrapper eQuantic.UI.Charts.ApexCharts.IChart — change the using
ChartData<T>, Dataset Same name, same namespace; they ship inside eQuantic.UI.Charts.ChartJs now, which you already reference

If you use the wrappers through their own packages, you have nothing to do. Only a project that referenced eQuantic.UI.Charts directly for those types has to move.

To 0.2.0-preview.49

eQuantic.UI.Core no longer exists. It was never one assembly: it was four unrelated things sharing a name, and every web app installed all four to use some of them. Each part moved to its only consumer.

What Where it lives now
[Page], [ServerAction], [ServerOnly], [Authorize], [AllowAnonymous] eQuantic.UI.Primitives, beside the other contract attributes
HtmlElement, HtmlNode, HtmlStyle, DynamicElement, IComponent, RenderContext, ClassBuilder eQuantic.UI.Web, where the DOM is realized
SeoBuilder, MetadataCollection, AssetCollection, IRequireAssets eQuantic.UI.Server.Metadata / eQuantic.UI.Server.Assets
ImageOptimizationState eQuantic.UI.Images

Start by assuming the line is dead. On one site, 31 of 36 files that imported Core used nothing from it at all. The instinct is to look up where each name went; for most files the answer is nowhere. Delete the using, compile, and let the compiler name the few that need something.

For the ones that do, it is one of four:

  • The file also imports eQuantic.UI.Primitives → delete the line. This was 45 of 51 files on one site, and 18 of 18 in the SDK's own samples.
  • The file uses only a contract attribute and does not import Primitives → change the line to using eQuantic.UI.Primitives;.
  • The file uses a DOM type → change the line to using eQuantic.UI.Web;.
  • The file imports a sub-namespace such as eQuantic.UI.Core.Metadata → the table above says where it went. These are the easy ones to miss, because they do not look like the line you grepped for.

A namespace can also live in a string. If your app generates, templates or compiles C# of its own, a playground or a code sample being the usual case, the compiler cannot see those. Grep the string literals too.

Three fixes you may have been working around. An anchor offset regression from .48 is corrected, so remeasure any offsets you tuned against it. [ServerOnly] on one declaration no longer silences the whole partial. And an AssemblyName ending in .App stops losing the executable.

To 0.2.0-preview.48

The pre-write-once component model is gone. This is the largest deletion the project has made, and the first release where "breaking" means something. If your app is written against the write-once vocabulary — StatelessComponent/StatefulComponent from eQuantic.UI.Primitives, VisualNode, the factories — nothing changes for you. The three samples build untouched.

What was removed, all of it from eQuantic.UI.Core:

Gone If you used it
StatelessComponent / StatefulComponent built on HtmlElement, ComponentState, ComponentState<T>, InputComponent<T> Move to the write-once model: state and lifecycle live on the COMPONENT, so a separate state class becomes fields plus OnMount — see Architecture for the shape
IIconProvider There is no provider any more. A .svg in Assets/ becomes a Vectors.Mark, and a hand-written glyph is an IconGlyph property — see Icons
SvgElement HtmlElement("svg", …) on the web, or a Vectors.Mark for something that has to work on all three targets
ComponentAttribute It marked nothing the compiler read. Delete it
Styling/Colors, Styling/Animations, Styling/EQ CSS values as strings, which the product principle rules out. Use ColorToken, Space, TypeRole and the transition channels — see Styling

The TypeScript half went with it: 260 lines of core/component.ts, and the ComponentState export. A runtime bundle you have vendored by hand needs replacing rather than patching.

Also removed: the three unpackaged template trees under Templates/content/. Nobody could scaffold them (dotnet new equantic-app has always used the write-once ones), but they were in the repository teaching the wrong model to anyone who opened the folder.

Why now. The SDK is preview and every consumer is one of our own projects. Doing this after 1.0 would mean carrying two component models and a compiler that reads both, for the sake of code that nobody outside this repository ever wrote.

And three renames, because the vocabulary stopped speaking the web's language

These bite an ordinary write-once app, unlike everything above. The rule behind them is one this project already had and had not finished applying: the abstract layer names what a thing IS, not what one target calls it.

Was Is Why
Sticky Pinned "Sticky" is CSS's word for it. A node that stays put while its container scrolls is pinned on every target
ZIndex (on Positioned) Layer A z-index is a CSS coordinate. What the node declares is which layer it is in
Alt (on Image) Label "Alt text" is HTML's name for an accessible name. The same string becomes an accessibilityLabel on the phones and a semantics label on Photon

The compiler will not help you here: these are renames, so the old names simply do not resolve. Sticky is the one to grep for first, because a pinned header is the shape almost every app has.

To 0.2.0-preview.47

Nothing breaks. Said plainly because the previous release broke four things and the question is now reasonable. Everything below is new surface, and existing code compiles unchanged.

The one behaviour that CHANGES without a compile error: a macOS app's .app bundle now comes from dotnet publish rather than from the build output, and it lands inside the publish directory. A script that copied the bundle from bin/<config>/<tfm>/ after a publish is still copying the development one — point it at bin/<config>/<tfm>/<rid>/publish/ instead.

If you turn on PublishTrimmed for the first time here, that is now supported and it was not before: a trimmed Photon app used to open its window and ignore every setting, silently. Trim warnings are zero on the sample; if you see one from your own code, it is yours.

To 0.2.0-preview.46

Four things stop compiling. All four have a one-line fix, and none of them is a behaviour change in your own code.

An icon is a glyph; a child is a node

Icons.Heart no longer converts to a node on its own, so a component that wants a CHILD wants Icon(...) around it:

// before
new IconButton(Icons.Heart, onPressed: Like)
new EmptyState(Icons.Search, "Nothing here")
SelectedGlyph = Icons.HeartFilled

// after
IconButton(Icon(Icons.Heart), onPressed: Like)
EmptyState(Icon(Icons.Search), "Nothing here")
SelectedGlyph = Icon(Icons.HeartFilled)

The errors are CS1503 and CS0029, and one site reported ten of them.

The conversion was removed rather than fixed because it could not be made to work: an implicit conversion on a framework type is silently DROPPED when the component crosses to JavaScript — the twin declared the glyph type and the call site emitted the raw string — so the same source built one thing on Photon and another on the web. A node is the same node on both.

The generated factory resolves capabilities itself

A component that takes a capability through its constructor no longer takes it through its factory: the factory asks the container. So the arity drops, and the error reads as if your own generated surface lost an overload:

No overload for method 'ThemeToggle' takes 1 arguments
ThemeToggle(controller)   →   ThemeToggle()

This is the point of the change — a site was threading an IThemeController through two constructors to reach one button — but the first error you meet does not say so.

Server-only types need to say so

The compiler walks further than it used to, so a type that only ever runs on the server and happens to live in the web project now fails with EQ2004 on things like CSharpCompilation.Create, Stopwatch.StartNew or LoggerExtensions.LogInformation. Mark the TYPE:

[ServerOnly]
public sealed class PlaygroundCompilation {}

The Roslyn pin moved to 5.9.0

eQuantic.UI.Compiler pins Microsoft.CodeAnalysis.CSharp 5.9.0. A project that references Microsoft.CodeAnalysis.CSharp.Features for its own tooling has to move to 5.9.0 in the same commit, or restore fails with NU1107 — naming your test project rather than the SDK.


Before that

Earlier releases are not covered here: this page starts where it starts, rather than pretending to a history nobody recorded at the time.

Clone this wiki locally