Repository navigation
Releases
v0.5.0
Minor Changes
-
f86a656: Dark mode and glass are easier to get right, for people and coding agents.
registry.jsonships appearance recipes (appearance): filling the page
in dark mode, a light/dark toggle, following the OS, remembering the choice
without a flash, glass and its backdrop, and making your own markup - and
Tailwind'sdark:variant - follow the provider.@fujiui/mcp0.2.0 serves
them through its newget_appearancetool.- Fixed:
registry.jsonhad no glass token values. The token scope parser
still looked for thedata-fuji-glassattribute removed in the theme/material
split, so every[data-fuji-material="glass"]value was dropped. Glass and
light-glass values are now recorded as theglassandglass+lightscopes. <html>takes the theme background once it carriesdata-fuji-theme-
set by the root provider'spersistor by the bootstrap script - so
overscroll and a page shorter than the viewport no longer show the browser's
white under a dark app. It is a zero-specificity rule, so any page background
of your own still wins.
-
f86a656:
Keyboardmirrors the physical keyboard by default, and a mirrored key now
presses its cap rather than only lighting it.captureKeyswas off by default and, when switched on, lit a cap for as long
as its real key was held. Two keyboards on one screen behaving as one is what
people expect of a soft keyboard, so it is now the default, and a mirrored
press does what a press on that cap does: the cap travels, and it clicks if the
board has a voice. Auto-repeat is excluded - a held key fires keydown over and
over, and replaying the strike and the click on each of them turns one held key
into a stutter no physical board makes.The listener is also scoped, which it was not before. A board captures only
while it is engaged: a floating board while it is open, an in-flow board
while focus is inside it. Defaulting a globalwindowlistener to on would
have meant every board on a page reacting at once - the documentation page
carries eight, so one keystroke aimed at the search field would have lit all
eight boards and clicked eight times. Click or tab into an in-flow board to
engage it;captureKeys={false}opts out entirely.A mirrored press deliberately does not call
onKeyPress. The real key has
already been delivered to whatever had focus, so reporting it again would type
every character twice. -
f86a656:
Keyboard: held caps repeat, and ⌘/Ctrl/Alt work as shortcut modifiers.- Hold to repeat. Holding a cap - Backspace, a letter, an arrow - now repeats
it like a real key: one press, a 400ms pause, then twenty a second until it is
released or the pointer slides off. Previously a cap reported once, on
release, however long it was held. Modifiers and toggles never repeat, and the
click that ends a hold reports nothing extra. - Shortcut modifiers. ⌘, Ctrl and Alt caps now latch like Shift: click ⌘,
then A, and the board reports ⌘A. The latched cap lights and announces
aria-pressed; the next ordinary cap spends it. A physically held modifier
counts too while the board is engaged. onKeyPress(key, modifiers). The callback gains a second argument,
{ shift, meta, ctrl, alt }(exported asKeyboardModifiers). Under ⌘ or
Ctrl a cap reports novalue, since a shortcut types nothing - a handler that
only appendskey.valueno longer types an "a" for ⌘A.
Non-breaking at runtime: existing one-argument handlers keep working. A test
assertingtoHaveBeenCalledWith(objectContaining(...))ononKeyPressneeds a
second matcher, e.g.expect.anything(). - Hold to repeat. Holding a cap - Backspace, a letter, an arrow - now repeats
-
f86a656: Give
Keyboardits own sound toggle.soundwas a one-way prop: a board either clicked or it did not, and the only
way to let someone silence one was for the app to build a control and wire it
up. Every app that wanted the obvious affordance built the same switch.soundTogglenow renders that control on the board itself, in a strip above
the caps: a speaker glyph plus a lamp lit in the board's owntone- the same
signal a latched Caps Lock gives, for the same reason (state on this board is a
light, never a redrawn legend). Two channels carry the state, and the icon
changes shape rather than only colour. The strip is row 1 of the cap grid, so
it inherits the deck's width and padding, and everything in it is sized from
--fuji-key-unitwith pixel floors, so it stays in proportion from a 30px-cap
diagram to a 92px-cap docked board. The control clears the 24px touch-target
floor at every board size.It defaults to
interactive, so a board you can press has the control and the
static diagram of a shortcut - which has nothing to sound and no business
growing chrome - does not. PasssoundToggle={false}where the app already
offers the preference itself, so it is not presented twice. Because the strip
is drawn above the caps, it is also the board's first tab stop; focus order
follows visual order.soundkeeps working unchanged, and now has the usual uncontrolled half -
defaultSoundplusonSoundChange- so an app that already owns a "keyboard
sounds" preference can keep owning it while the board draws the control.In development the board also calls out the one wiring that produces a dead
control: a visible toggle against a controlledsoundwith noonSoundChange
reports every press to nobody, so the button moves nothing and the board keeps
clicking.useControllableStatecannot catch that on its own - controlled with
no handler is legitimate for a read-only value, and only becomes a defect once
the component draws a control for it. The symptom is a toggle that works on
some boards and not others, which reads as a flaky component rather than as a
miswired prop.
Patch Changes
-
f86a656: Fixed:
BottomNavigationreported the wrong index for items after a centre
action. With anaction, the bar splits its items around the raised
button, and the second half was numbered from 0 again - so in a four-item bar
onItemSelectreported "Friends" as index 1 and "Playing" as index 0, and an
app switching screens by index opened the wrong one. Each item now reports its
own position initems. -
f86a656:
Card effect="tilt"leans more gently: at most 3deg at the corners (was 7deg), under a 1200px perspective (was 500px), with a 4px push-back (was 10px). A large card no longer swings visibly out of plane. -
f86a656: Align
ChatBubble's avatar with the bubble instead of with the metadata under
it.The row was
flex items-end, so the avatar bottom-aligned with the whole
message column - sender, bubble, and the timestamp/status line together. Any
bubble carrying metadata therefore pushed its avatar down past the bubble and
level with the timestamp, which reads as a layout bug at every avatar size and
gets worse as the avatar gets smaller: a 32pxsize="sm"avatar beside a
timestamp row ended up almost entirely below the bubble it belonged to.The row is now a grid with the avatar in its own column on the bubble's row, so
it is pinned to the bubble's bottom edge whatever else the column holds. The
vertical rhythm moved from agap-yto margins on the sender and metadata rows,
because a grid gap is paid between tracks even when one of them renders nothing.Grouped bubbles also keep the avatar in the DOM and hide it, rather than
swapping in a fixed 32px placeholder. The slot takes an arbitrary node, so the
avatar's own box is the only footprint certain to match the rest of the run -
the old placeholder silently misaligned every run built with the default 40px
Avatar.The tail no longer seams against the bubble either. The two are separate boxes
carrying the same opaque fill, and they used to meet exactly on the bubble's
edge - so wherever that edge landed on a fractional pixel, both were
antialiased against it and composited in turn, bleeding roughly a quarter of
the page colour through the join. Against light's near-identical page and
bubble that is invisible; in dark it drew a visible hairline between the tail
and the bubble, which is why the same build could look clean in one layout and
seamed in another. The tail now runs 1px under the bubble, so the two never
share an antialiased edge. Its visible silhouette is unchanged.
v0.4.0
Minor Changes
-
5cebea1: New:
ChatBubble.Typing(also exported asChatBubbleTyping). The "someone
is typing" indicator: three pulsing dots behind arole="status"with a visually
hiddenlabel(default"Typing"), so consumers no longer hand-build it. The
pulse is the sharedanimate-fuji-pulseand stops under
prefers-reduced-motion: reduce. Pure CSS, soChatBubblestays
server-renderable.New:
ChatBubble.Attachmenttakes apreview. Artwork - an image thumbnail,
a video poster - rendered in place of the icon, cropped into a small rounded
square (resizable through the newclassNames.preview). Image attachments had no
way to show what they were. WithoutonClickthe chip is still a plain<div>,
so it can sit inside a consumer's own link.New:
BottomNavigationitembadgeandbadgeLabel. A count pill pinned to
the item's icon, hidden at0and capped at99+. A numeric badge is announced
as part of the item's name ("Chats, 3 unread") through visually hidden text;
badgeLabelchanges that wording, or names a non-numeric badge. Previously an
unread count had to be drawn into the icon by hand, and was never announced.Fixed:
renderLinkdroppedaria-current.Navbar,BottomNavigationand
Breadcrumbhanded a customrenderLinkonly the item's content, so a router
link lost what the default anchor carried:aria-current="page"on the active
item - the only non-colour signal of it - plus the link reset and focus ring.
renderLinknow receives a third argument with exactly those props (href,
aria-current,className,children, and onBottomNavigationtheonClick
that reportsonItemSelect), exported asNavbarLinkProps,
BottomNavigationLinkPropsandBreadcrumbLinkProps. They are also applied to
the elementrenderLinkreturns, filling in only what it does not set itself,
so existing(item, children) => <Link href={item.href}>{children}</Link>
callbacks are fixed without changes. One visible effect: aBottomNavigation
router link now firesonItemSelect, as its default anchor always did.Fixed:
Statisticand the charts could fail hydration. Numbers were
formatted with the runtime's default locale, so a server and a browser in
different locales rendered different digits ("1,234" vs "1.234"). They now
format with a fixed"en-US"locale by default, and take alocaleprop to
localize explicitly.Statisticalso takesformatValuefor currency, compact
notation or units. This changes the default output for anyone whose runtime
locale was not English: passlocaleto keep localized figures.Fixed: line and bar charts had a 380px minimum width. The plot carried
min-w-[380px], so in a card on a 320-375px screen it overran the card and
could only be scrolled sideways. It now shrinks with its container like any
scaled image. At very narrow widths the axis text scales down with it; the
sr-onlydata table still carries the exact values.Fixed:
Carouselcoverflow ignoredslidesPerView. It now sets the centre
slide's width (100% / slidesPerView, clamped to at least 1), including the
responsive map form; without it coverflow keeps its 1.6 default. A non-looping
coverflow also now lets every slide become active - it computed its last index
as if it were the flat preset, which stopped short of the final slide.New:
TreeexpandOnSelect(defaulttrue, unchanged behaviour). Selecting
a parent both selected and toggled it, so a folder could not be selected without
opening or closing it.expandOnSelect={false}decouples them per the WAI-ARIA
tree pattern: a row click and Enter/Space only select, the chevron toggles on
click, and ArrowRight/ArrowLeft expand and collapse.Fixed: grouped
Timelinetimestamps were cut to 32px. The timestamp column
was a fixedw-8, which fit a two-digit year and nothing else - "Sep 14" or
"9:41 AM" overflowed into the title. Timestamps now share one column sized to
the widest in the group (min-w-8as the floor, so short stamps align as
before), and titles stay aligned across rows, including rows with no timestamp.Fixed:
FloatingActionBarfought consumer positioning. Its root carried
relative, so whetherclassName="fixed bottom-6 right-6"won depended on
the page's cascade-layer order. The root is now unpositioned and the column anchors to an
inner wrapper. Actions are no longer keyed bylabelalone - two actions with
the same label caused React key warnings - and take an optionalidto key by.New:
Cardpadding("none" | "sm" | "md" | "lg", default"md"- the
existing 20px). A consumerp-0for flush content only wins with the right
cascade-layer order, and leftCard.Mediacancelling 20px of padding that was
no longer there. The prop works regardless of stylesheet order, and
Card.Mediafollows it, so media still bleeds to the edge at any value. New type:CardPadding. -
5cebea1: Date and form-field fixes found while building example apps, plus two small
Calendaradditions.DatePickerandTimePickerignored theirFormField. Their triggers were
barePopover.Triggers, invisible to Base UI's Field:FormField.Label's
forpointed at an id nothing rendered (clicking it did nothing, and the
trigger had no accessible name),FormField.Descriptionwas never announced,
and<FormField invalid>painted neither the red border noraria-invalid.
Both triggers now render throughField.Control- the same Field-aware leaf
Inputuses - so they get the field'sid,aria-labelledby,
aria-describedby,data-invalid/aria-invalidanddisabledlike every
other Fuji field. A FormFieldvalidatefunction receives the selected day as
YYYY-MM-DD(DatePicker) or theHH:mmstring (TimePicker).TimePickeralso
gains thearia-labelledbyprop its siblings already had.Combobox,MultiSelect, andNumberInputerased their FormField label
reference. Each passedaria-labelledby={undefined}explicitly, which wins
Base UI's merge over the label id it sets - the same stomping pattern fixed for
data-invalidearlier. The fields stayed named only throughlabel[for]; they
now keeparia-labelledbypointing at the label too.minDate={new Date()}disabled today.Calendar(and soDatePicker)
compared bounds by timestamp, so today's midnight cell sat "before" aminDate
carrying the current time.minDate/maxDatenow compare by calendar day,
including when clamping the keyboard focus position.MultiSelect'ssizeonly changed its font size. The field hard-coded the
mdcontrol height, sosmandlgrendered as tall asmdbeside a
SelectorInputof the same size. Height, input row, and chip padding now
followsize.Slider'saria-labelnever reached the slider. It was spread onto the
wrapping group, leaving the focusablerole="slider"input unnamed. It is now
applied to the thumb input. A range value ([20, 80]) also rendered only one
thumb - the second value had no handle at all;Slidernow renders one thumb
per value, and the newgetAriaLabel(index)prop names each one
("Minimum price" / "Maximum price").New:
CalendarmarkedDatesandmarkedDateLabel. Pass a list of dates
(matched by day) or a predicate to put a small dot under days with something on
them. The mark is not visual-only:markedDateLabel(default"marked") is
appended to each marked day's accessible name, e.g. "Monday, May 20, 2024, has
tasks".New:
todayonCalendarandDatePicker. "Today" always came from the
real clock (reconciled after mount), so demos and tests could not pin it.
todaysets the date used for the highlight,aria-current="date", and the
initial month; it renders identically on server and client. Omit it to keep
the existing behavior. -
5cebea1: Overlay, palette, keyboard and field fixes found while building example apps
against the package, plus the small APIs those apps had to work around.Behaviour changes, under a minor bump (pre-1.0). SPEC.md §10 asks for the
callout, so: the standaloneKeyboard layout="numpad"no longer has a
NumLockcap (it is nowBackspace); an in-flowKeyboardno longer takes
focus when a cap is clicked; an explicitKeyboard widthis no longer capped
by the size's cap ceiling; aTextareagivenrowsno longer has the size's
minimum height; andCommandMenulists ungrouped items first even when a
grouped item comes earlier initems. Each is below, with why.Keyboardnever steals focus. Only afloatingboard swallowed the
mousedown that moves focus onto a clicked cap, so an inline keypad beside an
input blurred that input on every press and consumers wrapped it in a div
cancelling mousedown themselves. Every interactive board does it now. Caps are
still buttons reached with Tab and the arrow keys.Keyboardfills its container when asked. An explicitwidthwas still
capped by the size's cap ceiling (38px caps atmd), so in flow
width="900px"drew a 630px board and a numpad could never fill its card; an
explicit width now lifts that ceiling. A percentage was read against the grid
itself rather than the space it was given -width="100%"collapsed a numpad
to an 88px sliver - and is now a share of the box the board sits in, so
width="100%"spans its parent. The board still never outgrows that box. The
container-bound cap unit also forgot the deck's 1px border, leaving the
right-hand inset 2px thinner than the left; measured symmetric at 320px and
375px, with no horizontal overflow at either.Keyboard's numpad can delete. A PIN or OTP keypad built on
layout="numpad"had no way to remove a digit. Its top-left cap is now a⌫
Ba...
v0.3.0
Minor Changes
-
fafeb5f: New: the appearance bootstrap script ships.
buildAppearanceBootstrapScript()
returns the small pre-paint script that reads a returning visitor's saved
theme/material/radius/elevation and stamps thedata-fuji-*attributes before first
paint, sopersistno longer flashes the default theme on load. It comes with
APPEARANCE_STORAGE_KEY(thelocalStoragekeypersistwrites) and the
StoredAppearancetype describing the payload.It returns a string; the package never injects it. Where it runs is still the
app's decision -docs/ssr.mdshows the Next.js and Vite placements.Previously every consumer hand-copied this script out of the documentation,
which meant the storage key and payload shape were duplicated in every app that
usedpersistand drifted silently the moment either changed.SPEC.md§3 and
§6 are updated: §6 had stated the package does not ship the script. -
fafeb5f:
@base-ui/reactfloor raised to^1.7.0(from^1.6.0). It is a runtime
dependency, so this changes what resolves in every consumer's tree - an app
pinned below 1.7 will see a duplicate copy installed, or a resolution error
under a strict package manager.1.7 is what the overlay, menu and field primitives are built against here; the
positioner work inuse-untransformed-positioner.tsdepends on its
Positionerbehaviour, and the parts re-exported from it
(DialogRoot,TabsRoot, and the rest) inherit their props from that version. -
fafeb5f:
Cardgains aneffectprop, and its hover treatment now actually
animates.interactiveis replaced byeffect, which names what the card does instead
of asserting that it is interactive:effect="lift"- the previous treatment: scales up slightly, tips a degree
and deepens its shadow. Pure CSS, so it still works in a Server Component.effect="tilt"- new. Tracks the pointer and tilts the card in 3D towards
it, springing back on leave (the reference is motion.dev's tilt-card).
Skipped for touch pointers and underprefers-reduced-motion: reduce.
interactivekeeps working and maps to"lift".effectwins when both are
given, soeffect="none"opts a card back out.Fixed: the lift's scale and tilt snapped instead of easing. The transition
listedtransform, but Tailwind v4 emitsscale-105and-rotate-1as the
individualscaleandrotateproperties, neither of which was in the list.
Measured 60ms into a hover, the shadow was mid-interpolation while the scale
and rotation had already jumped to their final values. Both properties are now
named in the transition.Fixed:
prefers-reduced-motion: reducedid not suppress the lift. For the
same reason, themotion-reduce:transform-noneguard could not undo ascale
or arotate- a reduced-motion visitor still got the full scale and tip. Nor
could amotion-reduce:scale-100override fix it:.hover\:scale-105:hover
carries a pseudo-class and a media query contributes no specificity, so the
hover rule wins regardless of order. The moving half of the lift is now gated
behindmotion-safeinstead, so for a reduced-motion visitor those rules are
never emitted at all. The colour and shadow changes still apply.The tilt variant lives in its own client module (
CardTilt) so thatCard
itself stays free of a"use client"directive, which Server Component
consumers passingCardas a prop depend on. -
fafeb5f: New:
InfiniteScroll. Loads the next page when the end of a list comes
into view, for use in place of a pager or a "load more" button. Built on an
IntersectionObserver over a zero-height sentinel rather than a scroll handler,
which is what keeps a long list smooth. Re-entry is guarded: the observer can
fire several times before the parent re-renders with the new page, and without
the guard one scroll to the bottom fires three or four duplicate fetches. Works
with any content, includingDataTable(setpageSizeto the number of rows
fetched so far, since infinite loading is what replaces the pager).New:
FloatingActionBar. A speed dial: a circular trigger that fans a
column of labelled actions out from itself (the reference is motion.dev's
floating action button). Two details carry the effect - the actions stagger in
nearest-the-trigger first and reverse on close, so the column unfurls from the
button rather than a menu appearing; and each step further out is slightly
smaller, which is what gives the column depth instead of reading as a list.
The column is absolutely positioned against the trigger rather than being a
flex sibling, so opening it cannot move the trigger - the one element that must
stay put, since the pointer is already on it - and a dial can be dropped
anywhere without reserving space for its own expansion.directiondefaults to
"auto", which opens downwards when the dial sits near the top of the viewport
and upwards when it sits near the bottom.Deliberately not built on
Popover: the actions belong to the trigger, and a
portaled popup would cross-fade a separate surface in and lose that. Collapsed
actions stay mounted so they can animate, but areinertand hidden from
assistive technology. Takesdirection("auto"default,"up","down").Alertredesigned. It was a uniformly tinted block, which had to stay pale
enough for body text to sit on and so left every variant looking like the same
faint card. The tone is now washed in from the leading edge and fades out
before the text, letting it start saturated enough to identify the variant at a
glance, and the icon rides its own raised tile.Fixed: the tab indicator briefly overlapped the neighbouring tab. It eased
on--fuji-ease-spring, which overshoots - measured travelling to 198px on a
194px destination - so on the pill variant the opaque tile crossed the next
label before settling back. It now uses a decelerating curve that never passes
its slot.Fixed:
Popover's arrow rendered as an outlined diamond. It was a
rotate-45square with a border on all four sides; since only half of it is
ever visible, the far two edges showed above the panel. It is now a clipped
triangle in the panel's own fill, the same approachChatBubbleuses for its
tail, and meets the panel edge with no seam.Fixed:
AvatarGroup's overlap read as damage. The separating ring was
painted in--fuji-border-strong, drawing a grey arc across every neighbour.
It now uses the colour of the surface behind (overridable per group with
--fuji-avatar-group-ring), so each avatar reads as cut out of the one below,
and earlier avatars stack above later ones so "+N" tucks under the last face.Fixed:
RadioGroup's selection dot could not animate. It was unmounted
while unchecked, and an element that does not exist cannot transition, so
selection popped in instantly. It now stays mounted and scales in on the same
spring asSwitch's rolling thumb.Fixed:
Image's fullscreen close button was anchored to the viewport, so
on a wide display it sat in the far corner of the screen rather than near the
photo it closes. It is now pinned to the image's own top-right corner, on its
own scrim since the photo beneath can be any colour. The lightbox backdrop is
also darker than the shared overlay scrim: a photo is shown edge-to-edge with
nothing behind it, so the page stayed visible around it and competed with the
image.Fixed:
ChatBubblehad no shadow. Abox-shadowpainted a hard edge
across the join with the tail, which is why it was removed. The cast is now a
chainedfilter: drop-shadow()on the wrapper holding both bubble and tail, so
it follows their combined silhouette - and it scales with the elevation
appearance.Fixed: chat bubbles were invisible in light and dark. The bubble fills were
defined only for the glass theme, so the other two fell back to
--fuji-surface- exactly whatCardpaints, making an incoming bubble the
same colour as the card behind it.Glass tuning. Surface blur radii drop from 20/28/36px to 6/10/14px. The
larger radii obliterated everything behind the surface, which reads as a tinted
slab rather than glass; overlays were already corrected to 8px and surfaces
were missed. The white inset "edge" highlight is also removed from every glass
shadow - at 20% it read as a hard line along the top of every panel rather than
a specular edge.BottomNavigationtakesonItemSelect. An item without anhrefwas
inert markup - the bar could only be driven by navigation, so a tab bar
switching a local view had no way to report the choice, whichNavbarhas been
able to do since 0.2. Such an item now renders as a real<button>when a
handler is given, so it is focusable and operable from the keyboard rather than
being a mouse-only<span>. Its labels also move to the new--fuji-text-2xs
with a smaller icon.Fixed: a nested overlay panel was translucent in glass.
Calendar's
month/year chooser opens inside another glass surface, so it has no
backdrop-filter of its own - an ancestor with one is already a backdrop root -
and the tint was all it had. At 48% the date grid read straight through it.Fixed: a bottom
Drawerhad no bottom padding. Its safe-area inset was
applied as a barepadding-bottom, which replaced the panel's own padding
rather than adding to it - and since that env var is0pxon any desktop
browser, the sheet's last control sat flush against the screen edge. The inset
is now added to the panel padding, andside="top"gets the mirror treatment.Charts animate between data sets. Re-rolling a chart's data snapped: the
mount-time entrance keyframes never re-run on an element that stays mounted,
and neither a pat...
v0.2.1
Patch Changes
- 0158dd9: Updated package documentation and metadata: the README now links to the live
documentation site at https://fuji-ui.vercel.app/ (docs, components,
installation, and examples pages) as the primary place to browse Fuji, and
package.json'shomepagefield now points there instead of the GitHub
README. No API, component, styling, or build changes.
v0.2.0
Minor Changes
-
ce78068: Add
ChatBubble, a Fuji-native chat message bubble for messaging/inbox-style
UI, alongsideNotificationandList.align("incoming" | "outgoing", default"incoming") picks left-aligned
neutral styling vs. right-aligned contained/accent styling, reusing the same
tokens asButton/Badge- no new colors.avataraccepts anyReact.ReactNode(typically an<Avatar />);sender,
timestamp, andstatus("sent" | "delivered" | "read", rendered as an
icon and a text label, never color alone) round out the message metadata.groupedsuppresses the avatar/sender/timestamp/status and tightens spacing
for consecutive messages from the same sender, while still reserving the
avatar's footprint so the run stays aligned.ChatBubbleitself does not own
any list/collection state - the consumer decides which bubbles are grouped.childrenis a free-form content area (text, an attachment chip, a
typing-dots indicator, an audio-row composition, ...);ChatBubble.Attachment
(also exported asChatBubbleAttachment) is a small file/media chip
sub-component for the common case, renderable as a static chip or - with
onClick- a real, keyboard-accessible button.- Server-renderable (no
"use client"), theme/radius/elevation-aware, and
capped atmax-w-[75%]withbreak-wordsso it stays usable at 375px
widths.
-
ce78068: Add
Card.Media/Card.Overlaycomposition primitives, asizeprop to
Slider, andappearance/shapeprops toBadge; fix aBottomNavigation
underline bug, aButtonGroupdark-mode selection bleed, and aChatBubble
tail that never actually painted.Cardgained two new sub-components for image-heavy layouts (listing
cards, profile cards, full-bleed photo cards):Card.Media(position: "top" | "bottom" | "full", default"top") cancelsCard's own padding
on the relevant edge(s) so an<Image>reaches the card's outer border and
matches its corner radius, andCard.Overlayis a bottom-pinned gradient
scrim + content slot for captions baked into a photo. Both compose with
Card's existingHeader/Content/Footerslots.Card.Overlay's
gradient (from-black/85 via-black/45 to-transparent, was/75//25)
was strengthened after review found the original too light to keep body
text at an accessible contrast once an overlapping element (e.g. an
avatar pulled up with a negative margin) pushes text higher into the
scrim's lower-opacity zone.Slidergainedsize?: "sm" | "md" | "lg"(default"md"), scaling
both the track height (h-1/h-1.5/h-2.5) and thumb size
(size-3.5/size-4/size-5").Badgegainedappearance?: "soft" | "solid" | "bordered"(default
"soft", unchanged) andshape?: "rounded" | "square"(default
"rounded", unchanged) - existing usage is unaffected.BottomNavigation: its native<a>had no underline reset. This
package ships no CSS preflight, so the browser's default anchor underline
was rendering through the label text in both themes.
Added the sameno-underlinereset already applied to other native
elements elsewhere in the package.ButtonGroup: the managed (items-prop) radiogroup mode built its
shared border with-ml-pxnegative-margin overlap plus a z-index bump on
the opaque "contained" selected segment - in dark mode this let the
selected segment's own background bleed a visible sliver onto its
neighbor. Replaced with an explicit trailing border on each non-last
segment (applied after its own appearance classes, so it isn't cleared by
a selected segment'sborder-transparent) plus an outerborderand
overflow-hiddenon the container, which structurally cannot bleed since
segments no longer overlap. (An earlier pass useddivide-x/divide-y
on the container instead - that compiles to a zero-specificity:where()
selector in Tailwind v4, so it silently lost to the button's own
border-0reset and never actually drew a line; the per-button border
fixed that too.) The bare-children orientation classes (unmanaged mode)
are unchanged.ChatBubble: redesigned the bubble to a tooltip-style shape with a
small pointed tail (shown once per consecutive run, on the last message,
matching the existing sender/timestamp/status grouping rule). Went through
two implementations: the first used a rotated square tucked behind the
bubble with a negativez-index, which had two problems - the negative
z-indexresolved against whatever ancestor stacking context happened to
exist outside the component (fixable withisolate, but even once
visible, a rotated square only exposes a diamond-shaped sliver past the
bubble's rounded corner, reading as a separate floating chip rather than
part of the bubble). Replaced entirely with aclip-pathtriangle sitting
flush against the bubble's one squared-off corner (the tail-side corner's
radius is removed only on the last message of a run viarounded-b*-none),
so the tail is contiguous with the bubble instead of visually detached.
-
ce78068:
regularandfloatingelevation now use identical control heights and
panel padding - elevation changes shadow depth only, matching what
SPEC.md/README.md/docs/theming.md already claimed but the tokens didn't
actually deliver.The bug:
[data-fuji-elevation="regular"]intokens.cssoverrode
--fuji-control-h-sm(29px -> 30px),--fuji-control-h-md(38px -> 36px),
and--fuji-panel-p(16px -> 14px), whilefloatingkept the:root
defaults. So switching elevation at runtime silently resized every control
and panel by a couple of pixels - a real, visible layout shift for something
docs describe as "shadow depth only."The fix: removed the
[data-fuji-elevation="regular"]override block
entirely. Both modes now read the same:rootcontrol-height/panel-padding
values; only the[data-fuji-theme="*"][data-fuji-elevation="floating"]
shadow-token blocks still differ between the two modes, exactly as intended.This is a visible sizing change for anyone using
floating(or switching
between the two at runtime): controls and panels inregularare now
1-2px taller/more-padded than they were (matching whatfloatingalready
looked like), sinceregular's values were the ones removed, not
floating's. Docs (SPEC.md,README.md,docs/theming.md) updated to
match - all three previously described a "regular reads more compact"
distinction that no longer exists. -
ce78068: Fix an "earth" tone contrast bug and recolor
earth/forestto genuine
beige/sage tones, fix a Toast icon/title misalignment, fix a glass-theme
Toast contrast bug, and polish several components: Rating's tone type,
Spinner, Toast, FileUpload, MultiSelect, Switch, Collapsible, SearchInput,
and DatePicker.earthtone was nearly invisible as a solid fill, in bothlightand
dark(notglass) ---fuji-earthwas chosen too close in lightness to
the page background in each theme (measured 1.22:1 in light, 1.53:1 in
dark; every other tone measures 5:1+). It went through two passes: an
initial fix landed a darker/lighter tan by the numbers alone, then it was
recolored again against real references - Color Hunt's "beige" collection
forearthand "sage" collection forforest, so the two read as a
coherent, intentional warm-neutral pairing rather than two unrelated
accent colors. Final values:earthis#8b5e3cin light (5.16:1 against
the page, white foreground text),#c8a96bin dark (8.41:1, dark
foreground text), and a warm beige tint (#f3e4c9at 84% opacity, was
plain white) in glass.forestmoved from a fairly saturated true green
(#346742light /#83d292dark /#9bd8a8glass) to a muted sage
(#40513blight, 7.9:1;#a1bc98dark, 9.14:1;#b1d3b9glass) - its
-soft/-borderwashes were re-derived from the new base color in light
and dark so they stay the same hue as the accent. Also added--fuji-earth/
-foregroundto:root(the no-FujiProviderfallback) - every sibling
tone had one already;earthwas simply missing it, so a tone="earth"
component used with no provider anywhere in its ancestry had no color to
resolve at all. Consequently fixed three other places that had been using
earth-foregroundas a standalone accent color (a workaround for the
old, too-paleearth) -Rating,RadioGroup, andIcon's bare
(background="none") case now useearthdirectly like every other tone
does;Icon'sbackground="subtle"case (which has no separate
soft/tintedearthvariant to fall back to) keeps pairingearth's full
fill withearth-foregroundtext, now handled as its own explicit case so
it doesn't collide with the bare-case fix.- Toast title was visibly misaligned with its icon -
Base.Titlerenders
a native heading element, and without preflight (see SPEC.md §8) its
~0.83em native top margin (measured ~12px) was never reset, pushing the
title down and out of line with the icon beside it (measured ~11px off
center). Addedm-0toBase.Title/Base.Description, matching every
other native-element margin reset already applied elsewhere in this
package; icon and title are now vertically centered together (verified
~1px apart). - Rating used its own separate, now-stale
RatingTonetype
(warning | danger | success | info | primary) left over from before the
tone migration. It now uses the sharedComponentTone
(default | earth | fire | water | forest | sun); old values map
warning → sun,danger → fire,success → forest,info → water,
...