eQuantic.UIeQuantic.UI
Docs
Playground
GitHub
DocsDevelopment
Upgrading
Edit this page
54 min read
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.62
What got simpler. A class library shared by a solution's projects is a ProjectReference and nothing else: what a page uses of it reaches the browser, a static class, a record, a component and its strings (Build Flow). A field on a phone opens the platform's keyboard with no code of yours, and the screen makes room for it (Photon). ICamera answers on Android as it does on iOS, through the UseCamera(reason) and the CameraPreview an app already wrote (Capabilities). An Android app keeps its screen when it comes back, turns without crashing, and closes what is open on back. An iOS app launches when it is built with the iOS 27 SDK, with no delegate and no plist key of yours. An app builds whatever its folders, its assembly name and its title hold, a $, a backtick or a quote included, and every BunPackage it declares is installed. And more of the C# a page runs in the browser answers what .NET answers: a date and a time span read and written in the page's culture, a text's case, Split, LINQ's overloads, Convert's encodings, and a queue, a stack and a sorted set.
EQ2010: a component calls its own [ServerOnly] member
1
2
3
4
5
[ServerOnly]
private string Secret() => _vault.Read();
public override VisualNode Build(ComponentContext context) =>
Text(Secret()); // EQ2010
A member marked [ServerOnly] is refused in the code of its own type too. It built before, and the page's module called a member its twin does not have. Call it from a [ServerAction], whose body does not cross.
A culture a call names is the page's or the invariant one
1
2
3
4
5
DateTime.Parse(text, new CultureInfo("pt-BR")); // EQ2108
name.ToUpper(new CultureInfo("tr-TR")); // EQ2108
DateTime.Parse(text); // the page's culture
DateTime.Parse(text, CultureInfo.InvariantCulture);
A culture handed to Parse or TryParse of a date, a time or a time span, or to ToUpper or ToLower, crosses as CultureInfo.CurrentCulture, a null one or CultureInfo.InvariantCulture. Each built before: the reader failed the page, and the casing dropped the culture it was handed. The page's culture is the one the server installed, so a tr-TR page upper-cases i to İ with no culture named.
A date is read in the page's culture
1
2
DateTime.Parse("01/02/2024"); // the 1st of February on a pt-BR page
DateTime.Parse("01/02/2024", CultureInfo.InvariantCulture); // the 2nd of January on every page
Not a compile error. Parse and TryParse of the five time types read the invariant shapes whatever the page was, so a value written with ToString() did not read back. Name CultureInfo.InvariantCulture where a text is in the invariant form whatever the page: a value from an API, a query string, a file.
A record of a referenced project crosses whole
A record of another project that a Server Action answers has a twin now, the whole record, as an app's own record has. A method of it that a browser cannot run fails the build at that method, though no page calls it. Keep that method out of the type the page reads: an extension method in a class of its own is called the same way (#826).
More that stops the build
Forms that compiled and then answered wrongly in the browser, or failed there, stop the build now:
•
EQ1004: a DateOnly constructor that takes a Calendar, as a DateTime's already was, a reader's styles other than white space, a span handed to Parse or TryParse, and one handed to Convert.ToBase64String or Convert.ToHexString that is not a collection expression.
•
EQ2004: ParseExact, TryParseExact and GetDateTimeFormats, which built and failed the page.
For whoever hosts the compiler or writes against a shell
An app that writes pages meets none of these.
•
ModuleBundler.Bundle takes the folder bun runs in, after the path of bun.
•
Eq.SplitOnWhiteSpace is gone: a Split is Eq.StringSplit or Eq.StringSplitAny.
•
PhotonRealizer.Realize takes composing after markedText, so a call that passes pathCache or a later argument by position names it now.
•
An ObjCBlock is built over a static [UnmanagedCallersOnly] entry point and a context, read back with ObjCBlock.Context<T>(block), where it took a delegate.
•
On iOS the window is PhotonSceneDelegate's, and PhotonAppDelegate no longer holds it.
The browser answers as .NET does
Not compile errors. A page that relied on the old answer gets .NET's now:
•
TimeOnly.Parse("9:30 PM") is 21:30, where it was half past nine in the morning, and TryParse(text, out value) of the five time types answers true or false.
•
A TimeSpan writes the format it is given (ToString(@"hh\:mm") is 01:02, where every format wrote 01:02:00) and is read as .NET reads one: 10 is ten days, 24:00:00 twenty-four, and 00:60:00 throws.
•
ToUpper and ToLower change each code unit to exactly one other: ß stays ß, where the browser wrote SS.
•
Split answers as the overload C# binds: an array of separators splits at any of them, an empty separator splits nothing, and the count and the options apply. LastIndexOf of a string reads its start as the last char a match may cover.
•
FirstOrDefault, LastOrDefault and SingleOrDefault handed a default value answer it, where it was called as a predicate. Zip(second, third) answers triples, Average computes in the element's type, and SequenceEqual handed a comparer compares.
•
A foreach over a queue, a stack, a linked list or a sorted set that changes under it ends with .NET's InvalidOperationException, where foreach (var x in set) set.Add(x + 10); never ended. Their CopyTo and a sorted set's set operations answer, where each was a TypeError, and a null source handed to a collection's constructor throws .NET's ArgumentNullException.
•
Convert.ToBase64String and ToHexString write .NET's encoding, where each wrote 1,2,255.
•
An interpolation hole's alignment pads: {name,Widths.Name} named a member the twin does not have.
•
On Android, back closes an open dialog, sheet, drawer or menu before it leaves the app.
To 0.2.0-preview.61
What got simpler. A component hears the server: it subscribes to a typed topic, any code on the server publishes to the same ServerTopic<T>, and the payload arrives as the C# type it was, over one connection per page (Server events). A page requires its authorization with the [Authorize] a Server Action takes, and a Server Action refuses a request another site's page sends. A layout follows the box it lands in: a Drawing fills its column with SizeValue.Fill, a Positioned child is placed by fractions of its stack and shifted by fractions of itself, and GridTrack.AutoFill(min) repeats a track as often as fits. Type follows the window (TypeStyle.WithFluidSize), a control shows its press and its focus on every target (BoxStyle.Pressed), and a target under a pointer keeps a 24dp floor. The code editor completes as a word is typed. An edit under dotnet watch reaches the browser, and the page keeps its state. A number and a date print in the page's culture, from the format data .NET itself holds. And more of the C# a page runs in the browser answers what .NET answers: a class, a record and a struct are built and copied as C# builds and copies them, a nested class and an exception class of the app's cross, and a list, a set, an indexer and a null argument answer as .NET's.
Drawing.Width is a SizeValue
1
2
float w = drawing.Width; // no longer compiles
float w = drawing.Width.Value; // the dp width; check Width.Kind for a fill
A drawing's width can be a fill now. drawing.Height is the height you gave, 0 when the artwork's aspect decides it, and drawing.HeightAt(width) answers the height at a width. Construction is unchanged: new Drawing(art, 240) converts.
A filling child placed on one side fills to the other, on Photon too
A Positioned child that fills an axis and is anchored on one side of a stack whose size is known now fills from that edge to the opposite one on Photon, as the web always drew it (left: 100px; right: 0): in a 400dp stack, Start = 100 with a SizeValue.Fill box gives a box 300 wide at x 100, where Photon used to lay it out 400 wide and let it overhang. Nothing to change unless a native layout relied on the overhang.
ServerActionHub is gone
1
2
public sealed class Notifier(IHubContext<ServerActionHub> hub) // no longer compiles
public sealed class Notifier(IServerEventPublisher events)
The hub, its route /_equantic/hub and the AddSignalR() call AddUI made are gone. What the hub was reached for is a capability now: declare a ServerTopic<T>, say who may hear it in UseServerEvents, and publish with await events.PublishAsync(topic, payload) (Server events). An app that maps SignalR hubs of its own calls builder.Services.AddSignalR() itself.
A Server Action refuses a request from another origin
Not a compile error: a 403 Forbidden where the action used to run. A page served from another domain than its actions lists that origin, in Program.cs or under EQuantic:ServerActions:AllowedOrigins in appsettings.json:
1
builder.Services.AddUI(options => options.AllowServerActionOrigins("https://admin.example.com"));
The app's own host is the request's Host, so an app behind a proxy that rewrites it calls app.UseForwardedHeaders() with the proxies it trusts, or lists its public origin. A proxy that only ends TLS needs no setting, since the scheme is not compared. A request with neither an Origin nor a Sec-Fetch-Site header is not a browser's, and runs as before (Security).
A class-level [Authorize] guards the page too
1
2
3
[Page("/orders")]
[Authorize] // guarded the page's Server Actions, and guards the page now too
public sealed class OrdersPage : StatefulComponent { … }
An anonymous visitor is challenged by the app's default scheme where the page used to render, and a signed-in one without the policy gets 403. A page that stays public while its actions are guarded carries the attribute on each action instead (Security).
EQ1009: two constructors that take the same number of arguments
1
2
3
4
5
public sealed class Coded
{
public Coded(int code) { … }
public Coded(string text) { … } // EQ1009
}
A JavaScript class has one constructor, so a twin takes a branch per C# constructor on how many arguments arrived, and two that take one count cannot be told apart. Until now the twin kept one constructor, and a new that meant another ran it. Give one a count of arguments of its own, or say it with a static factory: static Coded FromText(string text) => new(text.Length). It holds for every class, record and struct that reaches the browser. A record's copy constructor is not counted, nor is the (SerializationInfo, StreamingContext) one Visual Studio's exception template writes.
More that stops the build
Forms that compiled and then answered wrongly in the browser, or failed there, stop the build now:
•
EQ2014: an exception built or tested where a type argument it carries is a type parameter (new Failed<T>() in a generic method, catch (Failed<T>), an exception class declared inside a generic class). Build and test it where its type arguments are concrete.
•
EQ2010: an extension method of a [ServerOnly] class called on a value that crosses (curve.Ease(t) is CurveEvaluator.Ease(curve, t)), and a type declared inside a [ServerOnly] class or an attribute, however the page names it.
•
EQ2007: a comparer that changes equality handed to ToDictionary, ToLookup, GroupBy or Distinct. Drop it, or normalize the keys on the way in. One that asks for the default passes.
•
EQ2004: a System.Range kept in a variable or handed to an indexer that takes the Range itself, a method group over a .NET extension (list.Any), and a .NET collection's own GetEnumerator() returned from yours (yield the items instead).
•
EQ1007: a second indexer in a type, a member on Item or SetItem beside one, and a type that implements IEnumerable<T> over two types.
•
An exception class of the app's is transpiled like any class, so a typed catch finds it and its members answer in the browser. One whose members reach what the browser cannot run fails the build with that member's own diagnostic: mark the class [ServerOnly].
And forms that failed the build pass it now: a dictionary built with StringComparer.Ordinal (the regression of .60), a call, an element or a property in front of ?., a .NET member reached bare through using static where its qualified spelling translates, and a number written as text with no culture named (EQ2110 is about reading one now, and EQ2109 is retired).
A completion provider takes a context and answers a list
1
2
3
4
5
Task<IReadOnlyList<CodeCompletionItem>> CompleteAsync(
CodeDocument document, CodePosition position, CancellationToken cancellation) // no longer compiles
Task<CodeCompletionList> CompleteAsync(
CodeDocument document, CodePosition position, CodeCompletionContext context, CancellationToken cancellation)
Wrap the items: new CodeCompletionList(items). The context says what asked (Trigger, and the Character that opened the list) and in which Language, and a list marked IsIncomplete is asked again whenever the word changes. A language derived from CurlyBraceLanguage renames its Keywords override to ReservedWords: Keywords is public now, the reserved words, the built-in types and the constants in one list, which a completion offers (Code editor).
The image optimizer takes the formats it encodes
1
2
options.UseImageOptimization(images => images.Formats = ["image/avif", "image/webp"]); // stops the app at startup
options.UseImageOptimization(images => images.Formats = ["image/webp"]);
Formats takes image/webp, image/png and image/jpeg, the formats an encoder here writes. AVIF never was one: listed first, it was negotiated with every browser that takes it and answered with JPEG. The optimizer runs on SkiaSharp now, its natives in the package: a .tiff source is refused, an animated GIF or WebP is served as it is, and a source past MaxSourceSize is refused whoever hands it over. new ImageOptimizer() and new BlurPlaceholderGenerator() take the ImageOptimizationOptions: resolve both from the services, where UseImageOptimization registers them (Image).
A Photon hit region says where its control is drawn
1
2
var (bounds, node, path) = hit; // no longer compiles
var (bounds, drawn, node, path) = hit;
For a test that reads what PhotonRealizer.Realize hands back. HitRegion gains Drawn, the control's own box without the slop that grows its target, after Bounds, in its constructor too. TextRegion, SheetRegion and CodeRegion gain members at the end (Visible, and Offered on a code region), so a positional deconstruction of one names them, and construction compiles as before.
A flex weight is refused where it is written
1
2
Spacer(flex: 0) // throws ArgumentOutOfRangeException: a spacer's weight is 1 or more
Gap(12) // the rigid gap
Not a compile error. A Flexible of weight zero takes no share of the leftover and keeps its basis, or its content's size, where the constructor raised a zero to 1 and the item grew. A negative weight, basis or shrink throws where it is written, on the server and in the browser.
What draws differently
Not a compile error, and nothing to change unless a layout leaned on the old drawing:
•
On the web a Text is a block wherever it sits, with a line box of its own, and takes the width its parent gives. As an inline span in a padded pill, a 10/15 label was 26.5px tall where its parts add up to 21.
•
On Photon a Text takes the width its parent decides (a Box with a width, a Row or a Column that stretches it), so a centred line is centred across that width, as on the web. A label that should hug sits in a Row, or names its width.
•
BoxStyle.Focus is the focus of the control the box is inside, on both targets. On the web it matched nothing, since a box is never focusable, and Photon never drew it.
•
A pinned header's ScrolledStyle applies every member on every target, an Elevation included.
•
On Photon a press inside a control's own box is that control's, before any neighbour's slop, and a transformed box takes the pointer where it is drawn.
•
A Checkbox or a Radio without a label takes a pointer on 24dp, where its target was its own 20dp box.
The browser answers as .NET does
Not a compile error. Code that leaned on JavaScript's answer gets .NET's:
•
A number and a date print in the culture the page was rendered in, from .NET's own NumberFormatInfo and DateTimeFormatInfo, where Intl laid them out by its own tables. A page with no culture installed formats in the invariant culture, never in the browser's locale.
•
A DateTime carries its Kind: Now is local and the local time is the browser's time zone, DateTimeOffset.Now carries the browser's offset, a UTC value crosses the wire with its Z, and a component out of range throws as .NET's constructor does.
•
A null-conditional read is null where its value is used, never JavaScript's undefined.
•
A null handed to a LINQ operator's sequence, to new Guid(text) or to a token's Register is caught by catch (ArgumentNullException e), and e.ParamName is .NET's. CompareTo(null) answers 1.
•
A list and an array sort by .NET's sort, unstable as .NET's is, BinarySearch answers the complement of the insertion point for a miss, IndexOf, Contains and Remove compare by the element type's equality (a NaN finds a NaN, a record finds its equal), and List<int> copy = new(source) is a copy, where it was an empty list.
•
A HashSet<T> is the runtime's class, by the element type's equality, and an element added while a foreach walks it throws InvalidOperationException.
•
A char's IndexOf and LastIndexOf given a start and a count search that range, and one outside the string throws ArgumentOutOfRangeException.
•
strip[1..3] over a type of the app's calls Slice(1, 2), as C# does, where it called the twin's slice(1, 3).
•
A framework exception's Message is the text .NET writes, a constructor without a message included.
•
A record, a struct and a tuple compare each member by its type's default comparer, as .NET does: an array or a List<int> held in one compares by reference.
•
A generic record equals only a value of its own closed type: new Box<int>(1) no longer equals new Box<double>(1).
•
with copies a record through its own copy constructor when it declares one, and a mutable struct or a value tuple is copied where C# copies it, so a write to one copy no longer shows in the other.
•
A class's twin runs the constructor the new names, with its initializers in declaration order and its : base(…) arguments, where it kept its widest constructor and called its base's with none.
•
A method declared with new and called through the base type reaches the base's method.
•
A dictionary reaches the browser enumerating in the order the server held it (3, 1 where it arrived as 1, 3). On the SDK's wire it is its [key, value] pairs, so a client outside the SDK that calls a Server Action by hand reads and sends a dictionary as [[key, value], …].
•
A Queue, a Stack, a LinkedList and a SortedSet cross as arrays in the order each enumerates, so one sent to a Server Action arrives as the collection.
•
A page answers HEAD as it answers GET, where a HEAD fell through to the fallback and answered 404.
•
Hand-written JavaScript that reads a transpiled value meets its C# shape: a class declared inside another is a module of its own named Owner$Inner, and a HashSet is the runtime's set.
To 0.2.0-preview.60
What got simpler. A page's server data reaches the browser by itself: a field, an auto-property and a captured primary-constructor parameter all hydrate, and a service the page only reads crosses as what it reads, never whole. A hover can move a box and change its shadow, on the web and on Photon. A route's title reaches the browser's tab, and two pages of one name each serve their own route. On Photon a screen reader hears a radio, a tab, a menu item, an option and a navigation destination as what they are, where each was a button. And more of the C# a page runs in the browser answers what .NET answers: a dictionary's order and its Add, LINQ over any sequence, a string's chars, an enum's text and casts, a catch by its type and its filter, GetHashCode, a Guid's text, a date printed through its specifier, a string search by its comparison, and a constant in its own type. A breakpoint inside a lambda, an initializer or a body with an out parameter binds on its own line.
IServerRenderingService takes the page's Type
1
2
await rendering.RenderPageAsync("HomePage", context); // no longer compiles
await rendering.RenderPageAsync(typeof(HomePage), context);
PreparePageAsync and IsSsrEnabled change the same way. The server holds a page by its type, so two pages of one name in two namespaces each answer at their own route.
StyleDiff is a class
1
2
var background = style.Hover!.Value.Background; // no longer compiles
var background = style.Hover!.Background;
BoxStyle.Hover, BoxStyle.Focus and Pinned.ScrolledStyle were nullable structs and are nullable references now, so .HasValue becomes is not null. Building one with new StyleDiff { … } compiles as before.
EQ2114: a service the page's browser code uses in a way the build cannot follow
A page whose browser-side code uses a service the container hands it, in a way the build cannot follow, fails the build with EQ2114, which names the page, the value and the expression: a method called on it, the value converted to text, compared, enumerated or stored in a collection, a member a .NET type declares (a list's Count), or a read while the page is constructed. Until now the whole service went into the page's HTML. Decide on the server, in PrefetchAsync or a [ServerOnly] member, and keep the result in a field, or call a [ServerAction] when the browser's state is an input.
The null check in a constructor is the common case:
1
2
_options = options ?? throw new ArgumentNullException(nameof(options)); // EQ2114
_options = options;
The browser constructs the page without the value, so the check threw there, and the container already refuses to build a page without a service it requires.
A search by a culture comparison fails the build
1
2
name.Contains(term, StringComparison.CurrentCultureIgnoreCase) // EQ1004
name.Contains(term, StringComparison.OrdinalIgnoreCase)
The same holds for StartsWith, EndsWith, IndexOf, LastIndexOf and Replace given a culture comparison or a CultureInfo. The browser has no culture-aware search, and the answer it gave was the ordinal one. A comparison held in a variable passes the build, and a culture one throws NotSupportedException when the call runs. Equals and string.Compare by a culture compare whole strings and keep working.
Two more forms that compiled and broke in the browser now stop the build: a UTF-8 literal ("ab"u8, EQ1004), and a .NET member reached bare through using static that has no translation (EQ2004). Write the member qualified where that form translates, or keep it on the server.
A dictionary built with StringComparer.Ordinal fails the build
1
2
Dictionary<string, Job> jobs = new(StringComparer.Ordinal) { ["a"] = a }; // EQ1004 on .60
Dictionary<string, Job> jobs = new() { ["a"] = a };
A regression of this release (#577), fixed in the next. A string key compares ordinally by default, in .NET and in the browser, so dropping the comparer changes nothing. A comparer that changes equality, such as StringComparer.OrdinalIgnoreCase, was refused before and still is, with EQ2007.
The browser answers as .NET does
Not a compile error. Code that leaned on JavaScript's answer gets .NET's:
•
Dictionary.Add of a key already there throws ArgumentException, as it does on the server. d[key] = value keeps replacing.
•
A dictionary enumerates in .NET's order, the order of insertion while nothing is removed (d[3] = 30; d[1] = 10; lists 3, 1 where it listed 1, 3), and adding a key while a foreach walks it throws InvalidOperationException.
•
An exception's name is its .NET type, so the console and a contained component print InvalidOperationException: … where they printed Error: ….
•
A string's foreach and ToCharArray give UTF-16 code units, so an emoji is two chars.
•
o is int over an object holding 2.5 is false.
•
CompareTo(string), and a sorted collection of strings built in the browser, order by the current culture (a, A, b, B), where they ordered by code unit.
•
IndexOf and LastIndexOf given a StringComparison check a start and a count as .NET does, and one past the end throws ArgumentOutOfRangeException, where the browser clamped it and answered.
•
A Guid parsed from text is lowercase, as .NET writes it.
•
An enum value no member names, sent to a Server Action, is refused with a JSON error, where it was read as the enum's first member.
•
A route's title, [Page(Title = …)] or MapPage<T>(route, title), titles the document, where the app's SetTitle always won.
•
A set or a dictionary the browser would answer differently stays out of the page: one with a comparer of its own, a sorted one in ordinal order, or one holding a class whose equality nothing can read. Decide on the server and keep the result.
•
Hand-written JavaScript that reads a transpiled value meets its C# type: a dictionary is the runtime's class (get, keys(), for...of), a long constant is a BigInt, and the hydration payload's keys are the names the twin declares (downloads, not Downloads).
To 0.2.0-preview.59
What got simpler. The code editor compares two texts: CodeDiffer finds what changed, by lines and by words, and the diff view shows it side by side or inline, over two texts or one file of a patch. A shortcut can answer only while the focus is inside its component, so two of one component on a screen no longer answer the same keys. And more of the C# a page runs in the browser answers what .NET answers: a number printed through its format specifier, a date's fractional Add*, a bool read from text, List.Remove, a record member's starting value, a class's default interface members, and the scope of a variable a pattern or an out declares.
A positional deconstruction of CodeGrid or ShortcutBinding takes three variables
1
2
3
var (origin, cell) = grid; // no longer compiles
var (origin, cell, _) = grid; // CodeGrid gained Rows
var (chord, onPressed, _) = binding; // ShortcutBinding gained Scope
Each gained a trailing optional member, so building one with two arguments compiles as before.
The code editor's find answers inside the editor
Not a compile error. Cmd/Ctrl+F opens a CodeEditor's find only while the keyboard is in that editor. Anywhere else on the page the browser's own find opens, where the last editor mounted used to take the keys wherever the focus was.
The dark palette is lighter
Not a compile error. PhotonTheme, the theme the templates start from, re-solved its dark colours so that text and controls clear APCA contrast: the primary blue goes from #5CA2E8 to #76BAFF, muted text from #8B95A3 to #ACB7C5, and a strong border from #3D4754 to #4C5664, with the danger, success and info colours lifted the same way. An app that pins dark-mode screenshots will see them move. An app on a theme of its own, or on MaterialTheme, is not affected.
To 0.2.0-preview.58
What got simpler. The C# a page runs in the browser answers what .NET answers: a number's text in both directions, a division by zero, a nullable's operators, a float's precision, a value's default, a dictionary's miss. The code editor gets an engine of its own and takes its input the way each platform gives it, and a published app no longer ships its C# inside its source maps.
The code editor's types moved to eQuantic.UI.Code
1
2
using eQuantic.UI.Primitives; // CodeDocument, CodeEditorController, CSharpLanguage… are no longer found here
using eQuantic.UI.Code; // add this
Thirty-five types: the document, the controller, positions and ranges, the highlighter, the history and the keymap, the languages and the provider interfaces. The assembly arrives with eQuantic.UI.Components, so the csproj does not change. CodeSurface takes an ICodeSurfaceModel where it took a CodeEditorController, which only a host that builds a surface itself meets.
ListDetail takes builders
1
2
ListDetail(list: Inbox(), detail: Message()) // no longer compiles
ListDetail(list: () => Inbox(), detail: () => Message())
Each shape builds its own copy of the panes, so none is mounted twice.
StyleClass and the rest of eQuantic.UI.Web.Styling are gone
StyleClass, Spacing, Breakpoint and [CompileTimeEvaluate], with StyleClass and StyleClasses on HtmlElement and IComponent. A StyleClass added a class name no stylesheet defined, so nothing it did is lost: style an escape-hatch element with HtmlStyle or ClassBuilder.
Thirteen icons took their upstream names
was
is
LucideIcons.Trash2
LucideIcons.Trash
LucideIcons.Building2
LucideIcons.BuildingComplex
LucideIcons.History
LucideIcons.RotateCcwClock
TablerIcons.BrandKakoTalk
TablerIcons.BrandKakaoTalk
TablerIcons.CurrencyRubel
TablerIcons.CurrencyRuble
TablerIcons.Foodsteps
TablerIcons.Footsteps
TablerIcons.GenderTrasvesti
TablerIcons.GenderTravesti
TablerIcons.Ikosaedr
TablerIcons.Icosahedron
TablerIcons.MoodConfuzed, MoodConfuzedFilled
TablerIcons.MoodConfused, MoodConfusedFilled
TablerIcons.SportBillard
TablerIcons.SportBilliard
TablerIcons.BrandAdobeAfterEffect
TablerIcons.BrandAdobeAfterEffects
TablerIcons.Physotherapist
TablerIcons.Physiotherapist
Each is the same drawing. LucideIcons.Trash itself now draws what Trash2 drew, the can with two lines, and no compile error says so.
A published app writes no source maps
Not a compile error. A Release build writes none and a publish never takes one. A Debug build writes them with the C#, for the browser's debugger. An error reporter that uploads maps itself sets <EQuanticSourceMaps>external</EQuanticSourceMaps>, which writes them without the C#. Until now every published app shipped its components' C# in its maps, [ServerAction] bodies included.
equantic.css is no longer written
An app that linked /_equantic/equantic.css through ConfigureHtmlShell removes the link: the generated sheet is inline already.
The browser answers as .NET does, exceptions included
Not a compile error, and the one most likely to surface in a running page: code that leaned on JavaScript's answer gets .NET's.
•
int.Parse("12abc") throws FormatException where it answered 12, and a failed TryParse leaves 0, not NaN.
•
An integer division or remainder by zero throws DivideByZeroException where it rendered Infinity or NaN.
•
x += 1 on a null int? stays null where it became 1.
•
A missing key on a SortedDictionary, a SortedList or a value-keyed Dictionary throws KeyNotFoundException, and a TryGetValue miss writes default.
•
true.ToString() is True.
•
int x = default is 0 and new long[3] holds longs, where they were undefined and plain numbers, and a filter written o.Status != default(OrderStatus) compares with the enum's zero, where every item passed it.
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
1
2
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
1
2
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
1
2
3
4
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
1
2
3
4
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
1
2
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
1
2
3
4
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
1
2
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
1
2
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
1
2
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
1
2
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:
1
2
3
4
5
6
7
8
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
1
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.
1
2
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
1
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:
1
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
1
2
3
4
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.
1
2
3
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
1
2
3
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
1
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:
1
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:
1
2
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 IS0.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
1
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:
1
2
3
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:
1
2
3
4
5
6
7
8
9
// 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:
1
No overload for method 'ThemeToggle' takes 1 arguments
1
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:
1
2
[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.
ON THIS PAGE
To 0.2.0-preview.62
EQ2010: a component calls its own [ServerOnly] member
A culture a call names is the page's or the invariant one
A date is read in the page's culture
A record of a referenced project crosses whole
More that stops the build
For whoever hosts the compiler or writes against a shell
The browser answers as .NET does
To 0.2.0-preview.61
Drawing.Width is a SizeValue
A filling child placed on one side fills to the other, on Photon too
ServerActionHub is gone
A Server Action refuses a request from another origin
A class-level [Authorize] guards the page too
EQ1009: two constructors that take the same number of arguments
More that stops the build
A completion provider takes a context and answers a list
The image optimizer takes the formats it encodes
A Photon hit region says where its control is drawn
A flex weight is refused where it is written
What draws differently
The browser answers as .NET does
To 0.2.0-preview.60
IServerRenderingService takes the page's Type
StyleDiff is a class
EQ2114: a service the page's browser code uses in a way the build cannot follow
A search by a culture comparison fails the build
A dictionary built with StringComparer.Ordinal fails the build
The browser answers as .NET does
To 0.2.0-preview.59
A positional deconstruction of CodeGrid or ShortcutBinding takes three variables
The code editor's find answers inside the editor
The dark palette is lighter
To 0.2.0-preview.58
The code editor's types moved to eQuantic.UI.Code
ListDetail takes builders
StyleClass and the rest of eQuantic.UI.Web.Styling are gone
Thirteen icons took their upstream names
A published app writes no source maps
equantic.css is no longer written
The browser answers as .NET does, exceptions included
To 0.2.0-preview.57
The native declarations moved to eQuantic.UI.Native.Hosting
RangeValue carries numbers only
SemanticNode deconstructs into thirteen parts
ImageOptimizationState is gone
Four overloads became one each, and most calls still compile
EQ2012: a [ServerAction] on an abstract component
A composed IServerPrefetch now runs
To 0.2.0-preview.56
AdjustableValue is RangeValue
WebFrame takes its content as one value
Navigable takes its handler first
The chart factories mirror their records
TypeStyle and SemanticNode deconstruct into their real shape
LinkRegion and RealizeResult carry more
To 0.2.0-preview.55
ButtonStyles is gone
FlexNode is host-only
EQ2111 where a service lookup has nothing to key on
An Adjustable with no value announces group
To 0.2.0-preview.54
Geometry moved to Primitives
Point's operators are host-only, and so are RRect and Matrix2D
ICanvasPainter takes a box and a point
The runtime's route surface
RenderContext's service surface is one method
Navigator.Go's parameter is destination
BarRect carries a Rect
To 0.2.0-preview.53
A role's face is a default, not a choice
A cold load with a fragment lands where a warm one does
A spreadsheet exists in the server's HTML
A cut line ends one way
LayoutConstraints, if you host the layout engine
To 0.2.0-preview.52
A cold load with a fragment lands where a warm one does
To 0.2.0-preview.51
LayoutNode.Children is read-only — the only source break
EQ2010 — a host-only symbol named from a component
A Mac in dark mode opens the app in dark mode
Photon honours Text.Align
A regional culture falls back through its parent
Variant.Link carries its ink in Base and Pressed
Web styling: the role styles the class, the node styles the element
To 0.2.0-preview.50
IWorkspace.OpenUrl hands over only the schemes your app opens
VisualNode.Key now does what it always said it did
Focus scrolling jumps instead of gliding
Two layout fixes that can move something
eQuantic.UI.Charts is a different package
To 0.2.0-preview.49
To 0.2.0-preview.48
And three renames, because the vocabulary stopped speaking the web's language
To 0.2.0-preview.47
To 0.2.0-preview.46
An icon is a glyph; a child is a node
The generated factory resolves capabilities itself
Server-only types need to say so
The Roslyn pin moved to 5.9.0
Before that