seed_ui 0.24.0
seed_ui: ^0.24.0 copied to clipboard
A themeable Flutter widget library — 45+ token-driven components plus context-free message, notification, modal and drawer APIs. No Material dependency.
Changelog #
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.24.0 #
An API review before 1.0: one name for one idea, and a way to set every component's own defaults for a subtree.
Changed #
- BREAKING —
onChangeisonChangedeverywhere.Card.onTabChange,Collapse.onChange,Countdown.onChange,Pagination.onChange,Steps.onChange,Tabs.onChangeandTour.onChangeare nowCard.onTabChanged,Collapse.onChanged, and so on. Seven controls saidonChangewhere the other thirty-nine saidonChanged; a kit that names the same idea two ways makes the caller guess.Tabs.onTabClickkeeps its name — it reports a press, whether or not anything changed, and is not the same event. - BREAKING — the rest of the
onChangefamily.onOpenChange(onDatePicker,DateRangePicker,Dropdown,FloatButtonGroup,MultiDatePicker,Popover,SelectandTimePicker),Progress.onProgressChange,ProgressSteps.onStepChange,Pagination.onShowSizeChangeandPasswordConfig.onVisibleChangeare nowonOpenChanged,onProgressChanged,onStepChanged,onShowSizeChangedandonVisibleChanged. The first pass renamed the callbacks named exactlyonChangeand left twelve spelledonSomethingChange, which is the same defect with a longer name.PopoverLayeralready saidonOpenChanged, so it andPopovernow agree. - BREAKING — one spelling for the accessibility label.
Badge.titleisBadge.semanticsLabel: it was never a heading, it was what a screen reader announces, andtitlereads like something drawn.FloatButtonandFloatButtonItemsaidsemanticLabel; nine other components saidsemanticsLabel, which is now the only spelling. - BREAKING —
Result.subTitleandStepItem.subTitlearesubtitle. One word, as Flutter spells it — and as this package's ownsubtitleFontSizetoken already spelled it two fields away. - BREAKING —
SwitchSizeis gone;Switch.sizetakes aControlSize.SwitchSize.smalland.defaultSizebecomeSoftSize.smalland.middle, there is now a.large, and a height of your own works as it does on aButton. The two presets were six unrelated numbers that were not in proportion — the small track was 1.75 of its own heights long where the standard one was 2 — so they were not one switch at two sizes. A switch is now drawn from its track height alone: twice as long as it is tall, with a gap of an eleventh of that height at each end. At 22 that gives back exactly the numbers the design started with; the small one is 4px longer than before. - BREAKING —
Listy.shrinkWrapandSortableList.shrinkWraparesizeToContent. One name with opposite defaults in two components, and the name was Flutter plumbing rather than anything a reader could weigh. The defaults are unchanged — a list is normally the page, a sortable list is normally embedded in one — and each now says so in its own doc. - BREAKING —
Popconfirm.disabledisPopconfirm.skipConfirmation. Everywhere else in the kitdisabledbars the control; here it did the opposite — it took the guard off and let the trigger act unasked. A name that means one thing in twenty-five components and its reverse in the twenty-sixth is a trap, whichever way round it is read. - BREAKING — defaults can now be set for
Badge,Checkbox,Form,Listy,Radio,SpinandSwitch. Reaching them meant the props they cover had to become nullable, soBadge.showZero,Badge.overflowCount,Listy.stickyandForm.layout,labelAlign,colon,requiredMark,disabledandtriggerare nownullwhere they used to carry a literal. What they resolve to has not changed: the widget's own word, thenComponentDefaults, then what it always was. Only code that read one of those fields back off a widget instance is affected.
Added #
-
Seven controls can now be left to keep their own state.
Switch,Checkbox,CheckboxGroup,RadioGroup,Slider,RangeSliderandMultiRangeSlidertakedefaultValue/defaultValues/defaultChecked, as the other eight data-entry controls already did. Theirvalue(andCheckbox.checked) is nullable to make room for it.A null
onChangedstill makes a controlled one inert — nothing can change a value somebody else is holding, which is Flutter's own reading — but an uncontrolled one now changes whether or not anybody is listening. -
Four more screenshots on pub.flutter-io.cn —
Table,DatePicker,Formand the sliders — filling the gallery's ten slots. They are rendered by./tool/shoot.shrather than captured by hand, so they can be made again after any change; the scenes live intool/screenshots/shoot_test.dart. -
Sliders reach assistive technology.
Slider,RangeSliderandMultiRangeSliderhad no semantics at all: the handles are painted rather than built, so a screen reader found a box with no value in it, nothing to act on, and no name. Every handle is now a node of its own, standing where the handle stands, carrying the value, the two values it would move to, and increase/decrease actions that make the same one-step move as the arrow keys. A newsemanticsLabelnames the scale; wheretooltipformats the value the same words are read out. -
Pagination,Segmented,RadioGroup,AvatarGroupandAvatarDefaultstake aControlSize. They already worked out a height from the preset; now a height of your own works there too.Badge,Card,CollapseandTabskeepSoftSize, having no single height to set — their size chooses padding and type, and a bare number would name nothing.
Fixed #
-
Twenty-one more token fields that were named and never drawn with. The same defect as the
Inputone below, found by a check written for it and now standing guard: a field declared, documented, resolved — and never read. Thirteen of them now draw what they say:TagToken.defaultBganddefaultColor,SegmentedToken.itemHoverColor(an unselected segment darkens its words under the pointer, not only its fill),RadioToken.dotColor,buttonBg,buttonCheckedBgandbuttonColor,ProgressToken.defaultColor,InputNumberToken.handleWidthandhandleBg,SelectToken.selectorBg,optionFontSizeandborderRadiusLG.BREAKING — eight are gone, having named something the kit does not draw:
AvatarToken.groupSpace(groupOverlappingabove zero is the gap),DrawerToken.footerPadding(a drawer has no footer),PaginationToken.itemBganditemActiveBg(a page is aButton, and its fill is the button's),SelectToken.clearBg(the clear mark has no disc behind it),TabsToken.itemActiveColor,cardHeightandverticalItemMargin. The example's own theme was setting two of them, which is how little they did. -
An
Input's border can actually be given a colour.colorBorder,hoverBorderColor,activeBorderColor,colorTextandcolorTextPlaceholderwere declared onInputToken, documented, resolved — and never read: the field asked the theme for all five, so naming them did nothing. That is why a transparent border had no answer. -
A new
InputToken.focusRing. Unset it echoes the field's focused border at twelve per cent, as before; name a transparent colour for a field with no chrome at all — a border made transparent still glowed without it. The example's Input page now shows the whole ofInputTokenat work. -
An
Avatarwith no picture takes its fill from a token. It was0xFFCCCCCC, written into the widget twice — the one thing in a token-driven kit a theme could not move.AvatarToken.bgnow names it, and an avatar's ownbackgroundColorstill wins. -
The published screenshots show real shadows.
flutter_testturns blur off so goldens stay stable across platforms, which draws every shadow as a hard-edged slab — the date picker's panel came out sitting on three grey rectangles. The harness turns it back on while it paints. -
The theme's font reaches every word.
SeedToken.fontFamilywas honoured by the fifty-odd text styles components build from nothing and lost by everything that merges into what is already in force — a form label, a card title, the words a caller passes in.ConfigProvidergoes outsideMaterialApp, whose ownDefaultTextStylethen sat below it carrying the platform font, so one app was drawn in two faces.materialThemenow names the family, the styles that were missing it carry it, and a test refuses any new style built without it. Rendering the screenshots is what showed it: the form's labels came out in the wrong font beside its fields. -
Every control down a form is the same width. A
Selecttook the width of its widest label where a text field took the whole column, so a form of both came out ragged. -
A
Tagnarrower than its words is cut short rather than overflowed. In a table cell or aWrapthat has run out of line it painted a striped overflow banner across the row; it now ellipsises, and keeps its natural width wherever there is room for it. -
A
FloatButtongiven no colour is the page's surface lifted off the page, rather than a solid black disc. It wassolid+defaultColor— the kit's inverted button — which is a strong thing to be the default for a button most callers write with no arguments at all. Name acolorand it is solid in that colour exactly as before, and a colour set throughFloatButtonDefaultscounts as naming one. -
Three small controls stop wearing a popover's shadow.
boxShadowSecondaryis the three-layer shadow a panel floats on — twenty-eight pixels of blur and eight of spread. Around aSwitchhandle it read as a grey disc beside the switch; under theSegmentedthumb it spilled below the control as a grey band; behind aFloatButtonit was a smudge the size of the button. Each now has a lift of its own, with a token to override it —SwitchToken.handleShadow,SegmentedToken.thumbShadowand theFloatButtonToken.shadowthat was already there with the wrong default. The twenty-odd other uses are panels, drawers, modals and drag feedback, where the shadow belongs. -
DatePickerpreset labels take the theme's font. -
A table narrower than its columns no longer overflows its box. The widths were worked out from what the cells wanted and handed on unsqueezed, so everything laid out by hand against them — a summary row, a run of merged cells, a heading of more than one row — drew wider than the table and threw a
RenderFlexoverflow. On a phone that was most of them: the demo page threw a dozen at 390 wide. Columns that sized themselves now give back what they asked above their floor, and a table whose floors alone will not fit takes the width it needs and scrolls sideways — which is whatscroll.xasks for, without having to know the number in advance. The same goes for the columns beside a pinned one. -
ComponentDefaultsreachesDateRangePicker,MultiDatePickerandMultiRangeSlider. All three asked for defaults nobody could supply: the lookup had no slot to answer from, so it always came back null. -
Seven defaults classes are exported.
BadgeDefaultsand its six companions were registered but unnameable outside the package, which is the same empty promise from the other end. Two tests now stand guard: every defaults a component reads must be reachable throughComponentDefaults, and every one registered there must be exported.
0.23.0 #
Added #
-
zones,boundsandsnapToMarkson all three sliders.A
SliderZonecolours a stretch of the scale — a safe heart rate, a budget already spent — drawn on the rail and under the track, because the zone colours the scale and the track is the answer.boundssays how far the handle may go when that is less than the whole scale. The scale still shows what it showed: narrowingminandmaxwould hide the rest of the day, which is not the same thing to say. A draggable span obeys it as one.snapToMarkslets the handle rest on the marks as well as on the steps — without a step the marks are already the only stops, and with one a mark at 33.4 on a scale of whole numbers could be read and never reached. A disabled mark is no magnet either. -
SliderMarksays more about itself.hiddenleaves a mark undrawn without taking it out of the list, asTableColumn.hiddendoes for a column.disabledmeans the handle may not rest there — which bites where the marks are the only stops there are, a slider with nostep, and is only a colour where there is one.sideputs the label before or after the rail, named by the flow rather than by the screen so there is no way to ask for a side that means nothing.SliderMark.dotis a stop with nothing to say. Marks are value types now, withcopyWith,==andhashCode. -
SliderMark.markBuilder, handed what the slider would have drawn and whether the handle has reached that mark — the shapecellBuilderandtagBuilderalready use, so wrapping keeps colour, size and state for nothing. A mark with no words is handed a box standing on the rail where its dot is — the dot being the thing anybody would press — so a menu or a popover can be hung on one. A mark takes a pointer only where it has been given something to do: a plain mark's label sits in the band beside the rail, and pressing the scale still moves the handle wherever the marks are. -
SliderToken.markColor,markDisabledColorandmarkFontSize. What a mark is drawn in belongs with the rest of the numbers;SliderMark.stylestays for the odd mark that has to stand out. -
RangeSlider.draggableTrack— the filled span moves as one, keeping its length. Only strictly between the handles, so they keep their own drag; pushed against an end the shift is cut back as one rather than the leading handle stopping while the trailing one goes on. -
MultiRangeSlider— handles you can put in and take out, withminCount,maxCountandMultiRangeSliderDefaults. Its own component rather than a flag: a pair of handles is a(double, double)and this is aList<double>, and a list of two is not the same promise as a pair. A tap on the rail puts a handle in and a tap on a handle takes it out — a tap on a handle has nothing else to mean. Taking one out is dragging it onto its neighbour — the two meet, one goes, and it is drawn faint on the way; a second tap would make a handle a switch and hold the first tap back behind the double-tap window, and a press held would carry a handle off under the finger of anybody who paused before moving it. The values come back in order however they were dragged or added.Slider's "Not here yet" is empty with it. -
MultiDatePicker.maxTagCountResponsive— the tags keep to one line, as many named as fit and the rest counted, worked out from the room the field has rather than from a number decided in advance. The line isSelect's own, pulled out intolib/src/utils/tag_line.dartrather than copied — the third such move after the clear mark and the tag itself, andSelect's tests pass untouched. It takes a trailing child where a control has one, a caret to type into, and manages without where none does.The line takes what the tags put on it rather than the width it was offered: claiming the offer meant a line under a loose parent — a page, a column that hands its children no width — took the whole of it, so everything fitted, nothing hid, and the control grew instead of collapsing. A tag squeezed into less room than its words want now ellipsises them instead of spilling over its own edge.
-
MultiDatePicker— a field that collects any number of days, withMultiDatePickerDefaults. Its own component rather than a flag: the value is a list, the field carries a tag for each day, and the panel stays open — picking a second day is the ordinary next thing to do, not a fresh start.The days come back earliest first whatever order they were pressed in, and the clock is dropped on the way in so the same day twice over is held once.
maxCountbars the rest of the panel once the list is full rather than refusing a tap that looked available, and a day already in can still be taken out.maxTagCountnames some and counts the rest.The panel is
DatePicker's, through the samePanelHostthe range picker uses; the tag isSelect's, pulled out into one widget rather than written twice.The field takes what it is holding rather than filling the page: as wide as its tags, or its placeholder while it holds none, wrapping when it is offered less and filling a width it is told outright.
tagBuilderdraws one tag given the picker's own, withDateTagcarrying the day, its label, whether the field is usable and how to remove it;removeIconreplaces the cross without touching where it sits or what it does. The overflow count is worded as aSelect's is.The panel draws what the owner settled on rather than what the tap asked for: it is redrawn a frame behind, so a controlled picker marks the day on the tap that took it instead of lagging a tap behind, and an owner refusing a change is shown refusing it.
DatePicker's "Not here yet" is empty with it. -
DatePicker.picker—week,month,quarterandyearalongsideday. The value stays aDateTime, the first day of whatever was chosen: a week is its first day counted from wherever the locale starts its weeks, a quarter the first day of its first month. Nothing here needs a type of its own — a week is a day you can add seven to. The panel opens at the depth that suits and stops there, and a week picker marks the whole row, since one press on any day of it is the same answer. -
DatePicker.cellBuilder, withDateCellandDateCellBuilder, on both pickers. The builder is handed the day, what the panel knows about it, and the mark the panel would have drawn — so a dot under a booked day is aStackover that child rather than a reimplementation of chosen, today, hovered and barred. -
DatePickerToken.mainAxisSpacingandcrossAxisSpacing— how far apart the days stand. The air is added around the cell rather than taken out of it:cellWidthandcellHeightare how big a day is, so asking for more air parts the days instead of shrinking them, and the panel grows by it. The band a range draws spans the whole pitch — the day and the air beside it — so a stretch still comes out as one band. -
wwandQin the date grammar, withweekOfYear,startOfWeek,quarterOfandstartOfQuarterbesidedateOnlyandaddMonths. Weeks are ISO — Monday to Sunday, week one holding the first Thursday — because the other reckoning gives a week 53 that is one day long. Quarter names come from the locale, in all eleven languages. -
DateRangePicker— a field that collects a stretch of days, withDateRange,DateRangePresetandDateRangePickerDefaults. Its own component rather than a flag: a range is picked in two goes, the second constrained by the first, and drawn as a band rather than a mark.The panel shows two months side by side — a range that crosses one is the ordinary case, and turning the page mid-drag loses the thread. The first tap puts an anchor down and the band then follows the pointer, so the stretch is visible before it is taken; nothing is handed back until both ends are in.
minDaysandmaxDaysare told while the second end is being chosen, so what would make too short or too long a range is greyed rather than refused after the tap. A range built the wrong way round turns itself over.The panel itself is
DatePicker's, not a copy of it: the grids and the header are built against aPanelHost, and the two pickers differ only in what they say about each day.startDateandendDatejoin the eleven languages. -
DatePicker's panel is now shared.PanelHost,PanelCap,DayGrid,MonthGrid,YearGrid,PanelHeaderandDatePanelStyleare the pieces a second picker builds on. None are exported: they are the kit's own scaffolding, likeTimeColumns. -
FormItem.dependsOn,FormRule.matchesandFormRule.against— fields that lean on one another. A rule comparing two fields goes stale the moment either moves, and the message sits on the field nobody is touching: change a password and the confirmation below it went on saying they differ. Naming what a field leans on has it asked again whenever one of them changes — typed into,setValueorsetValuesalike — and only once it has been asked at least once, so an unanswered form stays quiet. Amatcheswhose field is missing fromdependsOnthrows in debug rather than leaving a stale message to be found by looking.formMismatchjoins the eleven languages. -
Form.maxWidth. A form fills what it is given, and a wide page gives it the window — a line of boxes with one word in each. Naming a width caps the form against the leading edge. -
Form.maxWidth. A form fills what it is given, and a wide page gives it the whole window — a line of boxes a thousand pixels long with one word in each. A named width caps it and leaves it against the leading edge; a window narrower than the cap still gets all of it. -
FormList— a field that repeats. A list of rows the reader adds to and takes from, withadd,addAt,removeandmovehanded to the builder. Rows are known by a key rather than by their place: named after the index, taking the middle row out renumbers the last one into the name the middle row was using and its value slides up with it. The list's own rules count the rows, soFormRule.min(2)asks for two. A row of one field comes out ofvaluesas a value and a row of several as a map. -
TableColumn.showFrom— the width the table must have before that column is drawn.hiddenis a word said in advance; this one is a word about the room, so a column that only earns its place on a wide screen stands down on a narrow one instead of squeezing everything. It keeps its place among the columns you listed while it is down, ashiddendoes, so a sort or a filter keyed by that place goes on meaning what it meant.Which columns there are was settled before anything had been laid out; the table asks for its room first now, and everything else follows from that. A table given no width of its own — inside a horizontal scroll view — draws every column, there being nothing to be too narrow for.
The gallery has a group for it: narrow the window and the columns stand down one at a time, with the table's own width read out beside them.
-
DatePicker.showTime. The scrolling columns now stand beside the calendar, and they are the very onesTimePickershows — pulled out into one widget rather than written twice, so a time is picked the same way wherever it is asked for. Nothing is handed back until Ok, a day picked twice keeps the hour chosen in between, and Today becomes Now.disabledTimerefuses values in the columns as it does onTimePicker. The panel measures itself, so a longer format, a widertimeColumnWidthor a broadfooterBuilderall just work. -
DatePicker.presetsandDatePreset— a rail of named dates beside the panel.DatePreset.ofworks its date out when the preset is taken rather than when the panel is built, so a picker open across midnight cannot hand back yesterday. A preset landing on a blocked day is greyed, as Today already was. -
DatePickerToken.presetsWidthandtimeColumnWidth.
Fixed #
-
A mark taller than a small control was half dead to the pointer. The band was told a height rather than measured, so anything taller hung out of it: drawn, since the band does not clip, and unhittable, since a hit outside a box is no hit. The labels size the band themselves now — which also does away with the silent duplicate of every label that used to be laid out to give the band a width, and with the hand-written
ExcludeSemanticsthat stopped a screen reader reading the marks twice. -
A handle could hardly be dropped onto its neighbour. They counted as met once their values were equal — half a step, which on a scale of a hundred is two pixels of rail. The only way out of an added handle went through a catch nobody could hit. They meet once the discs cover one another now, with half a step kept as the floor for a scale whose steps are wider than a handle.
-
A draggable track swallowed the handles inside it. Which the press had taken hold of was decided by the value under it — strictly between the ends meant the track. That reads right for two handles, whose ends are the span's ends, and is wrong the moment a third stands inside: every drag meant for it moved the whole span. It is asked of the handles themselves now, and a handle always wins.
-
A slider moved a handle on the press rather than on the tap. A press is not yet a tap: pressed and then dragged, the nearest handle had already been moved under the finger — which is what left a draggable span no longer under the press by the time the drag began, so it behaved like an ordinary range slider. It acts on the way up now.
-
A control's own addon did not join the run. An
Inputcarries its search button attached, and that button worked its corners out for itself — so at the joint it rounded an edge another control was standing against, and the two met as two boxes. The addon asks the run for its corners now, as the control it belongs to already did.Compact's "Not here yet" is empty with it. -
Only the last control of a
Compactlooked highlighted. A joined run overlaps its controls by a line so their meeting borders draw one line, and whichever is painted later covers its neighbour's edge — which is exactly where the ring a control draws on hover or focus lives. The run now paints the control that has something to show last; it is still laid out in its place, and nothing else about the flex changes. -
A grey flash under the finger when picking in a
Select. The pointer is on the row when it is pressed, so the fill eased from the hover grey into the chosen colour — and back the other way when a press in a many-valued select took the value out. Only the row whose answer moved now lands at once; the rest go on easing their hover. The calendar's cells were fixed the same way and for the same reason. -
Selectfilled whatever width it was given. It now takes what it needs — the widest label it knows about, not the one it is holding, so the field does not change width every time somebody chooses something else — gives way when it is offered less, and fills a width it is told outright. The pickers already followed that rule; three controls of the same kind now behave the same way beside each other. In a mode holding several values the tags decide their own size, since they are wider than the labels inside them. -
The calendar's weeks ran into one another. A day cell was exactly as tall as the pill inside it, so the rows touched — invisible until a range drew a band across them, and then a month read as one grey block instead of six weeks. The cell is a shade taller than its pill now (
cellHeightdefaults tocontrolHeightSM + sizeXXS), and the band is only as tall as the pill, so the weeks keep a line of air between them. -
A day inside a range flashed grey under the pointer. The hover tint was the grey a day standing on the panel's own ground takes, painted over the band. It is a deeper tint of the band's own colour now.
-
A
Select's clear mark drew its cross low in its disc. The mark took its disc from the width of the box and its cross from the height, which agrees in a square and nowhere else — and a row that makes its children the height of the tallest hands it a box twice as tall as it is wide. Both are drawn from the shorter side and about the middle now, so the mark is right in any box it is given, and the button no longer lets itself be stretched. The kit had two classes calledClearIconPainter— one among the icons, whichInputused, and a copy insideselect.dartused bySelectand the two pickers. Fixing one left the other wrong, which is how a mark can look right in one control and crooked in the next. There is one now, among the icons where it belongs. -
A cleared value came back. Clear, pick a date, clear again: the picker handed back null and went on showing the date. A picker keeps a value of its own as the fallback for
value—value ?? _internal— and only wrote to it whilevaluewas null, so a date picked during that spell stayed in the fallback for good and showed through the momentvaluewent null again, which is exactly what clearing does. The fallback is kept in step now, whether or not somebody else is driving the control.TimePicker,Select,InputNumberandCheckableTagGroupall held the same shape and are fixed with it. -
A
DatePickerorTimePickercleared on the second click. The mark is drawn while the field is hovered, and hovering is a rebuild — so the mark was not there yet for a pointer that arrived and clicked in the same frame, which is what a mouse coming from the panel above does. The first click went to the calendar beside it and opened the panel; the second cleared. The slot now holds one target whenever there is anything to clear and decides what to do when it is tapped, by which time the hover is known whether or not a frame has been painted. The mark also shows while the panel is open, as aSelect's does — and its target is now the height of the field rather than the fourteen pixels of the glyph, which is what anybody aiming a little high or a little low was missing. -
No
Inputcould be selected with a pointer. The editable is built withrendererIgnoresPointer, and what stood above it was a tap-to-focus gesture and nothing else — so dragging across the words, double-tapping one and long-pressing for the toolbar all did nothing, in every field the kit has. Text could be read and never copied, which showed up first on a password revealed with the eye. Flutter's own selection gestures are in place now, the ones a Material field uses.The field is also named on its own node rather than through an ancestor: an editable gathers what is inside it and stops there, so a name written above it was read out as a caption standing beside an unnamed box. The placeholder names the field once and is no longer announced twice.
-
A dressed row lost the line under it. Flutter's
Tablepaints a row's decoration behind its cells, so a ground given byrowStyleorcellStylecovered the rule and the rows ran together. The rule rides the cell now, in front of whatever the row is wearing. Pinned by a pixel test, since painting order is a question no widget tree answers. -
A panel told its height opened all at once. A lazy body used to reckon the run of rows with one height for every panel, which left no room for a panel part of the way open. It keeps a running total over the panels instead, so a panel opening is only a term of that sum on the move, and the reveal is as smooth as it is in a table that builds every row.
-
A panel told its height pinned its words to the top. A named height is usually taller than what is in it; the content is centred down the panel now.
Changed #
-
The package ships what it is used with.
test/andtool/no longer go into the archive — they are the largest thing in the repository, one file of tests being three hundred kilobytes, and nobody who depends on the kit runs them. The documents and the example stay: the README links into the documents forty-eight times, and a link that only works on GitHub is half a document. -
DateRangePickerandMultiDatePickerare in the README's table, and the doc gate now fails when a component is missing from it. The table is what somebody sees on pub.flutter-io.cn before they see anything else, and two components had already gone missing from it the quiet way. -
The package's own description said "30+ components". It is 45+.
-
Breaking:
SliderMark's label is aString?rather than aWidget.SliderMark(20, Text('20%'))becomesSliderMark(20, '20%'); anything that was not words ismarkBuilder's job now. Nearly every mark was aTextwrapping a string, and the kit spells that the same way everywhere else. -
The table's document no longer claims two things that stopped being true. A width dragged in a stretched table is the width drawn — measured at 300 to 180 in a 600-wide table, the freed room going to the neighbour — where the document still called it a share; and the keyboard is no longer "not here yet". Both are pinned by tests now, so neither can drift back unnoticed.
0.22.0 #
Added #
-
Button,Checkbox,RadioandSwitchwork from the keyboard. They take their turn in the tab order and answer Space and Enter, and each wears a halo — the one a focusedInputalready wore — when the focus arrived that way. Until now Tab did not reach a button at all and Space did nothing to it, which on the web and the desktop is not a nicety.focusNodeandautofocuson each, for a focus you drive yourself. A button that can do nothing is not a stop on the way round, and neither is a disabled control.ButtonandSwitchgained the semantics to match. -
ThemeData.refineandToken.copyWith— a way to name a derived value outright. Until now the only inputs to a theme were the seeds, and a value that is itself derived could not be reached at all: the disabled ink is a quarter of the page's own ink, and to change it you had to movecolorTextBase, which drags the whole typography and every border with it.ThemeData( token: const SeedToken(colorPrimary: brand), refine: (t) => t.copyWith(colorTextQuaternary: disabledInk), )It takes the derived tokens rather than a list of values, so it can read
t.isDarkand name the light and the dark in one line — which matters here, since the disabled ink is black at a quarter in the light and white at a quarter in the dark. It survives inheritance: a nested provider that flips the brightness re-derives and is refined again. -
InputNumber,MessageandPopconfirmfinish the round. A number field offers increase and decrease — the arrows are pictures — and offers them only where there is a number to step, since a node that can be increased must say both what it holds and what it would hold after. A message is a live region, so its arrival is announced where the reader stands rather than waiting to be found. A popconfirm scopes and names a route from its own question, as a modal does. -
A tree node, an accordion header and a tooltip say what a picture was saying. A node reports whether it is picked and whether its branch is open; a leaf says nothing about opening rather than claiming to be shut. A header reports whether its panel is open. A tooltip carries its words on the thing it describes, whether or not the bubble opens — it appears on a hover nobody hovering with a keyboard can make.
-
A modal and a drawer say a window has opened, and name it. Both scope and name a route from their own title, so a reader is told what they have entered instead of meeting loose text over a dimmed page — an overlay is not a route the navigator announced, and nobody said it otherwise. A tab says it can be pressed and whether it is the one showing.
-
Selectand the pickers say what they are. A select said nothing — no name, no role, no news of the choice just made. It now reads as a button that is open or shut, named by itssemanticsLabelor its placeholder, with the chosen options spoken from theirfilterText, the plain words an option already carries for searching. The pickers do the same, and leave the value to the editable inside rather than saying it twice. An error status marks any of them invalid, and aFormhands each of them its label. -
A field says its name and its trouble out loud.
Input.semanticsLabelnames a field whose label is written outside the box — aFormfield's, a label in a column beside it — where before a screen reader heard "text field" and had to guess which one.InputStatus.errormarks the field invalid, so a red border is not the only way the news arrives; a warning is not an error and is not marked.FormItemhands its control the name, andFormFieldHandle.semanticsLabelpasses it to a control you build yourself. A label built of widgets rather than words is left unread instead of guessed at: half a label in a reader's ear is worse than none. -
The small things that answer a press answer the keyboard too — a tag's cross, an alert's, a notification's, a tour's step dots. Each is a stop with a ring round it, answers
SpaceandEnter, and says what it is out loud: the kit carries the word for close in every language it speaks, since a cross drawn as a glyph says nothing to a screen reader. -
UploadandStepsanswer the keyboard. An upload's drop zone is a stop and opens the picker onSpaceorEnter, wearing the outline a hover draws; each file's preview, download, retry and remove are stops of their own. A run of steps that answers a tap is one stop walked with the arrows, and a run that only reports progress is no stop at all — it is a picture, and a picture is not somewhere to stand. -
The pickers open and are walked with the keyboard.
↓orEnteron the field opens the panel. In aDatePickerthe sideways arrows step a day, the upright ones a week,PageUp/PageDowna month,Entertakes the day andEscputs the panel away; a barred day is stepped over and the month follows the cursor. In aTimePickerthe upright arrows step the column and the sideways ones move between them,Enterconfirms.The day the keyboard rests on wears the mark the pointer leaves, and only once a key has been pressed: a grey box on a panel nobody has walked yet reads as a mistake.
-
A tap in a table carries the keyboard cursor with it. Tapping a row left the cursor wherever it had been, so the next arrow press moved from somewhere else entirely — tap the third row, press down, and the first row answered. The hand and the keyboard share one cursor now, row and cell alike.
-
A table is read across as well as down. The keyboard cursor walks cells now, not only rows: the sideways arrows move along a row and outline the cell they land on, and
Enteracts on whatever that cell is — the box picks the row, the chevron opens it, anything else is the row itself. What stands in a cell was out of reach before.Spaceon a heading opens that column's filters. Two keys for the two things a heading does, since a reader who cannot see the funnel had no way in at all and overloadingEnterwould make which one happens a guess. -
A
Tableis walked with the keyboard. One stop for the whole table — a stop per row would be a page of stops — and the arrows do the walking.↓/↑move between rows,Home/Endreach the ends,Entertaps the row the cursor rests on,Spacepicks it, and the sideways arrows open and shut a row that opens.↑off the first row lands in the head, where←/→walk the headings that answer andEntercycles the sort.The cursor wears the mark the pointer already left, on rows and on headings alike: one look, whichever hand is on the table. It rides the notifier the pointer uses rather than rebuilding the table, since a lazy body has no row widget to rebuild and every cell on the page would go with it.
A cursor per cell is not here, nor a key that opens a filter panel; the document says so.
-
A
Treeis walked with the keyboard, and an accordion's headers are reached by tab. The tree is one stop:↓/↑walk the nodes on show,→opens a shut branch and then steps into it,←shuts an open one and then steps up to its parent,Enterchooses. The inward and outward arrows follow the reading direction.Collapsegives each header a stop of its own instead — the panels of an accordion are separate sections rather than one choice among several, and arrowing between them would say they were alternatives. -
A
Dropdownmenu is walked with the keyboard. A downward arrow on the trigger opens it; the panel then takes the focus, so↓/↑walk the rows,Home/Endreach the ends,Entertakes the row the keyboard rests on andEscputs the menu away. Barred rows are stepped over and the ends hold.A highlight, not a focus per row — forty rows that each took a
Tabwould be forty presses — and it is the mark hovering already left, soitemBuilderis toldhoveredfor it too: to a row, being pointed at and being rested on are the same news. Enter and Space are left to the trigger, which is usually aButtonthat answers them itself. -
Segmented,TabsandPaginationare walked with the arrow keys. Each is one stop in the tab order rather than one per option — a bar of fourteen tabs that took fourteen presses to walk past is a bar nobody walks past — and inside it the arrows move,HomeandEndreach the ends, and what cannot be chosen is stepped over.Which arrow steps which way follows the run and the reading direction: down a column it is Up and Down, and along a run that reads right to left the key pointing left steps on. A run stops at its ends rather than wrapping round. In a pager the arrows and numbers are the run, while the size changer and the jumper keep stops of their own.
focusNodeandautofocuson each.
0.21.0 #
Added #
DropdownToken.itemHeightandDropdownToken.itemPadding— how tall a row is at least, and how far its contents sit from its edges.
Fixed #
-
A
Timelinegave half its width to a label column, whatever was in it. Onelabelanywhere turned the run two-sided, and two-sided meant an even split — so a timestamp, or an icon, took half the row and left the content in a column half as wide as it should be. The label column takes the width of its widest label now, the same in every row so the axis stays one straight line.alternatestill splits evenly, since there the content takes both sides by turns; so do grouped rows, which cannot share the measuring. -
Button(block: true)threw inside aRow. It asked for an infinite width, which a row has none of. The stretching is the box's job now rather than the button's own row: given a width it fills it, given none it takes the width it wanted anyway.Expandedstill makes it fill a share of a row. -
A row drawn by
itemBuildercould not be taller than the kit's own. The row named a fixed height, so a builder asking for 52 was squeezed back tocontrolHeight— drawing a row inside a box you cannot resize is not drawing it yourself. The height is a least now, and a builder that asks for more takes it. -
A dimming barrier left its trigger undimmed. The barrier leaves a hole over the anchor so the trigger stays tappable while the card is open — but it was built from four strips that painted the dimming too, so the anchor was the one lit thing on a dimmed page and read as a mistake. The wash is one piece across the page now; only the tap-catching is cut away.
0.20.0 #
Added #
-
DropdownToken.shadow— what the panel casts, and an empty list casts nothing. With that, a transparentmenuBgand aborderRadiusof zero, the panel is invisible and apopupRendercan put a surface of its own where it was. Until now the background and the corners could be blanked but the shadow could not, so a menu styled that way kept a shadow nobody asked for. -
DropdownToken.gradient— a wash overmenuBg. Being a token it reaches every panel of the menu, so a submenu carries the same wash as the menu it opened from, which apopupRendercannot do: that dresses the panel it is handed and no other. -
DropdownToken.gap— how far a panel stands off what opened it: a menu from its trigger, a submenu from the row it belongs to. It wassizeXXSeither way and could not be said otherwise, so a theme with no size unit left them touching, reading as one surface rather than two. -
DropdownToken.border— a line around the panel, none by default. With it, a menu's background, corners, border and shadow are all token fields, so a house style is said once on aConfigProviderrather than built at each call site.
Fixed #
-
A submenu stood off its row, not off the menu it came out of. A row is inset from the panel by the menu's padding, so the gap came out that much smaller than it was asked to be — and with a padding wider than the gap the submenu landed on top of the menu it opened from. It is measured from the panel's edge now, while still lining up with its own row.
-
A submenu was dressed by nobody. It opened without naming the row it came from, so nothing it stood in reached it: a menu styled by a provider — or by a
token:resolved from one — opened a submenu in the kit's plain chrome. -
A popover was built outside the providers that stood over its trigger. An overlay is mounted above the app, so a
ConfigProvideraround one screen reached that screen but not the menus, option lists and picker panels opened from it — a screen that dressed its dropdowns found its dropdowns undressed, and only a provider at the root worked. What stands over the anchor is now carried over the panel, for every popover in the kit.
Changed #
- A panel with no rounding is no longer clipped. The clip is there so a
row's hover fill stops at a rounded corner; with no rounding there is nothing
to stop, and clipping anyway cut off whatever a
popupRenderdrew outside the panel — which is what somebody blanking the chrome is trying to do.
0.19.1 #
Fixed #
-
DropdownTokenwas mostly ignored. Atoken:on the dropdown itself reached only the dismiss barrier:menuBgandborderRadiushad to be set through aConfigProviderto do anything, andpaddinganditemHoverBgdid nothing wherever they were set — the menu drew its own inset and its own hover colour and never asked. The token now reaches the panel, the rows and any submenu, and every field it names is read.A
ConfigProviderinsidepopupRenderstill cannot restyle the panel, and that is by design: the panel is drawn around what the builder returns. The document says so now.
Changed #
- The documents keep to one shape, and CI holds them to it. Every one opens
with its widget's name, a sentence saying what it is, and an example; every
component with a
*Tokenclass ends with## Design tokens, spelled that way and no other.## Tokens,## Sizeand## Not yetare gone as second spellings of headings that already existed, andDropdown,SpinandNotification— which had token classes but no section naming them — have one. What goes between the ends is still the writer's: a forced## Propertiesin each of forty-four documents would flatten the prose for nothing. What is fixed is what a reader, or a program reading for one, must be able to find without guessing.
0.19.0 #
Added #
-
Dropdown.itemBuilderdraws the inside of every row, submenus included:itemBuilder: (context, item, hovered) => Row( children: [ Expanded(child: Text(item.label ?? '')), if (hovered) Text(counts[item.value].toString()), ], )The row's height, its highlight, the caret marking a submenu and the tap that chooses an item stay with the menu, so a builder is never asked to rebuild the machinery in order to change the look. Colour and text style are set around it, so a builder returning bare words is still greyed out when the item is barred and red when it is dangerous.
hoveredis the one thing a builder could not work out from the item it is handed.
Changed #
-
DropdownItem.labelandDropdownGroup.labelareString, notWidget. An item is what the menu is told; how it is drawn isitemBuilder's business. Kept as data a label can be read — searched, sorted, spoken to a screen reader, handed to a builder that draws it beside a count — none of which a widget could be. A label longer than its row is now cut with an ellipsis rather than running off the end.// before DropdownItem(value: 'edit', label: const Text('Edit')) // after DropdownItem(value: 'edit', label: 'Edit')A label that was more than words belongs in
itemBuilder.
0.18.0 #
Added #
-
Compact— joins a run of controls into one: square where they meet, rounded only at the ends, and a single line at the seam rather than two.Compact( children: [ Expanded(child: Input(placeholder: 'Search')), Button(icon: const Icon(Icons.search), onPressed: search), ], )Each control asks
CompactSlot.radiusOf(context, radius)which corners to draw, so a widget of your own joins in by asking too, and one that never heard of the group still stands in the run.Button,Input,InputNumber,Select,DatePickerandTimePickerask, and so does a wholeRadioGroup(optionType: button), which divides its outer corners between its two end buttons.The seam is closed in the layout rather than the painting, so the run is a line narrower and ends where its last control ends. Corners and seam both follow the reading direction. A row centres its controls rather than stretching them, so it stands in a
Wrapor an unmeasuredColumnwithout asking for a height;Expandedchildren keep their flex;block: trueshares the whole width; anddirection: Axis.verticalruns down a column.
Fixed #
-
A run of radio buttons stood a pixel taller than a button beside it. Each button centred its border on its own edge, so half a line hung outside the box top and bottom. The borders are drawn inside now, as every other control in the kit draws them, and the single divider between two buttons comes from their overlapping rather than from their strokes hanging over the join.
-
Selectstood two pixels taller than every other control of its size. The arrow named the control's height as its own, which is the height of the content, and the border then stood outside that.
0.17.0 #
Added #
-
Form— a set of named fields, the values they hold, and the rules they answer to.FormControllerholds the values and does the doing, so a button anywhere can submit;FormRulecovers required, min, max, pattern, email, url and anything of your own, each with the kit's words in eleven languages and yours where you give one.Form( controller: form, onFinish: save, child: FormItem<String>( name: 'email', label: const Text('Email'), rules: const [FormRule.required(), FormRule.email()], builder: (field) => Input( value: field.value ?? '', status: field.status, onChanged: field.didChange, ), ), )The control is built rather than handed over, since a form has to put the value into it and take changes back out and Flutter gives nobody a way to reach into a widget somebody else built.
field.statusis what a control that can recolour itself wants, so wiring one up is three lines and no colours.Ten named kinds save the wiring where the control is one of the kit's own:
FormItem.text,.number,.check,.toggle,.date,.time,.select,.selectMany,.radioand.slider. Each names its own type — they are static methods rather than named constructors, since a constructor ofFormItem<T>cannot fix whatTis — and draws the control that suits it, so the three lines every field used to repeat are gone.Four decisions worth knowing: the first refusal is the one shown; a field that has been told off is watched from then on whatever the trigger says; submitting puts the keyboard away before anything is decided; and a barred field's rules are not asked, since a required one the reader cannot type into would refuse the form for ever.
-
Input.value— the text the field is to show, for a caller that holds it.defaultValueis a starting point and cannot be changed afterwards, which is not enough for a form doingreset(). -
Button.onLongPress— held rather than tapped, as its own callback rather than a second reading of the tap. A button given only a hold is enabled and looks it: it does something. -
Button.feedback— whether a tap plays the click the platform plays and a hold gives the shudder it gives, which is what every other button on the device does. The kit says yes;falseis for a button that fires often enough that a noise each time would be a nuisance. -
ButtonToken.shadow— what a button casts. Nothing until it is named, which is how buttons here have always looked; named, it is cast by the variants that stand on a ground of their own and never by the flat ones or by a disabled button.
Fixed #
-
Selectopened its menu with the highlight on the first row, whatever was chosen, so the grey sat on one option while the tick sat on another and an arrow key moved from the top rather than from where the reader had left off. It opens on what is chosen, and on the first that can be chosen only where nothing is. -
CheckboxandRadiogave their labels the width the words wanted, so either in a column narrower than its own label ran off the end of its row rather than wrapping. The label gives way now. -
A button's shadow was never drawn. The flag saying a variant should cast one was set in three places and read in none, so no button in the kit had ever cast a shadow whatever it was told.
0.16.0 #
Added #
-
Table— rows and columns, with a heading. The first stage: the grid itself, sizes, borders, a title and footer, an empty state and a loading one. Sorting, filtering, selection, expandable rows and pagination follow.Table<User>( data: users, columns: [ TableColumn(title: const Text('Name'), value: (u) => u.name), TableColumn(title: const Text('Age'), align: TableAlign.end, value: (u) => u.age), ], )A
valueis the whole of most columns — the cell is that value as text — andbuilderis there for where text will not do. Naming both is worth it: the value is what a sort will compare and a filter will match, so aTagcan sort by the word inside it.A column that says nothing about its width fits its content, which is what a table is expected to do;
widthis then exact andflexa share of what is left. There is no key into a map to get wrong —Tis your own row type.scrollgives the body a height of its own, and the heading then stays put above it; a width of your own lays the table out at that width however narrow its box, with the heading and the rows in the same viewport so there is one offset between them rather than two kept in step. A scrolling table shares the width between columns that named neither awidthnor aflex: two tables can only agree on a measurement they both work out the same way, and left to intrinsic widths a short heading over long cells drifts thirty pixels out of line.TableColumn.fixedpins a column to an edge and lets the rest scroll past. It needs awidth— it is laid out apart from the columns that scroll — and it holds every row to one exact height, because three panes laid out side by side otherwise work out their own: measured, one wrapping cell put two of them a hundred and forty pixels out of step, and a floor rather than an exact height still left eight.scroll.xis a floor rather than a cap: a table is never laid out narrower than its own columns, so fifteen columns inside a declared eleven hundred widen the table instead of leaving four hundred pixels of it unreachable.A table with a
scroll.ybuilds only the rows on screen. Both axes belong to oneRenderTwoDimensionalViewportrather than to a scroll view each, which is also what makes a pinned column and a heading that stays put ordinary questions about where a cell is laid out. Measured over three thousand rows of fifteen columns: forty-five thousand paragraphs became a hundred and fourteen, and twenty scroll ticks five thousand two hundred milliseconds became a hundred and eighty-four. The count no longer moves with the data.Columns still fit their content, and are measured again only when the answer would change: measuring is exact and so proportional to the data, and every rebuild above the table used to pay for all of it — measured, a tap on a row of a five-hundred-row table cost a hundred and seventeen milliseconds, and now costs sixteen. The rows are compared element by element rather than by the list's identity, since
data:written inline is a new list every build.The widths are settled before a cell is built, from the text itself — a
TextPaintermeasures a string for the price of laying that string out — so a column with avalueis exact over every row. A column drawing with abuilderand naming novalue, or headed by something that is not aText, cannot be measured that way and should name awidth.The rows are held to one height, as pinning already held them: a row is found by multiplying rather than by laying out the ones above it. It is the height the row would have taken anyway — the cell's padding plus a line of its text, measured rather than reckoned — so a scrolling table's rows stand exactly as tall as a still one's. A table with no
scroll.ykeeps the grid it had, where a cell that wraps still grows its row.Sorting.
sortable: trueon a column lets its heading be tapped, and the column'svalueis what is compared — most columns need nothing else said. A tap cycles ascending, descending, and back to the order the rows came in, which is what a reader expects of a third tap. The whole heading answers it — padding and all, not just the word — and lights up under the pointer while it is a heading that will do something. The column being sorted by keeps that fill with no hand on it, arriving with adefaultSortbefore anything has been touched. Both carets are always drawn: one alone would say the column is sorted that way, where a column that merely can be sorted has to say so too. They darken under the hand over the theme's slow duration, as the reference transitions the sorter's colour, and the one in force keeps its own — the hand has nothing to add to a column already sorted by. They stand at the cell's trailing edge, or a column of headings would carry each pair at the end of a word of its own length.sortersays how to compare where the value will not do, and naming one makes the column sortable by itself.Table<User>( data: users, defaultSort: const TableSort(1, TableSortOrder.ascending), columns: [ TableColumn(title: const Text('Name'), sortable: true, value: (u) => u.name), TableColumn(title: const Text('Age'), sortable: true, value: (u) => u.age), ], )Ties keep the order they came in — Dart's
sortis only stable below thirty-two elements, so the row's original place breaks the tie — and a row with nothing in the column goes last whichever way round: the direction is applied to the comparison and not to that rule, and turned round with everything else a blank cell rose to the top of a descending sort. Left alone the table keeps its own sort fromdefaultSort; givensortit shows what it is told andonSortChangedsays what a tap would have made of it.Sorting and building only what is on screen are the same table: the rows are put in order once per data and sort, and the widths are measured from the rows as given rather than as shown — an order does not change how wide a word is, so sorting never re-measures.
The cells of the column the table is sorted by carry the same fill its heading does, down their whole length: the heading says which column is doing something and the fill says how far that reaches.
rowSortedBgis the token. A picked row keeps its own fill — what will happen to the row outranks what one of its columns is up to.Rows with rows under them.
TableExpandable.childrenmakes the table a tree: a row opens into its own rows rather than into a panel, each indented a step further in byindentSize. The mark rides in the first column rather than in a column of its own, and a row with nothing under it wears none. The tree is flattened to the rows on show, so narrowing, sorting and paging go on happening to the rows you handed over and a child follows its parent. Picking a row of a tree picks everything under it, however deep and whether or not it is on show, and a branch with some of its own picked stands half-picked;TableSelection.checkStrictlyleaves every row to answer for itself. Rows are let in and out rather than appearing — they grow the way an opened panel does, which costs the table its grid and so lays a tree's rows out by hand.Table<Person>( data: people, expandable: TableExpandable(children: (p) => p.reports), columns: columns, )A house style for the columns.
columnDefaultssays once what every column falls back to — where the cells sit, whether they cut off, how wide they are, which edge they are held at. A column that names the field wins, and it reaches the leaves of a group.Borders you can drag.
columnsResizableputs a grip on each column's trailing border; dragging it changes that column alone and the table grows or shrinks by the difference, rather than taking width from the neighbour.TableColumn.resizableis one column's own word either way,minWidthis how far in it may be dragged, andonColumnResizedis word of what happened. The grip claims the pointer as it arrives, or a sideways-scrolling table takes it for a scroll.What a row is known by.
rowKeysays what makes two records the same row, for tables whose rows are rebuilt rather than kept — a page fetched afresh hands back new objects, and without it what was picked came back unpicked and what was open came back shut. It answers for both.What is wider than this page.
selectionsputs a menu beside the box at the head: the box takes the page in front of the reader, and once a table is paged there was no way to ask for anything wider.TableSelectionEntry.all,.invertand.nonecover most of it.backToTopOnChangesends the rows back to the top when the page, the sort or the filters change.A row dressed from outside.
rowStylegives one row a ground and words of its own — overdue, or not to be acted on — without the table having to know what that means. What the table says about a row is drawn over it: the hand and the tick are washes, so the caller's ground is the outer one and both read at once. Words are merged rather than replaced.TableColumn.cellStylesays the same of one cell and is the narrower word: it covers what the row said where they disagree and adds to it where they do not. Its ground fills the whole cell, padding and all — abuilderpainting its own ground colours the words only, since it draws inside the padding.rowStyle: (context, order, index) => order.overdue ? TableStyle(color: token.error.bg) : null,What a column keeps to itself.
hiddenleaves a column out of the table without taking it out ofcolumns, so a sort or a filter keyed by its place goes on meaning what it meant; a group whose every leaf is hidden goes too.minWidthis a floor for a column that sizes itself, beating thecolumnMinWidthtoken.sortDirectionsnames the orders a heading goes round —const [TableSortOrder.descending]for a column that only reads one way — with the unsorted state always closing the round.sortIcondraws the mark in place of the carets, the same shape asfilterIcon.Filtering.
filtersputs a funnel at the head of a column, opening a menu of choices; the column'svalueis what a choice is matched against, so most columns need nothing else said.onFiltersays what a choice means where matching the value will not do.TableColumn( title: const Text('City'), value: (u) => u.city, filters: const [TableFilter('Bristol', 'Bristol'), TableFilter('Galway', 'Galway')], )Within one column the choices are alternatives; across columns a row has to answer every one, which is what narrowing twice means.
filterMultiple: falsemakes a menu behave as a set of radios. Nothing chosen is every row. Left alone the table keeps its own choices fromdefaultFilters; givenfiltersit shows what it is told andonFiltersChangedreports what a menu would have made of them.The funnel answers the hand apart from the heading it stands in — a rounded ground of its own, a step stronger than the heading's, and the mark darkens with it. The menu is as wide as its widest choice with a floor of a hundred and twenty, scrolls past two hundred and sixty-four, and its choices carry the kit's own menu padding rather than a number picked for them. Under them a rule runs the whole width of the block of buttons, with
sizeXSeither side andsizeXS - lineWidthabove and below so it does not add to the height.A choice can carry
childrenand stand for the ones under it: picking it picks them all, it stands half-picked while only some are, and the column is asked about the leaves so anonFilternever learns the choices were grouped.filterModesays how they are laid out —TableFilterMode.menuopens a branch beside the panel,TableFilterMode.treeputs the tree in one panel with a line that takes everything at once. The tree is the kit's ownTree, so a branch opens and shuts the way everything else does.filterSearch: trueputs a field above the choices for narrowing the menu itself; what is typed is matched against each choice's label, ignoring case and surrounding spaces, andfilterSearchMatchsays what typing means instead. A choice already ticked stays ticked while it is out of sight.filterPanelputs your own widget where the menu would be — a field to search by, a pair of dates. Itsbuilderis handed aTableFilterControls:chosen,choose,apply,clearandclose; itsplacementsays where the panel hangs from the mark that opens it, by its right edge where nothing is said, since the mark stands at the far end of the heading and aligning left edges would throw the panel out past its own column. What it gathers is held while it is open and reaches the table onapply, so a half-typed word does not re-narrow the rows. A panel is an ordinary widget tree, overlays included: aPopoverinside one opens over it without shutting it.filterIcondraws the mark at the head of the column in place of the funnel, told whether the column is narrowing anything.TableColumn( title: const Text('Name'), value: (u) => u.name, onFilter: (choice, u) => u.name.contains(choice! as String), filterPanel: TableFilterPanel( builder: (context, panel) => Input( onChanged: (typed) => panel.choose([if (typed.isNotEmpty) typed]), onSubmitted: (_) => panel.apply(), ), ), )Shutting one open row leaves another open row alone: a panel is keyed by the row it belongs to, so the one below does not inherit the element — and the finished animation — of the one being let go of. The mark on a row begins where the row already stands, so one built afresh as the grid closes up draws itself open rather than drawing a plus and opening again.
The table follows the direction the page reads throughout: the first column is the one against the leading edge, the marks a heading carries stand at its trailing edge and the filter panel hangs by that edge, a held column is held against the edge it names and casts its shade the way it covers, and a drag lands where the hand is; the rule between two columns falls on the side the next column is on; the neighbours slide the way the page reads; and a row or heading carried in the hand is drawn the way it was laid out, since the overlay it travels in knows nothing of the table it came from.
In a bordered table whose columns can be carried, the rule between two columns rides on the cell instead of being drawn by the grid, so it travels with the column rather than standing still while the cells slide over it. Which column goes without one is decided by where a column appears to stand, not by where it is laid out — otherwise the last column carried into the middle took its blank edge along.
Filtering happens before sorting, and the widths are still measured from the rows as given — so a column keeps its width while you filter rather than jumping about under the hand.
Picking rows.
selectionputs a column of boxes in front of the others, and a box at the head that takes every row on show.Table<User>( selection: TableSelection(onChanged: (rows) => setState(() => picked = rows)), columns: columns, data: users, )A row is itself rather than a key: what comes back are the records, and two rows that compare equal are one row as far as picking goes.
TableSelectionMode.radiopicks one at a time and drops the head's box, since taking every row is not something a column of dots can mean;selectablebars rows, and the head passes them over rather than counting them. The head answers for the rows on show — a filter narrows what "all" means — and a row already picked stays picked while a filter hides it. A picked row is tinted whether or not the pointer is on it, and a step stronger while it is.Rows that open.
expandableputs a column of chevrons in front of the others, and the row that is opened has a panel of your own drawing under it, across the whole table.Table<User>( expandable: TableExpandable(builder: (context, user, i) => Text(user.bio)), columns: columns, data: users, )A panel is never shorter than a row and free to be taller: a row whose height was named carries no vertical padding, so a panel padded the same way collapsed to the height of its text — twenty pixels under a row of sixty-four.
The panel reveals and hides with the same animation a
Collapsepanel uses, and the mark is a plus in a rounded square whose upright goes as the row opens, leaving a minus.expandablebars rows, which then show no mark;byRowTapopens a row from anywhere on it andshowColumn: falsedrops the column.A
Tablecannot span a row across its columns, so the panel sits between two grids rather than inside one — and those grids are handed one measured set of widths rather than each working out its own, or the columns would shift the moment the widest row fell on one side of a panel. It also means a table whose rows open is not lazy even withscroll.y: a panel is whatever height its content is, and a lazy body finds a row by multiplying.A page at a time.
paginationshows the rows a page at a time, with the kit's ownPaginationas the pager — so everything it can be told is told the same way, and a theme'sPaginationDefaultsreaches it as it reaches any other.Table<User>(pagination: const TablePagination(defaultPageSize: 20), ...)totalis for rows that come a page at a time: name it and the rows handed over are taken to be one page already, drawn as they came, with that number what the pager counts — so a page is fetched when it is asked for rather than all of them at once. Left null the table counts what it holds and takes the page out of it itself.Paging happens after narrowing and sorting. Everything that draws works from the page, so a row's index is its place on it and the heading's box takes the page rather than the whole table. Narrowing until the page you were on no longer exists lands you on the last one there is, never on an empty one. The page and the page size are controlled apart.
A bordered table paints its outline in front of the rows rather than behind them: a row with a fill of its own — the heading, the row that adds up — is opaque right to the edge, and painted straight over a frame drawn behind it. Measured at the left edge, a body row showed the rule at alpha 15 while the summary beside it came out pure white.
The pager stands outside the outline: drawn inside it, it pushed the frame below itself and left the last row with nothing under it, since the row's own rule is the one the outline stands in for.
positionsays where the pager stands and which edge it is drawn against —topStartthroughbottomEnd, ornonefor a table paged from elsewhere on the screen — and takes a list, so a long table can carry one at both ends. The default is a singlebottomEnd, and since the edge is part of the position rather than a knob of its own, ashowTotaldrawn beside the pager sits with it instead of hugging the leading edge.Columns under one head. A column with
childrenheads them: a title spanning what is under it, no cells of its own, nesting as deep as you like.TableColumn( title: const Text('Name'), children: [ TableColumn(title: const Text('First'), value: (u) => u.first), TableColumn(title: const Text('Last'), value: (u) => u.last), ], )A column that heads nothing stands the whole depth beside a group, so nothing is spanned downwards by hand. A sort and a filter belong to a leaf and are keyed by its place among the leaves. A
Tablemaps a row's children onto its columns one for one, so the heading is laid out by hand as a tree, against the same measured widths the body is given.A row that adds up. A column's
summarysays what it adds up, drawn in a row under the rest — on the column rather than in a list of cells, so there is nothing to keep in step with the columns.TableColumn( title: const Text('Age'), value: (u) => u.age, summary: (context, rows) => Text('total'), )A column that says nothing leaves its place empty, and a table where none says anything draws no such row. The rows handed over are the rows on show — a page of them where the table is paged, and what the filters left.
Cells that span. A column's
spansays how many places its cell takes up in a given row.span: (context, user, i) => i == 0 ? const TableCellSpan(columns: 2) : const TableCellSpan(),A cell that spans covers its neighbours and those are simply not drawn — the table works out which places are taken, so nothing has to return a nought to say it is covered, and a span asking for more than there is takes what there is. The body is then laid out by hand rather than as a grid, against the same measured widths the heading and the summary are given, which means such a table is not lazy.
The rules go on the cells rather than on the rows, or a line is drawn through the middle of a cell that reaches down, and what lights up under the pointer is decided by the cell it is on rather than by the row that cell sits in: a row beside a merged cell lights with that cell and not with the rows on its other side, while the merged cell itself lights everything it covers. A cell reaching down needs a row's height known before the fact, so a table with one holds every row to one height, as a lazy body does; spanning columns alone leaves the rows to their content.
A held column keeps its place.
fixedno longer takes a column out and stacks it at the edge before anything has moved: it stands where it was listed, among the others, and stops only when the scroll would carry it past its rest — behind the columns held before it. A loose column can therefore stand between two held ones and slide under them as the scroll catches up, and the order written is the order seen. Each held column casts its own shade at its own trailing edge and moves with it, as the reference hangs the shadow off the fixed cell itself: one belonging to the band jumped from edge to edge the moment the next column came to rest. It iscolorSplitat the column's own edge and gone twelve pixels out, the narrow edge the reference casts rather than a band. Its strength grows with how far the column has been held — nothing where the scroll has just caught it, full a shade's width later — so it arrives with the hand rather than switching on, and a column coming to rest covers the shade of the one it stops behind. Held cells stand onpinnedBgwith the row's own fill composed over it — one standing over the columns sliding under it has to be opaque, and stacking the two instead let the ground cover the fill, so a held column neither lit up under the pointer nor showed that its row was picked. Its heading is composited over that ground too, or the two per cent wash a heading carries let the other headings be watched travelling behind it.Sorting by more than one column.
TableColumn.sortPrioritylets several be in force at once: a column that names one joins what is already sorted, the higher number compared first, and a column that names none sorts alone.sort,defaultSortandonSortChangedtake a list, kept and reported in priority order so the priority means one thing everywhere.A heading held in view.
stickykeeps the heading against the top of the page while the rows scroll past it, withoffsetHeaderfor a bar of your own. It is for a table whose rows are part of the page; one with ascroll.ykeeps its heading already. The heading keeps its place in the layout and is only drawn lower down, so nothing moves and no space is taken twice, and it takespinnedBgunder it — a heading's own fill is a two per cent wash, and held over the rows it let them be read straight through.A width of its own.
TableScroll.toContent()asks for the width the columns themselves want, rather than a number: each is as wide as its widest cell, nothing is squeezed to fit or stretched to fill, and the table scrolls sideways where that is wider than its box.yas well for both ways.A width of its own.
TableScroll.toContent()asks for the width the columns themselves want, rather than a number: each is as wide as its widest cell, nothing is squeezed to fit or stretched to fill, and the table scrolls sideways where that is wider than its box.yas well for both ways.Nothing turns the lazy body off. Two things ask something of you first:
expandablewants apanelHeight, since a panel of whatever height its content happens to be cannot be reckoned with, and a pinned column wants awidth. Say neither and the table still works — it simply builds every row it was given, which for a few dozen is nothing and for a few hundred is the reasonpaginationexists. The documentation says so in one place.Rows you can move.
rowsDraggablepicks a row up and drops it into another's place, with the same mechanics as the columns: the others slide aside by exactly a row and the table keeps the order it is left in.onRowsReorderedis word of what happened. The order a drag changes is the one the rows came in — underneath narrowing and sorting — so a sort still has the last word. Every row is held to one height, as pinning holds them, and a row can still be tapped.Columns you can move.
columnsDraggablelets a heading be picked up and dropped on another column's place, and the table does the moving itself — there is no reordering logic to write.onColumnsReorderedis word of what happened rather than what makes it happen.Only the drawing moves: a sort and a filter go on naming a column by where it was listed, so carrying one about does not point them at its neighbour. While a heading is carried it is lifted, tilted and given a ground of its own, and the columns slide aside as it goes, each by exactly the width of the one carried. Nothing is reordered until the drop — the layout keeps its order and only what is painted moves — so when the order does change the offsets fall to nought against a layout that already matches, and nothing jumps. The drop hands every cell a fresh offset of nought rather than an old one to carry into its new place, so it settles at once instead of setting off again from the wrong side. Giving the drag up slides them back, and carrying one off the table sends the neighbours home until it comes back — leaving is not letting go.
Where a carried column would land is read from the finger's place against that layout rather than from whichever cell lies under it: the cells slide, so asking them chased the answer and the two columns swapped back and forth with the hand standing still.
-
Expandable.animateOnMount— starts shut and opens even whenexpandedis already set on the first build, for callers that add the widget at the moment the thing opens.CollapseandTreemount every panel up front and keep the old behaviour, which stays the default. -
Words for a
Table—resetandsearch, the word that clears aTablecolumn's filter menu, next tookwhich applies it, and the placeholder of the field that narrows one;selectAllfor the line that takes every choice in a tree of filters and for the box at the head of a table; and the words a screen reader is given —sortedAscending,sortedDescending,notSorted,filterColumn,filtering,expandRow,collapseRowandselectRow. All translated into the eleven languages the kit already carries.
Fixed #
-
Pagination— in a mirrored layout the arrows pointed at the buttons they sat beside rather than at the pages they went to. The way the pages run is the way the page reads. -
Table— a fill covered only as much of its row as the cell it was drawn in. Cells were centred against the tallest rather than sized to it, so a hovered row with one wrapping cell was lit in the middle and bare above and below, which read as a border nobody had asked for. -
Table— nothing in it was announced to a screen reader. A heading now says it is one, a heading that sorts answers as a button and carries the state its carets draw, the funnel is named apart from the heading it stands in and says whether it is narrowing, the mark that opens a row is named for what it will do, and the boxes in the selection column say which row they take. The keyboard is not here yet: nothing in the kit is reachable by tab, so it wants doing across the kit at once. -
CheckboxandRadio— neither said what it was or what state it was in. Both carry their own semantics now, so anything built out of them gains it, theTableselection column included. -
Tree— in a mirrored layout a shut switcher pointed away from the tree rather than into it. The chevron points the way the reading runs, which is the way the node would open; open, it points down either way. -
Popover— a click on a trigger that had opened its card by being hovered went to putting the card away and never reached what was under the finger. The trigger is cut out of what dismisses the card, as it already was forSelect,DatePickerandTimePicker. Tapping a tap-triggered card's own trigger now closes it, which is what every other floating surface in the kit does. -
A table told a column's place from the column it was named as. The two are different numbers whenever anything stands in front of the columns given — a column of boxes, a column of chevrons — and the table used one for the other in two places: a sort on the lazy body named the column beside the one whose heading was tapped, so a sortable first column with a selection simply did nothing; and a second drag of a heading moved the wrong column, since the drag spoke in names while the order it changed was in places.
On a table with a
scroll.ya span is a plan for the body, like the heading's: the rows are all one height, so a cell reaching down three of them is three times that, known before anything is laid out. The plan covers every row rather than the ones on show, since a cell starting above the screen still reaches into it, and the walk begins as far back as the deepest span goes. Three hundred rows built twelve hundred cells before and thirty-three after.On a table with a
scroll.yboth kinds of dragging are a shift the viewport adds where it places a column or a row, and the viewport is asked where a carried one would land — it laid them out, so it is the only thing that knows. Three hundred rows keep building forty-odd cells while a heading or a row is carried across them.On a table with a
scroll.ya grouped heading is laid out from a plan the viewport is handed — where every heading cell stands and how much of the grid it covers — so only the cells that start something are built and the rows stay lazy: three hundred rows built twelve hundred cells before, thirty-seven after.On a table with a
scroll.ythe summary is held at the foot the way the heading is held at the head, so it costs the lazy body nothing — three hundred rows built twelve hundred cells with one before, and fifty after.The marks a heading carries are sized against each other rather than each on its own: the mark that opens a row is a checkbox's size — the reference scales it to the same interactive size — and the carets and the funnel are both the icon size,
sortCaretSizebeing the pair's own height rather than one triangle's. The opening mark was a fifth larger than a box beside it, and the carets a fifth smaller than the funnel beside them.A table inside a scrolling page hands back what its rows cannot use: scroll views do not chain, so a table with a height of its own used to freeze the page for as long as the pointer was over it — thirteen drags without the page moving a pixel. One recognizer per axis rather than a pan for both, too: a pan asks the gesture arena for more movement before it will claim a drag, and the page's own vertical drag won every time — a hundred and thirty pixels of a hundred and fifty went to the page.
Repaint boundaries round each pane and inside the sideways scroll: measured over twenty ticks on three hundred rows, sideways went from 334ms to 210 and downwards from 135 to 103.
A pinned column casts a shade over the rows that have gone behind it, and only while there are any. It is a strip laid over those rows and not a
BoxShadowbehind the pinned pane: a shadow is painted behind the box that casts it, and the neighbouring pane is drawn afterwards, so the whole cast ended up under a column whose cells are mostly transparent — a grey wash across the column instead of a shade beside it.No scrollbar crosses a wide table, scrolling or still: it is a line the design did not ask for and it sits over the last row. The shade a pinned column casts is what says there is more to see.
A table that scrolls sideways answers a mouse drag and carries its heading along with the rows. Flutter does neither of the first two by default — the mouse is not among
dragDevices, and no scrollbar is built for the horizontal axis — which left a table that scrolled sideways with no way to scroll it on the web.A
flexcolumn is never squeezed belowcolumnMinWidth: past that the table grows and scrolls instead, since fifteen columns sharing eight hundred pixels came out thirty-seven each. A bordered table rules between every pair of columns, the pinned ones included.The name is Flutter's own too, so a file that wants both hides one:
import 'package:seed_ui/seed_ui.dart' hide Table;and the reverse.Emptygains anEmptySlot.tableso a kit-wide placeholder covers tables as well. -
Expandableno longer moves its content sideways as it reveals. The reveal passes loose constraints, so a child that hugs what is in it — a line of text, a row of buttons — was laid out narrow and centred for the length of the animation and then snapped to full width and the leading edge on the last frame. Measured in aTablepanel: text at 328.8 and 142.5 wide throughout the reveal, then 116 and 568. The width is now read once per layout and handed to the reveal, so the content stands where it will stand from the first frame.CollapseandTreereveal the same way and get the same fix.
0.15.0 #
Added #
-
FloatButtonGroup.direction— eight ways round the compass, orauto.FloatButtonGroup<T>(direction: FloatButtonDirection.top, items: items)The direction used to be four corners, so straight up could not be said at all: a group at the foot of a screen, with as much room to its left as its right, tipped its fan into a corner instead of opening symmetrically.
autocan now lean neither way — a fan straddles the trigger, a grid centres its columns — and a direction of your own overrides it outright. Carried byFloatButtonDefaultsas well. -
A run with nowhere left to go folds into a block. Twelve items in a row want more width than a phone has, and no direction fixes that; the far half used to land off the screen. It now fills the axis it has before starting a second lane, on either axis, since a column of twelve overruns a screen as surely as a row does.
-
Token.easeOutBack— the overshoot-and-settle curve, beside the motion tokens the kit already carries.
Removed #
FloatButtonDefaults.copyWith. Nothing called it, in the kit or out of it, and no otherDefaultsclass in the kit has one — it was there by habit rather than need, and public API costs something to keep.
Changed #
- A tap outside an open
FloatButtonGroupnow only closes it. It used to dismiss on the press and let the same gesture through to the page, so a control outside that toggled the group fired on the release, found it already shut, and opened it straight back up — a flicker instead of a close. The barrier claims the tap instead. Dragging is still never claimed, so the page goes on scrolling under an open group, anddismissible: falsestill removes the barrier entirely.
0.14.0 #
Changed #
-
Button.sizetakes aControlSize, not just aSoftSize. It was the last control still on presets alone, so a round button could not be given a diameter —ControlSize.height(54)would not compile.Button(size: const ControlSize.height(54), shape: ButtonShape.circle, ...)A measurement names a height and nothing else, so the type size, the corners and the padding come from the preset that height is nearest to, measured against the theme's own scale rather than numbers written down in the component.
Stepssizes its type the same way. A circle given two dimensions takes the larger side, as anAvatardoes.SoftSize.largeand the rest read exactly as before;ButtonDefaults.sizewidens to match.
0.13.0 #
Added #
-
Segmented.scrollButtons— a run too big to fit now says so.An arrow appears on whichever end has something hidden behind it, and each tap brings on one more segment; both show when there is more either way, and each goes when its end runs out. A run that fits shows neither. Segments that scrolled with nothing to say so looked like all the segments there were.
The arrows sit over the ends rather than beside them — a button taking space of its own would narrow the viewport the moment it appeared, hiding another segment and so keeping itself needed — and a step stops short by a button's width so the segment it brings on does not arrive underneath one.
arrowBg,arrowHoverBgandarrowColordress them;scrollButtons: falseturns them off. -
SegmentedController— moves the run from outside the build:next,previous,toStart,toEnd,scrollTo. AChangeNotifierreportingcanStepBackandcanStepOn, so a button of your own knows when to grey itself out. Scrolling only: which segment is selected stays withvalueandonChanged, and a controller that could also select would make two owners of one truth. -
Segmented.arrowBuilder— draw the arrows yourself. One builder for both, told apart by aSegmentedArrow, which is the shapeemptyBuilderalready has; a pair of props would have one widget written twice and free to drift. The kit places what you return and measures it, so a step still stops short of an arrow of your own size. -
A column scrolls too, with the arrows at its top and bottom and the carets turned to match. Given no height it grows as before, with nothing hidden and nothing to say.
-
DropdownEntry<T>— the menu carries the type of what it reports.Dropdown( menu: [ DropdownItem(value: RowAction.edit, label: const Text('Edit')), DropdownItem(value: RowAction.remove, label: const Text('Delete')), ], onItemTap: (action) => switch (action) { ... }, )DropdownItem.keywas anObject?, so every handler began by asking what it had been given. It is nowDropdownItem<T>.value, inferred from the items and handed back throughonItemTap— and an enum makes theswitchexhaustive. A submenu'schildrencarry their parent's type.A menu of items alone needs no type written anywhere. A
DropdownDivideramong them asks for one to be named once, on theDropdown: a divider carries no value, and Dart settles a list's element type before the menu's.
Changed #
DropdownItem.keyis nowDropdownItem.value, andDropdownisDropdown<T>.keynext to Flutter's ownKeyread as tree identity when it was nothing of the sort, andSegmentedandSelecthad settled onvaluefor this long ago. Rename the argument and drop theischeck the oldObject?forced on every handler.
0.12.0 #
Added #
-
ThemeData.materialTheme— the Material theme that matches a kit theme.final kit = ThemeData(dark: isDark); ConfigProvider( theme: kit, child: MaterialApp(theme: kit.materialTheme, home: const Home()), )The kit's theme is not Material's, and anything Material still draws keeps reading
MaterialApp.theme. A page transition paints its backdrop withcolorScheme.surface; left unset that is Material's light default, so under a dark kit theme every navigation flashed white before the page arrived.It is reached from the theme itself rather than through a
BuildContext, so noBuilderis needed between the provider and the app;context.softToken.materialThemeis the same value from inside the tree. A bridge for Material's own chrome, not a port of the kit's design language.
0.11.0 #
Added #
-
FloatButtonandFloatButtonGroup— the button that floats above the page, alone or opening into several.FloatButtonGroup<UserAction>( layout: const FloatButtonLayout.fan(jitter: 0.4, seed: 7), items: const [ FloatButtonItem(value: UserAction.edit, label: 'Edit'), FloatButtonItem(value: UserAction.share, label: 'Share'), ], onItemTap: (value) => handle(value), )It is a
Buttonunderneath with its control height pinned, soButtonColorandButtonShapeare the same types they are everywhere else rather than look-alikes, and hover, press and disabled behave as they already do.FloatButtonLayoutis sealed, not an enum:vertical,horizontal,fan,gridandcustomdo not all carry the same data, and an enum would have to hang a fan's radius on the group, where it would be silently meaningless for three variants out of five. A ring is not a sixth layout — it isfan(sweep: 2 * pi).The direction of travel is read off the trigger's place on screen: a group parked at the bottom right opens up and to the left.
labelis a widget hung outside the button's own box, andFloatButtonLabelPlacement.autoworks out the side from the layout — sideways along a column, above a row, and outward along its own spoke on a fan, so labels fan out with the buttons instead of piling up on one side.sizetakes a preset or a measurement of your own, on a button alone or on a whole group. A fan's radius is worked out from how many items share the sweep — the chord between neighbouring spokes is2r·sin(step/2)— so they stand clear of one another however many there are.fan(jitter:)scatters them along their spokes without being random: the sameseedgives the same arrangement on every open and in every test run, and an item is kept beyond the radius at which it clears the next spoke, so no arrangement can collide.The page underneath an open group keeps working — it scrolls, and its buttons still answer.
Items are data, so the group can size and place them.
valueis yours (which action is this?) andkeyis the tree's (which element?) — aGlobalKeythere lets aTouraim at an item.onItemTapfires with the value beside the item's ownonTap, asDropdownalready does.One
itemBuilderhook covers every wrapper, so there is nobadgeprop and notooltipprop:itemBuilder: (context, item, child) => Tooltip(message: Text(item.label ?? ''), child: child),dismissibleandcloseOnSelectanswer two different questions — a tap outside, and a tap on an item. Escape closes the group whatever they say: a menu with no way out from the keyboard is a trap, and no setting may make one.FloatButtonControllerdrives a group from outside the build, for a tour step that needs the items on screen before it can point at one; passing both a controller andopenis an assertion error.A scroll under an open group re-aims it rather than closing it — closing would contradict
dismissible: false.A label wears the kit's text style and nothing else — no plate behind it, because a caption that needs one is a caption you wrap yourself. The opening is shaped by
FloatButtonToken.curve.Positions are settled by a
Flowdelegate during paint, so a frame of the opening animation moves every item without laying anything out again. -
ControlSize.width(180)— the other half ofbox(). It names a width and leaves the height to the preset scale, whereheight()names a height and leaves the width to the component. Every control that took aControlSizetakes it:Input,InputNumber,Select,TimePicker,DatePicker,Progress,Steps,AvatarandSpin.Input(size: ControlSize.width(180)) // 180 wide, standard height Progress(percent: 0.5, size: const ControlSize.width(160))A circle has one measurement, so
widthandheightmean the same thing to anAvatar, aSpinor aStepsmarker:ControlSize.width(56)is the same 56-wide circle asControlSize.height(56). The names part company only where a control has two dimensions to name.
Changed #
-
ControlSize.fixedand.raware now.heightand.box. The old names said a measurement was given without saying which — and the one- dimension case meant different things in different places: a height on a field, a diameter on an avatar. A circle's height is its diameter, so one honest name covers both.ControlSize.fixed(36) → ControlSize.height(36) ControlSize.raw(200, 36) → ControlSize.box(200, 36)The classes behind them moved with the names, so the pair cannot drift apart:
ExplicitSquareSizeisExplicitHeight(itsdimensionis nowheight), andExplicitSizeisExplicitBox.SoftSizeis untouched —size: SoftSize.largereads exactly as before. It answers a different question: a preset follows the theme's scale, a measurement does not.No deprecated aliases. Two names for one idea is the confusion this removes, and 1.0 would freeze whichever survived.
0.10.0 #
Added #
-
A
Selectwith no width no longer throws. Its value area wasExpanded, which needs a width from above, so a select in aRowcrashed the way the pickers used to. With nothing to fill it now falls back to its widest label.It does not size itself the way the pickers do, and deliberately: a picker's format promises what the field can ever hold, while a select's content is whatever was chosen — and in tags mode the chips decide their own size. Given a width, it fills it.
-
A controlled
openno longer throws. HandingTimePickerorDatePickeranopenthat changed made them mount the overlay entry fromdidUpdateWidget— which runs inside a build, where marking the Overlay as needing to build is illegal. Both now wait for the frame to finish. A picker born withopen: trueopens as well, which it never did. -
Input,InputNumber,Select,TimePickerandDatePickertake aControlSize. Theirsizeaccepted only a preset, so an exact height meant wrapping the field. The line is drawn by what the preset actually feeds: these three feed the box alone — a height and a type size that can stay put — whileButtonandInputfeed four or five things at once and keepSoftSize.DatePicker(size: ControlSize.fixed(36)) // 36 tall DatePicker(size: ControlSize.raw(200, 36)) // 200 by 36explicitWidthgoes with it, so a two-dimensional size names the width as well — aSelector a picker givenraw(180, 36)needs noSizedBoxaround it.resolveHeightwas added besideresolve1Dfor it: a two-dimensional size has to give its height, not its larger side, or a 200-by-36 field would come out two hundred pixels tall.Inputwas the interesting one. The rule as first written said its preset feeds five things at once — height, type size, two paddings and a radius — so a bare number would supply one and leave the rest guessing. Measured rather than restated, that is wrong: the text sits exactly centred at 20, 36 and 56 pixels and nothing overflows, because the box holds the height up and the padding only adds air inside it.The two-dimensional form names the width as well, on all four.
Inputhas two ways out of its build — a plain field returns before the row that carries a search button — and an earlier attempt patched only the second, soraw()looked like it did nothing at all. Both honour it now.fixed()names a height and nothing else, so a field given one still fills the width it is offered: a text input has no content to measure itself against, unlike a picker whose format says what it can hold.InputNumberis anInputwith steppers, so it took the measurement the moment the field did — except for the stepper buttons, which are sized separately and would have stuck out of the border. In spinner mode a named width beats thespinnerWidthtoken.Buttonis the same question and still takes aSoftSize. Its size feeds four places, one of themButtonShape.circlewhere the height is a diameter, so it needs its own pass rather than a mechanical one.Source-compatible —
SoftSizeis aControlSize, so every existing call still reads the same. -
DatePicker— the kit's second picker, on the foundationTimePickerlaid. The value is aDateTimeat midnight: Dart's own date type, so nothing is converted on the way in or out, anddateOnlytrims one that carries a time.The panel has three depths. The header walks up — days to months, months to years — and picking walks back down, so a date years away is three taps rather than twenty-eight presses of a chevron. The grid is always six weeks; a month that fitted in five would shorten the panel and shift everything under it.
DatePicker( value: _startsOn, minDate: DateTime(2026, 1, 1), disabledDate: (day) => day.weekday == DateTime.sunday, onChanged: (date) => setState(() => _startsOn = date), )Typed as well as picked, and a day the month does not have is refused rather than rolled over —
DateTimeitself turns the 31st of February into the 3rd of March, which would land a typo somewhere else entirely. Leap years come out right, century rules included, because the arithmetic asksDateTimerather than working them out again.It carries the same field as
TimePicker: self-sizing width,status,prefix,suffixIcon,onClear,footerBuilder, controlled and uncontrolled modes, and the locale's own figures. -
DateFields,formatDate,parseDate,monthGrid,addMonths,daysInMonth,dateOnly,isSameDay,isSameMonth,weekdayOrder— the date core, exported on its own so every awkward case is reachable without a picker on screen. -
Calendar words in every language.
shortMonths,shortWeekdays,selectDate,today, andfirstDayOfWeekonSeedLocalizations. Most languages start the week on Monday; Japanese, Portuguese and Hebrew start on Sunday and Arabic on Saturday, and a calendar that always led with Monday would misread a month at a glance for everyone it is wrong for.The Turkmen and Hebrew abbreviations are the conventional ones as far as I can tell and deserve a native reader's eye.
0.9.0 #
Added #
-
TimePicker's panel is laid out properly. Five faults in the first cut, all found on screen before it shipped: it filled the viewport (a stretchedColumntakes whatever width it is given, and the overlay gives it the screen); tapping a value changed nothing visible (the panel lives in the overlay, a tree of its own, sosetStateon the picker never reached it); the digits sat crooked (a line height of 1 with no even leading split); values had no gap between them; and the field stayed blank until OK.The layout now follows the reference it was modelled on: a column the width of 1.4 control heights, cells one control height less four, eight rows deep, the digits set in from the start rather than centred, and the pill inset horizontally so the gap between values is real. A chosen value takes the same primary tint a chosen row takes in a
Select's list.Picking writes the field at once even where the value itself waits for OK — a panel showing a choice above a blank field reads as broken — and the chosen value glides to the top of its column, with room below the run so even the last hour can get there.
The field sizes itself to the format. A picker in a
Rowused to throw outright — its value area wasExpanded, which needs a width from above — so every one had to be wrapped in aSizedBoxmeasured by hand. The format already says what the field can ever hold, so it now says how wide it needs to be — or the placeholder does, whichever is wider, since the field shows both at different times and must not resize between them. A shorter placeholder is the lever for a narrower field.Told a width it fills it, as a form field should; merely offered an upper bound — by a
Wrap, a plainColumn, aRow— it takes what it needs, and gives way when there is less. Both look the same tohasBoundedWidth, and treating them alike stretched every picker across its parent.Chosen values land at once rather than easing up from the hover grey, which showed as a flash under the finger. The figures follow the locale, and a time typed back in those figures is read correctly. A picker with no
valuekeeps what it is given, from an optionaldefaultValue, the waySelectdoes — before this it forgot the choice the moment the panel closed. Addedstatus,prefix,suffixIcon,onClearandfooterBuilder.The field's own text carries the even leading split the rest of the kit uses, without which the glyphs sit above the middle of the box;
Select's field had the same fault and is fixed with it. -
fontWeightandfontWeightStrongin the theme. Weight was the one piece of typography the kit had no lever for: twenty widgets wroteFontWeight.w400orw600into their own text styles, and the only way to change one was to wrap that widget.SeedTokennow carries both, and every one of those twenty reads a token instead.ThemeData( token: SeedToken( fontWeight: FontWeight.w300, // ordinary text fontWeightStrong: FontWeight.w700, // titles, the chosen row ), )Defaults are
w400andw600, so nothing changes on screen.Three components draw a weight that is their own affair and now carry a token for it:
ButtonToken.fontWeight,ResultToken.fontWeightandTabsToken.fontWeightActive. -
Avatar.customSizeis gone; itssizeslot now takes a diameter directly, asSpin,ProgressandStepsalready did:Avatar(size: SoftSize.large) // the theme's scale Avatar(size: ControlSize.fixed(64)) // a diameter of your ownIt was the last "two props for one idea" left in the kit, after
Tag.customColor. An audit found no others. -
sizeanddisabledin the per-component defaults. They could only be said for a whole screen, throughConfigProvider.componentSizeandcomponentDisabled— so "small buttons on an otherwise normal screen" could not be said at all, even though the mechanism for it was already there.They resolve nearest first, and
componentSizekeeps working exactly as before:widget.size // 1. this widget said so ?? defaults.button?.size // 2. said about buttons ?? ConfigProvider.componentSizeOf(context) // 3. said about the screen ?? SoftSize.middle // 4. the kit's own defaultAdded to every defaults class whose component has the prop — fourteen for
size, fourteen fordisabled.Two things surfaced while wiring it.
Progressnever consultedcomponentSizeat all: itssizewas already nullable, so the earlier migration passed it by. And the docs never pointed from one mechanism to the other, which is how you go looking for a size inButtonDefaultsand come away thinking it cannot be set. -
TimePicker— the kit's first picker, and the shape aDatePickerwill follow. A time of day is aDurationsince midnight: Dart has no time-of-day type outside Material, which this package is built without, and aDurationneeds no conversion to be compared or handed toformatDuration, the format engineCountdownalready uses. One convention for the package, and no new name to collide with Material'sTimeOfDay.The format decides the columns.
'HH:mm'offers hours and minutes and hands back a value with no seconds in it;'h:mm a'grows a meridiem column and reads as a 12-hour clock. A panel offering a column the format would then discard would be collecting something it does not keep.TimePicker( value: _opensAt, format: 'HH:mm', onChanged: (time) => setState(() => _opensAt = time), )Typed as well as picked; an entry that is not a time leaves the value alone rather than clearing it, and a 12-hour format refuses
9:00rather than guessing which half of the day it means. A multi-column panel waits for OK, since an hour with no minute yet is not a time worth reporting.hourStep,minuteStep,secondStep,DisabledTime,hideDisabledOptions,showNow,needConfirm,allowClear, a controlledopen, and the usualsize,variantanddisabled— which followConfigProviderlike every other control.TimeFields,formatTime,parseTimeandnormalizeTimeare exported on their own: the awkward cases — midnight on a 12-hour clock, a half-typed entry, a format naming no minutes — are all reachable without a picker on screen. -
selectTime,now,amandpmonSeedLocalizations, filled for all twelve languages. -
Duration unit letters in every language —
dayUnit,hourUnit,minuteUnit,secondUnitonSeedLocalizations.Countdownalready translated its figures, but the letters informat: 'D[d] HH:mm'are literal text inside a pattern the app owns, so they stayed English beside Arabic-Indic or Chinese figures — which reads as a bug rather than a default. The kit cannot translate a pattern handed to it; it now ships the words to build one:final l = context.seedLocale; Countdown(target: deadline, format: 'D[${l.dayUnit}] HH[${l.hourUnit}]')Filled for all twelve languages, and the gallery's countdown page now follows its language picker.
0.8.0 #
Added #
-
ConfigProvider.defaults— default props for components, beside the tokens inThemeData.components. A token says how a button is drawn; a default says what a button is unless it says otherwise, which is the only way to state a shape, a variant, or whether a tag closes:ConfigProvider( defaults: const ComponentDefaults( button: ButtonDefaults(shape: ButtonShape.round), tag: TagDefaults(closable: true), ), child: ..., )It covers 30 components and 83 props — every prop that is a house-style decision rather than the state of one instance.
Alert.showIconis house style;Alert.type, success or error, is about that one message and stays where it is, as doloading,draggingandindeterminate.A widget's own prop always wins, and the sets merge slot by slot through nested providers.
ConfigProvider.defaultsOf<T>reads them for a widget of your own. The full table is in doc/theming.md. -
ButtonColorandTagColortake a colour of your own, not just a preset. The colour twin ofControlSize, and the same three call shapes:Button(color: ButtonColor.primary, ...) // follows the theme Button(color: const ButtonColor(Colors.white), ...) // a colour of your own Button(color: ButtonColor.fromString('#fff'), ...) // as CSS writes itEach is now a sealed class: the presets moved to
ButtonPreset/TagPresetand are still reachable by their old names, so existing code is unaffected andswitchstays exhaustive. A colour of your own is shaded into the same hover, press and disabled states a preset gets.parseHexColoris exported for the same job elsewhere. It reads#rgb,#rgba,#rrggbband#rrggbbaa— alpha last, as CSS writes it, not first asColor(0xAARRGGBB)does — and throwsFormatExceptionnaming what it got rather than quietly returning black. -
unintended_html_in_doc_commentadded to the lint set. An inline code span that opens on one line and closes on the next is not a code span, sodefaultsOf<ButtonDefaults>in one doc comment reached pub.flutter-io.cn as an HTML tag and cost ten points on the score.flutter_lintsdoes not carry that rule, so nothing local caught it; nowflutter analyzedoes. -
The gallery shows its version beside the logo, generated from
pubspec.yamlbytool/sync_version.shand guarded bytool/check_version.sh— a screenshot now says which version it is. -
ConfigProvider.componentSizeandConfigProvider.componentDisabled— one word for a whole subtree, the way a size context andDisabledContextwork. A dense screen setscomponentSize: SoftSize.smalland every button, input, select and tab in it follows; a form that goes read-only while it saves setscomponentDisabled: savinginstead of threadingdisabled:through every field. Both inherit through nested providers like the rest of the configuration, and both are read bycomponentSizeOf/componentDisabledOffor widgets of your own.What a widget states for itself always wins, so a control can stay live in a disabled subtree. A nearer container still outranks the screen: an
Avatarinside anAvatarGrouptakes the group's size.
Changed #
-
sizeanddisabledare now nullable on the components that follow the provider. They had concrete defaults (SoftSize.middle,false), which left a component unable to tell "nobody said" from "somebody said the default" — and so nothing could be layered underneath. Passing a value is unaffected; only reading one back off a widget instance sees the change:// before final off = button.disabled; // after final off = button.disabled ?? false; -
Tag.customColoris gone; itscolorslot now takes a colour directly. Two props for one idea meantcolorandcustomColorcould disagree, and only one of them could win.// before Tag(customColor: const Color(0xFF722ED1), ...) // after Tag(color: const TagColor(Color(0xFF722ED1)), ...)The same applies to the 83 props that now follow
ConfigProvider.defaults; the table in doc/theming.md lists them. Passing a value is unaffected — only reading one back off a widget instance sees the change.sizechanged onAvatar,AvatarGroup,Badge,Button,Card,Collapse,Input,InputNumber,Pagination,RadioGroup,Segmented,Select,Spin,StepsandTabs.disabledchanged onButton,Checkbox,CheckboxGroup,Dropdown,Input,InputNumber,Pagination,Radio,RadioGroup,RangeSlider,Segmented,Select,Slider,Switch,CheckableTagGroup,TreeandUpload.Per-item flags kept their concrete defaults, because they are about the item rather than the screen:
SelectOption,RadioOption,CheckboxOption,SegmentedOption,CheckableTagOption,DropdownItem,TreeNode,StepItem,TabItem,CardTabandTourButton. So didPopconfirm.disabled, which means "do not ask", not "cannot be used".
0.7.0 #
Added #
Listyshows a placeholder when it has no rows, instead of an empty scroll area —emptyContentif you name one, otherwise the app'semptyBuilder, otherwiseEmpty. A list still fetching its first page is not empty: nothing appears whileloadMore.loadingis true or another page is expected. The end marker stands down while the placeholder is up, so an empty list does not say the same thing twice, and the header stays put, because pull to refresh is wanted exactly when nothing came back.
Changed #
-
ConfigProvider.renderEmptyis nowemptyBuilder, and it is told which component is asking. The old name said how the thing was called rather than what it makes, and the callback took only a context — so a single app-wide builder had no way to tell a dropdown from a page-sized list and had to hand both the same placeholder. It now takes anEmptySlotalongside the context.Selectasks withEmptySlot.select,ListywithEmptySlot.listy.// before renderEmpty: (context) => const Text('Nothing'), // after emptyBuilder: (context, slot) => const Text('Nothing'),Until now
renderEmptywas consulted by exactly one component in the whole kit.The accessor moved with it:
ConfigProvider.renderEmptyOfis nowConfigProvider.emptyBuilderOf. Components should reach forConfigProvider.emptyFor(context, slot)instead — it falls back to the kit'sEmptyrather than handing back a null to deal with. -
Nested
ConfigProviders now inherit. A provider inside another one used to hand down its own theme whole, so one placed to say a single thing — rounder buttons on this screen — silently reset the palette to the stock blue and reverted every other component to the defaults. The kit's own Listy demo had that bug: its density block ignored the theme picker. A nested theme now takes what it does not state from the theme above it: say onlycomponentsand the colours stay, say only a seed and the brightness stays, say onlydark:and the palette it is flipped on stays.ThemeData.rawis still taken as final. -
emptyBuilderand thelocaleare inherited the same way. Both used to be read from the nearest provider alone, so any provider between them and the widget — one carrying nothing but a theme — erased them. -
Each provider now merges once, when it builds, instead of every reader searching the tree outwards. Widgets under a provider that rebuilds for reasons of its own are no longer woken.
Removed #
-
ConfigProvider(components: [...]), the parallel list of loose token objects.ThemeData(components: ComponentsConfig(...))says the same thing with the type checker watching, and a test already guarantees every token has a slot there. The 38 token dartdocs that pointed at the list now point atComponentsConfig.// before ConfigProvider(components: const [ButtonToken(borderRadius: 16)], ...) // after ConfigProvider( theme: ThemeData( components: const ComponentsConfig(button: ButtonToken(borderRadius: 16)), ), ... ) -
ConfigProvideris aStatefulWidgetrather than anInheritedWidget.of,componentOfandcontext.softTokenare unchanged; only a directdependOnInheritedWidgetOfExactType<ConfigProvider>()would notice.
0.6.12 #
Added #
Listy.separatorRender, a separator that is a widget rather than a border. The list draws a hairline under each row as part of the row's decoration, andListyStyles.itemcould restyle or drop it — but only as aBoxDecoration, which cannot say dashed, inset, or a gap. Supplying one takes the default hairline away, so the two are never drawn one over the other. It runs between rows only: never after the last, and never between a section's last row and the next header.
0.6.11 #
Removed #
lib/main.dart, a Flutter starter file that had been sitting in the package since before the first release. It put amain()and aMainAppwidget into everyone's dependency — 0.6.10 shipped it — and it was the only thing failingflutter analyze --fatal-infos. Nothing referenced it.
Fixed #
- A vertical
Sliderwrote each mark opposite the dot it names. The scale runs up the page, as a measure does, but the labels were laid out from the top while the dots were laid out from the bottom. Both now go through one reckoning, so they cannot drift apart again. - A vertical
Sliderwith marks clamped itself to the width of its groove and overflowed the row it sat in by the gap beside it. It is now as wide as the groove, the gap and its widest label together, measured from the labels themselves rather than guessed at.
0.6.10 #
Fixed #
- A component token set on one
ConfigProviderwas lost under any provider nested inside it — a theme switcher, a screen that recolours a corner — even when the inner one said nothing about that component. The search stopped at the nearest provider and fell back to the defaults, which is why a token set once at the top of an app still had to be repeated on every widget. It now carries on outwards until it finds one, and depends on each provider it consults so a change to the outer one still reaches through.
Added #
-
SliderandRangeSlider. A groove with one handle or two, withmin,max,step,marks,dots,included,verticalandreverse, and bothonChangedandonChangeComplete. A nullsteplets the handle rest only on the marks and the ends of the scale.Two widgets rather than one behind a flag: what they carry differs in type, and a single one would take a value that is sometimes a number and sometimes a pair.
Reading right to left turns the scale round on its own, and
reverseflips it back rather than naming a side — the usual rule, and the only one under whichreversemeans the same thing in both languages. The arrow keys move a handle one step, and the key that points along the groove is the one that advances the value, so a mirrored scale answers the same key the other way.A handle being moved carries its value above it, styled as the kit's tooltip is;
tooltipdecides what that says and may say nothing. It is drawn inside the slider rather than in an overlay, since it has to follow a handle that moves every frame — an ancestor that clips will clip it too.Editable range nodes are not here.
0.6.9 #
Fixed #
-
Timelineset its content adrift from the axis in a right-to-left layout. The columns are laid out in a row, which reverses itself when the language does, but their padding and alignment named sides outright — so the gap went to the far edge instead of against the line, leaving one column touching it and the other pushed away twice over. A collapsed item also revealed itself from the left rather than from where its text begins. -
A
Switchkept its thumb on the right when on and the left when off, so a mirrored one travelled backwards. The thumb rests at the start and moves to the end, as Material's own does, and the label inside the track keeps clear of wherever it is resting. -
AvatarGroupclipped each face leftwards, which lapped the wrong one over the other in a mirrored run;Listygroup headings andCard's skeleton bars read from the left rather than from where the line begins; andSortableListspaced its items by the right of each rather than after it. -
A
Treewas built by side throughout, so a mirrored one turned its rows over while leaving everything inside them behind: the depth guides and the expand switcher stayed at the left, the title read from the left of its own row, the drag grip's gap fell on the wrong side of it, and the drop indicator was inset from the left rather than from where the node would land. -
A
Stepsrail broke in the middle of a right-to-left run. The line between two markers is drawn as two halves, each keeping its gap on the side facing a marker, but the painter insets by side while the row hands the halves over in reading order. Where those disagreed the gap turned inward: the ends ran flush into the markers and a five-pixel void opened where the halves should meet. -
A
Stepsrail could be swallowed by its own inset. The rail takes whatever the steps leave it, so beside a short step — a vertical run on a phone, say — arailInsetof any size ate the whole slot and the line came out negative, drawn as nothing. Every inset past a small one then looked alike, because there was no line left to look at. The slot now keeps room for its gaps and the least line that still reads as one, and the step grows to fit. -
A
Stepspanel run pointed the same way whichever way it read: the strip is painted, and the painter knew its axis but not the direction, so the arrows faced right in a mirrored layout while the content beside them ran the other way. The canvas is reflected for a right-to-left run, which turns the shapes and their order together. -
ProgressBorderRadiuscould only name a side, which leftProgressSteps.stepRadiuswith no way to say what it means: it is handedisFirst, a place in the run, and the first step is on the right when the bar reads that way. AddedProgressBorderRadius.directionaland.horizontalDirectional, whose corners follow the reading order.toBorderRadiustakes the direction to resolve against; the existing constructors are unchanged and still mean the side they name. -
A
Tourpanel was built by side throughout: its close button sat in the right corner rather than the trailing one, the room reserved for it in the title cleared the wrong edge, the gap between the step dots and the buttons fell on the wrong side of them, and the panel grew — and held its outgoing copy — from the top left rather than from where it starts. -
A
Timelineitem's title, description and content always read towards the start of their block, so in the column standing before the axis the lines drifted away from the line they belong to instead of meeting it. Text faces the axis now — end for the near column, start for the far one, swapping with the item's placement. Labels follow the same rule.Two settings are needed, not one: the box alignment places a block narrower than its column, but a block as wide as the column — any text long enough to wrap — is placed by its paragraph alignment alone. Setting only the first left a short title against the axis with the description below it against the far edge.
-
A horizontal
Timelinepainted its thread rightwards from every dot, so in a right-to-left run — which a row reverses on its own — the first item sent its thread off the outer edge and the items stopped joining up. Which way the thread runs is now read off the direction, along with the rail insets and which end is dashed. -
Tabsscrolled to the mirror image of where it meant to in a right-to-left layout. Both the snap boundaries and the jump to the active tab took a tab's offset inside the strip as its distance along the scroll, but a horizontal bar that reads right to left starts at the far end: an offset of zero shows the content's right edge, and distances are measured from there. -
Selectlaid itself out physically: the wider inset belonged to the label and the narrower to the arrow, and a mirrored layout swapped the two. Its value, its placeholder and each option in the list were pinned to the left rather than to the edge the language starts at. Tags took their padding the same way round. -
An
Input's placeholder stayed on the left of a mirrored field while the typed text moved: it is drawn separately, andTextAlign.startwas mapped onto a physical left.start,endandjustifyfollow the reading direction now;leftandrightname a side outright and still do not mirror. -
Inputrounded the corners the addon is joined to by side rather than by reading order, so an attached button met a rounded end and the free end came out square. Its prefix and suffix insets followed suit. -
InputNumberdrew the rule between the field and its handles on their left rather than between the two.
0.6.8 #
Fixed #
- A
Dropdownsubmenu could only be reached by hovering, so on a touch screen a parent row such asMoredid nothing at all: hover has no counterpart there, and the row took no other action. A tap opens it now, and closes it again. - A
Dropdownsubmenu opened to the right in a right-to-left layout, back over the menu it belongs to rather than away from it.
0.6.7 #
Fixed #
- A
Segmentedinblockmode wrapped a label that would not fit, growing the whole strip a second line to suit its longest word. A segment is one line: what spills is cut with an ellipsis, where the label carriestext-overflow: ellipsisand the itemmin-width: 0. - A
Paginationwhose run of pages was too wide for its room overflowed rather than fitting. The run is atomic by design, so it cannot be given less space than it needs; it scrolls now, as a long row of segments does. Widest where the figures are — Arabic-Indic ones, or a longer word for/ page— but a narrow screen was enough on its own. - A run of
RadioGroupbuttons rounded the wrong corners in a right-to-left layout. The ends were square and the two rounded edges met in the middle, because the first button took the left corners while the row put it on the right. The rounding is directional now. - A
Badgecount sat off-centre and high in a language that writes its own figures. The reel measured0–9while drawing٠–٩, whose widths are their own, and the line box was forced to exactly the font size — square around Latin digits, which have neither ascender nor descender, and too tight for these, which pushed them up out of centre. The glyphs actually drawn are measured now, and the font is left to say how tall a line is. - A
Badgehung off the right of what it marks rather than the trailing corner, so it stayed on the wrong side in a right-to-left layout — and its overhang was pushed rightwards whichever way the layout read, which left it short of the corner and lying over the child rather than off it. - A
Badgecount that grew a digit changed the pill's padding in a single frame while the figures were still easing. The padding eases with them now. Ribboncame apart in a right-to-left layout. Its corners, its offset and its fold were placed physically while the column's own alignment was directional, so the two disagreed and the fold left the band it belongs to. Each is now given the kind of value it expects.
Added #
-
Localized figures.
SeedLocalizations.digitsgives the ten glyphs a language writes its numbers with, and Arabic ships the Arabic-Indic ones — CLDR's default for the language — so a badge counts٤٢and a countdown reads٠١:٠٢. Only the figures the kit writes itself are rewritten; numbers inside your own text stay yours. Glyph substitution, not number formatting: grouping and decimal marks need locale data the kit does not carry.The Maghreb writes Arabic with Latin figures, and matching on language alone cannot tell, so
copyWith(digits: SeedLocalizations.latinDigits)says so. -
SeedLocalizations.perPage, the/ pagethat follows a size inPagination's size picker. Missed when the rest of the words were gathered.
0.6.6 #
Added #
-
Localization. Twelve languages — en, ru, tk, de, fr, es, zh, ja, tr, pt, ar, he — through
SeedLocalizationsand an ordinaryLocalizationsDelegate, so the kit follows the app's locale and changes with it at runtime. That is what makes it work withintl,easy_localization,slangand the rest while depending on none of them: they all set the app'sLocale, which is all the delegate reads.ConfigProvider(locale:)overrides the delegate for a subtree, andcopyWithreplaces a single word without forking a language. A widget property still beats both, and with nothing wired up at all the words fall back to English rather than throwing — a widget kit has to draw in any application.Every word but
noMoreItemsis taken from the reference locale files rather than translated here.
Changed #
Modal.okText,Modal.cancelTextand the same pair onPopconfirmare now nullable, null meaning the word from the locale in scope. Passing a widget works exactly as before.
0.6.5 #
Added #
-
Countdown. Time to a moment or since one, counting either way, with the format tokens —Y M D H m s S, padded to the width of the run, with square brackets kept as written. A unit left out of the format rolls into the next one down, soHH:mm:ssreads26:00:00whereD[d] HH:mm:ssreads1d 02:00:00.Named
Countdownrather thanTimer:dart:asyncalready has one, and it is needed in the very file that shows this widget.A countdown rounds up to the smallest unit its format asks for — three and a half seconds left reads
00:04, since formatting the remainder as it stands opens a fresh countdown one short of its own length. It wakes only when the drawn text is due to change, and measures against the wall clock, so it stays right across a suspended app.CountdownControllerdrives one from outside the build. Changing the moment counted against never needed a handle — a newtargetdoes that — but pausing does: the count runs on the wall clock, and no arrangement of properties holds it still. Resuming gives the pause back rather than charging for it, and time added while paused is added to the figure on screen rather than to whatever the clock ran down to behind its back.
0.6.4 #
Added #
Badgecounts roll their digits into place, each place its own reel, turning the way the count moved. Ticking over takes the short way round: 9 to 10 rolls the units one step forward to 0 rather than nine steps back. Each place is a fixed cell, so a turning reel cannot shove its neighbours sideways, and99+is drawn still, being no number going anywhere. A count falling to nothing retreats rather than blinking out — keeping the count it was showing as it goes — and leaves the tree once gone.
Fixed #
- A standalone
Badgetook the full width of its row. A container told to align its child takes all the width it is offered. - A single-character count is round again: the padding that makes the pill a lozenge belongs only to counts of more than one character.
- The ring around a count is drawn outside it rather than as a border, which was eating into the height the tokens name and leaving the badge shorter than it asked to be.
0.6.3 #
Added #
Badge. A count, dot or status pinned to a corner, with overflow, a hidden zero, custom content and atitlefor assistive technology, since the digits as drawn are rarely what the count means.Ribbon, a label banded across a container's corner. Its own widget rather than aBadgeconstructor: the two share an idea but not a single property. Note it clashes with Material'sBadge; hide one at the import.
0.6.2 #
Fixed #
- Tapping a tab in a scrolling bar left it a few pixels past the leading edge. The label's weight animates on selection, so the tab that lost the bold narrowed while the bar was still travelling towards a position read before it did. The bar now re-aims once the type has settled.
- A snapping bar could not rest at the end of its run. Only tab boundaries counted as resting places and none of them is the maximum, so reaching the end hauled the bar back to the last boundary before it: the final tabs sat stranded past the trailing edge, tapping one near the end jerked the bar left, and every attempt to scroll to them sprang back. Both ends of the run now count too. Visible on bouncing (iOS) physics, where the pull back is not masked by the hard stop clamping physics make at the maximum.
- Snap boundaries were measured once per build and never again, so a bar that reflowed without rebuilding — a webfont arriving late — snapped to where the tabs used to be.
0.6.1 #
Added #
-
Tabs.snap. A flung bar settles with a tab against its leading edge rather than wherever the throw ended, so a long run cannot stop mid-label. Off by default: a bar of a few tabs has nothing to settle into.Snapping is to the measured tab boundaries, not to a fixed stride — tabs are as wide as their labels, so a page-sized step would land in the middle of one.
-
Upload.progress, a wholeProgressused as a template for the in-flight bar. Only its percent is replaced, so its colour, thickness and shape carry through — the same shapeStepsalready uses for its ring. -
SegmentedToken.itemHoverBg, which had been hardcoded.
Fixed #
Segmenteddrew its track with a translucent fill. In a dark theme that lightens the track above the page, leaving the elevated thumb darker than the groove it sits in: the elevation read inverted, and only the shadow separated the two. The track takes the layout background now, widening the dark-theme gap from 17 steps to 31. The shadow stays — it was not the problem.
0.6.0 #
Changed #
- Breaking.
Uploadgains the layouts its counterparts elsewhere offer:UploadVariantis nowtext,picture,cardsandcircleCards, in place oflistandcards.pictureis the new default and matches whatlistdrew;textdrops the preview,circleCardsrounds the tiles. - Breaking.
Upload.onTapis nowUpload.onPreview— it drives the preview button as well as a tap on the row.
Fixed #
Segmenteddrew its track with a translucent fill, which in a dark theme lightens it above the page — leaving the elevated thumb darker than the groove it sits in, so the elevation read inverted and only the shadow separated the two. The track takes the layout background now: the thumb is the lighter surface in both themes, and in dark the gap widens from 17 steps to 31.SegmentedToken.itemHoverBgis exposed alongside it.Uploaddrew its dashed outline around the prompt inside the drop zone rather than around the zone: a card was ringed about its plus instead of its edge, and a long hint ran flush to the dashes with nowhere to wrap. The dash is a foreground painter now, so it takes the zone's own box.- The paperclip's lower loop curved the wrong way, folding the bottom half of the glyph back into the strokes above it — half a clip reached the screen.
- A card's trigger showed a bare plus. It carries a word under it now, which a glyph on its own does not manage — it reads as decoration rather than as something to press.
Added #
-
UploadItem.id, andUploadItem.keywhich falls back to the name. Callbacks hand back the item they belong to, and a list is usually rebuilt between a tap and the handler running, so matching on object identity broke the momentcopyWithmade a new object. -
Upload.onDownload, with a button beside retry and remove. -
A paperclip on
textrows, and a spinner in place of the preview while a file is in flight — unless it brought one of its own. -
The glyphs
Uploaddraws now live with the rest of the kit's icons, and the plus thatTabshad copied is shared rather than duplicated. -
The gallery declared its image assets at the top level of its pubspec instead of under
flutter:, where nothing reads them — the logo never reached the bundle. -
Upload.itemBuilderandUploadActions, for replacing a row or tile while keeping the handlers the built-in one would have wired up.Transport stays out:
action,headersand the rest would mean HTTP inside the package, which costs either the web platform or the last of the zero dependencies. Sending bytes remains the app's, as picking them is.
0.5.0 #
Added #
-
MessagePlacement, so a toast can be anchored to the bottom of the screen as well as the top. Per call —message.success('Saved', placement: MessagePlacement.bottom)— or as a default throughmessage.config.Each edge keeps its own stack, matching how
notificationtreats its corners, so a toast at the top never reorders one at the bottom andmaxCountapplies per edge.
Fixed #
-
message.configandnotification.configcrashed when called from adisposewhile a card was still on screen: they asked a mounted listener to rebuild during unmount, when the framework has the tree locked. Restoring a global default on the way out of a page is exactly the shape that hit it.The stacks now defer that request to the end of the frame when one is in flight, and only then — the common path stays synchronous, so a toast still appears on the very next frame.
Changed #
- Breaking.
message.config(top: ...)is nowmessage.config(offset: ...). With two edges to anchor to, "top" named the wrong thing;offsetis the distance from whichever edge is in use, and is the wordnotificationalready used.
0.4.0 #
Removed #
- Breaking.
ProgressBorderRadius.fixed, an alias forProgressBorderRadius.all. The last duplicate in the public API.
Added #
- A logo, and a statement in
CONTRIBUTING.mdof what the version number will promise from1.0.0: the exported names and their signatures, not the token values or the pixels they produce.
0.3.0 #
Fixed #
Segmentedoverflowed instead of scrolling when its options were wider than the box it was given — on a phone, a run of five labels painted the debug stripes and put the last segments past the edge, out of reach. A horizontal run now scrolls. With room to spare there is nothing to scroll and the control is still exactly its options wide, andblock: trueis unaffected.
Changed #
-
Breaking.
Progress.sizeis typedControlSize?instead ofdynamic. It was the only public field in the kit with no type, soProgress(size: 'large')compiled and failed at run time. Numbers andSizebecome the types the kit already had:Before Now size: 20size: ControlSize.fixed(20)size: Size(200, 10)size: ControlSize.raw(200, 10)size: SoftSize.smallunchanged -
Breaking.
ProgressStepshad three ways to say one thing — afillparameter, astepFillparameter and astepFillgetter. Onlyfillremains.
Removed #
- Breaking.
ControlSize.from(dynamic), whose own doc comment called it legacy. WithProgress.sizetyped there is nothing left to convert. - Breaking.
ProgressBorderRadius.from(dynamic)— unused anywhere, and untyped. - Breaking.
Progress.onprogressChange, an alias foronProgressChangewhose lower-casepread as a typo. - Breaking. The deprecated
MessageTypetypedef. Before 1.0 is when a deprecation gets deleted rather than carried on.
None of the four were used by the kit, the gallery, the tests or the docs.
Added #
-
Screenshots on the package page. Each pairs the light and dark themes side by side, so one slot carries the theming story and the shape is one a gallery can render — a lone phone screenshot is a sliver.
-
Upload— a file list with a picker trigger, per-file progress, and retry and remove actions. Two layouts: rows, or a grid of tiles for images.It picks nothing and sends nothing. Opening a file dialog needs platform code and there is no such API in the Flutter SDK, so taking it on would mean either a plugin dependency every consumer inherits, or native code for six platforms to maintain. Instead
Uploaddraws the state and calls back:onPickopens whatever picker the app already uses, and the app owns the list, rebuilding items with newprogressandstatusas its upload runs.The dashed drop target is drawn here, but the operating system's drag events are not Flutter's to give either — pass them in through
dragging.The gallery demonstrates it against a real
file_picker, which is a dependency of the example and not of the package.seed_uistill has none beyond the Flutter SDK, and its own platform support is unchanged — the gallery's iOS deployment target moved to 14.0 to satisfy that plugin, which binds the example alone.
0.2.0 #
Changed #
-
Breaking. Text-carrying properties on the declarative components now take a
Widgetinstead of aString, matching Flutter's own convention (AlertDialog.title,ListTile.title) and the kit's existingAlert.messageandTabItem.label. Each is rendered inside aDefaultTextStylecarrying its own colour, size and weight, so a bareText('Saved')still needs no styling — and aRow, aRichTextor an icon now fits where only a string did.MessageConfig.contentWidgetNotificationConfig.message/.descriptionWidgetModalConfig.title/.content/.okText/.cancelTextWidgetPopconfirm.title/.description/.okText/.cancelTextWidgetDrawerConfig.titleWidgetResult.title/.subTitleWidgetTooltip.messageWidgetProgress.formatWidget Function(double)The imperative shorthands keep taking plain text, because they are one-liners inside callbacks and a
Text(...)there costs more than it gives:message.success('Saved'); notification.error('Upload failed', description: 'The server said no.'); Modal.confirm(title: 'Delete file?', content: 'This cannot be undone.');One rule covers it: a shorthand takes a
String, a config takes aWidget. Anything richer goes through the config the shorthand already wraps —message.open(MessageConfig(content: Row(...))).SegmentedOption.labelstays aStringfor the same reason: it already has achildbeside it for the widget case. -
Breaking.
CreateTabData.titleis nowCreateTabData.label, typedWidget?. It seedsTabItem.label, so it now shares that name and type.TabsController.setTitleis unchanged and still takes aString.
Removed #
- Breaking.
ModalConfig.childand thechildparameter of everyModalopener. WithcontenttypedWidgetthe two were the same thing;contentis the survivor, and it scrolls once it outgrows the dialog.
Fixed #
-
A dark theme left the system status bar unreadable: nothing ever stated a
SystemUiOverlayStyle, so the platform's dark icons stayed on a dark bar.ConfigProvidernow declares one matching its theme, through anAnnotatedRegionrather thanSystemChrome— the style belongs to its subtree instead of mutating global state. Only the icon brightness is set, leaving a translucent or coloured bar alone, andConfigProvider(systemOverlayStyle: false)hands control back to an app that drives its own chrome. -
The gallery declared a hosted
seed_uidependency, so it — and with it theexampleCI job and the published demo — was building against the release on pub.flutter-io.cn rather than the working tree. Adependency_overridesentry points it back at the repository, which immediately surfaced 80 call sites the previous setup had hidden.
0.1.0 #
Removed #
Breaking. Four names left the public API. All four were implementation details that no documented API returned or accepted, and nothing in the example or the docs used them:
ControlSizeResolver— the internalControlSize.resolve1Dhelper.SpinButton— chrome internal toInputNumber.detectBorderRadiusFromContext,detectBorderRadiusFromWidget.
Everything else the kit exported stays: types such as PopoverPlacement and
RailInsets appear in public signatures (Tooltip.placement,
TimelineToken.railInset), so callers need to be able to name them.
Changed #
-
Breaking.
Timeline.itemsnow takesList<TimelineEntry>.TimelineEntryis sealed over exactly two cases,TimelineItemandTimelineGroupItem. Lists of plain items keep working unchanged.TimelineGroupItemused to extendTimelineItem, which gave it fourteen inherited fields it never read — a caller reaching forcolorordoton a group got silence — and let a group nest inside another group, which compiled but drew an empty node. Neither is expressible now.
Fixed #
Toureased its panel into place but dropped the mask on at full strength, so opening a tour read as a flash. The dim now fades in over the theme's mid duration, matching the popover barrier.
Added #
- A snapshot test over the exported API (
test/public_api_test.dart). Any change tolib/seed_ui.dart's surface now shows up as a reviewable diff, and a bareexportwithout ashowclause fails the suite. CONTRIBUTING.md, issue forms and a pull-request template.- Tests for
TimelineGroupController, collapsible timeline groups, and the horizontal and reversed timeline layouts.
0.0.1 #
First public release.
Added #
- Token-driven theming.
SeedTokenseeds every colour, size and motion value;ConfigProvidersupplies the resolvedThemeDatato the tree, with per-component overrides throughComponentsConfigand algorithmic palette generation viagenerate. - Context-free feedback APIs.
message,notification,ModalandDrawerrender into the root overlay throughUiKit.navigatorKey, so they can be called without aBuildContext. - General:
Button— variant × colour, five shapes, sizes, loading and danger states. - Feedback:
Alert,Spinner,Spin,message,notification,Modal,Drawer,Popconfirm,Progress,Result. - Data entry:
Input,InputNumber,Switch,Checkbox,Radio,Select. - Data display:
Avatar,Card,Collapse,Empty,Listy,Popover,Segmented,SortableList,Steps,Tabs,Tag,Timeline,Tooltip,Tour,Tree. - Navigation:
Dropdown,Pagination. - Component gallery covering every widget in
example/. - Per-component documentation in
doc/.
Notes #
- Components build on
package:flutter/widgets.dartand carry no Material dependency, so they drop into Material and Cupertino apps alike. - The public API is not yet stable; breaking changes may land in any
0.xrelease.
