Skip to content

Releases

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 19 Sep 15:53
1b1a3d5

Minor Changes

  • f86a656: Dark mode and glass are easier to get right, for people and coding agents.

    • registry.json ships 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's dark: variant - follow the provider. @fujiui/mcp 0.2.0 serves
      them through its new get_appearance tool.
    • Fixed: registry.json had no glass token values. The token scope parser
      still looked for the data-fuji-glass attribute removed in the theme/material
      split, so every [data-fuji-material="glass"] value was dropped. Glass and
      light-glass values are now recorded as the glass and glass+light scopes.
    • <html> takes the theme background once it carries data-fuji-theme -
      set by the root provider's persist or 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: Keyboard mirrors the physical keyboard by default, and a mirrored key now
    presses its cap rather than only lighting it.

    captureKeys was 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 global window listener 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 as KeyboardModifiers). Under ⌘ or
      Ctrl a cap reports no value, since a shortcut types nothing - a handler that
      only appends key.value no longer types an "a" for ⌘A.

    Non-breaking at runtime: existing one-argument handlers keep working. A test
    asserting toHaveBeenCalledWith(objectContaining(...)) on onKeyPress needs a
    second matcher, e.g. expect.anything().

  • f86a656: Give Keyboard its own sound toggle.

    sound was 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.

    soundToggle now renders that control on the board itself, in a strip above
    the caps: a speaker glyph plus a lamp lit in the board's own tone - 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-unit with 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. Pass soundToggle={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.

    sound keeps working unchanged, and now has the usual uncontrolled half -
    defaultSound plus onSoundChange - 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 controlled sound with no onSoundChange
    reports every press to nobody, so the button moves nothing and the board keeps
    clicking. useControllableState cannot 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: BottomNavigation reported the wrong index for items after a centre
    action.
    With an action, the bar splits its items around the raised
    button, and the second half was numbered from 0 again - so in a four-item bar
    onItemSelect reported "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 in items.

  • 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 32px size="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 a gap-y to 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

Choose a tag to compare

@github-actions github-actions released this 16 Sep 13:57
bb63654

Minor Changes

  • 5cebea1: New: ChatBubble.Typing (also exported as ChatBubbleTyping). The "someone
    is typing" indicator: three pulsing dots behind a role="status" with a visually
    hidden label (default "Typing"), so consumers no longer hand-build it. The
    pulse is the shared animate-fuji-pulse and stops under
    prefers-reduced-motion: reduce. Pure CSS, so ChatBubble stays
    server-renderable.

    New: ChatBubble.Attachment takes a preview. Artwork - an image thumbnail,
    a video poster - rendered in place of the icon, cropped into a small rounded
    square (resizable through the new classNames.preview). Image attachments had no
    way to show what they were. Without onClick the chip is still a plain <div>,
    so it can sit inside a consumer's own link.

    New: BottomNavigation item badge and badgeLabel. A count pill pinned to
    the item's icon, hidden at 0 and capped at 99+. A numeric badge is announced
    as part of the item's name ("Chats, 3 unread") through visually hidden text;
    badgeLabel changes 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: renderLink dropped aria-current. Navbar, BottomNavigation and
    Breadcrumb handed a custom renderLink only 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.
    renderLink now receives a third argument with exactly those props (href,
    aria-current, className, children, and on BottomNavigation the onClick
    that reports onItemSelect), exported as NavbarLinkProps,
    BottomNavigationLinkProps and BreadcrumbLinkProps. They are also applied to
    the element renderLink returns, 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: a BottomNavigation
    router link now fires onItemSelect, as its default anchor always did.

    Fixed: Statistic and 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 a locale prop to
    localize explicitly. Statistic also takes formatValue for currency, compact
    notation or units. This changes the default output for anyone whose runtime
    locale was not English: pass locale to 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-only data table still carries the exact values.

    Fixed: Carousel coverflow ignored slidesPerView. 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: Tree expandOnSelect (default true, 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 Timeline timestamps were cut to 32px. The timestamp column
    was a fixed w-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-8 as the floor, so short stamps align as
    before), and titles stay aligned across rows, including rows with no timestamp.

    Fixed: FloatingActionBar fought consumer positioning. Its root carried
    relative, so whether className="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 by label alone - two actions with
    the same label caused React key warnings - and take an optional id to key by.

    New: Card padding ("none" | "sm" | "md" | "lg", default "md" - the
    existing 20px). A consumer p-0 for flush content only wins with the right
    cascade-layer order, and left Card.Media cancelling 20px of padding that was
    no longer there. The prop works regardless of stylesheet order, and
    Card.Media follows 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
    Calendar additions.

    DatePicker and TimePicker ignored their FormField. Their triggers were
    bare Popover.Triggers, invisible to Base UI's Field: FormField.Label's
    for pointed at an id nothing rendered (clicking it did nothing, and the
    trigger had no accessible name), FormField.Description was never announced,
    and <FormField invalid> painted neither the red border nor aria-invalid.
    Both triggers now render through Field.Control - the same Field-aware leaf
    Input uses - so they get the field's id, aria-labelledby,
    aria-describedby, data-invalid/aria-invalid and disabled like every
    other Fuji field. A FormField validate function receives the selected day as
    YYYY-MM-DD (DatePicker) or the HH:mm string (TimePicker). TimePicker also
    gains the aria-labelledby prop its siblings already had.

    Combobox, MultiSelect, and NumberInput erased their FormField label
    reference.
    Each passed aria-labelledby={undefined} explicitly, which wins
    Base UI's merge over the label id it sets - the same stomping pattern fixed for
    data-invalid earlier. The fields stayed named only through label[for]; they
    now keep aria-labelledby pointing at the label too.

    minDate={new Date()} disabled today. Calendar (and so DatePicker)
    compared bounds by timestamp, so today's midnight cell sat "before" a minDate
    carrying the current time. minDate/maxDate now compare by calendar day,
    including when clamping the keyboard focus position.

    MultiSelect's size only changed its font size. The field hard-coded the
    md control height, so sm and lg rendered as tall as md beside a
    Select or Input of the same size. Height, input row, and chip padding now
    follow size.

    Slider's aria-label never reached the slider. It was spread onto the
    wrapping group, leaving the focusable role="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; Slider now renders one thumb
    per value, and the new getAriaLabel(index) prop names each one
    ("Minimum price" / "Maximum price").

    New: Calendar markedDates and markedDateLabel. 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: today on Calendar and DatePicker. "Today" always came from the
    real clock (reconciled after mount), so demos and tests could not pin it.
    today sets 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 standalone Keyboard layout="numpad" no longer has a
    NumLock cap (it is now Backspace); an in-flow Keyboard no longer takes
    focus when a cap is clicked; an explicit Keyboard width is no longer capped
    by the size's cap ceiling; a Textarea given rows no longer has the size's
    minimum height; and CommandMenu lists ungrouped items first even when a
    grouped item comes earlier in items. Each is below, with why.

    Keyboard never steals focus. Only a floating board 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.

    Keyboard fills its container when asked. An explicit width was still
    capped by the size's cap ceiling (38px caps at md), 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...

