astryx_ui
A Flutter design system, token-compatible with Astryx. Faithful theme engine, themeable components, built on flutter/widgets.
0.0.7-dev
JayashBhandary/astryx_ui
What this package is like to depend on
Last release 11 days ago
13 Aug 2026
Too new to tell
only 1 release windows
Unknown
no stable releases
Nothing withdrawn
no release was ever pulled
0 months old
7 releases · first in 2026
7 releases in the last 12 months
see the full history below
Release timeline
7 releases · Aug 2026 to Aug 2026Releases
latest 7-
0.0.7-dev13 Aug 2026 pre-releaseRelease notes
Open source →Eighty-two new components, twenty-eight new templates, and the documentation for them. Every template upstream ships now has a written page —
planned/templates.dartis 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. -
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. -
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. -
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. -
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. -
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. -
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.
-
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. -
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. -
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. -
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. -
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. -
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. -
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.
-
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. -
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. -
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. -
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. -
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. -
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. -
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. -
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.
-
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: n
-
-
0.0.6-dev10 Aug 2026 pre-releaseRelease notes
Open source →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
LayoutBuilderinside 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.
- Fourteen template pages. Whole screens, assembled only from what the package exports and extracted from compiling widgets in
-
0.0.5-dev10 Aug 2026 pre-releaseRelease notes
Open source →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.
- The page registry mirrors upstream. Every page on
-
0.0.4-dev10 Aug 2026 pre-releaseRelease notes
Open source →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.
- Package metadata for pub.dev:
-
0.0.3-dev10 Aug 2026 pre-releaseRelease notes
Open source →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.
- A documentation site, in
-
0.0.2-dev09 Aug 2026 pre-releaseRelease notes
Open source →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-dev09 Aug 2026 pre-releaseRelease notes
Open source →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.
Something wrong with this page, or missing from it? Report a problem · Suggest a change — both templates arrive with the page filled in.