astryx_ui 0.0.8-dev copy "astryx_ui: ^0.0.8-dev" to clipboard
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 openenabled, 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, because enabled and entries both move over a widget's life. BrowserContextMenu is 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: false opts 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 contract AstryxDialog has — 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: hug sizes to its content, capped takes 62% of the viewport whatever is in it, tall takes 92%. snapPoints gives it more than one resting height as fractions of the viewport, and dragging the handle moves between them; dragging below the shortest dismisses unless dragDismissible is 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. label is 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. activeSheetId is 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 an AstryxBottomSheetPage, 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.

  • AstryxStepper and AstryxStep — progress through a sequence. Both orientations, and both of upstream's indicator positions: separated puts the indicator in the label row with the connector as a bar of its own, onTrack slots it into the line as a node with the label beside or below it. A step's index is its position in steps rather than something it declares, so the duplicate-index mistake upstream warns about at runtime cannot be written. status colours the indicator and, in auto, 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. onStepPressed makes 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, AstryxRadioIndicator and AstryxCheckIndicator — 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. AstryxCheckIndicator draws 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.checkbox and AstryxMenuItem.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. closeOnSelect overrides 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 is AstryxSelector, 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. What AstryxBottomSheet travels up on. Null keeps transition, so every existing overlay is unchanged.

  • AstryxLocalizations: stepperLabel, stepPosition, stepCompleted, stepCurrent, stepOptional, bottomSheetHandle and bottomSheetClose — 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. Delete and Backspace close the selected tab from the keyboard, which the strip's single tab stop would otherwise leave unreachable. AstryxTab.closeLabel overrides the button's accessible name, and AstryxLocalizations.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 collapsed AstryxLayout panel falls back to. Generic, because the slot is; name the panel with panelLabel and it is never read.

  • The landing page now carries the README. tool/gen_readme.dart parses ../README.md into lib/docs/readme.g.dart as DocBlocks, the same way gen_changelog.dart has 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 of DocsPageView so the landing page and the documentation pages draw a DocBlock the same way. DocsPageView now 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 docPages rather than typed, so the claim cannot outlive the registry.
  • README.md leads 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.html carries that description and the title astryx_ui — a Flutter design system, and gen_og.dart copies 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.

  • AstryxLayout no 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. Below panelCollapseBelow the panel becomes a disclosure banded across the top of the page — the same widget, the same content, collapsed and one press away. Name it with panelLabel; open it by default with panelInitiallyExpanded. The threshold is a number rather than an entry in a breakpoint table, for the reason AstryxAppShell.compactBelow is one: the width at which your panel stops fitting is a fact about your panel. Set it to double.negativeInfinity for a panel that must never collapse.

  • AstryxToolbar wraps 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.

  • AstryxSegmentedControl gives ground instead of overflowing. Its segments are Flexible and 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 Row that 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 an AstryxAppShell drawer, with the focus trap and the Escape key that a hand-rolled Row never got around to. example/test/template_responsive_test.dart keeps it that way.

  • The landing hero quoted a version by hand, and quoted the wrong one: Pre-alpha · 0.0.6-dev while pubspec.yaml said 0.0.7-dev. It reads astryxVersion now, 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 in lib/examples/theming_examples.dart to 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. AstryxCenter puts its ConstrainedBox outside its Align, and a ConstrainedBox handed 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 an Align above a ConstrainedBox, which is the order that works. The widget is unchanged: the same fault is in every AstryxCenter(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 pageplanned/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 (AstryxAppShell with AstryxTopNav above AstryxSideNav), Documentation (a rail, a measured column, and an AstryxOutline that tracks the reader), AI chat (transcript, tool calls, citations, composer, and the empty state before the first turn), Table page (filters pinned above, AstryxPagination pinned below) and Classic gallery (a wall of media tiles over one shared AstryxLightbox). They graduated out of planned/templates.dart now that the shell, chat, pagination and media components have landed.

  • Seventeen more template screens, which empties planned/templates.dart of everything that was waiting on an author rather than on a component. Every one is extracted from a compiling widget in example/lib/examples/template_*.dart and 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 no header: at all, so the drawer toggle moves into the page's own header via AstryxMobileNavToggle; the bar-only shell has no sidebar:, 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 an AstryxTable.
    • Editor, IDE and File explorer — a markdown canvas with an AstryxToolbar that edits the document rather than styling a hidden model, two AstryxResizeHandles with the sizes owned by the caller, and an AstryxTreeList beside 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 docPages rather 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 from astryx_ui and 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/widgets or 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 TrendChart from a forty-line CustomPainter. It resolves every colour through AstryxTheme.of(context).color(…) and takes a required label and a required semanticsValue, 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 Draggable and DragTarget. 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 tableAstryxTable has 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 an AstryxCollapsible, 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 AstryxPalette families are categorical, so none of them is a ramp. A HeatCell lerps between --color-background-muted and a semantic token, switching to that token's paired foreground past the midpoint, and puts text on it through DefaultTextStyle.merge plus AstryxTextColor.inherit. Every cell prints its own figure and every screen carries a legend: latency runs toward error, sales toward success, 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. An AstryxOutline in the panel tracks 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 with scrollable: false, where the body owns its own scroll view instead.

  • The Media groupAstryxAvatar with AstryxAvatarGroup, AstryxThumbnail, AstryxAspectRatio, AstryxCarousel, AstryxLightbox and AstryxMediaTheme, covering ten upstream components.

    • Two rules run through all of it. A picture of a thing is not the thing's name, so AstryxAvatar.name and AstryxThumbnail.label are 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 through AstryxMediaTheme.
    • AstryxAvatar folds in AvatarStatusDot, 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. A status without a statusLabel asserts: a coloured dot alone says nothing to a screen reader.
    • AstryxAvatarGroup keeps 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.
    • AstryxCarousel is 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.
    • AstryxLightbox is AstryxOverlay plus paging, a counter and a caption. It opens on initialIndex every time — a thumbnail grid opens the thumbnail that was pressed — and a single item drops the paging furniture entirely.
    • AstryxMediaTheme forces the onDark tokens 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.
    • AstryxAspectRatio does 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, lightboxNext and lightboxPosition.

  • The Command & search groupAstryxBaseTypeahead, AstryxTypeahead, AstryxCommandPalette and AstryxPowerSearch, covering twelve upstream components. One engine and three surfaces on it.

    • AstryxBaseTypeahead owns 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.
      • Enter with 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.
    • AstryxTypeahead is that engine with this package's field and rows on it, and it is not AstryxSelector: a selector picks from a set it can show you, a typeahead searches one it cannot.
    • AstryxCommandPalette folds 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 the AstryxHotkey that is actually bound.
    • AstryxPowerSearch puts filters beside the text as chips rather than inside it as syntax: status:failed typed 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. AstryxSearchQuery is the whole state as one comparable value.
  • AstryxLocalizations.typeaheadLabel, typeaheadPlaceholder, typeaheadSearching, typeaheadResults, commandPaletteLabel, commandPalettePlaceholder, commandPaletteNoResults, commandPaletteNavigate, commandPaletteRun, commandPaletteClose, powerSearchLabel, powerSearchPlaceholder, powerSearchAddFilter, powerSearchClear and powerSearchFilters.

  • Ten more Chat & AI components, which finishes the group's first pass — AstryxTokenChip and AstryxTokenizer, AstryxTokenTextController and AstryxChatTokenizedText, AstryxChatSendButton and AstryxChatDictationButton, AstryxChatSystemMessage, AstryxChatToolCalls and AstryxMarkdown, plus AstryxCitation.

    • AstryxTokenChip carries Chip on the end because AstryxToken is 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.
    • AstryxTokenizer commits on Enter or a delimiter, and takes the last token back on Backspace. A pasted list commits as one change: values is 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.
    • AstryxTokenTextController is the honest port of ChatComposerTokenElement. Upstream puts a real element in a contenteditable; EditableText edits a String, and a WidgetSpan in 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 — and AstryxChatTokenizedText draws real chips once the message is sent and there is no caret to protect.
    • AstryxChatSendButton is 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.
    • AstryxChatDictationButton does no speech recognition — there is no platform channel here and no dependency that would bring one. It is the control and its states, and unavailableReason is its tooltip, because a control dim for no stated reason is one a user assumes is broken.
    • AstryxChatToolCalls is 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, and running is a labelled spinner rather than a bare one.
    • AstryxCitation is 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.
    • AstryxMarkdown renders 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, toolCallFailed and toolCallPending.

  • The first three Chat & AI componentsAstryxChatLayout, AstryxChatMessage with AstryxChatMessageList, and AstryxChatComposer, 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.

    • AstryxChatLayout builds 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-frame jumpTo, 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 … unbounded from three widgets down, which says nothing about what to do.
    • AstryxChatMessage gives a bubble to AstryxChatRole.user and not to assistant: 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 ChatComposerTokenElement needs 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, chatFromAssistant and chatSystemMessage.

  • AstryxEntryAnimation, with AstryxEntryTransition — 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 a key change: 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 no IntersectionObserver, so it watches the enclosing Scrollable and 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, useStreamingText and useTranslator. Three resolve to things that already existed — the internal input container behind AstryxField and AstryxInputGroup, one line of AstryxTheme.of(context).mode, and AstryxLocalizations.of — and each page says so under the upstream name. example/lib/docs/pages/planned/hooks.dart is gone rather than empty.

  • AstryxRovingFocus, with .list and .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: onActivate commits on Enter and Space (a menu, a grid), onActiveChanged selects as the focus moves (a radio group). isEnabled marks the items movement skips.
    • .list wraps at the ends and .grid does 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, Home and End are the ends of that row.
    • Nothing an itemBuilder returns may be focusable — that would be a second tab stop, and the thing the widget exists to prevent. AstryxRovingFocusItem .showsFocusRing carries both ring conditions so a caller cannot get the pair wrong.
  • AstryxScrollOverflow and AstryxScrollEdges — 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, useLayer and useClickableContainer. Four resolve to things that already existed — AstryxOverflowList, AstryxTreeList, Flutter's Overlay plus AstryxOverlayStack, and a non-null onPressed — 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 no ClickableCard.

  • AstryxHotkeys and AstryxHotkey — keyboard shortcuts, bound in the widget tree rather than in a hook whose scope is invisible. A hotkey takes a LogicalKeyboardKey, not upstream's 'mod+k' string: a typo in a string is a shortcut that silently never fires.

    • AstryxHotkey.mod is 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.hotkey draws ⌘K or Ctrl+K from the same object that was bound. One definition, bound and displayed; the hint cannot describe a key that is not the one bound.
    • autofocus exists because key events walk up from whatever holds focus: an application-wide scope with nothing focused beneath it would swallow ⌘K silently. The node stays skipped by Tab, so it costs no tab stop.
  • AstryxScrollLock, with AstryxScrollLock.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 whileModalIsOpen near the root is the whole setup.
    • A locked subtree takes no pointer events at all. That is not over-reach: a page-level ListView is primary: true, and ScrollView wraps the ambient physics in AlwaysScrollableScrollPhysics for that case, which overrules anything handed down through ScrollConfiguration.
  • AstryxOverlayStack.openLayers and modalLayers — 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, useFocusTrap and useScrollLock, each on a page carrying the upstream name — because a reader arriving from useScrollLock is searching for a word that does not appear in this API. Two of the five resolve to things that already existed (AstryxTheme.of, MediaQuery and LayoutBuilder), one to AstryxFocusTrap, and two are the widgets above.

  • AstryxSyntaxToken, and AstryxThemeData.syntaxColor, hasSyntaxPalette and syntaxPalette — the syntax palette a theme carries, read back as Colors instead of as the raw CSS strings behind AstryxThemeData.tokens. All seven prebuilt themes ship a palette.

    • syntaxColor is 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. AstryxCodeBlock still 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: AstryxThemeProvider and the four scopes it installs, the overlay layer and its dismiss stack, AstryxLinkScope, AstryxLocalizationsScope, and AstryxSyntaxTheme. Each page records the upstream → Flutter mapping, including the one provider that does not exist here: upstream's LayerProvider is Flutter's own Overlay, and what the port adds is AstryxOverlayStack, so Escape closes one layer rather than all of them.

  • The date and time setAstryxCalendar, AstryxDateInput, AstryxDateRangeInput, AstryxDateTimeInput, AstryxTimeInput and AstryxTimestamp, with the value types AstryxDateRange, AstryxTime and AstryxWeekday. No intl dependency: the month and weekday names are AstryxLocalizations strings like every other, and the part order is a field's own AstryxDateFormat rather 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, Home and End walk the week, Page Up/Page Down the month and Shift with them the year, and nothing is picked until Enter. Every cell announces its full date, and today is named as well as ringed. AstryxCalendar.range takes 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-26 commits, and 31/02/2026 is refused rather than rolled over to the 3rd of March. A rejection reverts and is announcedAstryxNumberInput's contract, for the same WCAG 3.3.1 reason.
    • AstryxDateRangeInput — two fields joined by AstryxInputGroup into 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 at defaultTime; a time on its own commits nothing, because "the 4th" is a thing people mean and "half past two, some day" is not.
    • AstryxTimeInput9, 930, 9:30, 9.30 and 2:30 pm all commit, on either clock whichever one is shown, and the value is written back on the field's own. The arrow keys step by stepMinutes, and a step that would leave earliestlatest is 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. Past threshold it 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, timestampInHours and timestampInDays.

  • AstryxLink, with AstryxLinkUnderline — text that goes somewhere. An href is handed to the AstryxLinkDelegate and this package never decides what following means; an onPressed is called directly. AstryxLink.span puts one inside a sentence, the same wall AstryxCode runs 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.
    • visited is 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, with AstryxSegment — 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 not AstryxTabList (which switches what a page shows) and not AstryxToggleButtonGroup (settings that sit together), and it announces itself as the radio group it is.

  • AstryxToolbar and AstryxToolbarDivider — 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. One label is the trigger's name, its tooltip and the menu's. AstryxBreadcrumbs now 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.pagesFor is 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, paginationNext and paginationPage.

  • The navigation set, on one shared model: AstryxNavEntry with AstryxNavItem, AstryxNavSection and AstryxNavDivider. 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's SideNav, SideNavSection, SideNavHeading, SideNavItem and SideNavCollapseButton as one widget, three entry types and a button that appears the moment onCollapsedChanged is passed. The rows scroll and the footer does 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 with children opens a menu rather than indenting them, an item with a panel opens 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.
    • AstryxMobileNav and AstryxMobileNavToggle — the drawer and its button, ported from MobileNav and MobileNavToggle. The drawer is a real AstryxOverlay; the toggle with no controller drives the enclosing AstryxAppShell'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-accent when 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, with AstryxBreadcrumb — 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 no onPressed is 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 hatch AstryxCheckbox has, and the same rule: never to skip a label.

  • AstryxLocalizations.navLabel, navCollapse, navExpand, navOpen, breadcrumbsLabel and breadcrumbsMore.

  • AstryxSection, with AstryxSectionScope — 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 from h2 to h4, or repeats h2 for 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 in AstryxLayout.header; nesting stops at 6, where HTML stops.

  • AstryxResizeHandle, with AstryxResizeEdge — the drag target between two regions, ported from ResizeHandle and useResizable. It holds no size of its own: size in, onResize out, 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 to min and max, and it announces itself as a slider carrying the current size. label is required — nothing is painted on a handle, so without one a screen reader has a slider and no idea what it sizes.
    • edge is 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, with AstryxOutlineEntry — 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, with topOffset as the slack that stops it flickering between two.

    • An entry's anchor — usually an AstryxSection.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 selected in the semantics tree, so it is not conveyed by colour alone.
  • AstryxAppShell, with AstryxAppShellController and AstryxAppShellScope — the frame an application sits in: a full-width header, navigation beside the content, and the content itself. Below compactBelow the navigation moves behind a drawer, and the drawer is a real AstryxOverlay — 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 of useAppShellMobile: a header cannot know whether to draw a menu button without knowing where the navigation went, and that answer belongs to the shell.
    • compactBelow is 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.
    • header and sidebar take any widget. SideNav, TopNav and MobileNav are not ported yet, and the shell does not wait for them.
  • AstryxLayout, with AstryxLayoutPanelSide — 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 a Column. 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, so AstryxCode.span returns the same chip as an InlineSpan for Text.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. language is 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: true where 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 optional attribution (the em dash is the widget's), and a child for the quotations that are not one paragraph of plain text.

  • AstryxKbd, with AstryxKbdSize — one key cap or a chord of them. AstryxKbd('K') for the common case, AstryxKbd.chord for the rest. The glyphs are the caller's: this does not translate Ctrl to 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, and semanticsLabel is what turns ⌘ ⇧ P into a shortcut somebody can follow.

  • AstryxLocalizations.codeCopy and codeCopied, for the code block's button. Separate from textCopy, which is the selection menu's own verb.

  • AstryxItem, with AstryxItemDensity — the row the lists are built from: something at the reading-start edge, a label and a description, and something at the end. Ported from Item. A non-null onPressed makes it a button, the same rule AstryxCard follows; selected is 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's List and ListItem, where the item is AstryxItem and this is only the container: the dividers, the density every row inherits through a scope, and the list/listItem roles that let a screen reader say "Recent deploys, list, 3 items" before reading the first row. empty is what shows when there are no rows, because a list that renders nothing reads as a bug. It is a Column: it does not scroll and does not virtualise, the same limit AstryxTable carries.

  • AstryxTreeList, with AstryxTreeNode — 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, unlike AstryxRadioList — 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 on AstryxRadioList.
    • 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, with AstryxOverflowItem — a row that measures itself and moves its tail into a menu. An item carries both a child, drawn while it fits, and a label, which names it in a real AstryxDropdownMenu row 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 ValueNotifier rather than setState. 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, with AstryxMetadataItem and AstryxMetadataListDirection — the label-and-value pairs of a details panel, stacked or inline. A pair is one semantics node, so a screen reader reads "Owner, Ada Lovelace" rather than stopping between the two halves of one fact; semanticsValue is what a widget-valued pair announces, because a badge left to describe itself contributes one word of information.

  • AstryxEmptyState, with AstryxEmptyStateSize — 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 borrow AstryxBanner'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.overflowMore and overflowMenuLabel, for the overflow trigger. Deliberately not multiSelectorMore: 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, with AstryxSelectableCardControl — a card that is also a checkbox or a radio, for a choice that needs more than a line of text. Ported from SelectableCard. The whole card is the target; Space and Enter both activate it, as they do on a pressable AstryxCard. 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 rule AstryxCheckboxList applies to a checked row.

    • A radio card reports nothing when it is already selected, which is what a native radio does: reporting false would let a group end up with nothing chosen. A checkbox card deselects, as a checkbox does.
    • label is required and never painted, and the content keeps its own semantics nodes — the opposite of AstryxCheckbox, 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 name to group a set the way a browser groups native radios, so AstryxRadioList remains the control for four or more terse options.
  • AstryxSlider, with AstryxSlider.range, AstryxSliderMark, AstryxSliderOrientation and AstryxSliderValueDisplay — ported from packages/core/src/Slider/. min/max/step, marks, formatValue, both orientations, onChanged during a drag and onChangeEnd when it settles. A range keeps its thumbs in order and no closer than minStepsBetweenThumbs steps; each thumb is its own tab stop and its own announced slider carrying 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 fires onChangeEnd too, since there is no drag to end.
    • valueDisplay: tooltip shows nothing yet — upstream gets that bubble from the browser on hover. text is the visible-to-everyone alternative and is what the page demonstrates.
  • AstryxMultiSelector, with AstryxMultiSelectorTriggerDisplay — the same AstryxSelectorEntry options as AstryxSelector, because upstream shares those types between the two as well. A Set<T> value, checkbox rows, and a list that stays open as options are ticked. showSearch filters and drops a heading whose options all filtered away; showSelectAll ticks everything and clears it when everything is ticked; the trigger shows tokens up to maxBadges then "+n more", or a count. The field announces which options are chosen rather than how many.

  • AstryxComplexSelector, with AstryxComplexSelectorState — the headless half of upstream's selector family: this package draws the field, the trigger, the overlay, the focus trap and the barrier, and surfaceBuilder draws 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, with AstryxInputGroupText, AstryxInputGroupPosition and AstryxInputGroupScope — 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. AstryxInputContainer reads that scope, which is how a plain AstryxTextInput joins a group without being changed.

  • AstryxFormLayout, with AstryxFormLayoutDirection and AstryxFormLayoutScopevertical, horizontal (equal columns) and horizontalLabels. The last one is the only one that does more than space fields out: AstryxField reads 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; labelWidth is the honest Flutter version rather than laying every label out twice.

  • AstryxCheckboxList, with AstryxCheckboxOption and AstryxCheckboxListDensity — several independent choices under one label, one description and one validation state. Generic over the value type like AstryxRadioList, rather than upstream's string[]. Ported from packages/core/src/CheckboxList/: the checked-row tint, compact and balanced densities, optional dividers, per-row description, trailing, enabled and loading, and a group-wide readOnly that 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 are AstryxCheckboxes 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 over AstryxTextInput, ported from packages/core/src/NumberInput/. min, max, step, integerOnly, units, showClear, and a num? value so integerOnly yields ints and a fractional step yields doubles 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 parseNumberInput returning 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, with AstryxFile, AstryxFilePicker, AstryxFilePickRequest and AstryxFileInputMode — the chooser, the chosen list, and the limits, ported from packages/core/src/FileInput/ including validateFiles message for message and formatFileSize figure for figure. accept matches an extension, a type/* family or an exact MIME type; maxSize rejects; maxFiles truncates with a complaint; a caller's status beats 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 onPick asks the application to open one — the same shape as AstryxLinkDelegate. AstryxFile is a description (name, optional size and MIME type, plus an untouched handle) rather than dart:io's File, which does not exist on the web.
    • A file of unknown size passes a maxSize check: a reticent picker is not a large file.
    • dropzone mode 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's packages/core/src/ToggleButton/ToggleButton.tsx: always a ghost button, --color-overlay-pressed as the pressed fill, the label shifting to semibold, pressedIcon for an outline-to-filled swap, and the pressed state coming from the group when it is in one. Reports through onChanged like every other stateful control here, not upstream's onPressedChange.

    • 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 a TextPainter instead and renders one Text. Same stable width, one node.
    • Upstream's isIconOnly arrives as labelHidden, 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. pressedChangeAction is a React transition with an optimistic pressed state and no Flutter counterpart — drive pressed and loading yourself, as with every other control. children is not ported because the label is the text here, as on AstryxButton.
  • AstryxToggleButtonGroup, with AstryxToggleButtonGroupScope — several toggles acting as one control, the port of upstream's discriminated union on type. Two named constructors instead: .single takes a String? and clearing is reachable by pressing the button that is already on, .multiple takes a Set<String> and hands back a new set each time. Dart makes the wrong onChanged signature a compile error rather than a runtime surprise. A child's own enabled is ignored inside a group, which is upstream's behaviour (group?.isDisabled ?? isDisabled) reproduced and pinned rather than quietly improved; a grouped button with no value asserts 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 started now holds no Soon badges at all; the three that remain there are the deliberate N/A omissions.

    • Migration — what maps from Material and Cupertino, and what actually differs about each: IconButton requiring 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 /themes is, 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 that gothic renders 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.
  • AstryxVisuallyHidden has 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's VisuallyHidden both 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 measures Size.zero.

  • example/tool/gen_changelog.dart — parses CHANGELOG.md into lib/docs/changelog.g.dart as 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 an AstryxButton(destination:) uses; the web half is a conditional import in the shape of url_strategy.dart, and off the web it declines rather than guessing.

  • AstryxOverlay — the scrim-and-layer primitive, ported from upstream's Overlay. 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. alignment is 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. description is required, because a confirmation whose consequence is left to the title cannot be consented to. showCancel: false makes 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. waitDuration filters a mouse passing through, exitDuration is 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 same AstryxMenuEntry rows as AstryxDropdownMenu, with the same keyboard model, raised by a secondary click at the pointer or by a long-press on touch. maxWidth bounds 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, with AstryxCollapsibleController — a disclosure whose whole header is the button, carrying expanded in 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, and exclusive: true for 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 from dialogClose, 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 change pair 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.dart holds the coupling to the template field ids, and tool/gen_docs_md.dart builds the same URLs, so the markdown in doc/ 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 an Icons.* glyph — AstryxIconRegistry resolves everything through Lucide — but the flag shipped MaterialIcons-Regular.otf anyway, and Flutter web downloads every font in FontManifest.json before it paints its first frame. And the Firebase predeploy now builds with --wasm: skwasm.wasm is 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.

  • AstryxDialog is now AstryxOverlay plus a panel. The portal, the dismissal stack, the focus trap, the barrier and the animation moved into the primitive, so the two cannot drift; AstryxDialogController extends AstryxOverlayController and gains toggle(). No behaviour changed — the Phase 9 dialog tests pass untouched.

  • The menu surface and the row vocabulary moved out of dropdown_menu.dart into menu_surface.dart (internal) and menu_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 — and AstryxBlockWidth (internal) is the card's fill-a-bounded-width, shrink-to-fit-an-unbounded-one rule, moved out of card.dart. No pixel changed: the existing goldens pass untouched.

  • AstryxButtonSurface takes an optional selected, 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 it aria-pressed; Flutter's toggled flag is the switch's on/off, so selected carries it — the mapping SegmentedButton uses in the framework itself. Internal: AstryxButtonSurface is @internal and no call site outside the package can pass it.

  • gen_skill.dart publishes every written page except changelog and community. 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.dart picks a planned page rather than merely an unwritten one for its badge count. Getting started no longer has a planned page, 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 AstryxLocalizationsScope and useTranslator → AstryxLocalizations.of exceeded even a wide column by a few pixels. They are Flexible now, so a long title truncates instead.

  • AstryxInputGroup exposed 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: an ExcludeSemantics around the whole trigger. Removed before it shipped, and now pinned by a test that reaches the button by name.

  • AstryxTextInput's leading and trailing slots were invisible to assistive technology. The whole input container sat inside an ExcludeSemantics, which had been there to stop the editable announcing its content twice — and took the slots with it. The clear button showClear draws 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 building AstryxNumberInput, 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 input centred while Radio list sat at the reading edge, Timestamp centred one example and stretched the next, and Settings dialog was 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.dart enforces it, which matters because center is the default and an omitted align: 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 duplicate GlobalKey. 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 LayoutBuilder reflow — 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 on DocsController with 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: AstryxIconName names neither a monitor nor a phone, and it stays a transcription of upstream's IconName union 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.dart had fallen a release behind pubspec.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 an AstryxTable cell.
  • An AstryxGrid cell cannot hold a wrapped row of interactive widgets, or an AstryxTable. 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.com now 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 a DocStatusready, stub (ported, not written up), planned (not ported yet) or notPlanned (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.txt is every URL in upstream's sitemap.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-ignored scrape/, 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.dart carried 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 ports CheckboxInput rather than Checkbox.
  • 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.shouldInherit is 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: homepage and documentation now 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_ui followed by /plugin install astryx-ui@astryx-ui. The plugin's version is copied from pubspec.yaml when 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 from astryx_ui itself, with no Material anywhere: the navigation is a column of pressable cards, the example frames are cards, the Preview/Code switch is an AstryxTabList, the API references are AstryxTables. Pages are addressable by URL fragment on the web.
  • doc/ — the same content as markdown: 30 component pages under components/, seven guides under guides/ (installation, theming, design tokens, density, right-to-left, accessibility), and an index. Generated from the page model by example/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 by example/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/ by example/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 #

  • AstryxCard no longer asserts BoxConstraints forces an infinite width when given an unbounded width — inside a Row, an AstryxHStack, 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: AstryxApp and AstryxThemeProvider.
  • 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.dart for the theme layer without components.
4
likes
150
points
416
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

A Flutter design system, token-compatible with Astryx. Faithful theme engine, themeable components, built on flutter/widgets.

Homepage
Repository (GitHub)
View/report issues

Topics

#user-interface #design-system #widget #theming

License

MIT (license)

Dependencies

collection, flutter, lucide_icons_flutter, meta

More

Packages that depend on astryx_ui