monokit_ui 4.9.0
monokit_ui: ^4.9.0 copied to clipboard
A token-driven, widgets-first Flutter design system: light and dark themes, forms, overlays and app shells, built on widgets rather than Material.
Changelog #
All notable changes to monokit are documented here. The format follows Keep a Changelog, and the project follows semantic versioning.
3.0.0 is the first version published to pub.flutter-io.cn. Everything below it was released privately, and is kept here because what changed in those versions is still the history of this API.
4.9.0 #
The swipe grammar, which MonokitList.swipeActionCell has been describing to
nobody since 3.2.0.
Added #
-
MonoListRowSwipeandMonoListRowAction. "Leading is constructive, trailing destructive with hold-to-confirm" — the grammar the list-row board specifies and the package had no trace of.swipeActionCell(72) had exactly one consumer before this: its own definition.A destructive action is held, not tapped.
holdToConfirmis 800ms and the label saysHold…while the finger is down. A swipe is already a coarse gesture; pairing it with a single tap to destroy something puts a mis-swipe and a mis-tap one motion apart, and the hold is what separates them.Touch swipes; pointer hovers. The same actions, two idioms — at pointer density the cells appear on hover and the row does not drag at all, because a pointer never learns a gesture.
The grammar is asserted rather than documented: a destructive action on the leading edge, or a third action on either side, fails in debug. Two per side is what one gesture can reach.
-
MonokitLabels.holding(Hold…), so the confirm copy localises with everything else rather than sitting hardcoded in a widget. -
An
interacthook on the golden harness, which drives a scene into a non-resting state before the snapshot. Every defect this suite has caught since 4.4.0 lived in a state that had no baseline, and resting is the state a scene gets for free. The swipe scene now renders one row held open on each side and one at rest.
Fixed #
-
The moving layer is opaque. The cells are not revealed by appearing — they are always there, and the row slides off them.
MonoListRowpaints its ground at alpha 0 unless it is hovered or selected, so the first render of this component put a green stripe and a red stripe down every row in the list, permanently, and the tests were green through all of it. The first golden showed it in one frame. The row now paintsbackground(or a caller'sground, for a row on a card) under itself and clips, so a row dragged past its stops no longer paints over its neighbours. -
The whole row answers the finger. The drag detector deferred to its child, so a swipe that started on the gap between the title and the chevron reached nothing.
-
The screen edges are left to the platform.
interaction.notesis explicit — "monokit NEVER claims edge swipes — they belong to system and OS navigation" — and a full-width row with a horizontal drag detector claims precisely those. A drag beginning within 20 of either screen edge is now declined for its whole duration, so the back gesture and the drawer reach the OS. Declining on start rather than clamping is the point: a row that answers the first few pixels and then lets go has already eaten the gesture.
Notes #
-
Every threshold came from
contract/interaction.jsonrather than being chosen:holdToConfirm800ms,dismissFraction0.30,dismissVelocity700px/s,rubberBand0.55. -
The swipe has no keyboard idiom, and that is filed rather than invented. Touch swipes and pointer hovers are both specified and both implemented; a keyboard user can do neither. Read across from the media-chrome rule — a hold is "an accelerator for a control that already exists, never the only path" — the swipe is an accelerator for actions that must also live in the row's own menu, and the component documents that rather than asserting it, because no component can verify its caller built the other path. Whether the contract should say so, and what activation means for a held action under a keyboard, are recorded in
monokit-spec/record/AMENDMENTS.md. -
The hold is a raw pointer listener, not a long-press recogniser. A sustain begins when the finger lands and ends when it lifts;
onLongPressUponly fires once a long press has been recognised, so a release at 300ms went unheard entirely and the action fired anyway. -
MonoListRowfills the height it is given when that height is bounded and loose — 2400px in a golden surface. Every real caller puts rows in aColumnor aListView, both of which pass unbounded height, so nothing is wrong today. Recorded because it is the same shape as the checkbox defect fixed in 4.4.0, and the next caller that lands one in aStackwill meet it.
4.8.0 #
Findings from reading the remaining component boards against their widgets.
Fixed #
-
The segmented control is a capsule, track and raised pill both. The board draws them at full radius, which is what makes the pick read as sliding within a groove; the widget used
lgandmd, so it read as three adjacent buttons. Thelinevariant is untouched. -
A selected list row wears the muted wash, not just the brand ink. Both the list-row and drawer boards draw the current row on
muted. Selection rested on the title colour alone, which left the selected row looking like its neighbours. -
The command palette sits in the top third of the window, not centred. A palette centred vertically reads as a dialog. Its panel is
xxlon e2, notxl. -
The palette's search mark was U+2315, which the bundled IBM Plex Sans does not carry — it rendered as a tofu box. Now
MonoIcons.search.
Added #
-
MonoProgress.onMedia. Twelve boards compose components over media; five widgets had anonMediamode. Progress was the clearest gap — its board draws the fed bar in on-media ink and says "the hairline exists only in the feed", while the widget only ever resolved brand-on-muted. Emerald over media disappears into what is behind it, and the default track (muted, a 5% ink) vanishes entirely.colorandtrackColorstill win where a caller sets them. -
The accordion chevron takes the emphasis beat. The board names two roles - "chevron rotates on the emphasis role · body reveals with enter" - and both were on
motion.duration, which isenter. The chevron is the thing that says a panel is opening, so it takes the longer beat and the body follows it. -
The selection controls were 20px tall.
MonoCheckbox,MonoSwitchandMonoRadioGroupall rendered a 20px-high hit area — under half the 44 a finger needs, and the checkbox board says plainly "in a row the whole 44px row is the target (Pressable inflates the hit area)". The visual box is unchanged; only the hit area grew. -
An avatar announces a person, not an image. It carried
image: true, which makes a screen reader say "image, Ama Serwaa" for something the user thinks of as a person. The photo is how the person is drawn, not what is being announced.
Changed #
MonoButton.pendingreplacesisLoading, matchingMonoPhase.pending,MonoInput.pending,MonoField.pendingand the board's own "pending never claims done". The button was the one control calling it something else.isLoadingstill constructs, deprecated, until 5.0.0.
Notes #
-
A correction to 4.7.0. That release said the combobox's check "carried nothing" because U+2713 was not in the bundled font. Verified against the font's character map since: U+2713 is present and rendered fine. Only the chevrons (U+2303/U+2304) were tofu. Moving the check to an icon was still right — the icon board asks for stroke glyphs on a 24 grid, never a text character — but the stated reason was wrong.
The same check clears the rest of the kit: the guillemets in
MonoPagination, the chevrons inMonoAccordion, the ellipsis inMonoBreadcrumband the minus inMonoQuantityStepperare all present in the font and render. They remain text characters where the icon board asks for icons, which is a consistency question rather than a rendering defect, and is not fixed here. -
The boards draw button labels at 13px and tab labels at 15px. Neither is on the type ramp, and the package matches
contract/typography.json's 14px, so this is the board fighting the contract rather than the package drifting. Filed rather than followed. -
The palette's panel is 560 wide (
dialogMd) where the board says 520 — one of the three "measure that is not a token" cases already filed. -
MonoListRowhas no swipe actions at all, andMonokitList.swipeActionCell(72) has exactly one consumer: its own definition. The board specifies a whole grammar — "leading is constructive, trailing destructive with hold-to-confirm", becoming hover-revealed actions at pointer — and none of it exists. That is the fifth token group found published and unread, and it is a feature rather than a fix, so it is recorded here rather than rushed into this release. -
Swept and correct, pinned because nothing was watching them:
MonoPageDotsandMonoStepProgressboth announce position and size, andMonoTrustBadgecarries its tier as a spoken label.MonoSkeleton,MonoProgress,MonoSpinner,MonoButton,MonoImmersiveFeedandMonoMetaLineall honour reduced motion.MonoToastandMonoBannerdo not consult it - and do not animate either, so there is nothing there to reduce. -
Other components whose boards use on-media inks and whose widgets have no
onMediamode:MonoMediaCard(scrimStrong,glassBorder),MonoAvatar,MonoListRow,MonoUploadSlot,MonoCard. Some of those may be composition rather than a widget mode — the drawer's nav rows turned out that way — so each needs checking rather than assuming.
4.7.0 #
The combobox changes shape with density, the last unimplemented board becomes a component, and the kit stops drawing icons as text.
Added #
-
MonoChromeScopeandMonoChrome- chrome recede, the last board with no implementation. Three policies chosen up front per screen, never per component:persistent(live viewing, calls in grid),resting(the feed, player, gallery, reader),hidden(camera pre-roll, screening, preview).Four things are the component:
- Hold to clear is a sustain, not a toggle. The screen is clear only while the finger is down and chrome is back on release, which keeps long-press inside its two sanctioned meanings rather than inventing a third.
- One capability, three idioms. Touch holds; pointer never learns a gesture and rests ambiently, returning on movement; keyboard cannot hold at all (a held key repeats), so while chrome is hidden the first key returns it and is consumed.
- A timed recede travels toward its edge; a held peek does not move. That is a recorded divergence - the specification gives recede a small translation either way - because the finger is still on the glass and every control must be where the thumb left it on release.
- The clock belongs to the item. A new subject resets the timer, and the
default delay is the ten seconds an immersive item holds, so a rest never
cuts an item short.
MonoMetaLinedepends on this: a rest at three seconds would bury its second fact.
subjectTruth: truemarks what never recedes - captions, the recording dot, an active call timer, the live badge. And nothing auto-hides while assistive technology is active, which the scope checks itself rather than trusting the caller to.Carries a ruling that diverges from the specification: the feed is Resting, where the spec files feed browsing under Persistent.
Changed #
-
Touch summons the sheet picker; pointer floats the menu. A menu anchored to a field is a cursor's answer - it appears where the pointer already is. Under a thumb that same menu opens near the top of the screen, behind the keyboard, at whatever width the field happened to be. At touch the panel now rises from the bottom edge: full width to the sheet measure, radius
xxlat the top only, a handle, a labelled scrim, andSafeAreabelow. Type-ahead stays at both densities.The fork is at presentation only. The body - search, divider, option list - is built once and each density supplies its own chrome, so there is one list to keep correct rather than two.
Fixed #
- The chevron and the check were literal Unicode characters.
MonoComboboxdrew its chevron as U+2303/U+2304 and its selected-option check as U+2713. None of the three is in the bundled IBM Plex, so the chevron rendered as a tofu box and "the pick carries the check" carried nothing. Both are nowMonoIcon, which the icon board asks for anyway - a 24 grid at 1.5 stroke, never a glyph. The chevron rotates on the state role and carries its open/close label, matchingMonoSelect.
Notes #
-
This shipped in every release the combobox has existed in, and no test could have caught it: nothing asserts that a glyph resolves. It surfaced the moment the component got its first open-overlay golden, added in this release.
-
Golden coverage. An audit of the 162 public widget classes against the golden scenes found 69 with no baseline. Most are resolvers, scopes and controllers that cannot render; six that could, and should, now do:
MonoChip,MonoListRow, the closed combobox,MonoTrustBadge,MonoStepProgresswithMonoPageDots, andMonoEmptyState.Four of those were changed during this release series with nothing watching. The chip's own 4.1.0 entry says it outright: "No golden baseline shifts, because no golden rendered a chip." All five rendered correctly on inspection - so the gap was a standing risk rather than a backlog of hidden defects, which is the more reassuring of the two answers.
Still uncovered: the media, chat and commerce surfaces, and
MonoPagerandMonoModal, both shipped in 3.2.0 without scenes.
4.6.0 #
Menu rows join the density ladder, and the three surfaces that belong to one density say so.
Fixed #
- Menu rows are 44 at touch and 36 at pointer.
MonoSelectandMonoDropdownMenupinned theirs atspacing.xxxl- a fixed 32, short of both steps and under the 44 minimum touch target, which makes it an accessibility defect rather than a metric drift.MonoCombobox's rows had no height floor at all. All three now readdensity.menuRow, which had been published since 4.3.0 with no consumer.
Changed #
-
MonoDrawer,MonoSheetandMonoCommandPalettedocument the density they belong to, in the boards' own terms: the drawer is expanded-and-pointer only and "the two never coexist" with the tab bar; the sheet is a touch surface whose jobs pass to modal and menu at pointer; the palette "on touch does not exist", because Search is the palette on a phone.Deliberately not enforced. An assert would fail a gallery, which legitimately renders every component at one density, and a predicate nothing calls would be one more published-and-unread vocabulary - the thing the last three releases have been correcting. The component that would consume this is a navigation scaffold the package does not have; that is the real gap.
Notes #
MonoCombobox's remaining clause - "touch summons the sheet picker; pointer floats the menu" - is a shape change rather than a metric, and is not in this release. It routes the whole overlay throughMonoSheetat touch, which is a different interaction model, not a token swap.- Worth knowing while testing: an already-inserted overlay does not rebuild when the theme above it changes, so a test that pumps two densities in one body measures the first one twice. It does inherit the theme correctly on first build.
4.5.0 #
The two components the Atlas specifies and the package had never implemented, and a typeface bug that two releases shipped.
Added #
-
MonoMetaLine- one fact at a time on the item's own ten-second clock, completing the set exactly once. Every value the board names was already a token: the exit ismotion.state(100ms) onaccelerate, the enter ismotion.enter(150ms) ondecelerate- "enters run one step longer than exits" - and the rise iss4.Four clauses are the component. It reserves the width of its longest fact, so the caption never reflows as facts trade. It is not a live region: the whole set is one label read once, or a screen reader is interrupted every five seconds. It stops when the item stops - pass the item's playing state and a paused post has a still caption; reduced motion holds the first fact and never starts. And it must never carry what the user has to act on - a price, an error, a code hold still and always.
-
MonoUploadSlot- both forms. The media tile carries its position badge, so the order the buyer sees is the order the seller arranged; the document row sits onrow2and says what a usable one looks like, which prevents more re-takes than any error message after the upload.The dashed ring is painted rather than approximated. Flutter's
Bordercannot dash, and the dash is not decoration: it is the difference between "a thing could go here" and "a thing is here". A solid ring reads as a bordered box, which is the shape 4.3.0 spent its time removing. -
MonokitTypography.figures- tabular figures with the family left alone.
Fixed #
-
Tabular figures are not the mono register.
typography.tabular()switches the family to mono as well as setting the figures, which is right for a reference or a URL and wrong for a distance on a caption, a phone number being typed, a one-time code, or a position badge. The boards draw the two as disjoint sets: 28 spans ask fortabular-nums, 51 ask forfont-mono, and none ask for both.Four call sites move to
figures(). Two of them shipped:MonoInput'stabularFiguresoption and the OTP code have been setting digits in monospace since 4.1.0.tabular()keeps its meaning for the genuine mono register.
Notes #
- Found by looking at a golden, not by a test. The OTP scene rendered six empty wells - no digit, no typeface - which is how this survived two releases with a golden watching it. That scene now carries a code, and both new components ship with baselines.
MonoMetaLine's emptiness guard is ininitStaterather than the constructor:facts.lengthis not const-evaluable, and asserting it there would stopconst MonoMetaLine(...)compiling in the feed item that builds one per post.MonoPagerhas the same latent constraint.
4.4.0 #
Width class resolves.
MonoWidthClass has existed as an enum since 3.2.0 with nothing to compute it
and nothing to provide it - the specification's own largest recorded gap, and
the second half of the adaptive model 4.3.0 started reading. Density said what
a control measures; width says how a screen composes. They are independent: a
touch laptop is expanded and touch at once.
Added #
-
MonokitBreakpoint- the three thresholds (600 / 960 / 1280) that cut the width axis into four bands, andclassOf(width). Semantic, not device names: a desktop window dragged narrow is compact and gets the compact composition. -
MonoWidthScope- resolves a class from the width of its own region and hands it down. The contract says the page inset is "resolved per layout scope, never once per screen", and that is why this is a widget rather than a function of the window: a screen split into a 280 sidebar and a 1120 pane holds two classes at once, and asking the window would tell both of them the same thing and compose the sidebar as though it had 1120px.MonokitAppinstalls one at the root, so an app that never splits its layout gets the right answer without doing anything.of(context)falls back to the window when no scope is above;maybeOftells a real scope from that fallback. A scope whose width is unbounded - inside aRow, a horizontal scroll view - defers to the window rather than resolvingwidefrom infinity. -
MonoWidthClassLayouton the enum:columns(4 / 8 / 12 / 12),pageInset, andatLeast- which reads the way the boards are written ("at medium and up the alert caps at a measure"). -
MonoPageInset- pads by whatever the surrounding scope resolved.
Changed #
Two components now compose by width, so the mechanism above is not a third
vocabulary nothing reads. MonokitContainers had zero consumers before
this release.
-
MonoAlertcaps at the content measure at medium and up - "never the window". An alert is a sentence, and a sentence stretched across 1400px is not more readable for the pixels. Unchanged by density: a width decision, not a metric one. -
MonoImmersiveFeedletterboxes above compact. Compact is full bleed; at medium and up the column caps atMonokitContainers.feedand centres, which is what that token was written for - "the reason a post does not stretch to 1200px on a tablet". Inside the column the item is composed as compact again, whatever the window is: more pixels buy context around the subject, never a wider subject.
Notes #
- On a phone nothing changes. Both new behaviours begin at medium (600), so a compact app renders identically. What changes is tablet and desktop, where an alert and a feed item previously spanned whatever they were given.
- Three clauses could not be implemented, because the measure they name is
not a token: the textarea and step-progress "field measure" (420 in two
boards) and the command palette's 520 panel. Filed as monokit-spec#4 rather
than guessed -
contentanddialogSmare both close enough to look right and wrong enough to be wrong.MonoDialog's 480 default panel, which matches none of the three dialog containers, is filed there too. - An earlier note claimed
MonokitGutterwas missing the 32 that expanded and wide use. That was a misread: the 32 isMonokitPageInset.expanded, which exists, and the contract's gutter group is base/medium/media only.
4.3.0 #
The adaptive system starts being read.
A conformance pass against the Atlas's 43 component boards found that the
density group shipped in 3.2.0 was almost entirely unconsumed: row1/row2/
row3, controlHeight and minTarget had zero consumers across all 53
widget files, and only five files referenced density at all. The package was a
touch-only realization with an adaptive vocabulary attached as documentation.
This release wires the first of it, and corrects three things 4.1.0 got wrong.
Changed #
-
MonoListRowrides the row ladder. 48/64/88 at touch, 40/56/76 at pointer. It previously derived its height fromspacing.giant- a fixed 48 that never moved - so a list on a desktop rendered at touch metrics. This is the change with the widest blast radius in the release: every list in every host gets shorter at pointer density. -
Invalid recolours the well; it no longer draws a ring.
destructiveSoftfill withdestructiveTextink, acrossMonoInput,MonoInputOtp,MonoSelectandMonoCombobox. 4.1.0 signalled invalid with a destructive ring, which no board does - "invalid recolours the well itself, no second border language". It also settles a question 4.1.0 left open: the ring means focus and only focus, so one ring on screen at a time holds. -
Field inset comes from density - 16 at touch, 12 at pointer - rather than from the size axis. 4.1.0 used a flat 12, which was the pointer value applied everywhere.
-
A textarea's resting height is
density.textareaMin(88/72), not a line-height derivation. 4.1.0 computed 76 because this token did not exist. -
Radii corrected to the surfaces the boards name: card
lg(wasxl), the centred dialog and the sheetxxl(both werexl). -
The tab bar is chrome height -
MonokitChrome.tabBarHeight, 56 - rather thandensity.minimumTarget, andshowLabelsnow defaults to true. The Atlas draws it with "labels always on"; an icon-only destination asks the user to recognise a glyph nobody taught them. Icon-only remains available. -
MonoTooltipis a pointer affordance. At touch density it renders its child and its semantic label and builds no hover machinery at all - touch reveals the same label through the platform's long-press. An explicitlyopentooltip is still honoured everywhere.
Added #
MonokitDensity.chip(36/32),menuRow(44/36),textareaMin(88/72) andfieldInset(16/12) - four metrics the boards use that the density group did not carry.MonokitChrome.tabBarHeight(56).MonoListRow.overline, the third line the 88/76 rhythm exists for. The row had two line counts and a ladder with three rungs.test/adaptive_conformance_test.dart- eight tests that pump the same widget at both densities and assert the heights differ. A widget that reads no density token renders identically at both, which is invisible to a golden (one density) and to every test that only pumps one.
Notes #
- The 14px field padding that earlier notes flagged as an off-grid problem was
a misread: those 14s were menu and list rows, not fields.
MonoFielddraws 16 at touch and 12 at pointer, both on the 4pt grid, and the question dissolves. - Still unread:
MonoWidthClasshas no scope and no consumer, which is the specification's own largest recorded gap.MonoMetaLineandMonoUploadSlothave no implementation. 26 of the 43 boards are unaudited.
4.2.0 #
Seven roles the catalogue was missing, and the reason it mattered more than a missing icon usually does.
Added #
-
MonoIconsgains the account surface:language,notificationOff,key,block,signOut,reminderandverified. Same vendor family as the rest, so the catalogue still reads as one hand.These are not exotic. Settings, a blocked list, a way out and a "you are verified" mark are what a product has on day one. The catalogue had none of them, and since 4.0.0 made
MonoIconDataours to construct, a consumer could not draw its own either - not even to pass intoMonoListRow, whoseiconis typedMonoIconData?. A missing role was therefore not an inconvenience but a wall: the product either went without the row or forked it.Each new role is pinned distinct from the neighbour it would otherwise be mistaken for. Silencing notifications is not muting audio (
mutestays the audio one); leaving a session is not deleting an account; having passed a check is not the idea of protection.
Notes #
- This does not reopen
MonoIconDatafor construction, which stays closed on purpose - the vendor leak 4.0.0 shut is worth keeping shut. It closes the gap the other way, by carrying the roles. A genuinely product-specific mark still has nowhere to live, and that remains an open question rather than a settled one.
4.1.0 #
Two components brought back into line with what the design actually draws, and the tests that should have been holding them there.
The larger half is the text field. An audit against the Monorithm Atlas found
the input rendering as the exact inverse of its own specification: the contract
opens with "A well rather than a bordered box. The field recedes and the value
the user typed is what reads", and the widget shipped a bordered box with a
transparent fill, a 14px value at regular weight, and a 40px height on a touch
product whose own minTarget token is 44. Seven measured differences, none of
which any test was watching, because every test asserted colours and none
asserted shape.
Changed #
-
A text field is a well now, not a bordered box.
MonoInput,MonoInputOtp,MonoSelectandMonoComboboxrender as a filled recess atmutedwith no border in any state, radiusxl(14, waslg/10). The four read their look from one newMonoFieldSkinrather than from four copies of the same decoration - which is how they drifted from the design together and then from each other. -
Field height resolves from density instead of
spacing.huge. The newMonoInputSizepicks one of three steps:small=controlHeight(44/36),medium=row1(48/40),large=controlHeightLarge(56/48). Every step clears the minimum touch target; the old fixed 40 did not.This is a visible change to every field, and the default (
medium, 48) is 8px taller than what shipped in 4.0.0. Passsize:to choose another step. -
The value the user typed is set loud. 20/600 at
large, 14/500 below it, where all four controls previously usedbodyMedium(14/400). A placeholder is the same style one weight lighter inmutedForeground, so it reads as absent text rather than as a short value.tabularFigures: trueopts a field into fixed-advance digits; the OTP takes them always. -
The focus ring finally does what the contract has always said.
contract/interaction.jsoncarries afocusRinggroup - width 2, offset 2, "painted OUTSIDE the control's bounds so it never shifts layout, and bound to focus-visible only" - and all three clauses were unimplemented. The width was 3 at half alpha, the offset was applied by nothing, and every control showed the ring on pointer focus.The offset could not be applied: the ring was a
BoxShadow, and a shadow's spread starts at the border box, so a gap was unrepresentable and the ring read as a thicker border.MonoFocusRingOverlaypaints a real outline outside the bounds, taking no part in layout - a field no longer grows when it takes focus, so its siblings stop shifting as the user tabs through.ringWidth3 -> 2 andringAlpha0.5 -> 1.0.ringOffsetstays 2 andcolors.ringdoes not move. This is system-wide: every focusable control wears this ring, not only fields. -
The ring is keyboard-only now. A tapped field already announces itself with the caret; a ring on top of that is a second answer to a question nobody asked.
MonoInputwas settingfocusVisibleto plainhasFocus, which is the bug -MonoTabsandMonoPressablehad it right all along.The OTP cell is the exception and wears no focus ring at all. The brand caret in one of six boxes is already the whole signal, and ringing the cell as well makes the row read as six controls rather than one. It still rings when invalid, because that is a different message.
-
Invalid wears the ring without focus, in
destructive. An error found on submit is now visible on a field nobody is standing in. -
MonoFieldcarries a pending affordance, which its contract has always required and it has never had. It reports the wait in the message slot as a live region and leaves the control at full strength - a field that greys out while a network call runs reads as "you may not edit this", which is the opposite of what is true. -
A selected
MonoChipnow takes the soft brand pair -primarySoftunderprimaryText- instead of inverting to a solid foreground fill with page-coloured text.02-color-and-surface.mdhad already assigned this job: "primary/primarySoftfollow the same solid/soft grammar for brand emphasis (selected chips, active filters, highlighted rows)". The chip was the only component not honouring it.The old reading was that a chip row is selection rather than intent, so it should not spend the brand colour. The standard had decided otherwise, and the result was a selected chip that read as a solid black pill - outranking the primary button beside it, which inverts the intended hierarchy.
This changes how selected chips look. The API is untouched, so nothing breaks at compile time; if you were relying on the inverted pill visually, this is the release that moves it. No golden baseline shifts, because no golden rendered a chip - which is part of how the drift went unnoticed.
-
Press and hover now darken a chip toward its own ink, both weights. While selection inverted, the selected chip lerped toward
backgroundinstead: the one fill in the kit that got lighter under the finger.
Added #
MonoInputtakes a controlledvaluealongsideonChanged, matching every other stateful control in the kit.controllerandinitialValuestill work; the three are mutually exclusive and asserted.MonoInput.pending- an affordance for asynchronous validation, distinct from disabled as the Field contract requires. It shows a spinner and announces itself, and deliberately does not block typing: taking the field away because a network call is slow is how a form loses a keystroke.MonoFieldSkinandMonoInputSize, exported, so a consumer composing its own field can sit on the same ladder.MonoFocusRingOverlay, for any control that needs the ring without the layout shift.MonokitDensity.controlHeightLarge(56/48). Not incontract/space.json, which stops at one control height; filed as an amendment.MonoChiphas tests. It had none, in a package where every other component does, which is why nothing caught the divergence. They pin the treatment to the tokens rather than to literal colours, so a palette change moves them and a grammar change breaks them.- 14 structural tests for the field: is there a fill, is there a border, which step of the ladder, is the ring outside the box, do all four controls still agree. The cosmetic values were already guarded; the shape was not, which is why it could invert without a single failure.
Notes #
- Selection remains legible without colour: the label still thickens to
FontWeight.w500and the node still reportsselected, so neither a monochrome display nor a screen reader depends on the fill. MonoTextareaextendsMonoInput, so the skin had to learn the difference: a multi-line field takes body leading rather than the label register's tighter 1.2, and gains vertical padding a centred single line does not need. Caught by the golden diff - the textarea had shrunk 27px - and now pinned by a test.- Two Atlas values were not adopted literally. Horizontal padding is drawn at
14, which is off the 4pt grid the whole system is built on, so fields use the
neighbouring token (12) - a 2px difference, and no ungridded number in the
kit. The ring extends
ringOffset + ringWidthpast the control, so an ancestor that clips will trim it; this matchesoutline-offseton the web. - The Input contract still carries a MUST to "colour its border with destructive", which a well has no border to colour. Its own description asks for the well. Both are filed for the system owner rather than settled here.
4.0.0 #
The removals 3.2.0 announced. 3.2.0 published the specification's token names alongside the ones this package had invented and deprecated the latter; this release deletes them, so there is one vocabulary rather than two. If you moved your call sites when the deprecation warnings appeared, there is nothing to do here.
Two other things go with them: the icon catalogue stops leaking its vendor, and the chat receipt stops speaking in transport facts.
Breaking #
- The package-specific colour names are gone. Every one now carries the name the specification gives it. Values did not move — this is a rename, and the goldens are unchanged.
MonoIconDatais opaque. It no longer exposes the vendor's path data as a public field, and it can no longer be constructed from a raw vendor constant. While it did, a change of icon vendor was a breaking change for every consumer rather than an internal detail, which is exactly what the "accept a role, never a vendor glyph" clause exists to prevent.MonoIconsis the API; a role missing from it should be added to it.withSemanticLabelcovers the case where one call site needs a narrower announcement than the catalogue can know.MonoReceiptStateis gone;MonoReceipttakes aMonoPhaseand a label. Three of its five values described a pipe rather than an intent, and its labels were hardcoded English in a system with translatable chrome.onStatusbecame four tokens —destructiveForeground,successForeground,warningForeground,infoForeground— as the contract names them. They share a value today; naming them per family is what lets one move later without dragging the other three.lib/src/widgets/components.dartis deleted. A second barrel that nothing imported, ten files out of date, and shipped in the archive regardless.
Migration #
// colours — the name changes, the value does not
colors.page → colors.background
colors.foregroundMuted → colors.mutedForeground
colors.foregroundSubtle→ colors.mutedText
colors.fill → colors.muted
colors.separator → colors.border
colors.elevated → colors.popover
colors.tint → colors.primaryText
colors.onPrimary → colors.primaryForeground
colors.danger → colors.destructive
colors.dangerSoft → colors.destructiveSoft
colors.dangerText → colors.destructiveText
colors.onLive → colors.liveForeground
colors.canvas → colors.mediaCanvas
colors.mistFill → colors.glassFill
colors.mistLine → colors.glassBorder
colors.onStatus → colors.destructiveForeground // or the family you meant
// icons — construct from the catalogue, not from the vendor
MonoIconData(HugeIcons.strokeRoundedSearch01) → MonoIcons.search
MonoIconData(x, semanticLabel: 'Find') → MonoIcons.search.withSemanticLabel('Find')
// receipts — a phase and your own copy
MonoReceipt(state: MonoReceiptState.read) → MonoReceipt(phase: MonoPhase.succeeded, label: 'Seen')
MonoReceipt(state: MonoReceiptState.failed) → MonoReceipt(phase: MonoPhase.rejected, label: 'Not sent')
Still open #
Nine token values differ from the contract on purpose — this ground is mist
where background is white, the glass pair is mist-tinted, muted and border
are alpha rather than opaque. They are written up in monokit-spec's
record/AMENDMENTS.md and are the system owner's to rule on; whichever way each
goes, the names above are settled.
129 exported classes across 41 files still ship without a contract. The README marks which parts of the API are specified and which are provisional, and contracts for the rest are being written.
3.2.0 #
Conformance. An audit of this package against the specification at monokit.monorithm.dev found twenty divergences, and the structural one was that the two had stopped sharing a vocabulary: forty-three specified colour tokens did not resolve under their specified names. Governance is not ambiguous about which side moves — "where a realization contradicts this site, the realization is what changes" — so the specified names are now the API, published alongside the ones this package invented. Nothing is removed here and no value moved, so this is additive; the old names go away in 4.0.0.
Three components the specification names were missing outright, and the two the design boards depend on were among them. Two more findings were fixed by the tests written for this release rather than by the audit: the pager's drag surface was only as tall as its content, and the modal's Escape key never fired because its shortcut sat below the focus scope rather than above it.
Added #
MonoPager,MonoPageDots,MonoModal— specified components that had no implementation. The pager commits past 30% of its width or 700px/s, rubber-bands at 0.55 past the end stops, claims one gesture axis, and ships arrow keys and pointer-density chevrons in the same change as the gesture. The dots widen the active dot rather than only recolouring it, and expose one semantics node reporting position and length instead of a row of anonymous stops. The modal owns the focus trap, the exclusion triad, focus restoration and a labelled dismiss barrier —MonoModalBarrierScopeapplies focus, pointer and semantics exclusion together, since any subset leaves the overlay reachable by whichever modality was missed.MonoPhase— the five visible phases of a command: pending, reconciling, succeeded, rejected, stalled. None of them existed, which meant rejected and stalled could not be told apart: a declined payment and a dropped connection rendered the same and call for opposite responses.MonoPhaseBadgerenders one, quietly.MonoSkeleton.sweepOnce— a single sweep rather than a loop. A looping shimmer means loading; one sweep means settled.- The specified colour names.
background,cardForeground,popover,popoverForeground,mutedForeground,mutedText,muted,accent,accentForeground,border,primaryForeground,primaryText,destructive,destructiveSoft,destructiveText, the four status foregrounds,liveForeground,mediaCanvas,glassFillandglassBorderall resolve. - Nineteen tokens that were simply absent:
input,secondary,secondaryForeground,overlayScrim,glassFillLight,glassBorderLight,chart1–chart5, and the eightsidebar*roles, plusMonokitColors.glassBlur. - Semantic layout tokens —
MonokitContainers,MonokitChrome,MonokitPageInset,MonokitGutter,MonokitIconSize,MonokitListandMonoReachSide, mirroring the shape the specification's own generator emits. The raw four-point scale was already here; what was missing was every number the language actually names, so a developer reading "a 64px list row" had nothing to reach for. Density now resolvesminTarget,controlHeight,gap,row1–row3andiconChrome. - Motion role names —
press,state,emphasis,screen, at the values they already had. MonokitRadii.xs(4), which the scale was missing.- IBM Plex Sans Arabic, bundled and wired as a fallback on every register. IBM Plex Sans has no Arabic block — the Arabic cut is a separate family — so until now every Arabic string fell back to a platform face at different metrics, in a product that ships Arabic as one of five languages.
- Twelve icon roles the design boards needed and had to reach past the catalogue
for:
list,shield,trash,flag,wifiOff,eyeOff,zap,phoneIncoming,phoneOutgoing,plus,refresh,crop. MonokitLabels.active, so the live badge has a localisable label.
Changed #
MonoIconresolves its own size and stroke. The default size now comes from density — 20 at touch, 16 at pointer — where it was hardcoded to 16, which under-sized every default icon in a touch product. The optical stroke floor of 1.75 is applied at 16 and only at 16,activerenders at 2.0, and the constructor asserts the five sanctioned sizes. Icons that mean direction — back, forward, the chevrons, send, reply — now mirror in RTL; icons that depict an object do not.MonoLiveBadgeno longer defaults to'LIVE'. That string was uppercase where the content rules call for sentence case, English in a five-language product, and the wrong word: the state a merchant surface reports is active. It now falls back toMonokitLabels.active.proseHeadingtracking is -0.22, not -0.1 — the contract sets -0.01em at 22px, so it had been running at less than half the specified track.headlineLargeis -0.36, not -0.35.MonoPager's panes fill the frame, so the drag surface is the whole pager rather than only as tall as the content inside it.
Deprecated #
Both removed in 4.0.0:
- The package-specific colour names, in favour of the specified ones:
page,foregroundMuted,foregroundSubtle,fill,separator,danger,dangerSoft,dangerText,onPrimary,onLive,onStatus,canvas,elevated,tint,mistFill,mistLine. MonoReceiptState. Three of its five values —sent,delivered,read— are facts about a transport rather than phases of a command, and a component takes the phase, never the transport type. UseMonoPhase.
Known gaps #
Not fixed here, and tracked for 4.0.0 because each is breaking: MonoIconData
still exposes raw vendor path data publicly, which is the leak the "never a vendor
glyph name" clause exists to prevent; and 129 exported classes across 41 files
still ship without a contract — the README now marks which parts of the API are
specified and which are provisional.
3.1.0 #
The Makola realization finished its design work the way governance asks: the proposal register updated, nothing invented privately. This release builds that register — the three components it raised, and the smaller gaps the same screens stepped into on the way to them.
Added #
MonoImmersiveFeed, a primitive rather than a component: a full-viewport vertical feed that owns layout, gesture and resource policy, and composes the item presentation as its child. One swipe advances one item however hard the fling; the builder is told each item's phase (active,near,far) so items own their own decode, prefetch and release;dataSaveris a first-class input that switches neighbour keep-alive off;onExposurefires only when scrolling settles, because an item scrolled past was never actually seen; arrow and page keys page (jumping under reduced motion); andrestorationIdcarries the position across navigation.MonoTrustBadge, the tiered credential: an ordered run of filled dots plus the tier's name — always as text, dots-only is not permitted. Brand ink on the soft brand fill on app surfaces, mist chrome and the on-media inks over media, andlapsedmutes the badge in place without changing its geometry. The lowest tier renders like every other tier: a starting point, never a warning.MonoMediaCard, the bounded media object with a lifecycle:live | ended | archived | sold. A non-live card asserts without astateLabel— lifecycle is never carried by treatment alone — and a historical price renders in the muted label register, never typographically identical to a live one. The declared placeholder stands in for missing media, and the caption lives outside the media box so it renders before decode and survives text scaling.MonoChip: selection by inversion — the chosen chip swaps to the foreground fill — so a chip row reads without leaning on the brand colour.MonoListRowandMonoListGroup: rows as wells between collapsed hairlines at the 48/64 rhythm, with an optional muted footer for the group's fine print.MonoStepProgress: discrete progress for bounded flows — "which step of how many", whereMonoProgressanswers "how much". The strip announces as one progress semantic; the segments are presentation, not stops.MonoButtonSize.cta: the block commitment control — the 48 rhythm at thexxlradius. A size rather than a variant, because any weight can be the commitment.MonoBottomNav.showLabelsandMonoBottomNav.onMedia: labels under the icons at the label floor (semibold when selected — a second, non-colour signal), and a translucent mist treatment for bars composed over the media canvas. Icon-only on the page surface stays the default.- Eight
MonoIconsroles:back,store,camera,share,settings,notification,editandhome.
Changed #
MonoButtonSizegained a value, so an exhaustiveswitchover it in consuming code needs actaarm. Nothing that already rendered changed how it renders.
3.0.0 #
Focus handling had two halves and the system only ever shipped one. There were ring tokens, a traversal policy and a focus trap — and nothing anywhere that dropped text-input focus in response to a gesture. On a phone the software keyboard could only be closed with its own Done key.
The framework is half the reason. EditableText's default tap-outside action
deliberately ignores a touch tap on native Android/iOS; it only unfocuses on
desktop, on the web, or for a non-touch pointer. That is a considered default
rather than a bug, but it means the policy has to come from the design system,
and MonoInput never set onTapOutside. So it came from nowhere.
Breaking #
- The package is now
monokit_ui. Imports becomepackage:monokit_ui/monokit_ui.dart, and the bundled font families are addressed aspackages/monokit_ui/IBM Plex Sans(only relevant if you name them directly rather than going throughMonokitTypography.plex()). Nothing else is renamed: everyMono*widget andMonokit*token class keeps its name. Repositories that depend on this one by git ref are pinned to v2.x tags, so they are unaffected until they bump. - An open compact
MonoScreensidebar now makes the page inert. It took taps, keyboard traversal and screen-reader focus with no scrim in front of it, so the sidebar only ever looked modal. The page is now excluded from semantics while it is open, which means its own sidebar trigger no longer appears in the tree or reports an expanded state. Hiding a disclosure control with nothing in its place would strand screen-reader users, so the dismiss barrier over the page is a labelled button instead —MonokitLabels .closeSidebar, "Close sidebar" by default. Any test asserting the trigger stays reachable while the sidebar is open needs updating.
Added #
MonokitFocus.dismissKeyboardOnTapOutside(defaulttrue), the token that owns the behaviour. It lives beside the ring geometry on purpose: this group is the system's answer to "what does focus do", and for a long time it only answered "what does focus look like".- Per-field overrides on
MonoInput,MonoTextareaandMonoInputOtp, for the case the token cannot cover — a chat composer whose message list is tappable wants to survive taps on it. Settingfalserestores Flutter's platform default rather than inventing a third behaviour. MonoField.focusNodeandMonoFieldLabel.focusNode, the<label for>equivalent. Tapping an associated label focuses its control and the label carries a semantics tap action. Opt-in, so existing labels are unchanged.MonoOverlayFocusController.restoreTextInputFocus, to opt back into full restoration on touch.
Fixed #
- Tapping away drops the keyboard. Dismissal lives on the field rather than
on the screen:
EditableText'sTextFieldTapRegionalready computes "outside" with group semantics, so moving between two fields, or tapping a combobox panel, is correct for free. A screen-level handler would have had to reimplement that grouping and would still miss every field outside aMonoScreen. Scroll-to-dismiss falls out of the same change, since a drag opens with a pointer-down outside the region. MonoInputadvertisedtextField: truewith no semantics action at all.TextSelectionGestureDetectorbuilds withexcludeFromSemantics: true, so the pointer gestures contribute nothing, and there was noSemantics.onTapto make up for it — a VoiceOver or TalkBack double-tap on a Monokit field did nothing. Material'sTextFieldcompensates the same way.- The whole decorated field joins its tap region, not just the
EditableText's own box. Clicking a field's own padding read as a tap outside it, which on desktop unfocused and then immediately refocused via the selection gestures — flickering the ring and tearing down the IME connection for nothing. The combobox and command-palette panels get the same treatment, so picking an option cannot drop the query field out from under a selection still in flight. - Closing an overlay no longer re-raises the keyboard. Restoring focus is a keyboard-navigation contract and right on desktop; on touch it meant every sheet, drawer, menu and popover close threw the keyboard back up. Restoration into a text input is now declined on touch platforms — which needed saying twice, because the enclosing scope still remembers the field as its most recent focused child and replays that when the modal's own node is disposed.
MonoDialogandMonoCommandPaletterestore focus on close. Both trapped focus and then abandoned it, never having joined the shared controller.MonoDialognow takes focus when it opens.MonoFocusTrap's autofocus is skipped when the enclosing scope already has a focused child — which is always the case for anOverlayEntryover a live page. Sheet and drawer have always worked around this with an explicit request; dialog had not, so Esc-to-dismiss only worked if you had tabbed into it first.
178 tests, up from 146. The new matrix is per-platform deliberately: every mobile row in it is Monokit policy rather than framework behaviour, so nothing but a test will notice if it regresses.
2.2.0 #
One fix. The badge was the last component whose status treatment disagreed with the rest of the system.
No API changes — this is a pure visual correction, so unlike 2.1.0 the version number means what it says.
Fixed #
- Every semantic badge status is now a soft fill with its contrast-safe
text.
success,warningandinfowere saturated fills ononStatuswhiledangeralone useddangerSoft/dangerText, so a row of status badges came out half shouting and half whispering — with the quiet one being the alarm.MonoAlerthas always used soft+text for all four, and the destructive button uses it too; the badge was the outlier, which is what makes this a defect rather than a preference. livedeliberately stays solid. It is an attention signal rather than a status, and the loudness is the point.neutralwas already a soft fill.
onStatus is now read only by MonoBubble. It is still live, but if that call
site ever changes the role should go with it.
2.1.0 #
The three things 2.0's plan specified and 2.0 shipped without. Small, but they are the last places where the system contradicted its own stated rules.
Note on the version. These are enum member removals, so by strict semver this is a major. It is tagged 2.1.0 deliberately: monokit has one consumer, which pins an exact tag, so nothing upgrades into the break unattended. Read the migration below before bumping the pin.
Breaking #
MonoBadgeVariant8 → 6:{neutral, success, warning, danger, info, live}. A badge reports state, so the ladder is the state vocabulary and nothing else.primary,secondaryandoutlinewere three ways to say "no particular status" — andoutlinedrew a hairline border that the grouped surface model had already abolished. All three collapse intoneutral;destructiverenames todanger, matching the colour role it has resolved to since 2.0.- The default badge is now
neutral, notprimary. An emerald-filled badge competed with the primary button for attention. Badges no longer solicit. MonoResolvedBadgeStyle.borderColoris gone.outlinewas its only setter, so after the trim it was permanently null.MonoTabsVariant {defaultStyle, line}→{line, segmented}, andlineis now the default.defaultStylewas already documented as "a compact segmented control", so this names what it always was. The default changed because tabs usually sit on a card that is already the focus, where a filled strip is a second competing surface;segmentedremains right when the choice itself is the content, as in a short filter row.
Fixed #
MonoAccordion's panel height now springs. It was the last spatial animation still on a curve —AnimatedSizewithCubic(0.2, 0, 0, 1)— which the 2.0 motion doctrine had ruled out and 2.0 then skipped. Height is now driven byMonoSpringControlleron thespatialspring. The ceiling onheightFactoris deliberately left open so the overshoot reads as momentum; only the floor is clamped, becauseAlignasserts on a negative factor and a collapse undershoot is invisible anyway. Mounting an already-expanded item still adopts its resting state outright rather than animating open.
Migration #
MonoBadgeVariant.primary → MonoBadgeVariant.neutral
MonoBadgeVariant.secondary → MonoBadgeVariant.neutral
MonoBadgeVariant.outline → MonoBadgeVariant.neutral
MonoBadgeVariant.destructive → MonoBadgeVariant.danger
MonoTabsVariant.defaultStyle → MonoTabsVariant.segmented
Tabs that relied on the old segmented default must now ask for it explicitly.
2.0.0 #
Monokit stops mirroring the shadcn base-nova web reference and becomes its own
system, built on four decisions: springs for anything spatial, adaptive
density, grouped surfaces, and emerald on mist with a lifted dark mode.
This is a hard break. There are no deprecation shims.
Breaking #
- Colours: 52 fields → 35 semantic roles. Named for their job, not a palette
position.
background→page(and its value changed:#FFFFFF→#F1F3F3),mutedForeground→foregroundMuted,border/input→separator,popover→elevated,muted/secondary→fill,destructive*→danger*,mediaCanvas→canvas,liveForeground→onLive,glassFill/glassBorder→mistFill/mistLine. AddedforegroundSubtle,tint,onStatus. Removed: all eightsidebar*,accent/accentForeground(the reference already hadaccent == primary),secondary*,popover*,input,overlayScrim, and the per-status*Foregroundfamily. tintandprimaryare now separate roles.tintcolours interactive text and icons;primaryfills solids. Identical in light, divergent in dark.- Dark no longer bottoms out at black.
pageis#161B1D,card#22292B,elevated#2E3639. The mediacanvasis still true black in both modes. MonoElevationTier→MonoElevation {flat, raised, floating}. The old five tiers resolved to three distinct shadows;shadowColoris now theme-derived rather than a fixed near-black that was invisible in dark mode.MonokitDensityData→MonokitDensity, withmodenullable meaning adaptive.touchTarget48 → 44.MonoButtonVariant6 → 5:{filled, tinted, secondary, ghost, destructive}.outlineis gone (a bordered button contradicts a borderless surface model);linkfolded intoghost, which now takes the tint.MonoButtonSize8 → 3{sm, md, lg}plus aniconOnlyflag.MonoPopoverPlacement,MonoTooltipPlacementandMonoHoverCardPlacement— three identical 12-member enums — collapsed intoMonoPlacement.MonokitComponentThemes17 slots → 3 (button,card,screen), with acopyWith. Fourteen were never read by any widget.MonoGlassSurface→MonoMediaChrome. It never contained aBackdropFilter, so this is a rename more than a removal.MonoCard.showBorderdefaults tofalse, and itstierenum plus deprecatedelevationdouble collapse into oneelevationfield.
Added #
MonoSpringController— velocity-preserving spring driver, withmonoProject,monoRubberBandandmonoNearest. The three spring tokens (spatial,effect,celebrate) previously had zero call sites.- Real gestures.
MonoSheetandMonoDrawerare draggable with velocity projection and rubber-band overdrag;MonoScreen's compact sidebar has edge-swipe;MonoGalleryViewergains drag-to-dismiss viaonDismiss. MonokitScrollBehavior, installed byMonokitApp— bouncing overscroll on every platform.WidgetsAppprovides none, so physics previously fell through to Flutter's platform default.MonokitThemeData.brightness,lerp, andMonoAnimatedThemeso a theme change cross-fades rather than snapping.MonoSurfaceRoleand aMonoSurfacethat is now the one way a surface is drawn.
Fixed #
MonokitThemeData.light() != MonokitThemeData.light().MonokitElevation,MonokitDensityData,MonokitBreakpointsandMonokitHapticshad no==/hashCodewhile the theme compared them by field, soMonokitTheme.updateShouldNotifyfired on every rebuild.MonokitColors.==allocated two 51-entry maps per call, on that same hot path. It is now alistEqualsover one declared field list.- Two breakpoint ladders.
MonoScreenreadMonoScreenTheme's private copy while everything else readtheme.breakpoints, so overriding one did nothing. - Reduced motion was honoured at 6 sites and bypassed at ~25. All reads now
route through
MonokitMotion, enforced by a source test. MonoEmptyStatehad no background and relied on the page being white; under the grouped model it rendered mist-on-mist.- Shadows. Ten hand-rolled
boxShadowblocks consolidated intoelevation.resolve, removing nine uses of a spacing token as a blur radius. MonoSheetdrew a drag handle it could not honour — there was no drag gesture anywhere in the library.
Unreleased #
Balance the design tokens and components to the shadcn base-nova web
reference. Never carried a version of its own — it landed before the 2.0.0 tag
and shipped inside it.
Changed #
- Tokens.
accent/accentForegroundnow the bold brand green (#007A55/#006045on#ECFDF5), matching the reference's--accent == --primary. Radius scale rebalanced to the reference multipliers (sm 6,md 8,xl 14) and extended withxxl 18/xxxl 22/xxxxl 26.overlayScrimlightened from ~60% to ~10% black.MonokitFocusgains aringAlpha(0.5) and aringShadow()helper, and defaultsringWidthto 3 — the single source of truth for every control's ring. - Focus rings. Every control (input, textarea, select, combobox, input-otp,
checkbox, radio, switch, button, nav) now draws the ring from
MonokitFocusat the referencering/50strength; input-like controls gained the previously missing invalid-state ring (destructive/20). - Accent application. Only menu/select items paint the green accent (with an
accentForegroundfollow-through on text/icons/description so nothing is dark-on-green); button/tab/accordion/command/navigation/pagination hovers were repointed to neutralmuted, matching the reference. - Destructive. Buttons and badges use the soft
destructiveSoft/destructiveTexttint instead of a solid fill. - Tooltip. Inverted-neutral surface (
foreground/background) instead of the brand color. - Overlays. Popover/hover-card/dropdown/context-menu/select/dialog/command surfaces
use a translucent
foreground/10hairline instead of an opaque border; dialog/drawer/ sheet/command scrims are~10% black + 4px backdrop blur. - Misc parity. Input/select/button/otp radius
md → lg; card radiuslg → xl+ hairline; alert radiusmd → lg; tabs list/trigger radii + activeshadow-sm+ neutral line indicator; progress track8 → 4px; spinner inheritsforeground; switch unchecked track usesinput; sidebar container usessidebar/sidebarBorder; bubbletinteduses the softprimarySoft. - Polish. Selected radio is now a filled primary circle with a
primaryForegrounddot (was a hollow ring);kbddrops its border/shadow and usesmutedForegroundtext; avatar border is a translucent hairline; empty state gains the reference's dashed rounded-xl frame and muted icon chip. - Control sizes (touch targets) intentionally kept larger than the web reference.
0.10.0 #
Interaction-rebuild scoping, part 2 — the remaining stateful widgets.
Performance #
- Extends the 0.9.0
ListenableBuilderscoping to every widget that still rebuilt its whole subtree on each hover / press / focus tick:- Toggles (
MonoCheckbox,MonoSwitch,MonoRadio): the emptysetState(() {})states listener is gone; the visual and itsSemantics(focused:)node build inside aListenableBuilderon the states controller, so a pointer frame rebuilds only that leaf, not theFocusableActionDetectorabove it. - Trigger widgets (
MonoSelect,MonoCombobox,MonoDropdownMenu,MonoInput): same treatment on the trigger/field.MonoInputpasses itsEditableTextthrough as the builder'schild, so hover/focus never rebuilds the text field — only the bordered container's decoration. - Item collections (
MonoTabs,MonoAccordion): each trigger now scopes to its own per-item states controller, so hovering one tab/section rebuilds only that trigger — siblings and the (accordion) expandable content stay put.
- Toggles (
- Open/close, selection, and keyboard-highlight
setStatecalls are unchanged; only the interaction-tick rebuilds were scoped. Behavior is preserved (existing widget/semantics suites stay green).
Performance #
MonoPressablenow delivers hover / press / focus ticks to aListenableBuilderwrapping only the state-consuming visual leaf, instead of callingsetStateon the whole widget. ItsSemantics/FocusableActionDetectorsubtree no longer rebuilds on every pointer frame. This scopes the 14 widgets built onMonoPressable(attachment, bottom nav, breadcrumb, bubble, command palette, context menu, dialog, drawer, media, sidebar, navigation menu, pagination, popover, sheet).MonoButtonbumps aValueNotifieron state change rather thansetState, and builds its style + contents + three nested implicit animations inside aListenableBuilder, so the outerSemantics/FocusableActionDetectorno longer reconstructs on hover.- The remaining
setState-per-tick widgets (checkbox, switch, radio, select, combobox, dropdown, input, tabs, accordion) can adopt the same pattern incrementally; theirSemantics(focused:)entanglement wants per-widget care.
0.8.0 #
Input hardening.
MonoInput/MonoTextareagain arestorationId: with it set (and no external controller), the field's text and selection survive the app being killed and relaunched (viaRestorationMixin+RestorableTextEditingController).- Opt-in
showCounterrenders acurrent/maxcharacter counter whenmaxLengthis set. The length is always exposed to screen readers (maxValueLength/currentValueLength) regardless of the visible counter.
0.7.0 #
Production-readiness pass, part 2 (P1/P2 hardening).
Performance #
RepaintBoundaryaround all anchored-overlay content, so a menu's hover/ highlight ticks no longer repaint the page behind it.
API & keyboard #
MonoSelectgainsopen/defaultOpen/onOpenChange, matching the controlled-open contract of dropdown, combobox, and popover.MonoComboboxsupports Home / End / Page Up / Page Down; the command palette supports Home / End (both were arrow-only).
Diagnostics #
debugFillPropertiesonMonoButton,MonoSelect,MonoInput, andMonoTabs, so they expose their state in the Flutter DevTools inspector.
Example #
- Added
MonoComboboxandMonoNavigationMenugallery demos.
0.6.0 #
Production-readiness pass (audited against the Material reference).
Overlays & layout #
- Added
MonoAnchoredLayout/MonoAnchoredLayoutDelegate, a viewport-awareCustomSingleChildLayoutthat replacesCompositedTransformFollowerin select, dropdown, combobox, popover, context menu, tooltip, and hover card. Overlays now flip on their measured size, clamp to the safe area (padding + keyboard insets), and cap their height to the space actually available, so they can no longer render off-screen. Select and dropdown scroll the selected row into view on open. MonoDialogandMonoSheetnow cap their height to the visible area, scroll overflowing content, and lift clear of the software keyboard.
Text input #
MonoInput/MonoTextareanow have full text selection: engine-neutral drag handles (MonoTextSelectionControls), a token-styled cut/copy/paste/ select-all toolbar (monoContextMenuBuilder), aRawMagnifierloupe, and the standard selection gestures. Selection requires anOverlayancestor (as everyEditableTextdoes);MonokitAppprovides one.
Accessibility & feel #
- Fixed-height controls (button, badge, bottom nav, input) grow with the OS
text scale via
monoScaledExtent, clamped at 2x, so labels no longer clip. MonokitHapticsis now actually invoked on activation (was a dead token, still disabled by default).- Reduced-motion is honored by the dialog entrance and radio group; the
accordion chevron mirrors in RTL; select and dropdown menus show a token
RawScrollbaron desktop/web. - A missing
Overlayancestor now asserts (was a silent no-op).
Buttons #
- Intrinsic-safe layout (removed the
Flexiblelabel wrapper) and the pressed state is cleared when a button is disabled mid-gesture.
0.5.0 #
Accessibility — focus & semantics #
- Added
MonoOverlayFocusController, a shared capture-and-restore focus contract adopted by combobox, select, dropdown menu, context menu, hover card, popover, sheet, and drawer (replacing eight divergent per-widget implementations). Overlays now restore focus to whoever held it before opening, falling back to the trigger. - Added
MonoFocusTrapand applied it to the modal surfaces (dialog, sheet, drawer, command palette) so keyboard traversal wraps within the modal instead of escaping to the page behind it. - Added
MonoHeadingand marked dialog, alert, card, sheet, drawer, and screen header titles as headings for screen-reader navigation. - Added
MonoAnnouncer(imperative screen-reader announcements) and announce page changes fromMonoPagination. - Tab triggers now report
inMutuallyExclusiveGroup;MonoScreenassigns a reading order so the floating region is read after page content. - Added the
MonokitFocustheme token (focus-ring width/offset) and the overridableMonokitLabelsvocabulary; component semantic labels now fall back toMonokitTheme.of(context).labels.
Breaking #
- Removed the
state/command/honest-state vocabulary:MonoCommandPhase,MonoAvailability,MonoPending,MonoReconcile,MonoOptimistic, and theMonoOrderStatuscommerce widget. - Removed the opt-in
package:monokit/material.dartMaterial interop layer. - Renamed the internal
src/components/directory tosrc/widgets/(no public import changes — everything is re-exported frompackage:monokit/monokit.dart).
0.4.0 #
- Added a
restorationScopeIdtoMonokitAppandMonokitApp.router, forwarded toWidgetsApp/WidgetsApp.router. Setting it establishes the root restoration scope and theRouter's restoration id, so a declarative router (e.g. go_router with its ownrestorationScopeId) can restore the current location after the OS kills and relaunches a backgrounded app. Null by default, so existing apps are unaffected.
0.3.1 #
- Renamed
MonoIcons.user's semantic label from Profile to Account (Monorithm tab-5 terminology).
0.3.0 #
- Added
MonoBottomNav, an icon-only bottom navigation primitive with per-item semantics (label, button, selected), token styling, and bottom-inset handling. - Added
MonoIcons.user.
0.2.0 #
- Migrated the theme to the canonical mist, emerald, status, media, glass, sidebar, elevation, density, breakpoint, haptic, and motion vocabulary.
- Added honest-state feedback, engine-neutral media and communication surfaces, and commerce components.
- Added shared logical placement and surface primitives while retaining the existing controlled overlay contracts.
- Deprecated numeric card elevation and legacy motion names.
0.1.0 #
- Introduce the widgets-first Monokit theme, app wrapper, state system, and page shell.
- Add foundational surfaces and feedback components: buttons, badges, cards, avatars, separators, skeletons, spinners, keyboard hints, alerts, progress, dialogs, and toasts.
- Add accessible form controls and navigation primitives including
EditableTextinputs, checkbox, radio group, switch, tabs, accordion, select, popover, tooltip, and dropdown menu. - Add responsive sidebar, chat message, bubble, attachment, and message scroller components.
- Add an example gallery and opt-in Material theme adapter for hybrid apps.