astryx_ui 0.0.8-dev
astryx_ui: ^0.0.8-dev copied to clipboard
A Flutter design system, token-compatible with Astryx. Faithful theme engine, themeable components, built on flutter/widgets.
Changelog #
All notable changes to astryx_ui are documented here.
The format follows Keep a Changelog and this project adheres to Semantic Versioning.
Unreleased #
Fixed #
-
The browser's own context menu no longer opens on top of
AstryxContextMenu. On the web a secondary click is the browser's before it is the application's: Flutter reported it and the menu opened, and then the browser raised its Back / Reload / Inspect menu over the top — two menus for one click, with the wanted one underneath. Suppressing it was documented as the app's job, which made a broken right-click the default for anyone who had not read that paragraph.The widget now handles it. While one is mounted and able to open —
enabled, with entries — the browser's menu is suppressed, and the last one to leave the tree puts it back. The claims are counted, so several context menus on a page cost one channel call between them, and re-derived on rebuild, becauseenabledandentriesboth move over a widget's life.BrowserContextMenuis whole-document rather than per element, so "while the page has a context menu of its own" is the narrowest scope Flutter offers;AstryxContextMenu.suppressBrowserMenu: falseopts a widget out of it.It never enables a menu it did not disable: an app that called
BrowserContextMenu.disableContextMenu()at startup keeps its own setting when the last widget unmounts. Off the web the whole thing is inert.
0.0.8-dev #
Five components upstream ships that this port did not: the whole BottomSheet
family, the whole Stepper family, the three indicators, and the two
selectable menu rows. Ten of the twenty-eight gaps in COMPONENTS.md are
closed; twelve of the eighteen that remain are table capability hooks.
Added #
-
AstryxBottomSheet— a modal anchored to the bottom edge. The same contractAstryxDialoghas — trapped focus, Escape, a scrim that dims and dismisses, focus handed back to whatever opened it — put where a thumb can reach it. Three height budgets:hugsizes to its content,cappedtakes 62% of the viewport whatever is in it,talltakes 92%.snapPointsgives it more than one resting height as fractions of the viewport, and dragging the handle moves between them; dragging below the shortest dismisses unlessdragDismissibleis false. Values outside(0, 1]are dropped rather than thrown for, because a sheet that threw for a mistyped snap point would take the screen down with it. It travels up from the edge it is anchored to rather than fading in as the other viewport overlays do — fading leaves no trace of where it came from, which is the one thing the shape is meant to say — and it stops widening at 640px, past which it is a dialog stuck to the bottom of a desktop window.labelis required, unlike a dialog's: a sheet has no header to derive a name from. -
AstryxBottomSheetSwitcher— a flow of sheets sharing one scrim. Choose a payment method, confirm it, see the receipt: three steps that would otherwise dim the page, undim it and dim it again, reading as three interruptions where the user performed one task.activeSheetIdis the whole state — a non-null id is the step on screen, null closes the flow — because a controller and an id would be two sources of truth for the same thing. A step is anAstryxBottomSheetPage, a description rather than a widget: only the active one is built. Upstream's retained-sheet choreography, where the outgoing sheet travels to meet the height of the incoming one before fading, is not ported; the two cross-fade while the layer resizes. -
AstryxStepperandAstryxStep— progress through a sequence. Both orientations, and both of upstream's indicator positions:separatedputs the indicator in the label row with the connector as a bar of its own,onTrackslots it into the line as a node with the label beside or below it. A step's index is its position instepsrather than something it declares, so the duplicate-index mistake upstream warns about at runtime cannot be written.statuscolours the indicator and, inauto, gives it a glyph, but never recolours the connector — that reports progress and nothing else — and the step the flow is on keeps its current-step indicator whatever its status says.onStepPressedmakes every enabled step pressable, ahead as well as behind. The connector paints at its final length on the first frame, so a stepper that opens mid-flow does not play its own history back at the reader. -
AstryxCheckboxIndicator,AstryxRadioIndicatorandAstryxCheckIndicator— the stateful control visuals as widgets, for the row this package has no widget for: a custom listbox, a menu of layer visibilities, a table cell, anything that has to draw the same box as a real checkbox rather than an approximation of one. All three are decorative — hidden from assistive technology, owning no role, focus or gesture — because an indicator announced beside the control that owns the name is the same thing said twice.AstryxCheckIndicatordraws no chrome at all: it is the glyph, in a reserved slot so a row does not shift when the mark appears.AstryxSelectionIndicator, the shared chrome they are built from, stays internal — it takes a resolved theme rather than reading one. -
Selectable menu rows —
AstryxMenuItem.checkboxandAstryxMenuItem.radio. A checkbox row is announced with a checked state rather than as a button and leaves the menu open, so several settings can be toggled in one visit; a radio row adds that it is one of a mutually exclusive group and closes the menu, because the choice has been made.closeOnSelectoverrides either default. Both are controlled, like every other selection control here. Once any row in a menu reports state every row pays the same gutter, so the labels stay in one column rather than stepping in and out as settings are toggled. A menu that shows a current value is still the wrong shape — that isAstryxSelector, which announces itself as a listbox — and these are for the settings a menu really owns. -
AstryxOverlay.transitionBuilder— an entry and exit of the caller's own, given the layer's animation, for a motion the three presets do not cover. WhatAstryxBottomSheettravels up on. Null keepstransition, so every existing overlay is unchanged. -
AstryxLocalizations:stepperLabel,stepPosition,stepCompleted,stepCurrent,stepOptional,bottomSheetHandleandbottomSheetClose— the strings the two new components announce. A stepper's numbers are decoration, and a screen reader is not shown pictures. -
AstryxTab.onClose— closable tabs. A non-null callback puts a close button after the label, for editor tabs and anything else the user opens and puts away. The strip owns no list, so removing the tab and choosing what is selected afterwards stays with the caller. The button is always drawn rather than revealed on hover — hover only raises its contrast — because touch has no hover and an action that exists only under a cursor does not exist on a phone.DeleteandBackspaceclose the selected tab from the keyboard, which the strip's single tab stop would otherwise leave unreachable.AstryxTab.closeLabeloverrides the button's accessible name, andAstryxLocalizations.tabClose(label)supplies the default, "Close card.dart" — a strip of open files is a row of identical "Close" buttons otherwise. -
AstryxLocalizations.layoutPanelLabel— the disclosure title a collapsedAstryxLayoutpanel falls back to. Generic, because the slot is; name the panel withpanelLabeland it is never read. -
The landing page now carries the README.
tool/gen_readme.dartparses../README.mdintolib/docs/readme.g.dartasDocBlocks, the same waygen_changelog.darthas always parsed the changelog, and the front page renders the sections a first-time reader needs — Install, What you get, Why not Material, What is not in 1.0 and For AI coding agents — rather than paraphrasing them into a third copy that goes stale. Relative links in the markdown (CHANGELOG.md,doc/README.md) are rewritten to absolute repository URLs at generation time, because the site is not served from the repository root. -
lib/docs_ui/doc_blocks.dart— the block renderer, lifted out ofDocsPageViewso the landing page and the documentation pages draw aDocBlockthe same way.DocsPageViewnow calls it rather than owning a private copy of the switch. -
The install command is on the front page, above the fold and with a copy button, next to what it gets you. The first thing a package page should answer is how to add it.
-
What is new, on the front page. The newest release's summary and its first few entries, read out of
changelogBlocks— so the front door says what shipped without anyone remembering to update it.
Changed #
- pub.dev and GitHub are now the largest things on the landing page after the hero. They were two supporting-sized links in a wrapping row under the buttons, at the size the page uses for footnotes — the two destinations somebody arriving from a search result is most likely to want, drawn as the least visible thing on the screen. They are full pressable cards now, with an icon, a heading, a line saying what is on the other side and an external-link glyph, sitting directly under the hero and repeated as large buttons in the closing call to action.
- The landing page states what the package covers in numbers it counts
itself — 111 components, 42 whole screens, 19 controllers — read from
docPagesrather than typed, so the claim cannot outlive the registry. README.mdleads with the install. It also drops two claims that stopped being true several releases ago: roughly 30 components are in scope for 1.0 (there are 111 written pages) and the what this is not framing that opened the file. Added a Why not Material section, because that is the question the package exists to answer.- The site's own link preview says what the package is rather than that it
is documentation. Somebody following a shared link is usually deciding
whether to add the dependency, not looking up a parameter — so
example/web/index.htmlcarries that description and the titleastryx_ui — a Flutter design system, andgen_og.dartcopies both into the home page's Open Graph card.
Fixed #
-
The IDE template's editor tabs could not be closed from the tab. Closing the file you were reading meant finding it in the editor's overflow menu. The tabs carry their own close button now, and closing the selected one hands the editor its neighbour rather than the far end of the strip.
-
AstryxLayoutno longer keeps its panel beside the body at any width. A 320px rail beside a 390px phone left 70px for the page, and everything in it was clipped rather than narrow. BelowpanelCollapseBelowthe panel becomes a disclosure banded across the top of the page — the same widget, the same content, collapsed and one press away. Name it withpanelLabel; open it by default withpanelInitiallyExpanded. The threshold is a number rather than an entry in a breakpoint table, for the reasonAstryxAppShell.compactBelowis one: the width at which your panel stops fitting is a fact about your panel. Set it todouble.negativeInfinityfor a panel that must never collapse. -
AstryxToolbarwraps onto a second run rather than running off the edge. It behaves as a row in every window wide enough for one. Narrower than that, a formatting bar whose last two buttons are past the right-hand side is a bar those buttons have left. Arrow-key traversal is unchanged — it walks the focus nodes, not the geometry. -
AstryxSegmentedControlgives ground instead of overflowing. Its segments areFlexibleand their labels ellipsise, so the control still hugs its labels wherever there is room and shrinks where there is not. A four-segment filter is wider than a phone, and a track that runs off the edge hides the segment on the end — a filter nobody can reach rather than one that looks cramped. -
Every documented template now lays out at 360, 390, 600 and 768 logical pixels. Twelve of them overflowed on a phone: the split screens and the workspaces lost their bodies to a fixed rail, the IDE and the settings area hand-rolled a
Rowthat could not collapse, and a dozen bands of badges, pagers and filters ran past the right-hand edge. The IDE's file tree and the settings rail now collapse — the settings rail into anAstryxAppShelldrawer, with the focus trap and the Escape key that a hand-rolledRownever got around to.example/test/template_responsive_test.dartkeeps it that way. -
The landing hero quoted a version by hand, and quoted the wrong one:
Pre-alpha · 0.0.6-devwhilepubspec.yamlsaid0.0.7-dev. It readsastryxVersionnow, which is generated from the pubspec. -
A feature card claimed eight built-in themes. Seven ship with the package; the eighth in the site's picker is
acmeTheme, defined inlib/examples/theming_examples.dartto prove the engine works. -
The 0.0.7-dev release had no changelog heading. Everything it contained was still filed under Unreleased after the tag, so this file and the published version disagreed about what shipped.
-
Nothing on the landing page was measured. Every column on it asked for a maximum width and none of them got one, so the hero paragraph ran the full width of the window.
AstryxCenterputs itsConstrainedBoxoutside itsAlign, and aConstrainedBoxhanded tight constraints — which is what a scroll view's cross axis hands down — has its maximum enforced away to the parent's width. The page now measures itself with anAlignabove aConstrainedBox, which is the order that works. The widget is unchanged: the same fault is in everyAstryxCenter(maxWidth:)inside a scroll view, and fixing it is a library change with its own goldens to regenerate.
0.0.7-dev #
Eighty-two new components, twenty-eight new templates, and the documentation for
them. Every template upstream ships now has a written page —
planned/templates.dart is gone rather than empty.
Navigation, Date & time, Providers and Hooks & controllers are complete:
every component and hook upstream ships in those groups now has a written page,
and Chat & AI, Command & search and Media are complete too. Two package
widgets changed and the documentation site got a layout fix of its own — all
three in Fixed.
Added #
-
Five template screens, each assembled only from what the package exports: Shell navigation (
AstryxAppShellwithAstryxTopNavaboveAstryxSideNav), Documentation (a rail, a measured column, and anAstryxOutlinethat tracks the reader), AI chat (transcript, tool calls, citations, composer, and the empty state before the first turn), Table page (filters pinned above,AstryxPaginationpinned below) and Classic gallery (a wall of media tiles over one sharedAstryxLightbox). They graduated out ofplanned/templates.dartnow that the shell, chat, pagination and media components have landed. -
Seventeen more template screens, which empties
planned/templates.dartof everything that was waiting on an author rather than on a component. Every one is extracted from a compiling widget inexample/lib/examples/template_*.dartand built from nothing but what the package exports:- Shell with side nav and Shell with top nav — the two halves of
shell_nav. The rail-only shell has noheader:at all, so the drawer toggle moves into the page's own header viaAstryxMobileNavToggle; the bar-only shell has nosidebar:, so there is no drawer and the bar scrolls its own destinations instead. - Design documentation and Technical documentation — specimens drawn
straight from
theme.color, and an API reference whose property table is anAstryxTable. - Editor, IDE and File explorer — a markdown canvas with an
AstryxToolbarthat edits the document rather than styling a hidden model, twoAstryxResizeHandles with the sizes owned by the caller, and anAstryxTreeListbeside a table. - Gallery hero, Mixed gallery and Side gallery — media as evidence, media at sizes that differ on purpose, and one item at a time with the rest beside it.
- Product gallery and Product detail — filters beside a wall that does not filter itself, and a page where exactly one control spends money.
- Library and Messaging shell — one selection shared between a grid and a table, and a list that selects rather than filters.
- Incident console, Settings with sidebar and AI chat landing — a
wall display whose relative stamps keep ticking from one captured instant, a
settings area reached from an
AstryxSideNav, and the composer in the middle of the page before it moves to the bottom of a transcript.
- Shell with side nav and Shell with top nav — the two halves of
-
A landing page. The site opens on a front door rather than on the first page of the registry: a hero, four counts read out of
docPagesrather than typed, six feature cards each carrying a live specimen, and a footer. It links to pub.dev, the repository and upstream Astryx. Like everything else here it is built fromastryx_uiand nothing else, which makes it the largest single specimen on the site. The brand in the top bar is the way back to it, the sidebar is absent while it is showing, and any URL naming a page —/card— still opens that page directly. -
The last six template screens, each of which needs something the package does not have. None of them fakes it: every page names the gap, fills it from
flutter/widgetsor from the token layer in as few lines as the screen needs, and marks the seam where a real implementation goes.- Portfolio dashboard and Table page with chart — the package ships no
charting widget and does not intend to, so both draw a
TrendChartfrom a forty-lineCustomPainter. It resolves every colour throughAstryxTheme.of(context).color(…)and takes a requiredlabeland a requiredsemanticsValue, because a picture of a trend announces nothing. The headline figure is still text above the curve. - Kanban board — dragging is a gesture rather than a surface, so this uses
the framework's own
DraggableandDragTarget. The drag is the enhancement: every card also carries a move menu naming the other columns, both routes call the same function, and both confirm with a toast. A board only a pointer can rearrange is a board much of a team cannot use. - Grouped table —
AstryxTablehas no row grouping, and a data row drawn to look like a header is announced as data, sorted into the middle of the set and offered to the checkbox column. So it is one table per group inside anAstryxCollapsible, sharing one sort and one selection, with the columns declared once and given fixed widths so they line up down the screen. - Table page with heatmap and Retail heatmap table — the ten
AstryxPalettefamilies are categorical, so none of them is a ramp. AHeatCelllerps between--color-background-mutedand a semantic token, switching to that token's paired foreground past the midpoint, and puts text on it throughDefaultTextStyle.mergeplusAstryxTextColor.inherit. Every cell prints its own figure and every screen carries a legend: latency runs towarderror, sales towardsuccess, and a hue does not imply a direction.
- Portfolio dashboard and Table page with chart — the package ships no
charting widget and does not intend to, so both draw a
-
AstryxLayout.scrollController— the body's scroll controller, for anything beside the body that has to know where it has got to. AnAstryxOutlinein thepaneltracks the reader by watching where the headings are, which needs the scroll position of the view the anchors live in, and that view belongs to the layout. Asserts when paired withscrollable: false, where the body owns its own scroll view instead. -
The Media group —
AstryxAvatarwithAstryxAvatarGroup,AstryxThumbnail,AstryxAspectRatio,AstryxCarousel,AstryxLightboxandAstryxMediaTheme, covering ten upstream components.- Two rules run through all of it. A picture of a thing is not the thing's
name, so
AstryxAvatar.nameandAstryxThumbnail.labelare required and are the accessible names — an avatar without one is an unlabelled image, and a row of them is a row of unlabelled images. And a picture is whatever colour it happens to be, so anything drawn over one goes throughAstryxMediaTheme. AstryxAvatarfolds inAvatarStatusDot, because a dot beside an avatar is two things a reader has to associate. Fallbacks run image → initials → icon, and a failed image falls back to initials rather than a broken glyph. Astatuswithout astatusLabelasserts: a coloured dot alone says nothing to a screen reader.AstryxAvatarGroupkeeps the overflow chip rather than shipping it separately: "+4" is the only thing saying the row is a sample rather than the whole set, so it is pressable, and the group is announced as a group with the names and the remainder.AstryxCarouselis one tab stop, disables its controls at the ends rather than hiding them, and puts the position in the container's semantics value. There is no autoplay and no way to ask for one: content that moves on its own must be pausable (WCAG 2.2.1), and the pause control is invariably the least obvious thing on screen.AstryxLightboxisAstryxOverlayplus paging, a counter and a caption. It opens oninitialIndexevery time — a thumbnail grid opens the thumbnail that was pressed — and a single item drops the paging furniture entirely.AstryxMediaThemeforces theonDarktokens and a scrim for its subtree and changes nothing else: spacing, radius, type and motion stay put, because the foreground colour and the ground behind it are the only two things a picture underneath can break.AstryxAspectRatiodoes not replace Flutter's — it adds the radius, the ground and the clip from the token layer. The ground is the point: an unloaded image leaves a hole, and a hole reads as a bug rather than a wait.
- Two rules run through all of it. A picture of a thing is not the thing's
name, so
-
AstryxLocalizations.avatarGroupMore,avatarGroupCount,carouselPrevious,carouselNext,carouselPosition,carouselLabel,lightboxLabel,lightboxClose,lightboxPrevious,lightboxNextandlightboxPosition. -
The Command & search group —
AstryxBaseTypeahead,AstryxTypeahead,AstryxCommandPaletteandAstryxPowerSearch, covering twelve upstream components. One engine and three surfaces on it.AstryxBaseTypeaheadowns what is the same whatever a search input looks like: the query, the debounce, the request, the keyboard, the overlay and the announcements. The field never loses focus — arrows move an active index while the caret stays put, which is the ARIA combobox pattern and the only arrangement where typing, correcting and choosing are one gesture.- A stale response cannot overwrite a newer one: each call carries a generation, so the slow first request landing after the fast second is dropped rather than showing results for what was typed three keystrokes ago.
Enterwith nothing highlighted is left to the form, because a typeahead must not swallow the key that submits the search. A source that throws leaves an empty list rather than taking the screen down.- The result count is announced through a live region: a dropdown appearing is silent to a screen reader, so there is otherwise no way to know a search answered.
AstryxTypeaheadis that engine with this package's field and rows on it, and it is notAstryxSelector: a selector picks from a set it can show you, a typeahead searches one it cannot.AstryxCommandPalettefolds in upstream's input, list, group, item, empty state and footer, because a palette assembled from six pieces is six chances to get the keyboard wrong. Queries match keywords as well as labels — a command nobody can find by the word they thought of is a command that is not there — the highlight returns to the top on each keystroke, and each row draws its shortcut from theAstryxHotkeythat is actually bound.AstryxPowerSearchputs filters beside the text as chips rather than inside it as syntax:status:failedtyped into a box is a thing to learn, get wrong and be told off about, and the error message for a mistyped query is a feature nobody budgets for.AstryxSearchQueryis the whole state as one comparable value.
-
AstryxLocalizations.typeaheadLabel,typeaheadPlaceholder,typeaheadSearching,typeaheadResults,commandPaletteLabel,commandPalettePlaceholder,commandPaletteNoResults,commandPaletteNavigate,commandPaletteRun,commandPaletteClose,powerSearchLabel,powerSearchPlaceholder,powerSearchAddFilter,powerSearchClearandpowerSearchFilters. -
Ten more Chat & AI components, which finishes the group's first pass —
AstryxTokenChipandAstryxTokenizer,AstryxTokenTextControllerandAstryxChatTokenizedText,AstryxChatSendButtonandAstryxChatDictationButton,AstryxChatSystemMessage,AstryxChatToolCallsandAstryxMarkdown, plusAstryxCitation.AstryxTokenChipcarriesChipon the end becauseAstryxTokenis already the design-token interface every token enum implements: a design system cannot have two things called Token, and the older one is load-bearing in every theme. Its remove button is named after what it removes, since a row of five buttons all called "Remove" is a row a screen-reader user cannot choose from.AstryxTokenizercommits on Enter or a delimiter, and takes the last token back on Backspace. A pasted list commits as one change:valuesis the parent's list and does not update until it rebuilds, so committing twice in a frame built the second value on a stale list and threw the first away.AstryxTokenTextControlleris the honest port ofChatComposerTokenElement. Upstream puts a real element in acontenteditable;EditableTextedits aString, and aWidgetSpanin an editable's span tree breaks the caret, the selection maths and backspace. So a token being typed is styled text — every character still counts as itself — andAstryxChatTokenizedTextdraws real chips once the message is sent and there is no caret to protect.AstryxChatSendButtonis now public rather than private to the composer, because send-becomes-stop is a component upstream ships on its own and two implementations of it would eventually disagree. The composer builds one.AstryxChatDictationButtondoes no speech recognition — there is no platform channel here and no dependency that would bring one. It is the control and its states, andunavailableReasonis its tooltip, because a control dim for no stated reason is one a user assumes is broken.AstryxChatToolCallsis collapsed and summarised in the row: what a reader wants from a tool call is usually "did it work" rather than the JSON. Every status is paired with its word, andrunningis a labelled spinner rather than a bare one.AstryxCitationis named for its source, not its number: "Source 1: scheduler/health.md" rather than "1". A row of bare numerals is a puzzle instead of a bibliography, and the source is in the accessible name whether or not the pointer ever finds the tooltip.AstryxMarkdownrenders headings, paragraphs, lists, fenced code, blockquotes, rules and inline spans with the design system's own widgets. Tables, images, footnotes, nested lists, task lists, inline HTML and cross-block selection are absent rather than half-drawn: a table rendered as run-together text is worse than one nobody rendered, because the reader cannot tell it was a table. Unsupported input degrades to paragraphs and never throws, which is what matters for arbitrary model output.
-
AstryxLocalizations.chatDictationStart,chatDictationStop,tokenRemove,tokenizerLabel,tokenizerPlaceholder,tokenizerValue,citationLabel,citationLabelled,toolCallsLabel,toolCallRunning,toolCallSucceeded,toolCallFailedandtoolCallPending. -
The first three Chat & AI components —
AstryxChatLayout,AstryxChatMessagewithAstryxChatMessageList, andAstryxChatComposer, absorbing eight upstream widgets between them. The group is a large family and this is the start of it; each page says what is not there yet.AstryxChatLayoutbuilds the transcript reversed, so the newest turn sits at offset zero. Growing content therefore cannot move what is on screen — no scroll correction, no post-framejumpTo, none of the jitter those produce — and a reader who scrolled up to re-read something is left where they are. A jump-to-latest button appears once they are away from the bottom. Reversed in the widget tree, oldest-first on screen, so semantics traversal reads the conversation the way it happened.- It asserts a readable message when given an unbounded height. The raw
failure is a
RenderFlex … unboundedfrom three widgets down, which says nothing about what to do.
- It asserts a readable message when given an unbounded height. The raw
failure is a
AstryxChatMessagegives a bubble toAstryxChatRole.userand not toassistant: an answer is the content of the page, often with a code block in it, and wrapping that in a rounded box makes it read as an aside. Every turn is announced with who said it, because layout carries that for a sighted reader and carries nothing at all for anybody else.- A turn's actions are always visible. Upstream reveals them on hover; this widget set cannot, because touch has no hover and the density system suppresses hover styling there.
AstryxChatComposer— Enter sends, Shift+Enter starts a line, and an Enter that could not send is still claimed so it cannot silently insert one. One control sends and becomes stop generating, with a name that changes with it; it is always present, because on a touch keyboard there is no Shift to hold and the button is the only way to send. The drawer sits inside the composer's own surface, so what is about to be sent is unmistakable.- Inline tokens are not ported: upstream's
ChatComposerTokenElementneeds a rich-text editing controller this package does not have, and a version that let a caret walk into the middle of a chip would be worse than the gap. It has a planned page of its own now rather than being folded into a claim the composer does not honour.
- Inline tokens are not ported: upstream's
-
AstryxLocalizations.chatTranscript,chatComposerLabel,chatComposerPlaceholder,chatSend,chatStop,chatScrollToLatest,chatFromUser,chatFromAssistantandchatSystemMessage. -
AstryxEntryAnimation, withAstryxEntryTransition— content animated in once, from tokens, obeying reduced motion by not animating rather than by animating quickly. It runs once per element, so replaying it is akeychange: a new key says "this is new content", which is what the animation is saying too. The transition widgets are dropped once it finishes rather than left as an opacity layer on every card that ever entered. -
AstryxContainerReveal— an entry animation triggered the first time its child is scrolled into view. Flutter has noIntersectionObserver, so it watches the enclosingScrollableand compares its own bounds against the viewport's.- The child is laid out before it is revealed, so the page height and the scrollbar are right from the start.
- The check is posted to the end of the frame: a scroll position notifies before the frame that moves anything, so measuring inside the callback reads the previous frame's geometry — and a reveal that checks a position its child has already left never fires.
- With no enclosing scrollable it reveals immediately. Content that never appears because a widget was looking for a viewport that does not exist is the worse failure by a wide margin.
-
AstryxKeyboardHint— a hint that appears while the user is navigating by keyboard and steps back on a pointer, reading the same last-input signal as the focus ring so the two can never disagree. It keeps its space by default: a hint that appears on the first keystroke and shoves a row sideways draws the eye to the wrong thing at the wrong moment. It hides a hint, never a control. -
AstryxStreamingText— text revealed at a steady rate as it arrives, so a model's bursty output reads as typing rather than twitching. It never rewinds: a non-continuation is a rewrite and is swapped in whole. The whole text is the accessible name from the first frame — a live region firing per token would restart the sentence eighty times a second, which is not a reading experience. -
The Hooks & controllers group is finished:
useInputContainer,useKeyboardHint,useEntryAnimation,useContainerReveal,useImageMode,useStreamingTextanduseTranslator. Three resolve to things that already existed — the internal input container behindAstryxFieldandAstryxInputGroup, one line ofAstryxTheme.of(context).mode, andAstryxLocalizations.of— and each page says so under the upstream name.example/lib/docs/pages/planned/hooks.dartis gone rather than empty. -
AstryxRovingFocus, with.listand.grid— a set of items that is one tab stop, traversed with the arrow keys. The ARIA composite pattern as a primitive, for building a composite this package does not ship; the ones it does ship already behave this way.- Roving focus is not selection:
onActivatecommits onEnterandSpace(a menu, a grid),onActiveChangedselects as the focus moves (a radio group).isEnabledmarks the items movement skips. .listwraps at the ends and.griddoes not, because wrapping off the end of a row is right for a menu and wrong for a calendar, where it silently changes the week. In a grid,HomeandEndare the ends of that row.- Nothing an
itemBuilderreturns may be focusable — that would be a second tab stop, and the thing the widget exists to prevent.AstryxRovingFocusItem .showsFocusRingcarries both ring conditions so a caller cannot get the pair wrong.
- Roving focus is not selection:
-
AstryxScrollOverflowandAstryxScrollEdges— whether a scroller has content past either edge, with optional edge fades. A clipped edge with nothing at it looks like the end of the content, and a user who cannot tell the difference stops scrolling.- Listens to scroll notifications rather than owning a controller, so it works
over any scrollable without being handed one — including a
ScrollMetricsNotification, which is the resize case a listener on the offset alone misses. - The fades never hit-test, and the content behind them stays in the semantics tree: a fade is an extra, never a gate.
- Listens to scroll notifications rather than owning a controller, so it works
over any scrollable without being handed one — including a
-
The rest of the Hooks & controllers group is documented:
useScrollOverflow,useOverflow,useListFocus,useGridFocus,useTreeFocus,useLayeranduseClickableContainer. Four resolve to things that already existed —AstryxOverflowList,AstryxTreeList, Flutter'sOverlayplusAstryxOverlayStack, and a non-nullonPressed— and each page says so under the upstream name, including why a tree's arrows are not a flat list's and why there is noClickableCard. -
AstryxHotkeysandAstryxHotkey— keyboard shortcuts, bound in the widget tree rather than in a hook whose scope is invisible. A hotkey takes aLogicalKeyboardKey, not upstream's'mod+k'string: a typo in a string is a shortcut that silently never fires.AstryxHotkey.modis Command on a Mac and Control everywhere else, so one definition is correct on both — and because the hotkey knows which modifier it resolved to,AstryxKbd.hotkeydraws⌘KorCtrl+Kfrom the same object that was bound. One definition, bound and displayed; the hint cannot describe a key that is not the one bound.autofocusexists because key events walk up from whatever holds focus: an application-wide scope with nothing focused beneath it would swallow⌘Ksilently. The node stays skipped by Tab, so it costs no tab stop.
-
AstryxScrollLock, withAstryxScrollLock.whileModalIsOpen— the page behind a modal, frozen. A scrim blocks presses and a wheel is not a press, so without this the content behind an open dialog still scrolls under the pointer.- Not wired in by default: the lock belongs to the scrollable a product wants
frozen, and this package cannot know which that is. One
whileModalIsOpennear the root is the whole setup. - A locked subtree takes no pointer events at all. That is not
over-reach: a page-level
ListViewisprimary: true, andScrollViewwraps the ambient physics inAlwaysScrollableScrollPhysicsfor that case, which overrules anything handed down throughScrollConfiguration.
- Not wired in by default: the lock belongs to the scrollable a product wants
frozen, and this package cannot know which that is. One
-
AstryxOverlayStack.openLayersandmodalLayers— the dismiss stack as something to listen to, and the distinction the scroll lock needed. A layer is modal when it dims the page; a popover, a menu and a tooltip are not, and freezing a page because a tooltip appeared would be a bug with a very confusing report. -
The Hooks & controllers group is documented:
useTheme,useMediaQuery,useHotkeys,useFocusTrapanduseScrollLock, each on a page carrying the upstream name — because a reader arriving fromuseScrollLockis searching for a word that does not appear in this API. Two of the five resolve to things that already existed (AstryxTheme.of,MediaQueryandLayoutBuilder), one toAstryxFocusTrap, and two are the widgets above. -
AstryxSyntaxToken, andAstryxThemeData.syntaxColor,hasSyntaxPaletteandsyntaxPalette— the syntax palette a theme carries, read back asColors instead of as the raw CSS strings behindAstryxThemeData.tokens. All seven prebuilt themes ship a palette.syntaxColoris nullable, alone among the accessors on that class: a palette sits outside the 184 core tokens, so a theme either carries one or does not, and throwing would punish a caller for the theme's silence.- Nothing in the package paints with these.
AstryxCodeBlockstill does not highlight — shipping a tokeniser for every language somebody might paste is not a design system's job — so this is the seam for a highlighter a caller wires themselves, and the reason it can take its colours from the theme rather than from fourteen hex values beside it.
-
The Providers group is documented:
AstryxThemeProviderand the four scopes it installs, the overlay layer and its dismiss stack,AstryxLinkScope,AstryxLocalizationsScope, andAstryxSyntaxTheme. Each page records the upstream → Flutter mapping, including the one provider that does not exist here: upstream'sLayerProvideris Flutter's ownOverlay, and what the port adds isAstryxOverlayStack, so Escape closes one layer rather than all of them. -
The date and time set —
AstryxCalendar,AstryxDateInput,AstryxDateRangeInput,AstryxDateTimeInput,AstryxTimeInputandAstryxTimestamp, with the value typesAstryxDateRange,AstryxTimeandAstryxWeekday. Nointldependency: the month and weekday names areAstryxLocalizationsstrings like every other, and the part order is a field's ownAstryxDateFormatrather than the locale's, because the place a form is used is not always the place its data came from.AstryxCalendar— a month grid, six rows always, so paging never moves what is below it. One tab stop: the arrows move a roving focus,HomeandEndwalk the week,Page Up/Page Downthe month andShiftwith them the year, and nothing is picked untilEnter. Every cell announces its full date, and today is named as well as ringed.AstryxCalendar.rangetakes two presses in either order — pressing the earlier day second completes the range backwards rather than starting over.AstryxDateInput— a text field first, with the calendar behind a button, because a picker that can only be clicked is slower than a keyboard for anybody who knows the date they want. Parsing is forgiving about shape and strict about meaning:4-8-26commits, and31/02/2026is refused rather than rolled over to the 3rd of March. A rejection reverts and is announced —AstryxNumberInput's contract, for the same WCAG 3.3.1 reason.AstryxDateRangeInput— two fields joined byAstryxInputGroupinto one control with one label. The validation belongs to the pair: an end before its start is a bad pair rather than a bad date, so the message sits under the group and the ends are not silently swapped. Nothing is reported until both are readable and in order, so no caller models "start but no end".AstryxDateTimeInput— a date beside a time. A date picked on its own commits atdefaultTime; a time on its own commits nothing, because "the 4th" is a thing people mean and "half past two, some day" is not.AstryxTimeInput—9,930,9:30,9.30and2:30 pmall commit, on either clock whichever one is shown, and the value is written back on the field's own. The arrow keys step bystepMinutes, and a step that would leaveearliest–latestis refused rather than wrapped past midnight.AstryxTimestamp— relative, and re-rendered as it ages by a single-shot timer that re-books itself at a distance matching how fast the text can change: thirty seconds while it is minutes old, an hour once it is days old. Pastthresholdit writes the date instead. The exact instant is the accessible name, so a screen reader never gets only a relative phrase whose anchor it cannot see.
-
AstryxLocalizations.monthNames,monthNamesShort,weekdayNames,weekdayNamesShort,timeAnteMeridiem,timePostMeridiem,calendarLabel,calendarPreviousMonth,calendarNextMonth,calendarToday,calendarRangeStart,calendarRangeEnd,dateInputOpenCalendar,dateInputRejected,timeInputRejected,dateRangeStartLabel,dateRangeEndLabel,dateRangeInvalid,dateTimeInputDateLabel,dateTimeInputTimeLabel,timestampJustNow,timestampMinutesAgo,timestampHoursAgo,timestampDaysAgo,timestampInMinutes,timestampInHoursandtimestampInDays. -
AstryxLink, withAstryxLinkUnderline— text that goes somewhere. Anhrefis handed to theAstryxLinkDelegateand this package never decides what following means; anonPressedis called directly.AstryxLink.spanputs one inside a sentence, the same wallAstryxCoderuns into.- An external link says so in its accessible name, not only in its glyph: the user who cannot see the glyph is the one most disrupted by a window they did not expect. The underline is on by default for a related reason — in running text colour alone separates a link from an emphasised word, and for a colour-blind reader it separates nothing.
visitedis the caller's to track. A browser knows a link's history and Flutter does not, so there is nothing here to read it from.
-
AstryxSegmentedControl, withAstryxSegment— one choice out of a few, every label visible. Keyboarded as an ARIA radiogroup: one tab stop, the arrows move and choose, they wrap at both ends, both axes work, and the inline pair mirrors under RTL. It is notAstryxTabList(which switches what a page shows) and notAstryxToggleButtonGroup(settings that sit together), and it announces itself as the radio group it is. -
AstryxToolbarandAstryxToolbarDivider— a band of controls that is one tab stop. A formatting bar of twelve buttons is twelve presses to walk past otherwise, which is why the ARIA toolbar pattern exists.- The children stay ordinary traversable widgets, which is how Tab enters on the first one. What makes the band a single stop is that Tab pressed inside it leaves: the toolbar steps focus to its edge and lets Flutter's own traversal carry past. Nothing is asked of the children, so anything focusable can go in.
-
AstryxMoreMenu— the "…" trigger and its menu as one widget, because that composition is the one people get subtly wrong: an unnamed trigger, or one opening a menu with no name of its own. Onelabelis the trigger's name, its tooltip and the menu's.AstryxBreadcrumbsnow uses it for its collapsed steps rather than assembling the same button by hand. -
AstryxTabMenu— a tab that opens a menu. Drawn as a tab, indicator and all, so a reader looking at a report still sees something in the strip claiming it; announced as a menu button, because telling a screen-reader user this is a tab and then opening a menu is a promise the widget cannot keep. -
AstryxPagination— page-at-a-time controls, one-based as they are to the person reading them. The ends are always shown and the middle gaps; a gap of exactly one page is drawn as the page, since "1 … 3" hides nothing and costs a press. The gap is not a button — it stands for a range, and a control that cannot say where it would take you is not worth offering.- The arrows disable at the ends rather than disappearing: a control that
vanishes moves everything beside it.
AstryxPagination.pagesForis the arithmetic on its own, static and pure, so what a reader sees can be tested without a widget tree.
- The arrows disable at the ends rather than disappearing: a control that
vanishes moves everything beside it.
-
AstryxLocalizations.linkExternal,toolbarLabel,moreMenuLabel,paginationLabel,paginationPrevious,paginationNextandpaginationPage. -
The navigation set, on one shared model:
AstryxNavEntrywithAstryxNavItem,AstryxNavSectionandAstryxNavDivider. The rail, the bar and the drawer take the same list, so a navigation written once can be shown three ways — which is what stops an application from having a rail and a drawer that disagree about where its pages are.AstryxSideNav— upstream'sSideNav,SideNavSection,SideNavHeading,SideNavItemandSideNavCollapseButtonas one widget, three entry types and a button that appears the momentonCollapsedChangedis passed. The rows scroll and thefooterdoes not, so a rail of forty destinations cannot push the account row off the bottom of the window.- Collapsed, the labels leave the screen and not the semantics tree. Each row keeps its label as its accessible name and gains a tooltip that shows on focus as well as hover. That is the only concession to hover in the whole widget set, and it is allowed only because the name is still announced and still reachable without a pointer.
AstryxTopNav— the same entries along a row. An item withchildrenopens a menu rather than indenting them, an item with apanelopens that panel (upstream's mega menu, which is any widget here rather than four components), and a section becomes a menu named after the group: a bar has no room for a heading over a group, and a menu is exactly what holds one.AstryxMobileNavandAstryxMobileNavToggle— the drawer and its button, ported fromMobileNavandMobileNavToggle. The drawer is a realAstryxOverlay; the toggle with no controller drives the enclosingAstryxAppShell's drawer, because two sources of truth for whether the navigation is open is one too many. Closing after a choice is the caller's: a drawer that closed itself would take a mis-tap as a navigation.AstryxNavIcon— the fixed square a row reserves for its icon. The square is the point, not the icon: labels line up whether or not their neighbours have icons, and a collapsed rail keeps its glyphs on one axis. It carries the row's state too, taking--color-icon-accentwhen the row is the current destination.AstryxNavHeadingMenu— the workspace switcher: the name of the thing you are looking at, and the way to look at a different one. Announced as one name — "Acme Corp, Production" — because that is one fact about where the user is.
-
AstryxBreadcrumbs, withAstryxBreadcrumb— the trail back up a hierarchy. It collapses in the middle, never at the ends: the first step is the way out to the top and the last is where the user is, so dropping either to save room throws away the two the trail exists for. What is dropped goes into a menu where it was, and stays reachable.- The row is measured by a render object rather than counted, so the answer
is right at every width, and the count settles for the reason
AstryxOverflowList's does. A step with noonPressedis a label rather than a link, because a link to the page you are on is a link that does nothing.
- The row is measured by a render object rather than counted, so the answer
is right at every width, and the count settles for the reason
-
AstryxItem.labelHidden, which is what a collapsed rail row is. The same escape hatchAstryxCheckboxhas, and the same rule: never to skip a label. -
AstryxLocalizations.navLabel,navCollapse,navExpand,navOpen,breadcrumbsLabelandbreadcrumbsMore. -
AstryxSection, withAstryxSectionScope— a titled band of page content. The heading level looks after itself: a section inside another section is one level deeper, so a page assembled from parts nobody wrote together still produces an outline a screen reader can navigate. That fault — an outline that jumps fromh2toh4, or repeatsh2for something that is plainly a sub-part — is the commonest accessibility defect on a long page, and the one nobody can see by looking at the screen. The top level is 2, because 1 belongs to the page's own title inAstryxLayout.header; nesting stops at 6, where HTML stops. -
AstryxResizeHandle, withAstryxResizeEdge— the drag target between two regions, ported fromResizeHandleanduseResizable. It holds no size of its own:sizein,onResizeout, so the number lives in the state that also lays the region out.- Operable from the keyboard, which is the part hand-rolled resize
handles almost always miss: Tab reaches it, the arrows move it by
step, Home and End go tominandmax, and it announces itself as a slider carrying the current size.labelis required — nothing is painted on a handle, so without one a screen reader has a slider and no idea what it sizes. edgeis one value rather than an axis and a direction, because those two can be set inconsistently and this cannot. The inline edges mirror under RTL so the same physical drag grows the panel either way; the block edges never mirror.
- Operable from the keyboard, which is the part hand-rolled resize
handles almost always miss: Tab reaches it, the arrows move it by
-
AstryxOutline, withAstryxOutlineEntry— the on-this-page contents. What it tracks is where the anchors are, not what the scroll offset says: an offset means nothing on its own, since a page of short sections and a page of long ones put the same number in different places. The active entry is the last one whose heading has passed the top of the viewport, withtopOffsetas the slack that stops it flickering between two.- An entry's
anchor— usually anAstryxSection.headerKey— earns its keep twice: without it the outline cannot know where the heading is, and pressing the entry has nowhere to scroll to. Upstream gets both from the DOM id it links to. - The entry being read is marked by an accent rule and
selectedin the semantics tree, so it is not conveyed by colour alone.
- An entry's
-
AstryxAppShell, withAstryxAppShellControllerandAstryxAppShellScope— the frame an application sits in: a full-width header, navigation beside the content, and the content itself. BelowcompactBelowthe navigation moves behind a drawer, and the drawer is a realAstryxOverlay— it traps focus, closes on Escape or a press on the scrim, hands focus back to the button that opened it, and joins the same dismissal stack as every dialog and menu. Growing back out of the compact layout closes it, because a drawer left open would be a second copy of the navigation now sitting beside the content.AstryxAppShell.of(context)is the port ofuseAppShellMobile: a header cannot know whether to draw a menu button without knowing where the navigation went, and that answer belongs to the shell.compactBelowis a number, not an entry in a breakpoint table. The package still has no breakpoint system, and this does not introduce one: the width at which a particular navigation stops fitting is a fact about that navigation.headerandsidebartake any widget.SideNav,TopNavandMobileNavare not ported yet, and the shell does not wait for them.
-
AstryxLayout, withAstryxLayoutPanelSide— the page inside the shell: a pinned header, a scrolling body, an optional panel beside it and a pinned footer. A page title that scrolls away takes the reader's place in the hierarchy with it, and a Save button that scrolls away is a Save button people cannot find — which is the whole reason this is a widget rather than aColumn. The panel scrolls on its own, because one tied to the body's scroll position disappears while you are reading it.- Upstream's five components —
Layout,LayoutHeader,LayoutContent,LayoutPanel,LayoutFooter— are slots here. A slot cannot be put in the wrong order, left out of its parent, or nested inside another by mistake.
- Upstream's five components —
-
AstryxCode— a symbol or a value in the code family. Flutter has no inline element, soAstryxCode.spanreturns the same chip as anInlineSpanforText.rich, middle-aligned rather than baseline-aligned because the chip carries padding of its own. This is the one place the port cannot follow upstream's shape, and the span is the bridge. -
AstryxCodeBlock— a fenced block with the language, a copy button and optional line numbers. Nothing is highlighted: upstream colours its blocks with a JavaScript tokeniser that has no counterpart here, and code coloured by a grammar that does not match the language is code that lies about what it means.languageis a label the reader sees, not an instruction.- The copy button carries the whole string, including the lines scrolled out of sight, and reports back in place by becoming a tick for two seconds rather than raising a toast — a page of ten blocks that shouts once per copy is a page nobody reads.
- Long lines scroll sideways rather than wrapping, because in code a line
break is not a neutral event;
wrap: truewhere the layout matters more. Line numbers are decoration: not copied, and not read aloud.
-
AstryxBlockquote— a quotation with a rule down its reading-start edge, an optionalattribution(the em dash is the widget's), and achildfor the quotations that are not one paragraph of plain text. -
AstryxKbd, withAstryxKbdSize— one key cap or a chord of them.AstryxKbd('K')for the common case,AstryxKbd.chordfor the rest. The glyphs are the caller's: this does not translateCtrlto⌘on a Mac, because only the caller knows whether the shortcut is the platform's or the product's own. A chord is one semantics node, andsemanticsLabelis what turns⌘ ⇧ Pinto a shortcut somebody can follow. -
AstryxLocalizations.codeCopyandcodeCopied, for the code block's button. Separate fromtextCopy, which is the selection menu's own verb. -
AstryxItem, withAstryxItemDensity— the row the lists are built from: something at the reading-start edge, a label and a description, and something at the end. Ported fromItem. A non-nullonPressedmakes it a button, the same ruleAstryxCardfollows;selectedis a selection rather than a press state, and survives the pointer leaving. A disabled row stays in the semantics tree — a control that vanishes when it is disabled tells a screen-reader user the option does not exist rather than that it is unavailable. -
AstryxList— upstream'sListandListItem, where the item isAstryxItemand this is only the container: the dividers, the density every row inherits through a scope, and thelist/listItemroles that let a screen reader say "Recent deploys, list, 3 items" before reading the first row.emptyis what shows when there are no rows, because a list that renders nothing reads as a bug. It is aColumn: it does not scroll and does not virtualise, the same limitAstryxTablecarries. -
AstryxTreeList, withAstryxTreeNode— nested, expandable rows. Keyboarded as an ARIA tree: the whole tree is one tab stop, Down and Up move, Right opens a branch and then steps into it, Left closes it and then steps out to the parent, Home and End jump to the ends, and the inline arrows mirror under RTL. The arrows deliberately do not wrap, unlikeAstryxRadioList— a tree is a hierarchy, and jumping from the last leaf back to the first root loses the reader's place.- Expansion and selection are carried as ids, not as flags on the nodes,
so rebuilding the tree from fresh data does not lose which branches were
open. Expansion is controlled (
expanded) or uncontrolled (initiallyExpanded); selection is always the caller's, as onAstryxRadioList. - Pressing a leaf chooses it; pressing a branch opens or closes it and chooses it. A tree where clicking a folder does not select it is a tree where the folder can never be the answer.
- Expansion and selection are carried as ids, not as flags on the nodes,
so rebuilding the tree from fresh data does not lose which branches were
open. Expansion is controlled (
-
AstryxOverflowList, withAstryxOverflowItem— a row that measures itself and moves its tail into a menu. An item carries both achild, drawn while it fits, and alabel, which names it in a realAstryxDropdownMenurow once it does not: nothing is hidden that a user cannot get to, and the items the menu stands in for leave the semantics tree so nobody hears the same list twice.- The count is only known during layout, so it reaches the label through a
ValueNotifierrather thansetState. The trigger's width depends on the count and the count depends on the trigger's width, but that circle only turns one way — a wider label hides more items, and hiding more never narrows the label — so the row settles within a frame or two of a resize.
- The count is only known during layout, so it reaches the label through a
-
AstryxMetadataList, withAstryxMetadataItemandAstryxMetadataListDirection— the label-and-value pairs of a details panel,stackedorinline. A pair is one semantics node, so a screen reader reads "Owner, Ada Lovelace" rather than stopping between the two halves of one fact;semanticsValueis what a widget-valued pair announces, because a badge left to describe itself contributes one word of information. -
AstryxEmptyState, withAstryxEmptyStateSize— what a list, a table or a panel shows when it has nothing to show. Not an error: nothing has gone wrong when a new project has no deploys, so it does not announce itself, take a status colour, or borrowAstryxBanner's urgency. The title is a level-3 heading, standing in the outline where the missing content would have been, and the icon is decorative and excluded. -
AstryxLocalizations.overflowMoreandoverflowMenuLabel, for the overflow trigger. Deliberately notmultiSelectorMore: that one names chosen options that are not shown, this one names items that did not fit, and the two are not the same sentence in every language. -
AstryxSelectableCard, withAstryxSelectableCardControl— a card that is also a checkbox or a radio, for a choice that needs more than a line of text. Ported fromSelectableCard. The whole card is the target; Space and Enter both activate it, as they do on a pressableAstryxCard. Selection shows three ways at once — the control fills, the border takes the accent, and the surface takes--color-accent-muted— and the border and the tint are dropped when the card cannot be operated, the same ruleAstryxCheckboxListapplies to a checked row.- A radio card reports nothing when it is already selected, which is what
a native radio does: reporting
falsewould let a group end up with nothing chosen. A checkbox card deselects, as a checkbox does. labelis required and never painted, and the content keeps its own semantics nodes — the opposite ofAstryxCheckbox, whose label is the whole of it. A card announced by its contents is a sentence nobody can act on.- Each card is its own tab stop. There is no shared
nameto group a set the way a browser groups native radios, soAstryxRadioListremains the control for four or more terse options.
- A radio card reports nothing when it is already selected, which is what
a native radio does: reporting
-
AstryxSlider, withAstryxSlider.range,AstryxSliderMark,AstryxSliderOrientationandAstryxSliderValueDisplay— ported frompackages/core/src/Slider/.min/max/step, marks,formatValue, both orientations,onChangedduring a drag andonChangeEndwhen it settles. A range keeps its thumbs in order and no closer thanminStepsBetweenThumbssteps; each thumb is its own tab stop and its own announcedslidercarrying the formatted value, plus increase and decrease actions for a switch or voice user.- Upstream is a native
input[type=range], so the keyboard map is written out here: arrows a step (mirrored under RTL), Page keys ten, Home and End the ends. A keyboard move firesonChangeEndtoo, since there is no drag to end. valueDisplay: tooltipshows nothing yet — upstream gets that bubble from the browser on hover.textis the visible-to-everyone alternative and is what the page demonstrates.
- Upstream is a native
-
AstryxMultiSelector, withAstryxMultiSelectorTriggerDisplay— the sameAstryxSelectorEntryoptions asAstryxSelector, because upstream shares those types between the two as well. ASet<T>value, checkbox rows, and a list that stays open as options are ticked.showSearchfilters and drops a heading whose options all filtered away;showSelectAllticks everything and clears it when everything is ticked; the trigger shows tokens up tomaxBadgesthen "+n more", or a count. The field announces which options are chosen rather than how many. -
AstryxComplexSelector, withAstryxComplexSelectorState— the headless half of upstream's selector family: this package draws the field, the trigger, the overlay, the focus trap and the barrier, andsurfaceBuilderdraws the contents. For a calendar, a swatch grid, a two-pane picker. Reporting a value deliberately does not close the surface, so a multi-step picker can stay open; upstream's four-argument render prop arrives as one state object. -
AstryxInputGroup, withAstryxInputGroupText,AstryxInputGroupPositionandAstryxInputGroupScope— inputs and affixes joined into one bordered control. The group carries the label, description and status for the row; each child learns its position from the scope and squares the corners that meet a neighbour, directionally, so a group mirrors under RTL without being told which way it runs.AstryxInputContainerreads that scope, which is how a plainAstryxTextInputjoins a group without being changed. -
AstryxFormLayout, withAstryxFormLayoutDirectionandAstryxFormLayoutScope—vertical,horizontal(equal columns) andhorizontalLabels. The last one is the only one that does more than space fields out:AstryxFieldreads the scope and moves its label and description beside the control, collapsing back to a stack below upstream's own 480px. Upstream sizes that column to its widest label, which CSS grid does for free;labelWidthis the honest Flutter version rather than laying every label out twice. -
AstryxCheckboxList, withAstryxCheckboxOptionandAstryxCheckboxListDensity— several independent choices under one label, one description and one validation state. Generic over the value type likeAstryxRadioList, rather than upstream'sstring[]. Ported frompackages/core/src/CheckboxList/: the checked-row tint,compactandbalanceddensities, optional dividers, per-rowdescription,trailing,enabledandloading, and a group-widereadOnlythat does not dim.- Keyboarded as a checkbox group, not a radio group. Every row is its own
tab stop and Space toggles the focused one — the opposite of
AstryxRadioList, whose whole group is one stop with the arrows moving inside it. A checkbox list built on the radio pattern swallows Tab and traps anyone using a keyboard, so a test pins the difference by counting traversable stops in both. - Upstream composes its own
List/ListItem, which this port does not have. The rows areAstryxCheckboxes in a tinted, inset container instead, and the inset is paid whether a row is checked or not so nothing shifts sideways as rows are ticked.
- Keyboarded as a checkbox group, not a radio group. Every row is its own
tab stop and Space toggles the focused one — the opposite of
-
AstryxNumberInput— a numeric field overAstryxTextInput, ported frompackages/core/src/NumberInput/.min,max,step,integerOnly,units,showClear, and anum?value sointegerOnlyyieldsints and a fractional step yieldsdoubles without a second widget. Commits on a stepper, an arrow key, blur or Enter — never mid-keystroke.- Out-of-range typing is rejected, not clamped, which is upstream's
parseNumberInputreturning null rather than the nearest legal number. The text reverts and the refusal is announced through a live region (AstryxVisuallyHidden), because reverting in silence tells a screen-reader user nothing — WCAG 3.3.1. Pressing a stepper does stop at the boundary, as a browser's spinner does. - Upstream is an
<input type="number">, so three behaviours it gets from the browser are written out here: the arrow keys stepping the value, the steppers themselves (drawn rather than left to a UA hover affordance — nothing important may live behind hover, and a thumb has no arrow keys), and refusing letters as they are typed. The wheel-changes-a-focused-number behaviour is deliberately not ported.
- Out-of-range typing is rejected, not clamped, which is upstream's
-
AstryxFileInput, withAstryxFile,AstryxFilePicker,AstryxFilePickRequestandAstryxFileInputMode— the chooser, the chosen list, and the limits, ported frompackages/core/src/FileInput/includingvalidateFilesmessage for message andformatFileSizefigure for figure.acceptmatches an extension, atype/*family or an exact MIME type;maxSizerejects;maxFilestruncates with a complaint; a caller'sstatusbeats the field's own so a server rejection is not overwritten locally.- The dialog is a seam, not a feature. Flutter ships no file picker and
this package depends on no plugins, so
onPickasks the application to open one — the same shape asAstryxLinkDelegate.AstryxFileis a description (name, optional size and MIME type, plus an untouchedhandle) rather thandart:io'sFile, which does not exist on the web. - A file of unknown size passes a
maxSizecheck: a reticent picker is not a large file. dropzonemode is a zone, not a drop target. External file drag-and-drop needs a platform channel Flutter does not ship; the panel takes clicks, taps and the keyboard, and the page says so.
- The dialog is a seam, not a feature. Flutter ships no file picker and
this package depends on no plugins, so
-
Fourteen localised strings for the above, on
AstryxLocalizations: the stepper names, the rejected-number announcement, the file prompts and the four file validation messages, and the multiple selector's count, tail, select-all row and no-matches line. -
AstryxToggleButton— a button with a sticky on state, for a toolbar control or a filter that stays down rather than for an action. Ported from upstream'spackages/core/src/ToggleButton/ToggleButton.tsx: always a ghost button,--color-overlay-pressedas the pressed fill, the label shifting to semibold,pressedIconfor an outline-to-filled swap, and the pressed state coming from the group when it is in one. Reports throughonChangedlike every other stateful control here, not upstream'sonPressedChange.- The pressed label's width is reserved so a toolbar cannot shuffle
sideways as toggles are pressed. Upstream reserves it by rendering a second
hidden copy of the label at semibold; ported literally that leaks — every
toggle would answer
find.text('Bold')twice, in your tests as well as these — so the port measures with aTextPainterinstead and renders oneText. Same stable width, one node. - Upstream's
isIconOnlyarrives aslabelHidden, the name the form controls already use for "keep the accessible name, drop the text". As upstream does, it squares the button and takes the label as its tooltip. - Two upstream props are deliberately absent.
pressedChangeActionis a React transition with an optimistic pressed state and no Flutter counterpart — drivepressedandloadingyourself, as with every other control.childrenis not ported because the label is the text here, as onAstryxButton.
- The pressed label's width is reserved so a toolbar cannot shuffle
sideways as toggles are pressed. Upstream reserves it by rendering a second
hidden copy of the label at semibold; ported literally that leaks — every
toggle would answer
-
AstryxToggleButtonGroup, withAstryxToggleButtonGroupScope— several toggles acting as one control, the port of upstream's discriminated union ontype. Two named constructors instead:.singletakes aString?and clearing is reachable by pressing the button that is already on,.multipletakes aSet<String>and hands back a new set each time. Dart makes the wrongonChangedsignature a compile error rather than a runtime surprise. A child's ownenabledis ignored inside a group, which is upstream's behaviour (group?.isDisabled ?? isDisabled) reproduced and pinned rather than quietly improved; a grouped button with novalueasserts in debug. -
Pages for both, in Actions, with six examples: a filter toggle, an icon-only watch button with an icon swap, single and multiple groups, a vertical group, and the four states side by side.
-
The five remaining guide pages, which had been placeholders since the site was scaffolded.
Getting startednow holds no Soon badges at all; the three that remain there are the deliberateN/Aomissions.- Migration — what maps from Material and Cupertino, and what actually
differs about each:
IconButtonrequiring a name, a dialog being a widget rather than a route, one radio group rather than a tile per option. Plus the habits that survive a migration and quietly undo it, how two theme systems behave in one tree, and an order of work that keeps the app running throughout. - Working with AI — the generated agent skill: installing it as a Claude Code plugin, what each reference file holds, why it is generated rather than written, and why a page that is not finished is never published to it.
- Themes — the gallery upstream's
/themesis, with the four new examples below. Also what actually differs between the eight (accent, type scale, corner radii, motion), that the named typefaces are not bundled by either implementation, and thatgothicrenders the same in both brightnesses because its tokens are single values rather than pairs. - Changelog — this file, rendered.
- Community — where to report what, the fidelity rule that decides whether a divergence is a bug, the loop for contributing a component, and the checklist a component has to pass to be called finished.
- Migration — what maps from Material and Cupertino, and what actually
differs about each:
-
AstryxVisuallyHiddenhas a page. It was badged Soon while the widget had been exported since 0.0.1-dev — the placeholder was written from the upstream sitemap and never rechecked against the package. The page leads with the distinction the widget's own doc comment calls the most common false friend in the port: upstream'sVisuallyHiddenboth names controls and announces changes, and only the second needs a widget here, because every control in this package takes its accessible name as a required parameter. Two examples: a live region announcing a character count, and proof that the hidden child measuresSize.zero. -
example/tool/gen_changelog.dart— parsesCHANGELOG.mdintolib/docs/changelog.g.dartas documentation blocks. The changelog stays a single file at the repository root, where pub and GitHub look for it, and the site compiles it rather than keeping a second copy somebody has to remember to update. -
Four examples in
example/lib/examples/themes_examples.dart: all eight themes side by side, each in both brightnesses, six colour tokens sampled from inside each one, and the same four controls rendered eight times. -
Links that leave the site now open. Every page carries them — the upstream page it ports, the repository, the issue tracker — and until now they were painted, underlined and inert, which reads as a failed click. The documentation app installs an
AstryxLinkDelegate, so they go through the same seam anAstryxButton(destination:)uses; the web half is a conditional import in the shape ofurl_strategy.dart, and off the web it declines rather than guessing. -
AstryxOverlay— the scrim-and-layer primitive, ported from upstream'sOverlay. The modal contract with no opinion about what is on the layer: scrim, focus trap with restore, Escape closing one layer rather than the stack, and an entry that honours reduced motion.alignmentis what makes a sheet a sheet, so there is no separate component for one. -
AstryxAlertDialog— a dialog with the answers built in, and three deliberate differences from one: the barrier does not dismiss, there is no close button, and focus starts on cancel so an Enter pressed out of habit deletes nothing.descriptionis required, because a confirmation whose consequence is left to the title cannot be consented to.showCancel: falsemakes it an acknowledgement. -
AstryxHoverCard— the preview a tooltip is too small to hold. It stays open while the pointer is on the card, which is the whole component: that is what lets the content hold a link or a button.waitDurationfilters a mouse passing through,exitDurationis the grace period for crossing the gap. A long-press reaches it on touch, and that path alone arms a dismissing barrier since touch has no pointer-exit. -
AstryxContextMenu— the sameAstryxMenuEntryrows asAstryxDropdownMenu, with the same keyboard model, raised by a secondary click at the pointer or by a long-press on touch.maxWidthbounds it because a menu anchored to a point has nothing else to bound it. Documented with what it cannot fix: a right-click is undiscoverable and has no keyboard equivalent, so nothing may live only there. -
AstryxCollapsible, withAstryxCollapsibleController— a disclosure whose whole header is the button, carryingexpandedin its semantics rather than leaving a rotated chevron to say it. Collapsed content is not in the tree at all: no layout, no semantics, no focus stops behind a closed section. -
AstryxCollapsibleGroup— sections as one block, andexclusive: truefor an accordion, where the group owns which one is open. Not the default, and documented as such: it saves space by removing the one thing a set of sections is good for. -
AstryxLocalizations.alertDialogCancel— "Cancel". Separate fromdialogClose, which is the same gesture but not the same sentence. -
.github/— four issue templates, a pull request template, a contributing guide and a code of conduct. The bug form asks for the four things that decide whether a widget bug reproduces — theme, brightness, density, text direction — because a report without them is a guess; the others cover a feature, a component that has no page yet, and an app someone has built with the package. -
A Showcase page, and a
Report a bug/Request a changepair at the foot of every page. The links are built from the page itself, so the form arrives with the component already named —issue_links.dartholds the coupling to the template field ids, andtool/gen_docs_md.dartbuilds the same URLs, so the markdown indoc/and the live site point at the same prefilled form. The showcase itself is empty and says so: submissions land there as they arrive.
Changed #
-
The documentation site loads about 30% less. Two changes, both to the build rather than to any widget.
uses-material-design: false: nothing in the site imports Material or draws anIcons.*glyph —AstryxIconRegistryresolves everything through Lucide — but the flag shippedMaterialIcons-Regular.otfanyway, and Flutter web downloads every font inFontManifest.jsonbefore it paints its first frame. And the Firebase predeploy now builds with--wasm:skwasm.wasmis 1.5 MB gzipped against CanvasKit's 2.8 MB, and a browser without WasmGC still gets the JS build Flutter emits alongside it. Measured before first frame, gzipped: 6.1 MB → 4.3 MB. -
Sidebar groups stay collapsed. They were collapsed by default already, but two things kept reopening one: the URL's page on boot, and every navigation after it — including into a group the reader had just closed. Neither does now. A non-empty filter query still forces every group open, because a search whose matches stay hidden has not searched anything.
-
AstryxDialogis nowAstryxOverlayplus a panel. The portal, the dismissal stack, the focus trap, the barrier and the animation moved into the primitive, so the two cannot drift;AstryxDialogControllerextendsAstryxOverlayControllerand gainstoggle(). No behaviour changed — the Phase 9 dialog tests pass untouched. -
The menu surface and the row vocabulary moved out of
dropdown_menu.dartintomenu_surface.dart(internal) andmenu_entry.dart, so the dropdown and the context menu render the same rows and arrow the same way. The public names are unchanged. -
The selection box and the block-box width rule are now shared rather than copied.
AstryxSelectionIndicator(internal) is the bordered box the checkbox, the radio and the selectable card all fill — one copy of upstream's three hover percentages instead of three — andAstryxBlockWidth(internal) is the card's fill-a-bounded-width, shrink-to-fit-an-unbounded-one rule, moved out ofcard.dart. No pixel changed: the existing goldens pass untouched. -
AstryxButtonSurfacetakes an optionalselected, which the toggle button passes and the other two buttons do not. Absent rather than false on a plain action, so nothing announces "not selected" about a Save button. Upstream spells itaria-pressed; Flutter'stoggledflag is the switch's on/off, soselectedcarries it — the mappingSegmentedButtonuses in the framework itself. Internal:AstryxButtonSurfaceis@internaland no call site outside the package can pass it. -
gen_skill.dartpublishes every written page exceptchangelogandcommunity. An agent gains nothing from the release history or from how to file an issue, and the changelog alone would put every bullet of every release into the reference file it opens to learn what a token is. -
sidebar_test.dartpicks aplannedpage rather than merely an unwritten one for its badge count.Getting startedno longer has aplannedpage, and the old expression could open a group with nothing to count.
Fixed #
-
An anchored overlay whose trigger subtree changed shape on the frame it opened measured a render object that had not been laid out yet, returned an empty surface, and never retried — so the content stayed invisible for as long as the overlay was open, and the failure looked like the content rather than the measurement. It now waits one frame and measures again. Found while building the typeahead, whose live-region announcement was doing exactly that.
-
The docs site's previous/next footer no longer overflows when both page titles are long. It stacked below 520px already; above that the two buttons were unbounded, and a pair like
AstryxLocalizationsScopeanduseTranslator → AstryxLocalizations.ofexceeded even a wide column by a few pixels. They areFlexiblenow, so a long title truncates instead. -
AstryxInputGroupexposed a latent layout fault in the affix. Stretching the joined row handed every child an infinite height, which the affix's own minimum height then tried to honour and asserted on. The row lets each child take the height its size token gives it instead. Found by the tests, not by a consumer. -
AstryxMultiSelector's clear button announced as nothing, for the same reason the file field's did an entry ago: anExcludeSemanticsaround the whole trigger. Removed before it shipped, and now pinned by a test that reaches the button by name. -
AstryxTextInput'sleadingandtrailingslots were invisible to assistive technology. The whole input container sat inside anExcludeSemantics, which had been there to stop the editable announcing its content twice — and took the slots with it. The clear buttonshowCleardraws therefore announced as nothing at all while staying perfectly clickable, and the same held for anything a caller put in either slot. Only the editable is excluded now. Found while buildingAstryxNumberInput, whose steppers live in that slot. -
The documentation site's example previews aligned inconsistently. Sixty-odd examples were centred and two hundred were not, and the split did not follow anything a reader could predict:
Text inputcentred whileRadio listsat at the reading edge,Timestampcentred one example and stretched the next, andSettings dialogwas the one template of nineteen not stretched. The alignment is now a property of the group rather than of the example — Actions, Overlays and Status centre everything, because every example in them is a single control with no natural edge, and every other group aligns to the reading start.example_block_test.dartenforces it, which matters becausecenteris the default and an omittedalign:is how the rule would break by accident. -
The documentation site's API tables keyed their rows by property name, so a class documenting two constructors with a parameter of the same name — the new group's
onChanged, once per constructor — crashed the page with a duplicateGlobalKey. Keyed by position now. No library code involved; it is listed because the first legitimate content of that shape found it.
0.0.6-dev #
Documentation and tooling only. No library code changed, so nothing here can break a consumer.
Added #
- Fourteen template pages. Whole screens, assembled only from what the
package exports and extracted from compiling widgets in
example/lib/examples/template_*.dart: four sign-in screens (bare, carded, SSO, split), three forms (contact, two-column, payment), settings as a page and as a dialog, a centred hero, a record detail page, a dashboard, a table screen, and a theme showcase holding one of every component. Each page states what it is made of and why each control was chosen over the one next to it. - A monitor / phone switch on every preview. The phone pins the example to
390 logical pixels and draws the frame's edge, which is the only way to watch
a
LayoutBuilderreflow — a two-column form becoming one, a tile row restacking. Width only: touch density is a separate axis with its own picker. It is not drawn when the viewport is already about phone-width, and the frame gives way rather than overflowing if the window is narrowed after the fact. The choice sits onDocsControllerwith the other pickers, so it is made once for every example on the page and survives navigation. The glyphs are Lucide's, reached directly by the documentation app:AstryxIconNamenames neither a monitor nor a phone, and it stays a transcription of upstream'sIconNameunion rather than growing to suit this site. example/lib/docs_ui/segmented.dart— the button-group picker the top bar and the new width switch share, instead of the private copy the top bar had. It also names the group to a screen reader, so "Mobile" is a choice about something.
Fixed #
- The previous/next page footer overflowed below about 520 logical pixels: some
page titles are long —
InternationalizationProvider— and two of them will not sit side by side on a phone. The pair now stacks. example/lib/docs/version.g.darthad fallen a release behindpubspec.yaml; regenerating the snippets brings it back to the package version.
Known limitations, now documented #
Three layout traps found while building the templates, all of them the same
root cause — a widget that measures its children intrinsically cannot measure
the touch-target wrapper or a LayoutBuilder inside them, so they assert in
touch density:
AstryxText(truncateTooltip: true)cannot be used inside anAstryxTablecell.- An
AstryxGridcell cannot hold a wrapped row of interactive widgets, or anAstryxTable. Cells of text, badges and figures are fine.
0.0.5-dev #
Documentation, tooling and tests only. No library code changed, so nothing here can break a consumer.
Added #
- The page registry mirrors upstream. Every page on
astryx.atmeta.comnow has a route here — 163 placeholders alongside the 37 written pages, across seventeen groups, including the ones that are entirely unwritten: Navigation, App shell, Chat & AI, Command & search, Date & time, Media, Providers, Hooks & controllers and Templates. Each placeholder carries a description, the upstream page it will be written from, and aDocStatus—ready,stub(ported, not written up),planned(not ported yet) ornotPlanned(deliberately omitted). A missing route and a widget nobody has thought about are no longer indistinguishable. - A sidebar that survives two hundred pages. Groups collapse, each showing how many pages it holds; the group containing the current page opens on load and on every navigation; a Written pages only switch hides the placeholders; a search expands every group so a match is never hidden in a collapsed one. Placeholders carry a status badge, and say not written yet in their accessible name, so a screen-reader user does not have to open a page to find out it is empty.
- A sitemap parity test.
example/test/upstream_pages.txtis every URL in upstream'ssitemap.xml, captured 2026-08-10; the test fails when a component upstream ships is claimed by no page here. The fixture lives beside the test rather than in the git-ignoredscrape/, so it is present in a fresh clone. - A contrast test for the documentation site's own colours. Every foreground the docs chrome paints over every background it paints it on, in all eight themes and both brightnesses, against WCAG 2.1 AA — 4.5:1 for body text, 3:1 for large text and control furniture. The package's existing contrast tests check the engine; this checks the choices.
- Sidebar tests covering the three behaviours the flat list did not have: staying collapsed, marking empty pages, and hiding them.
example/lib/docs/groups.dart— the group names and the reference file each is published to, in one place.tool/gen_skill.dartcarried its own private copy of that map, and a group added to a page file but forgotten there made the generator exit 1.
Changed #
- The generators publish written pages only.
doc/gets no file for a placeholder; the index names it with its status instead of linking it. The agent skill omits a group whose pages are all placeholders entirely — an agent told about a widget the package does not export will call it, and the call will not compile. - Inline
`code`in the documentation renders as a padded, rounded chip rather than text with a background colour, which put the first and last character flush against the edge of the highlight. upstream:on a page now claims every upstream sub-component it absorbs —Table / TableCell / TableHeaderCell / useTableSelection / …— which is what lets the sitemap check tell an absorbed component from a missing one. Names are corrected where they were wrong, so the checkbox page portsCheckboxInputrather thanCheckbox.- Documentation spacing: more room above a heading than between paragraphs,
more between list items than between the lines inside one, and a bullet
column sized to its bullet rather than to
10.. scrape/is git-ignored.
Fixed #
- The sidebar threw The Scrollbar's ScrollController has no ScrollPosition
attached on desktop and web, where
PrimaryScrollController.shouldInheritis false and the scroll view and its scrollbar disagreed about inheriting it. The sidebar owns its controller now.
0.0.4-dev #
Added #
- Package metadata for pub.dev:
homepageanddocumentationnow point at https://astryxui.web.app, where the documentation site is hosted. - Installation instructions for the published package, in the README and on the
site's installation page. The version constraint they quote is generated from
pubspec.yaml, so a released constraint cannot fall behind the release. - The repository is a Claude Code plugin marketplace, so the agent skill
installs with
/plugin marketplace add JayashBhandary/astryx_uifollowed by/plugin install astryx-ui@astryx-ui. The plugin's version is copied frompubspec.yamlwhen the skill is generated, so it cannot fall behind a release either.
Changed #
- The documentation site is deployed to Firebase Hosting at
https://astryxui.web.app. The Hosting configuration is deliberately not in
version control, and so is absent from the published archive; the deploy
command and the target it needs are documented in
example/README.md.
Fixed #
- The README linked a
dev/directory that is not part of this repository, so five links 404'd on GitHub and on pub.dev. They now point at the documentation, the changelog, or the issue tracker. - The README and the installation page still said the package was unpublished.
0.0.3-dev #
Documentation only. No library code changed, so nothing here can break a consumer.
Added #
- A documentation site, in
example/. Every component with prose, live examples, the source that produced them, and an API reference — viewable in any of the eight themes, either brightness, both densities and both text directions. Built fromastryx_uiitself, with no Material anywhere: the navigation is a column of pressable cards, the example frames are cards, the Preview/Code switch is anAstryxTabList, the API references areAstryxTables. Pages are addressable by URL fragment on the web. doc/— the same content as markdown: 30 component pages undercomponents/, seven guides underguides/(installation, theming, design tokens, density, right-to-left, accessibility), and an index. Generated from the page model byexample/tool/gen_docs_md.dart.- A skill for AI coding agents, in
.claude/skills/astryx-ui/. The rules the widget set is built to, a component index, per-group API references, every public enum's values, and the mistakes a generator makes without them. Generated from the same source byexample/tool/gen_skill.dart. - Snippet extraction. Every code block in the site, the markdown and the
skill is lifted from a real, compiling widget in
example/lib/examples/byexample/tool/gen_snippets.dart. A snippet cannot describe something the package does not do, because the preview and the code come from the same lines. - Example tests that render every page and build all 156 examples, so a layout error in a documented example fails the suite rather than being found by a reader.
Changed #
example/is the documentation site rather than a gallery of demo pages. The theme, brightness, density and direction pickers survive; the demo and gallery scaffolding they lived in does not.
0.0.2-dev #
Fixed #
AstryxCardno longer assertsBoxConstraints forces an infinite widthwhen given an unbounded width — inside aRow, anAstryxHStack, or a horizontal list. It fills a bounded width as before and shrinks to fit an unbounded one, matching how a block box sizes itself in CSS.
0.0.1-dev #
First development preview. The API is unstable and may change without a major version bump until 0.1.0.
Added #
- Package scaffold: pubspec, lint configuration, directory layout, license and attribution files.
- Token layer: colour primitives (OKLCH, RGBA, CSS colour parsing, light/dark pairs) and the full token set — colour, spacing, radius, size, border, shadow, duration, ease, font weight, text size and typography tokens.
- Theme engine:
defineTheme, the token resolver, scale expansion for colour, type, radius and motion, contrast and HCT helpers, style overrides, syntax themes and the theme registry. - Theme runtime:
AstryxThemeData,ResolvedTokenSet,AstryxTheme,AstryxShadow, font stacks, token-to-Flutter conversions, and per-component theme classes. - Prebuilt themes: neutral, stone, butter, chocolate, gothic, matcha and y2k.
- App layer:
AstryxAppandAstryxThemeProvider. - Foundation: density, focus ring, focus trap, focus-visible tracking, link delegate, motion, overlay positioning and stack, RTL helpers, semantics, size scope, states controller and tap targets.
- Components — layout and typography (stack, grid, center, divider, heading, text, icon), actions (button, icon button, button group), feedback (progress bar, skeleton, spinner), forms (text input, checkbox, radio list, selector, switch, toggle row, field), overlays (dialog, dropdown menu, popover, toast, tooltip, anchored overlay), surfaces (badge, banner, card, palette) and data display (table, tab list).
- Icon registry backed by Lucide, mirroring Astryx's semantic icon names.
- Localizations via
AstryxLocalizations. - Secondary entry point
package:astryx_ui/theme.dartfor the theme layer without components.