Read more

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 15 Sep 17:21
afc415a

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 the data-fuji-* attributes before first
    paint, so persist no longer flashes the default theme on load. It comes with
    APPEARANCE_STORAGE_KEY (the localStorage key persist writes) and the
    StoredAppearance type describing the payload.

    It returns a string; the package never injects it. Where it runs is still the
    app's decision - docs/ssr.md shows 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
    used persist and 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/react floor 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 in use-untransformed-positioner.ts depends on its
    Positioner behaviour, and the parts re-exported from it
    (DialogRoot, TabsRoot, and the rest) inherit their props from that version.

  • fafeb5f: Card gains an effect prop, and its hover treatment now actually
    animates.

    interactive is replaced by effect, 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 under prefers-reduced-motion: reduce.

    interactive keeps working and maps to "lift". effect wins when both are
    given, so effect="none" opts a card back out.

    Fixed: the lift's scale and tilt snapped instead of easing. The transition
    listed transform, but Tailwind v4 emits scale-105 and -rotate-1 as the
    individual scale and rotate properties, 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: reduce did not suppress the lift. For the
    same reason, the motion-reduce:transform-none guard could not undo a scale
    or a rotate - a reduced-motion visitor still got the full scale and tip. Nor
    could a motion-reduce:scale-100 override 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
    behind motion-safe instead, 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 that Card
    itself stays free of a "use client" directive, which Server Component
    consumers passing Card as 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, including DataTable (set pageSize to 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. direction defaults 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 are inert and hidden from
    assistive technology. Takes direction ("auto" default, "up", "down").

    Alert redesigned. 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-45 square 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 approach ChatBubble uses 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 as Switch'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: ChatBubble had no shadow. A box-shadow painted a hard edge
    across the join with the tail, which is why it was removed. The cast is now a
    chained filter: 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 what Card paints, 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.

    BottomNavigation takes onItemSelect. An item without an href was
    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, which Navbar has 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 Drawer had no bottom padding. Its safe-area inset was
    applied as a bare padding-bottom, which replaced the panel's own padding
    rather than adding to it - and since that env var is 0px on any desktop
    browser, the sheet's last control sat flush against the screen edge. The inset
    is now added to the panel padding, and side="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...

Read more

v0.2.1

Choose a tag to compare

@github-actions github-actions released this 11 Aug 15:15
397fbe1

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's homepage field now points there instead of the GitHub
    README. No API, component, styling, or build changes.

v0.2.0

Choose a tag to compare

@github-actions github-actions released this 10 Aug 12:41
31eaf01

Minor Changes

  • ce78068: Add ChatBubble, a Fuji-native chat message bubble for messaging/inbox-style
    UI, alongside Notification and List.

    • align ("incoming" | "outgoing", default "incoming") picks left-aligned
      neutral styling vs. right-aligned contained/accent styling, reusing the same
      tokens as Button/Badge - no new colors.
    • avatar accepts any React.ReactNode (typically an <Avatar />); sender,
      timestamp, and status ("sent" | "delivered" | "read", rendered as an
      icon and a text label, never color alone) round out the message metadata.
    • grouped suppresses 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. ChatBubble itself does not own
      any list/collection state - the consumer decides which bubbles are grouped.
    • children is a free-form content area (text, an attachment chip, a
      typing-dots indicator, an audio-row composition, ...); ChatBubble.Attachment
      (also exported as ChatBubbleAttachment) 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 at max-w-[75%] with break-words so it stays usable at 375px
      widths.
  • ce78068: Add Card.Media/Card.Overlay composition primitives, a size prop to
    Slider, and appearance/shape props to Badge; fix a BottomNavigation
    underline bug, a ButtonGroup dark-mode selection bleed, and a ChatBubble
    tail that never actually painted.

    • Card gained two new sub-components for image-heavy layouts (listing
      cards, profile cards, full-bleed photo cards): Card.Media (position: "top" | "bottom" | "full", default "top") cancels Card's own padding
      on the relevant edge(s) so an <Image> reaches the card's outer border and
      matches its corner radius, and Card.Overlay is a bottom-pinned gradient
      scrim + content slot for captions baked into a photo. Both compose with
      Card's existing Header/Content/Footer slots. 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.
    • Slider gained size?: "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").
    • Badge gained appearance?: "soft" | "solid" | "bordered" (default
      "soft", unchanged) and shape?: "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 same no-underline reset already applied to other native
      elements elsewhere in the package.
    • ButtonGroup: the managed (items-prop) radiogroup mode built its
      shared border with -ml-px negative-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's border-transparent) plus an outer border and
      overflow-hidden on the container, which structurally cannot bleed since
      segments no longer overlap. (An earlier pass used divide-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-0 reset 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 negative z-index, which had two problems - the negative
      z-index resolved against whatever ancestor stacking context happened to
      exist outside the component (fixable with isolate, 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 a clip-path triangle 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 via rounded-b*-none),
      so the tail is contiguous with the bubble instead of visually detached.
  • ce78068: regular and floating elevation 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"] in tokens.css overrode
    --fuji-control-h-sm (29px -> 30px), --fuji-control-h-md (38px -> 36px),
    and --fuji-panel-p (16px -> 14px), while floating kept 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 :root control-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 in regular are now
    1-2px taller/more-padded than they were (matching what floating already
    looked like), since regular'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/forest to 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.

    • earth tone was nearly invisible as a solid fill, in both light and
      dark (not glass) - --fuji-earth was 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
      for earth and "sage" collection for forest, so the two read as a
      coherent, intentional warm-neutral pairing rather than two unrelated
      accent colors. Final values: earth is #8b5e3c in light (5.16:1 against
      the page, white foreground text), #c8a96b in dark (8.41:1, dark
      foreground text), and a warm beige tint (#f3e4c9 at 84% opacity, was
      plain white) in glass. forest moved from a fairly saturated true green
      (#346742 light / #83d292 dark / #9bd8a8 glass) to a muted sage
      (#40513b light, 7.9:1; #a1bc98 dark, 9.14:1; #b1d3b9 glass) - its
      -soft/-border washes 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/
      -foreground to :root (the no-FujiProvider fallback) - every sibling
      tone had one already; earth was 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-foreground as a standalone accent color (a workaround for the
      old, too-pale earth) - Rating, RadioGroup, and Icon's bare
      (background="none") case now use earth directly like every other tone
      does; Icon's background="subtle" case (which has no separate
      soft/tinted earth variant to fall back to) keeps pairing earth's full
      fill with earth-foreground text, 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.Title renders
      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). Added m-0 to Base.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 RatingTone type
      (warning | danger | success | info | primary) left over from before the
      tone migration. It now uses the shared ComponentTone
      (default | earth | fire | water | forest | sun); old values map
      warning → sun, danger → fire, success → forest, info → water,
      ...
Read more