Changelog
Every release, every API change — written so an agent can migrate from it alone.
Changelog
This changelog is written for the AI agents that develop the apps
consuming lotusui. From the next release on, it records every
API-visible change — created, updated, renamed, removed — with exact
symbols, old→new forms, and replacement guidance, precise enough to
migrate a consuming app from this document alone. There is
deliberately no migrate tool: read the entries for the versions you
crossed, apply them, then run go build ./... until clean — the
compiler is the safety net. lotusui update prints the relevant
sections when upgrading vendored components.
Format: Keep a Changelog sections, SemVer versions (Go modules enforce it). Until v1.0.0 breaking changes may land in minor versions; each will be recorded here in full.
[Unreleased]
Nothing yet.
[0.7.0] - 2026-08-04
Changed
| Symbol | Kind | Notes |
|---|---|---|
HoverCard.Width |
behavior | Was a fixed card width, now a maximum. The card hugs content up to Width (zero → 320dp). A short tip on a short trigger no longer sits in an empty 200–320dp slab that only looked “centered” in chrome. |
Popover.Width |
behavior | Same hug doctrine: zero still matches the anchor width; non-zero is a max (was forced exact width). |
DropdownMenuTrigger.Width |
behavior | Zero → min 224dp and grow with content (was forced exact 224). Non-zero → hug up to Width. Align uses the measured panel size. |
DropdownMenuSub.Width |
behavior | Zero → min 200dp and grow; non-zero → hug up to Width. |
| Stack / Wrap / Separator docs | docs | Wrap gets its own page (Chakra); Hairline moves off Stack onto Separator (shadcn). Stack page is VStack/HStack (+ Spacers). |
Tabs horizontal strip |
behavior | Uses Wrap (not Flex+Rigid) so whole tabs flow to the next line at intrinsic width under a narrow Max.X — never a 1-character squeezed label. TabsDefault well grows with wrapped height. Vertical stays a column Flex. |
AnnotatedText |
behavior | Segments compose with Wrap (gap 0, Middle) instead of horizontal Flex — same squeeze fix as Tabs/HStack. |
| Wrap / Tabs wrap docs demos | docs | Demos use full box width (no fake Max.X) so resize reflows; demobox heights grow and shrink to measured content (DemoH is a floor, not a one-way ratchet). |
| Responsive docs page | docs | New Guides page (/responsive/): doctrine, LayoutPage, Wrap, SimpleGrid, Tabs/AnnotatedText, Split panes, primitive chooser, what is out of scope. |
lotusui release versions.json |
behavior | Every release (including patch) archives the previous root at /vPREV/ and lists vNEXT at / — the docs switcher always shows the latest tag, not only minor/major lines. |
[0.6.0] - 2026-08-04
Added
| Symbol | Kind | Notes |
|---|---|---|
CardProps.Pad() unit.Dp |
method | Content inset for Size (MD → 20dp). Replaces the leaked CardPad(Size) free function — padding lives on Props like every other size-driven metric. Split pane helpers use CardProps{}.Pad(). |
SplitColumnScroll |
func | Fixed-height column viewport; stack natural SplitBoxes inside — the column scrolls. maxH <= 0 → Max.Y. Track MajorPadding 0. |
SplitBoxScroll |
func | Hug while short; past maxH (0 = Max.Y) body scrolls inside the card. Content Max.Y = maxH − 2×CardProps{}.Pad(). |
SplitBoxFillScroll |
func | Card chrome at least maxH (Min.Y = Max.Y). Re-asserts content Min.Y after Card zeros it — Flexed body + Rigid footer; no outer list. |
TitleWithIcons |
func | In-content section title (LabelTitle) with trailing icon widgets; Space.XS between icons only. Counterpart to TopBar (screen chrome / leading). |
GlossaryTerm |
type | {Term, Tip} — literal match + Caption tip for AnnotatedText. |
GlossarySeg |
type | {Text, Term} run from SplitGlossary. |
SplitGlossary |
func | Segment text into plain/term runs; longest Term wins on overlapping spans. |
AnnotatedText |
func | BrandFg terms with optional HoverCard tips; cards[i] for terms[i] (nil/short = ink only, never panics). Does not change HoverCard defaults. |
OpenURL |
func | Starts the platform URL handler (open / xdg-open / cmd start); does not wait. No widget, no bundled icon. |
Wrap |
func | Wrap(gap unit.Dp, align layout.Alignment, children ...layout.Widget) — Chakra Wrap / CSS flex-wrap beside HStack. Measures each child at intrinsic width; wraps when the next would exceed Max.X; same gap for items and lines. Prefer over HStack for flowing chips/badges (HStack squeezes under narrow Max.X). Prefer SimpleGrid for equal cells / fixed columns. |
Removed
| Symbol | Replacement |
|---|---|
CardPad(sz Size) unit.Dp |
CardProps{Size: sz}.Pad() (zero Size → MD = 20dp). |
SplitBoxNatural |
SplitBox — the alias added nothing; natural-height is just SplitBox. |
Changed
| Symbol | Kind | Notes |
|---|---|---|
PopoverAlign zero value |
behavior | Was PopoverStart, now PopoverCenter — matches shadcn Popover / HoverCardContent (align="center"). PopoverStart is still available (non-zero). A start-aligned HoverCard on a short trigger (e.g. "GB") sat flush-left so the bulk of a 200–320dp card looked far from the anchor; center is the intended default. Same default for Popover and DropdownMenuTrigger.Align. Set Align: PopoverStart or PopoverEnd explicitly when needed. |
| Card / Split / ListView docs | docs | Hard rules under existing sections (Sizes and spacing / scroll grammar): pad vs Max.Y via CardProps.Pad(); List/Scrollable vs Flexed; Scrollable shadowRoom vs Split Pad (never nest); pane MajorPadding 0. No fake CardPad capability in PARITY. |
| HoverCard docs | docs | Glossary / compact section: multi-site Layout, Width/OpenDelay/Side overrides, pointer to AnnotatedText; Tooltip is wrong for stay-open glossary. |
[0.5.1] - 2026-08-04
Fixed
| Symbol | Kind | Notes |
|---|---|---|
HoverCard multi-site |
behavior | Calling Layout multiple times on one HoverCard in a frame (shared tip across every "GB" in a table) used one event tag and painted a floating card at every call site while open — stacked identical panels. Each call is now a distinct trigger site; only the hovered site paints. |
Tooltip multi-site |
behavior | Same shared-Layout stacking footgun as HoverCard; only the hovered site paints. |
DropdownMenuSub multi-site |
behavior | Same: only the hovered submenu site paints the side panel. |
DropdownMenuTrigger multi-site |
behavior | Same: each Layout call is a distinct trigger button; only the active site paints the floating menu. |
Popover multi-site |
behavior | Same: while Open, only the first Layout call in a frame paints the panel. |
Select multi-site |
behavior | Same: while open, only the first Layout call in a frame paints the options panel. |
[0.5.0] - 2026-08-04
Added
| Symbol | Kind | Notes |
|---|---|---|
HoverCard |
type | Hover-preview card on the portal layer (shadcn Hover Card): Layout(th, gtx, content, trigger). Stays open while the pointer is over trigger or card. |
HoverCardSide |
enum | HoverCardBottom (default), HoverCardTop, HoverCardLeft, HoverCardRight. |
HoverCard.Side |
field | Where the card floats relative to the trigger. |
HoverCard.Align |
field | PopoverAlign along the side (PopoverStart / Center / End). |
HoverCard.OpenDelay / CloseDelay |
field | Zero keeps Radix defaults (700ms / 300ms). |
HoverCard.Width |
field | Card width; zero means 320dp (shadcn w-80). |
HoverCard.Disabled |
field | Prevents opening (chakra). |
DropdownMenuTrigger.Icon |
field | Optional icon on the trigger; empty label + Icon = icon-only (Breadcrumb ellipsis → menu). |
DropdownMenuTrigger.Size |
field | Shared size preset for the trigger button. |
Fixed
| Symbol | Kind | Notes |
|---|---|---|
Tooltip background |
paint | Inverted chrome fill used Fill (paints Constraints.Min, often 0×0 in the floating layer) so only FgInverted ink showed; now paint.Fills the clipped label rect like Popover/Select. |
| Breadcrumb demo ellipsis | docs | Usage demo now matches shadcn breadcrumb-demo.tsx: the … is a ghost icon DropdownMenuTrigger that opens Documentation / Themes / GitHub. |
[0.4.0] - 2026-08-04
Added
| Symbol | Kind | Notes |
|---|---|---|
SelectOption.Meta |
field | Optional secondary text on the far right of an option row (and the closed trigger). Empty omits it. The selected check still sits after Meta. |
Changed
| Symbol | Kind | Notes |
|---|---|---|
Select Meta spacing |
layout | Meta keeps 8dp left of the trailing chevron/check (was flush on the closed trigger; 4dp on option rows). |
Select option gap |
layout | Open panel options: Space.XS from the panel’s top/bottom, half of that between rows; hover/selected pills span the full panel width (horizontal panel inset removed — content padding stays on HoverRow). |
DropdownMenu item gap |
layout | Same vertical rhythm and full-width rows as Select. |
Button disabled color |
paint | Disabled walks a brighter step of the button's own scale (Soft/Accent → Subtle; Color solids → .200 fill / .600 ink; outline/ghost/link → lighter ink) — never a darker press-step. Cursor drops while disabled/loading. |
Checkbox / RadioGroup / Switch / Slider disabled color |
paint | Colored on/checked/fill states mute brighter along BrandScale / BrandSubtle (not darker, not FgDisabled grey). TabsLine honors Disabled (label + bar + cursor). |
[0.3.0] - 2026-08-03
One root per concept. Two different ideas were both spelled
Opts: a component's per-call prop bag (ButtonOpts) and the
constructor for a list of choices (Opts, RadioOpts, TabOpts).
The collision forced an exception the moment both existed on one
component — Toggle's list constructor had to be called
ToggleOptions because ToggleOpts was already its prop bag.
The prop bags become XxxProps, which frees the Opt* root entirely
for the choice family. The rule is now one sentence: Props is how
to render, Option/Options/Opts is what to choose from.
Props is also the word the docs and the web component vocabulary
this library follows already use.
This is a mechanical rename — no behaviour changes, no field changes. Your compiler finds every site.
Changed — breaking
| Old | New | Migration |
|---|---|---|
ButtonOpts |
ButtonProps |
rename the type; fields unchanged |
BadgeOpts |
BadgeProps |
" |
CardOpts |
CardProps |
" |
AlertOpts |
AlertProps |
" |
AlertDialogOpts |
AlertDialogProps |
" |
AvatarOpts |
AvatarProps |
" |
AvatarGroupOpts |
AvatarGroupProps |
" |
ButtonGroupOpts |
ButtonGroupProps |
" |
FieldOpts |
FieldProps |
" |
TableOpts |
TableProps |
" |
ToggleOpts (the prop bag) |
ToggleProps |
" — and note ToggleOpts now names the option constructor below |
ButtonGroupItem.Opts |
ButtonGroupItem.Props |
the field holds a ButtonProps, so it is Props too |
Opts(labels …string) |
SelectOpts(labels …string) |
the bare name never said which component's options it built; now the four are parallel |
ToggleOptions(labels …string) |
ToggleOpts(labels …string) |
the exception is gone — ToggleOpts is free now that the prop bag is ToggleProps |
After this, every choice component reads the same:
SelectOpts / RadioOpts / TabOpts / ToggleOpts build
[]SelectOption / []RadioOption / []TabOption / []ToggleOption
into an Options field, addressed by Value() / SetValue().
[0.2.0] - 2026-08-03
Every choice component now speaks VALUES instead of indexes —
Select, RadioGroup, Tabs and ToggleGroup, on HTML's
<option value="…"> model. An option index is a fact about one
list's current order, so it must never reach app state, storage or a
wire; each component's cursor is therefore unexported and callers
read/write Value()/SetValue(). Reordering or rewording an option
list can no longer change what stored data means.
The contract is shared as BEHAVIOUR, not as one type: each component
keeps its own option struct carrying only what it renders
(SelectOption, RadioOption, TabOption, ToggleOption), each
with a label-only constructor. In every one of them:
Val()is the stored value:Value, orLabelwhenValueis empty. Icon-only options must carry an explicitValue— an empty Label has no value to store.- The component exposes
Value()/SetValue(v)/Clear()/Chosen();ToggleGroupaddsValues()/SetValues(vs)for its multi-select mode. - The zero value selects the FIRST option.
SetValuewith an UNKNOWN value clears the choice rather than falling back to option 0, so a stored value that no longer exists can never silently mean something else.
Every index-aligned []bool / []string sidecar disappears into the
option struct: what used to be three parallel slices that could fall
out of step is now one list of options.
Added
| Symbol | Notes |
|---|---|
SelectOption |
struct{ Label, Value string } — one choice: what the user reads, what the app stores. Empty Value means the Label IS the value (HTML's rule) |
SelectOption.Val() |
the stored value: Value, or Label when Value is empty |
Opts(labels ...string) []SelectOption |
shorthand for label-only lists (Opts("10","25","50")) — use explicit Values whenever the label is prose that might get reworded |
Select.Value() string |
the chosen option's value; "" when nothing is chosen |
Select.SetValue(v string) |
choose the option carrying v; an UNKNOWN value clears the choice (a stored value that no longer exists shows the placeholder instead of silently meaning option 0) |
Select.Clear() |
drop back to the placeholder state (was: Selected = -1) |
Select.Chosen() bool |
whether an option is currently selected |
RadioOption |
struct{ Label, Value, Description string; Disabled bool } — one radio choice, carrying what RadioGroup used to keep in parallel slices |
RadioOpts(labels ...string) []RadioOption |
label-only shorthand |
RadioGroup.Value/SetValue/Clear/Chosen |
the choice contract, as on Select |
TabOption |
struct{ Label, Value, Icon string; Disabled bool } — one tab |
TabOpts(labels ...string) []TabOption |
label-only shorthand |
Tabs.Value/SetValue/Clear/Chosen |
the choice contract, as on Select |
ToggleOption |
struct{ Label, Value, Icon string; Content layout.Widget } — one toggle of a group |
ToggleOptions(labels ...string) []ToggleOption |
label-only shorthand. It carries the LONGER name because ToggleOpts is already the Toggle widget's per-call options struct (the ButtonOpts/BadgeOpts convention) — Opts, RadioOpts and TabOpts are its siblings |
ToggleGroup.Value/SetValue/Clear/Chosen |
single-select choice |
ToggleGroup.Values() []string / SetValues([]string) |
multi-select choices, in Options order; unknown values ignored |
IconLock |
mono fluent:lock-closed-24-regular ("lock") — the edit pen's sibling for LOCKED rows: rendered where the pencil would sit when a row is read-only by construction |
New component families:
| Symbol | Notes |
|---|---|
ButtonGroup(th, ButtonGroupOpts, …ButtonGroupItem) |
attached controls: neighbours overlap 1dp so their borders collapse to one line, inner corners square, only the group's outer corners rounded |
ButtonGroupOpts{Vertical} |
stack the group top-to-bottom |
ButtonGroupItem{Btn, Label, Opts, Separator, Widget, Flex} |
a button slot, the hairline seam, or any widget (an Input) with flexed weight |
ButtonGroupSeparator() |
ready-made separator item |
AttachedEdges{Start, End, Top, Bottom}, ButtonOpts.Attached |
squares the corners on attached sides and drops the seat shadow; ButtonGroup sets it for its children |
InputOTP{Length, Groups, Filter, Disabled, Invalid} |
one-time-code slots driven by ONE hidden editor — click anywhere, type or paste |
InputOTP.Value() / SetValue(s) |
the code typed so far |
Kbd(th, key) |
keyboard-cap hint ("⌘K"), for Input End slots and prose |
DropdownMenuSub, .Item(th, label, items…) |
nested submenu: a chevron row whose side panel opens while the pointer rests on the row or the panel |
New capabilities on existing components:
| Symbol | Notes |
|---|---|
AvatarOpts.Badge, AvatarBadge{Color, Icon} |
status dot on the rim, ringed in the panel background |
AvatarGroup(th, AvatarGroupOpts, …AvatarOpts), AvatarGroupOpts{Size, Count, CountIcon} |
ringed overlap with an optional trailing "+N" bubble |
BreadcrumbLink, BreadcrumbPage, BreadcrumbSep(th, icon), BreadcrumbEllipsis |
the composable trail pieces — custom separators, collapsed depth, a dropdown in the row |
BadgeOpts.Start / End |
arbitrary leading/trailing widgets (a Spinner); the widget owns its colour |
DropdownMenuCheckboxItemIcon, DropdownMenuRadioItemIcon |
gutter rows with a leading icon |
DropdownMenuTrigger.Variant, .Align |
trigger button style (zero = outline); panel edge against the trigger |
PopoverAlign (PopoverStart, PopoverCenter, PopoverEnd), Popover.Align |
which panel edge lines up with the anchor |
TooltipSide (TooltipBottom, TooltipTop, TooltipLeft, TooltipRight), Tooltip.Side |
where the label floats |
Slider.Values []float32 |
multi-thumb mode, kept ordered — two entries is the range slider |
Slider.Vertical |
rotates the axis; the value grows upward |
Toast.ID, Toast.Loading, Toaster.Update(id, Toast) |
the promise pattern: add a loading toast, replace it in place with the outcome |
ToastSuccess, ToastInfo, ToastWarning |
joins ToastDefault / ToastDestructive |
ToggleGroup.Outline / Disabled / Vertical / Spacing |
group-wide chrome, stacked axis, gap override (default 2dp) |
ToggleOpts.Content, ToggleGroupItem.Content |
arbitrary content inside the toggle's chrome (the font-weight selector) |
Input.Top / Bottom |
full-width rows INSIDE the frame, above/below the editor line — the block addons |
Accordion.Bordered, AccordionItem.Disabled |
boxed outline with side padding; dimmed item that ignores clicks |
Pagination.Simple |
numbered links only — no previous/next, no elision |
TableOpts.Footer []string |
a final emphasized row (totals) above the caption |
AlertDialogOpts.Size, .Media |
width preset; a medallion above the title |
RadioGroup.Descriptions []string |
index-aligned muted description under each label |
ClampCorner(r, size) int |
caps a corner radius at half the smaller side — past that a rounded rect grows spurs instead of staying a capsule |
IconMail, IconMessage, IconBell, IconCreditCard, IconWallet, IconBuilding, IconShare, IconTrash, IconMoreHorizontal, IconDot, IconPlus, IconSearch, IconMinus, IconArrowLeft, IconArchive, IconClock |
mono Fluent icons for the new examples |
Changed — breaking
| Old | New | Migration |
|---|---|---|
Select.Options []string |
Select.Options []SelectOption |
label-only lists: wrap with Opts("a","b"). Lists whose choice is stored: give each option an explicit Value ({Label: "One per environment (recommended)", Value: "per-env"}) |
SelectGroup.Options []string |
SelectGroup.Options []SelectOption |
same as above |
Select.Selected int (exported) |
unexported cursor | reading d.Selected → d.Value(); writing d.Selected = i → d.SetValue("…"); Selected: -1 (placeholder) → d.Clear(). The zero value still selects the first option |
RadioGroup.Layout(th, gtx, labels ...string) |
RadioGroup.Layout(th, gtx) |
move the labels onto the struct: RadioGroup{Options: lotusui.RadioOpts("Default", "Compact")}, or []RadioOption{{Label: …, Value: …}} when the choice is stored |
RadioGroup.Selected int (exported) |
unexported cursor | r.Selected → r.Value(); r.Selected = i → r.SetValue("…"); Selected: -1 → r.Clear() |
RadioGroup.Disabled []bool |
RadioOption.Disabled |
Disabled: []bool{false, true} → set Disabled: true on the option itself |
RadioGroup.Descriptions []string |
RadioOption.Description |
Descriptions: []string{"a", "b"} → set Description: on each option. (Added earlier in this same unreleased entry and never tagged, so it is removed outright rather than deprecated) |
Tabs.Layout(th, gtx, labels ...string) |
Tabs.Layout(th, gtx) |
move the labels onto the struct: Tabs{Options: lotusui.TabOpts("Account", "Password")} |
Tabs.Update(gtx, n int) |
Tabs.Update(gtx) |
drop the count — the options are on the struct now. The ordering contract is unchanged: Update must still run before anything reads the selection in a frame, and Layout still never processes clicks |
Tabs.Sel int (exported) |
unexported cursor | tabs.Sel → tabs.Value(); tabs.Sel = i → tabs.SetValue("…"); bodies[tabs.Sel] → switch on tabs.Value() |
Tabs.Icons []string |
TabOption.Icon |
Icons: []string{a, b} → set Icon: on each option |
Tabs.Disabled []bool |
TabOption.Disabled |
as RadioGroup above |
ToggleGroup.Layout(th, gtx, size, items ...ToggleGroupItem) |
ToggleGroup.Layout(th, gtx, size) |
move the items onto the struct as Options []ToggleOption |
ToggleGroupItem |
ToggleOption |
same fields plus Value; ToggleGroupItem{Icon: X} → ToggleOption{Value: "…", Icon: X} — give icon-only options a Value |
ToggleGroup.Sel int (exported) |
unexported cursor | g.Sel → g.Value(); g.Sel = i → g.SetValue("…"); Sel: -1 → g.Clear() |
ToggleGroup.On []bool (exported) |
unexported state | g.On → g.Values(); g.On = []bool{…} → g.SetValues([]string{…}); if g.On[i] → slices.Contains(g.Values(), "…") |
Fixed
Event ordering — the class of bug behind all of these: Gio commits a keystroke inside the editor's own layout, so anything read before that call describes the PREVIOUS frame.
| Fix | Notes |
|---|---|
| Typed characters appeared one frame late | InputOTP read its text before laying the editor out; the editor now runs first (into a macro) and paints last. Input.Transform rewrote the previous frame's text, so a typed "A" flashed before folding to "a" — the rewrite now lands in the same frame, re-laying out only when it actually changes the text |
InputOTP focus ring |
drew a square border over rounded slot corners, and lost a 1dp sliver to the next slot's fill (reading thinner at the junction). It now strokes the slot's exact rounded shape in a pass after every slot |
| Rounded chrome grew corner spurs | when a radius exceeded half the smaller side — fully-rounded Skeleton, small Badge, Progress fills, Kbd, Button, input frames. All clamp through ClampCorner |
Separator / VerticalHairline claimed unbounded height |
a vertical rule inside a row measured at natural height reported the full 1<<20 constraint, so any box containing one grew enormous. It now falls back to the row's Min, or one line-height |
Tooltip and Toaster woke every frame |
while waiting on a clock (the 450ms reveal delay, toast expiry) they invalidated per vsync. Both now schedule a single timed wake-up (op.InvalidateCmd{At:}) |
InputOTP length and caret |
the over-length guard rewrote the editor's text; it now uses Gio's Editor.MaxLen, and the active-slot ring follows the editor's real caret (Selection()) rather than the text length, so arrow keys and clicks move it |
Digit-only InputOTP on mobile |
now sets key.HintNumeric, so phones raise the number pad |
[0.1.0] - 2026-08-02
The initial release: the ikaito design language for Gio (gioui.org) — desktop, mobile and web from one Go codebase.
Components
Component vocabulary and docs anatomy follow the conventions the web settled on; the color engine is lotusui's own.
Button— variantsButtonDefault/Secondary/Destructive/Outline/ Ghost/Link; icon-only and with-icon;Roundedpill form; loading (spinner orLoadingText); disabled; 1dp press nudge; focus ring.Badge—BadgeDefault/Secondary/Destructive/Outline/Ghost;Icon; rawBg/Fgstatus pairs.Input— variants outline/subtle/flushed;Filter/Transforminput mechanisms;Error;Start/Endslots; suffix addon.Field— label, helper, error, required, around any control.Checkbox— indeterminate, invalid, disabled.Switch— animated on the shared clock; invalid, disabled.Select— floating panel on the portal primitive;Groupswith labels and separators; check-marked selection; scrollable (7-row cap) opening aligned to the selection; placeholder; invalid.Tabs—TabsDefault(muted well, raised active tab),TabsLine,TabsSubtle;Vertical;Icons; per-tabDisabled; the explicitUpdate-before-Selcontract.Dialog— full-window scrim, width presets per size, entrance motion (Appear()), Escape/backdrop dismissal viaonClose, corner ✕ (suppress withHideClose).DropdownMenu—DropdownMenuItem,DropdownMenuItemIcon,DropdownMenuShortcutItem,DropdownMenuCheckboxItem,DropdownMenuRadioItem,DropdownMenuLabel,DropdownMenuSeparator.Card—CardOutline/Elevated/Subtle, size-scaled padding, equal-height rows viaMin.Y; the image and edge-to-edge patterns documented.Accordion,Alert(+Action,Color),AlertDialog,Avatar,Breadcrumb,Pagination,Popover,Progress,RadioGroup,Separator,Skeleton(+SkeletonCircle),Slider,Spinner,Table,Textarea,Toast/Toaster,Toggle/ToggleGroup,Tooltip— each with live per-example docs and an API table.DropdownMenuTrigger— the anchored floating menu (close on selection/outside/Escape;KeepOpenfor checkbox/radio menus).- One surface grammar: borders never change with elevation; resting surfaces carry the 1dp seat, floating layers the full shadow.
- Strictly monotonic size ladders 2XS→2XL, pinned by a test.
- Extensions beyond the usual web catalogs:
Grid(spans, auto-flow),SimpleGrid(responsive columns, equal-height rows),Split+VSlide(the carousel motion grammar),Stack(VStack/HStack),ListView(virtualized;Scrollable,HoverRow), plus the page/layout utilities. Floating— the portal primitive (end-of-frame layer on Gio'sop.Defer): escapes parent clips, paints above everything, wins hit-testing. Select's panel is built on it.
Sizing and color
- Shared
Sizeenum:Size2XS…Size2XL,SizeMDdefault, one meaning across every component. - Semantic
Palettetokens in paired fill/ink slots;Schemewith interaction steps (.500 base → .600 hover → .700 pressed);ColorScaleC50–C900 with ten stock scales andScaleFrom(anchor); per-instanceColor ColorScaleon Button/Badge — one anchor in, the whole ladder out. Precedence:Scheme>Color> the variant's role scheme. DefaultDarkPalette— dark mode is a palette:NewTheme(WithPalette(lotusui.DefaultDarkPalette)).- Theme construction:
NewTheme(WithPalette/WithRadius/WithSpace/ WithTextSize/WithFaces)— everything resolves once, nothing per frame.
Tooling (go run github.com/ikaito-com/lotusui/cmd/lotusui …)
add/update— the ownership model: vendor a component's source into the app (exported core identifiers auto-qualified, unexported helpers carried into a CLI-owned companion); update with a true three-way merge (base reconstructed from the Go module cache).registry— generatesregistry.json, the build-time catalog for the CLI and AI agents (never read by app code at runtime).skills— installs the agent skill files into.claude/skills/.icons— Iconify fetch + normalization + typed constants;theme— palette generation from an anchor ortheme.json, with WCAG contrast checks;verify— offline drift checks (icons, theme, API baseline, registry);api— the exported-API baseline;init— app scaffold;release— the version ritual.- First block:
login-form(registry/blocks/login).