← ui

Updates

You own the source you install. These changes reach your project when you choose to update.

How to update a component

Preview the files and dependencies that would change. Replace the item name with the component you use.

npx shadcn add @ag/telegram-chat --dry-run

Use --diff to inspect changes. If you customized the source, merge the changes you want. To replace your installed copy, use:

npx shadcn add @ag/telegram-chat --overwrite

Review changes to shared dependencies and styles, then run your project’s checks.

  • New: a site's feed read into items (title, address, date, picture), newest first. parseFeed reads RSS 2.0 and Atom (a blog, a YouTube channel's uploads, a podcast) and runs anywhere; feedOf finds the feed a page names; readFeed fetches one on a server through public-fetch.
  • isCrawler(agent): the crawler test the route uses, for a site's own routes (a link meant for one open must not be used up by a link preview).
  • domains on visitsRoute and visitHits: the other domains a site's pages are served on (a short domain, an emoji one in punycode). A page posted from one was refused as another site's, and a visit from one counted it as an outside referrer.
  • Owners: a site whose pages belong to people says whose each page is (ownerOf(host, path), an id that never changes, so a renamed person keeps one history) and who is reading (readerOf(request)). Page rows carry owner and self; visits_owner_views counts, per owner and day, the visits that read any of their pages, each once, kept for good. A person's own reading is kept, flagged, and counted nowhere, a page's views included.
  • Hits: visitHits({ db, site, readerOf }) records a redirect the site answered (a person's /gh), which no page sensor sees: whose, the name, what it opened or null for nothing, the outside referrer, the country, each caller once a day. A crawler's (a link preview's) and the owner's own are kept, flagged, never counted. visits_hit keeps them VISIT_DAYS, visits_owner_hits the counts for good.
  • readOwner(db, { site, owner, days }): one owner's numbers, counts only (views per day and all time, what each name opened, referrers, countries), never a visit.
  • readOwners(db, { site, days }): every owner's, for the site's own view: whose pages were read most, and the names asked of anyone that opened nothing.
  • forgetOwner(db, { site, owner, paths }): an account deleted leaves nothing of theirs: their counts, hits, the rows of their pages with those pages' lines, and the views under each of their addresses (paths, /ada; matched exactly, so /ada_b stays).
  • Migration: a store provisioned before today runs VISITS_MIGRATIONS once (two ALTER TABLE visits_page), THEN VISITS_SCHEMA, which adds the new tables and indexes. A site that runs VISITS_SCHEMA on start fails on the new index until the migration has run.
  • New: publicFetch, fetch for an address a stranger named. It asks the public internet only: http and https, every address the host resolves to checked against the loopback, private, link-local (the cloud's metadata service), shared, documentation and multicast ranges, redirects followed by hand and each hop checked again. The check is made where the connection resolves the name (undici's fetch with a checking lookup), so a name server answering public when checked and private when fetched (DNS rebinding) gets nowhere; in Bun, whose undici ignores the dispatcher, the check made before the request holds everything but that. isPublicAddress is the check alone. Server-only; depends on undici.
  • New: Unfurl and hostOf, moved out of web-preview-unfurl, plus hrefOf (a link as an address: as written when it names a scheme, else on https). It fetches nothing, so a browser can import it. Migration: import Unfurl and hostOf from lib/web-preview-facts instead of lib/web-preview-unfurl.
  • unfurl fetches through publicFetch: a server unfurling what visitors type never asks its own network (http://169.254.169.254/, localhost, 10.0.0.5), on the first request or after a redirect. An address with another scheme (file://) is refused rather than read as a host. It now depends on @ag/web-preview-facts and @ag/public-fetch.
  • framing and livePage fetch through publicFetch too.
  • A panel inside a frame already drawn (a panel's body, or InFrame) draws the inset surface, whatever its material: a step of light off what is under it, a hairline and the theme's corner, no glass of its own. A panel that is a link brightens a step under the hand, alone or inset. Every box that stands in another is now drawn the same, in one place.

social-profile, web-preview

  • They draw their surface by default again, inside a frame too, where it is the inset one (panel): inside a panel they drew none, and stood bare beside a GitHub card that had a box. frame={false} still draws none.
  • style="compact": the link in one row, social-profile's compact row's shape (the page's picture, else its icon on a tile, where a face stands; the title; the domain under it), for a hover card on a link's mark or a list.
  • New: an email address as the page shows one, beside social-profile's shapes. compact, one row to copy it or write to it, for a hover card or a list; window, the new message a mail app opens to it, drawn, for a story of where an address leads. Takes a mailto: link or the bare address.

github, tweet

  • The cards (repository, thread, comment, profile) and the post are a panel, no longer a box of their own: alone the panel's card, inside a frame the inset surface, like every other. A quoted post inside a post is the inset one. Both now depend on @ag/panel.
  • A glass container's spill (GlassSpill, on every glass Panel) blurs only its band now: four strips along the edges instead of one layer the size of the container, which the browser blurred whole before the mask cut it to the band (a 632×475 panel: about 300,000 pixels a frame, now about 71,000). It draws the same: a pixel comparison of a profile panel shows no difference past the screen's dithering.
  • --glass-saturation, inherited: how much every layer of glass (a body, a front, a spill) deepens the colour behind it. 1, the default, is each layer's own deepening as before; 0 blurs the picture and leaves its colour as it is. Set it on any ancestor, the page's root included.
  • Each flag is now its own SVG file, drawn through image, instead of inline art: any client component that drew one shipped every flag's drawing with it (about 500 KB, 108 KB compressed). The item installs the route that serves them, app/flags/[shape]/[file]/route.ts, read from the installed country-flag-icons and prerendered for every flag at build, so the CDN serves /flags/3x2/ES.svg and no function runs. Migration: shadcn add @ag/flag --overwrite brings the route; a site serving /flags already must move one of them.
  • The route reads a flag's file synchronously: on a site with Cache Components an awaited read is I/O that made the route dynamic, a function per flag, where it is now prerendered for every flag as said above. Update the route with the item.
  • An icon in a translucent colour is dimmed as one shape again. The rule that draws its strokes opaque and masks the whole icon by the colour's alpha wrote the opaque colour as rgb(from currentColor r g b), which keeps the origin's alpha, so the strokes stayed translucent (their crossings doubled) under a mask that dimmed them twice. It is / 1 now. Update app/tokens.css (shadcn add @ag/tokens --overwrite).
  • New: TabsPanels, a track the TabsPanels stand straight in, one for each tab, swiped between with a finger or two on a trackpad. It is the lightbox's own track (slide-track, in @ag/lightbox-motion, which the item now installs), the one engine both drive: the panel follows the hand toward a short line, one tab a swipe, held at the first and the last tab without moving past them, and every move glides from where the panels stand to rest exactly on one. Up and down stays the page's, a sideways scroller or a slider inside a panel keeps its own gestures, a mouse drag stays a text selection, and a press, a click and the keys are unchanged; choosing by swipe is pressing the tab, so links and a controlled value work as they do, and a tab pressed glides the track there on the same curve. To adopt it, wrap the panels: <TabsPanels>{panels}</TabsPanels> inside the Panel (or the Tabs).
  • In a track every panel stays laid out beside the others, so showing a tab no longer rebuilds its panel's style and layout; one not shown is inert. The track is as tall as the panel shown. A mount="active" panel keeps its element there (its place on the track), its content mounted only while shown; mount="seen" also mounts the panels either side once a swipe starts.
  • A sideways wheel swipe over the tab bar (anywhere in the Tabs) swipes between the panels, as one over a panel does; nested tabs keep their own.
  • In a panel, TabsPanels runs to the panel's edge: the track reaches over the body's padding, read off the body itself (p-5, or whatever its bodyClassName makes it), and each panel puts it back inside itself. A swipe no longer shows the panels clipped in a line inside the frame, and the padding beside them takes a swipe too. Panel marks the element whose padding is its body's (data-panel-pad).
  • TabsPanels takes onTrace: every decision of its swipe (the stream's axis, a finger's, the engine's lines), for a page that shows or keeps them.
  • New file: slide-track (createSlideTrack, SLIDE_TRACK, SLIDE), the lightbox's slide track out of the lightbox: a row of slides in a real scroll container driven by one writer for every input, one slide a gesture, held at both ends, a glide that starts where the slides stand. The lightbox runs on it unchanged (the same wheel gestures, before and after, land on the same slides with no overshoot), and tabs swipes on it.
  • Breaking: SLIDE_GAP is removed. The track reads the room between slides from its own CSS gap (the lightbox's stays 32px in lightbox.css): a gap kept as a number beside the CSS drifted the track off every slide where a rem is not 16px. Migration: set the gap on the track in CSS.
  • A slide tips past its halfway point (SWIPE_TIP), so where a swipe will land is on the screen before it does. A finger holds the picture 1:1 and on release it lands where the motion would have coasted (swipeTip), so a quick short flick turns it as a native pager does. A wheel moves the slides by swipeGain: the same hand a slide cost before (swipeCommitPx, 225 px on a wide screen) now carries the picture to its halfway point, where it tips. Every move after the hand lets go is a fall: it leaves at the speed the slides were moving, never a jump, speeds up and lands, from the tip in CARRY_MAX (240 ms) and sooner the shorter the way (carryMs), shortened under a hand faster than that. Before, the slides leaned toward an invisible line and either crept and wound back or shot off from 13% across. A wheel's gesture ends the moment the hand lifts, read off its speed: once its pace (the mean size of its last 6 deltas, swipePace) falls under half its own peak (SWIPE_LIFT) while the wheel phase sees the device's coast, the trackpad is coasting (a slow drag never lifts early: it ends on silence), and the slide decides there and the coast moves nothing; after any decision, a pace rising by half over its lowest since is the hand pushing again, and a new swipe (swipeAgain). Before, the coast inched the slides on for half a second and then wound them back. Breaking: SWIPE_LEAN and swipeGive are removed; SlideTrack.wheel(dx, handBack, coasting) takes the wheel phase's read and no speed (the track measures its own), and SlideTrack.glideTo(i) none either.
  • A hand back on the trackpad inside the last swipe's coast is a new swipe: the wheel phase's start breaks a spent gesture, beside the surge and the turn, so a second flick the same way is no longer swallowed as tail. SlideTrack.wheel takes it as handBack.
  • lightbox-wheel-phase opens a new stream when a delta turns to the other axis (AXIS_TURN, inside a stream already AXIS_HELD along one): a coast never changes axis, so a swipe down straight after a sideways one is read at once instead of going to the sideways track until the coast was cut short. PhaseRead.why says why a stream opened (first, silence, interrupted, turned).
  • lightbox-wheel-phase ends a stream after SWIPE_END of silence. It never ended one before, so after a slow sideways swipe with no coast the lightbox read the next stream's axis off the old swipe's travel: a swipe up to dismiss went on sideways.
  • createSlideTrack traces every decision with the engine's whole state when its debug is on, and watches the scroller once a frame for the browser moving it, snapping held off with nothing driving, and a rest between two slides. Its pitch is read from the layout's width, not the drawn one, so a track measured while it scales in no longer keeps a wrong pitch.
  • onTrace: every line the lightbox's debug trace writes, handed to a function as well, for a page that shows or keeps them.
  • A click or tap beside a zoomed picture zooms it back to fit, as Escape does (the next one closes, as at fit). It did nothing before.
  • New: ScrollStory, a story told on the scroll from one list of chapters (id, label, mark, its scroll as span/play/hold, and content({ p, ambient, active, driven }), what it draws at its moment). The beats, each chapter's progress, the picker's seeking and the stacked carousel (a phone, reduced motion) are derived from the list. layout="deck" slides each card in from the side (card-gallery); layout="stack" crossfades the chapters in one card. StoryCard sets a chapter's words (title, body, action, aside) beside its scene, on either side. A device stands taller than its card: the story keeps room round its cards (ScrollStory's overflow, a length) and a card's scene reaches into it (StoryCard's overflow), never cut by the deck's edge. StoryTitle is the two-tone headline, one line or two, never three: each part unbroken, sized to fit the words' column. The cards stand on the theme's floating surface, --card, the layer above the page's --background.
  • New: AccountMenu, the way in and the account as one control, for any auth library: user ({ name, image, href }, null signed out, undefined not known yet), providers, onSignIn(provider) and onSignOut. One provider is one button with its mark and words (sign in with github); several are one sign in opening a menu of them. Signed in, the face and name open a menu: the profile link, your rows as children, sign out. A press returning a promise shows a spinner, width kept, until the page leaves, user changes or the promise rejects. SignInMenu drops the ways in from any trigger (a vote's, with a reason); ProviderGlyph draws a simple-icons mark. md or lg, plain or glass.
  • The signed-out face's line is the chevron's on screen (1.33 px), at either size; it was 2 px at lg, heavier than everything beside it. The chevron sits concentric with the pill's round end, as the face does with its start, no longer 20 px of air from the edge.
  • icon: the signed-out control's own icon in place of the face, drawn as any icon beside words and concentric with the pill's start, for a site whose own mark is a person.
  • New: ContactsApp, a picture of iOS 26's Contacts app drawn from a .vcf's text, for an IphoneFrame's screen: view="import" is the sheet iOS raises over an opened .vcf (pressed lights Create New Contact, the instant before it saves), view="contact" the saved card (back and Edit, the face, the round actions dimmed where the card has no channel, each kind of field its own group with its glyph at the end, the tab bar). As iOS 26 draws it: a poster in the photo's colour (average, the photo's mean, prepare-media's; iOS's grey without one), everything on it white, the controls the registry's liquid glass. scroll (0 to 1) scrolls the saved card from its top to its foot, for a story's scroll to drive. Every length is an iOS point as a share of the screen's width, so it is the same screen at any frame size. contactsAppScreen(view, average) is the frame's style: the status bar's ink and the ground under it, which the frame draws. Fields are read by contact's lib/vcard-core.
  • view="preview": the sheet iOS 26 raises over Safari for an opened .vcf, done (a blue ✓) and share in glass, the face larger, no commands under the card; it scrolls with scroll as the saved card does. Under the name, the company or else the nickname, in every view.
  • The import sheet's top corners clip with clip-path, never overflow: hidden, which kept the glass on it from blurring everything (see device-frame).
  • CONTACTS_APP_CONTROLS: where a finger presses the app's controls (the preview's done ✓), in points on the 402-point screen, for a story's Touch; done swells while pressed (data-pressing round the app), as iOS 26's glass does.
  • If you installed it earlier today: scheme is gone, the poster takes the photo's colour; pass average to both ContactsApp and contactsAppScreen.
  • The MacBook's display clips with clip-path instead of overflow: hidden. Its corners are uneven (rounder at the top), and Chrome clips such a box through a mask of its own, which made a glass Panel on the screen lose its spill's mask and blur everything inside it.
  • look="safari-ios": Safari on an iPhone (iOS 26), filling the screen of the box it is put in (an IphoneFrame's): the page under the status bar, back, the address and tabs at the foot, glass floating over the page or (material="plain") the opaque bar iOS drew before. Every length is an iOS point as a share of the screen's width; a scripted visit plays on it as on a Mac. The page is padded by --ag-browser-bottom as well as --ag-browser-top.
  • A typed address can start pasted ({ kind: "type", pasted, text }): the start of text lands whole at once and only the rest is typed, a route added to an address known by heart (👤.to/ada pasted, /gh typed). A pasted that does not start text throws.
  • BrowserCapturePage draws a captured site (mise browser-shot) from its manifest as it is: the picture for the scheme it stands in, children laid over it in the page's coordinates. The manifest's type is lib/browser-capture, shipped with browser: BrowserCapture, foundOn, middleOf, and atFound(capture, name), the style that sits a box on a thing --find found (plain values, so a server component computes it). browser-shot writes each picture's served address (--served, else what follows public/ in its output path) with its content's hash, so a page imports the JSON and never builds a path; a host on Next 16 allows the ?v= query on that folder (images.localPatterns). --phone captures the mobile layout, --find name=selector records where things are.
  • A site's captures as code: lib/browser-capture's CapturePlan declares them (each page's url, phone, width, tall, the things it must show as find, optional for a site that refuses headless browsers), and mise capture <plan.ts> in this checkout makes them: every page in both schemes into the plan's folder under public/, one CaptureManifest the site imports as it is. Idempotent and all or nothing: shot into scratch and moved into place only when every required page succeeded; the folder then holds exactly the plan. A page missing what it must find fails the run, which catches a capture of a deploy still rolling out.
  • A reload step ({ kind: "reload" }): the page loads again in place, its history and scroll unmoved. A page its server changes between loads is a function of the serving in BrowserSession's pages ("example.com": (serving) => …): 0 its address's first load, 1 the next; back, from the cache, keeps the serving it last showed.
  • averageOf(bytes): a picture's mean colour from bytes fetched at request time, the same measure a prepared picture's average is (a contact's poster, a quote's ground).
  • New: a code editor window in Visual Studio Code's look, Dark Modern or Light Modern by the scheme it stands in (scheme): the title bar and command centre, the activity bar, the explorer's tree of the project, tabs with the unsaved dot, breadcrumbs, line numbers, the current line, the caret, the status bar (Ln, Col, the language). Ide draws one instant (project, files, active, open, dirty, caret, explorer); IdeSession plays an edit (steps) on the playhead, its clip from codeTimeline(files, steps), and duration holds its last frame to a longer shared clock (a browser on the same Mac reloading after the save). The code is coloured by code's highlighter in the browser, each character as it reads in the finished file.
  • Ide's highlight marks lines as code spells them ("6,10", "1,4-6"), with code's own wash and bar, for a still that points at what to paste. blink blinks the caret as a focused editor's does (none under reduced motion); a playing IdeSession holds it on, as VS Code does while keys land. ide.css ships beside it for the blink's keyframes.
  • Ide's colours: the file's colours read ahead on the server (codeColours(text, langOf(path), VSCODE_THEMES), lib/code-tokens), so a still is coloured at once and the browser loads no highlighter. IDE_THEMES is gone from ide: it is VSCODE_THEMES in lib/code-tokens, importable where a client module's export is not (a server component).
  • New: Touch, a finger on a phone's screen as iOS draws one with Show Touches: a soft white disc that comes down, holds and lifts over one tap (tap, 0..1), at x, y in the box it presses on. touchPressed(tap) says when the control under it is pressed, so the control lights in step. A phone story's taps, as macos's pointer shows a Mac's clicks.
  • New: an edit of a project's files as steps (open, type at a line and column or at the caret, save, each with a delay), timed into a clip (codeTimeline, one chapter per file opened and per thing typed) and folded into the frame an editor draws (codeFrameAt): the tabs, the file in front as typed so far, the caret, what is unsaved, a modal editor's mode and command line. ide and terminal-editor draw it.
  • New: a modal editor in terminal's window, helix's look: line numbers, the code highlighted, a block caret in normal mode and a bar in insert, the status line with the mode (NOR, INS), the file and [+] while unsaved, the command line where :w is typed and answered. It plays the same code-session steps as ide: <Terminal alt="…"><TerminalEditor files={…} steps={…} /></Terminal>.
  • children: a full-screen program drawn in the window in place of a session (terminal-editor), wearing the window's palette (useTerminalPalette).
  • lib/code-tokens: code's highlighter as one ink per character (codeColours, runsOf) for a view drawing text that is not all there yet, and useCodeColours, the same read in the browser. CODE_THEMES is the one spelling of code's two themes.
  • codeColours runs on Shiki's core with its JavaScript regex engine, loading only the grammar and the two themes asked for: a page colouring code in the browser no longer downloads Oniguruma's WebAssembly (about 600 KB, 226 KB compressed). useCodeColours takes given colours read ahead, and then loads nothing.
  • New: Stepper, step through an ordered set of anything, the current one said in words between ‹ and ›: a month, a frame of twelve, how deep a tree is open. A press is a step, held it repeats, an end dims (wrap goes round); the value is a spinbutton said in its words (the arrows, Home, End), and the widest option keeps its room so nothing beside it moves. Owned (value) or its own (defaultValue). StepChevron is the chevron alone, a step a press and repeating while held, never taking focus. "stepper" now finds this item; number is "number field".
  • Its ‹ › are stepper's StepChevron (the same press, hold and repeat); their slots are step-previous and step-next, where they were number-previous and number-next. number now depends on stepper.
  • value is optional: give defaultValue and the slider keeps its own, saying each change (onValueChange); give value and whatever moves it outside moves the thumb, as before.
  • New: a browser window to put a page in, Safari's look (look="safari", the only one so far), plain or as Safari 26's glass (material="glass": the controls as glass capsules over the page). Browser is controlled (url, address, editing, load, back, forward, scroll, tabs); BrowserSession plays a visit (start, steps: type, go, back, scroll; pages by address) on the playhead, its clip from browserTimeline; BrowserLive shows a site live in an iframe, or its unfurl where the site refuses framing. Whether a site frames is read from its headers by mise live-page <url> (livePage in lib/browser-framing), since a browser cannot tell a refused frame from a loaded one.
  • macos takes windows as children, laid out in its stage's pixels on the same playhead, and front, the app named in the menu bar: <Macos front="Safari" …><BrowserSession className="absolute …" … /></Macos>.
  • full shows the whole address at rest (Safari's "show full website address"), and an address is shown as it was written, in the field and in the visit's chapter titles (a typed one as typed), never through URL, which turned a host in other scripts into punycode (👤.to read xn--mq8h.to).
  • link makes the address live in a window that is otherwise a picture: the field opens it, a copy shows in it under the hand, the share glyph shares it (the system's sheet, else a copy). BrowserSession's linked passes the address of the page on screen, its page staying inert.
  • A typed address can redirect ({ kind: "type", text, to }): one load, the field turning to where it landed as the page comes. A redirect written as a type and then a go loaded twice.
  • The load bar is Safari 26's: a hairline, inset from the field's round ends, a paler blue.
  • The sidebar, new tab and tabs glyphs show from a 36rem window (was 32rem), so a mid-size window gives their room to the address instead of cutting it.
  • In glass, the page scrolls under a band behind the controls, as Safari 26's does: blurred and tinted with the page's ground, never sharp behind the window buttons.
  • A glass capsule is as long as its glyphs, each a square of its height: one glyph alone (share, in a narrow window) is a circle, no longer a padded oval.
  • mise browser-shot <url> <out> captures a site as it looks, in the dark and the light scheme, for a page that refuses to be framed: <out>.dark.webp, <out>.light.webp, and <out>.json with each one's MediaAsset (prepare-media's: size, blur, colour). Made at authoring time and served by the site itself.
  • patience: how long a source may take once asked for before the next in fallbacks is tried. A host whose network is blocked hangs rather than fails (a minute or more), so the chain never moved on; with patience it degrades to the next copy as an error would: <Image src={cdnUrl} fallbacks={["/local/copy.webp"]} patience={1500} />.
  • A clip told to play (playing) plays in development too: React mounts an effect twice there, and the cleared animation frame was still named, so the clock stood still at 0.
  • variant="dots": each option a dot, the chosen one stretched into a short pill (a pager, a story's acts), its words kept for a screen reader, its target 24 px.
  • Its demo's act and beat pickers are a toggle-group (dots for the acts), where loose buttons stood.
  • Panel is the one container, plain or glass. Glass is a face of it, not a family of its own: the glass face is built of the material's parts (a back layer that blurs, what it holds, the spill, the front surfaces, the edge), never a blur on its own root, so glass inside it, a head included, blurs what passes under it. It used to blur on its root, which left anything inside nothing to blur.
  • head and foot: front surfaces across its top and along its bottom (a tab list, a board's column names, a card's buttons), headHeight and footHeight tall (3rem), told to what it holds as --panel-head and --panel-foot. In glass each is the material's front (glassFront), plain a solid strip a step darker than the card. action stands at the head's end. A head and a title together throw: name a panel with a head in it, or with a heading above it.
  • maxHeight and height: its body is the one capped scroller (ScrollArea), the head and the foot laid over it so what scrolls passes beneath them; thumbs on (thumb). pin pins the head to the page while a growing panel scrolls under it, and --panel-under is where what pins under the head (a table's header row) pins. glide eases the body to a new height. over lays the fronts over content that scrolls by itself (a board's columns, each a ScrollArea cover="var(--panel-head)").
  • spill (on in glass), radius, bodyClassName (what lays out what it holds, on the panel itself unless it has a head, a foot, a height or a glide), frame={false} (its layout and context, no surface: frame={!useInFrame()} in what draws a box only when alone), and in glass backColor, frontColor, tint, tintAmount, rim, rimWidth, rimColor.
  • usePanelHead() and usePanelBody() tell what is inside where it stands: a tab list fits a head, a tab's panel keeps its place in the panel's scroll.
  • New, replacing telegram-profile: who is behind a handle on Telegram, GitHub or YouTube, from one shape of facts (ProfileFacts: platform, url, handle, name, kind, bio, photo, verified, counts), drawn by SocialProfile as a card or as one compact row for a hover card or a list. A composition on Panel, plain or glass from the nearest MaterialProvider (or material, scheme), drawing no box inside a frame already drawn (frame decides when given); the face is avatar's, the button the registry's Button in the platform's colour with its mark dimmer than the words. SocialMark draws a platform's mark. Fetchers, at a build or into a cache, no key: fetchTelegramProfile (lib/social-profile-telegram), fetchGithubProfile (lib/social-profile-github, a token optional), fetchYoutubeChannel (lib/social-profile-youtube). Any other platform maps to the same facts and passes mark.
  • telegram-profile is removed. Migration: <TelegramProfile profile={p} /> is <SocialProfile facts={p} />; import fetchTelegramProfile from lib/social-profile-telegram instead of lib/telegram-profile-data; in the facts kind: "user" is "person" and followers is counts[0].value; delete the old telegram-profile.tsx and telegram-profile-data.ts.
  • The card style is a Panel: plain or glass (material, else the nearest MaterialProvider; scheme), the picture running to its edge, and inside a frame already drawn it draws no box (frame decides when given). A className that stripped its border and fill to sit in a glass box is no longer needed: pass material="glass", or put it in the box. telegram and x are unchanged. It now depends on panel and material, and is a client component.
  • What a hover card holds stands in it as in a frame (InFrame): a profile, a stat, a web preview draws no box of its own there. It now depends on panel.
  • TabsBox is removed: tabs own no container. Compose them with Panel, the list as its head: <Tabs defaultValue="ops"><Panel head={<TabsList>…</TabsList>} action={…} maxHeight="30rem">…panels</Panel></Tabs>. Migration: the tabs' props (defaultValue, value, size, variant, material) go on Tabs; tabs={…} becomes head={<TabsList>…</TabsList>}; maxHeight, height, action and spill go on Panel; top is pin; --ag-tabs-box-under is --panel-under; a title is a heading above the panel. A box that grew and glided takes glide.
  • A TabsList in a panel's head fits the strip itself: a segment stands as far from the panel's side as from the strip's top, a line fills the strip with its active stretch on the strip's rule, and plain a line's chosen tab is cut from the body, a folder's tab.
  • The tabs of a panel that scrolls share its one scroller, and each keeps its own place in it: a tab never scrolled opens at its top, and coming back finds one where it was left. In a panel of a set height a tab's panel fills it, and what scrolls itself (a log) fills that.
  • tabs no longer depends on scrims, scroll-fade or scroll-thumb.
  • New: ScrollArea, the one capped scroller, replacing scroll-fade. Its ends fade where there is more to show (the edge fade, 1rem, CAPPED_FADE); fade={false} only inside a glass panel, whose spill ends it. thumb adds overlay thumbs, laid over it from beside it so its fade never fades them. cover and coverEnd are a head and a foot laid over its ends: what it holds starts and ends clear of them and passes beneath them, a focused row stops clear of them, the thumbs run between them. render makes the scroller another element (Base UI's list in a select or a combobox). No padding of its own; at its ends the scroll goes on into the page. A data-slot given to it is kept.
  • Every capped vertical scroller in the registry is one now, so they end alike: dialog and sheet's bodies (their slots dialog-body and sheet-body kept), select's, people-picker's and chip-input's lists, table's choices, icon-picker, the gallery toolbar's settings, gallery-filters' sections, file-view's text, player's playlist, command and country-picker. Each of these items now depends on scroll-area. icon-picker hands the scroll on to the page at its ends, where it used to hold it.

scroll-fade

  • Removed, in favour of ScrollArea (scroll-area). Migration: <ScrollFade className="max-h-80 …"> is <ScrollArea className="max-h-80 py-5 …">: there is no default padding any more, so add the py-5 it had. A panel that scrolls is <Panel maxHeight> or <Panel height>.
  • GlassPane is removed: a glass container is <Panel material="glass">. Migration: front={…} is head={…} (frontAt="bottom": foot={…}), frontHeight is headHeight or footHeight, --glass-pane-front-height is --panel-head or --panel-foot, frontClassName is headClassName or footClassName; radius, backColor, frontColor, spill, tint, tintAmount and rim keep their names. The panel pads what it holds (bodyClassName="p-0" for none). GlassSpill is re-exported.
  • GlassSpill: a band of glass along the inside of a container's edge that blurs what lies under it (--glass-spill-blur, 4px), so what runs near the edge spills its light into the band and round the corners, and a box's padding is lit by its content, never a flat strip. It follows the rounded outline, deepest at the edge, width px in (10), and takes the same radius as GlassEdge, any CSS length. A glass Panel wears it by default, with spill={false} to keep content crisp to the edge. A blur mixes both ways, so at a corner the glass's own tone also reaches a little into the content; a one-way spill is still open.
  • Placed right after its scroller (its sibling), ScrollThumbs lays itself over the scroller from outside it, so a fade on the scroller (a mask, which fades everything inside) no longer fades the thumbs. Placed as the scroller's first child it works as before, for a scroller with no fade.
  • A thumb's track ends where its scroller's corners curve, read from the scroller's own border radius, so in a rounded box it no longer runs through the corner and out past the edge.
  • The track runs between what covers the scroller's top and foot (scroll-padding-top and -bottom: a head and a foot laid over it).
  • The thumbs hang from the view's real edge, the scroller's padding taken back. A scroller padded at its top no longer pushed them down by the padding.
  • A scroller on glass (--glass-body) gets plain thumbs, a quiet line, never a sliver of glass on glass. An explicit material still wins.
  • ag-edge-fade-y keeps a covered foot whole too (--edge-cover-end, a length), as it did the top (--edge-cover): a foot laid over a scroller.
  • In glass the group is a glass Panel, its name and aside on the panel's head. It used to rebuild the glass by hand, its body blurring on its own root.
  • Its frame is Panel's (frame decides as before), so in glass it has the panel's spill and an edge over its content.
  • Its frame is Panel's (frame decides as before). A glass log is no longer clipped to its corners, which cut its own shadow.
  • A glass FieldSet is a glass Panel drawn as the fieldset, named by its legend (aria-labelledby).
  • A glass accordion standing alone is a glass Panel drawn as its root.
  • A glass quote's surface is a glass Panel.
  • A press that ends a text selection in a row (rows whose words are text to read) no longer selects or opens the row.
  • New: Contact, a vCard drawn as a phone shows a contact, from its text or its parsed fields (parseVcard, lib/vcard-core: 3.0 and 4.0, folded lines, escapes, Apple's labelled groups, an inline photo). It shows the face, the name and who they are, then each phone, email, page and address as a labelled row, a link where it opens something. It has no frame of its own and renders on the server.
  • Sizes past the input's heights, in Tailwind's steps, for the one press a section is about (a call to action): xl 48 px, 2xl 56 px, 3xl 64 px. In glass each is a pill like the others, its radius half its height. Before this, a taller button made by overriding the height kept the smaller size's corner.
  • icon and iconEnd: the icon in its own slot, sized by the button and a step dimmer than the words (60%, 90% under the hand), so the words lead. Only the slot dims, never an image that is part of the words. Migration: move an icon written as a child into icon (<Button icon={<Download />}>save</Button>). A child icon still draws, but it no longer dims. An icon-only button (icon-*, the icon as its child) keeps its icon whole.
  • useFormField(form, path, label?) binds one path with no Field round it: value, change, leaving, the error when it is due, checking, read-only while sending, and anchor, a ref callback that places the control for the summary. input spreads on a text control, as c.input does. For a control that draws its own label and error (a name written on a card, a cell in a table). form.Field is built on it, unchanged for its callers.
  • A failed submit focuses the first field to fix in the order the fields stand, read from the form's own places rather than from Field's markup, so a headless control is focused too.
  • inspect is now data-view, a noun for what it is: data, viewed as a tree or as text. Migration: install @ag/data-view and delete inspect.tsx; Inspect is DataViewer (not DataView, which is JavaScript's own), InspectProps DataViewerProps, InspectNode DataNode, InspectKind DataKind, inspectValue dataOf, inspectXml dataOfXml, and the slots inspect-* are data-view-*. "inspect", "json viewer" and "object inspector" still find it.
  • variant="show" presents data to be read (a format on a landing page, a response in a doc): keys and values only, with no toolbar, no size column and no row actions. The rows are text: a drag selects across keys, values and rows as it does in any text. A link value selects like the rest and opens with ⌘-click (Ctrl elsewhere); its glyph opens it with a plain press. Branches still open. The default, "explore", is the tool as before.
  • itemName names an array's item beside its index (0 github for { platform: "github", … }), so a list of records reads as what it holds and not as 0, 1, 2. The search finds an item by its name too. Undefined keeps the index alone.
  • How deep the tree is open is one stepper, ‹ none · 1 · 2 · all ›, where a row of loose buttons stood. It says how deep the tree IS open, so a branch closed by hand moves it.
  • A date's "how long ago" is relative-time's, so the server's words and the reader's agree (it was its own clock, and the page re-rendered on load); XML, read with the browser's own parser, waits for the page to be live rather than showing "cannot read it" first.
  • The tree's rows are text in the default variant too: a drag selects keys and values and from row to row, and a press on a value is the reader's (select it, ⌘-click a link). A row still opens from its key, its chevron or its keys.
  • DialogContent takes bare: no surface, no padding and no motion of its own, for an object lifted off the page to be worked on (a card written on where it lies). Still a dialog: focus held, Esc and a press outside close it. With no fade of its own a view transition can carry the object in whole. backdrop adds classes to the dim behind a centred one (a stronger dim, a blur).
  • Code takes bare: no frame, no bar and no copy of its own, for a frame that holds it (a tab of a Panel showing an example's code, its copy in the panel's head).
  • Code takes material: "glass" draws the block on the one glass material, its own border and ground dropped for it; "plain" (default) is the frame as before. Set on the block rather than read from a MaterialProvider, since it renders on the server. The item now depends on @ag/material.
  • Its frame is Panel's, plain or glass.
  • A folded row still says which row it is: its name stays, in the outline rows' quiet voice (smaller, mono, fainter), and the folded row is a little taller to hold it (16 px). Before, a folded row showed only its arrow.
  • InputGroup takes a prefix: text the value continues (github.com/, @), joined to it with no gap, so the two read as one address. start stays for an icon, set a little apart. A field that showed a prefix through start showed a gap inside the address; move that text to prefix.
  • New: ground="none", the light alone over whatever lies under it (behind a page's subject, where a ground would cover what is round it), its alpha dithered. Say what it lies over with over when the light is near that in tone, a shadow on a dark page above all: the dither is then sized to one level of what is seen, where one step of alpha moved no pixel and the fall banded.
  • Fix: the canvas is enlarged pixel for pixel. Smoothed up from its half-size painting, it averaged the dither away and the light fell in rings on a retina screen.
  • New: pick a country from a field over every flag. Found by its English name, its own (españa, 日本), its code, the names it goes by (usa, uk, holland) or a pasted flag emoji; the reader's countries first (the one the device's time zone is in, then the browser's languages, read on the device and sent nowhere), or the ones you pass as suggested. Arrows walk the list, Enter picks. CountryPicker is the panel; CountryPickerPopover opens it from your own trigger and shuts on a pick. It carries every flag's art, about 180 KB (45 KB gzipped): LazyCountryPickerPopover keeps that off the page's first load, fetching it as soon as its trigger is drawn so it is there before it is pressed (preloadCountryPicker to fetch it earlier).
  • New: a security hologram for any rounded surface. HoloLight measures the hand over what it holds, and HoloLean inside it leans a few degrees toward it (pose="flat", or "turned" beside a headline); Holo lays a print (laminate, web or none) and a seal you place, shown in a band of foil where the light falls, which answers the hand's place and the tilt it gives and eases as the lean does: a whisper at rest and under a fifth under the hand. floor keeps some of the print outside the band (also inherited as --holo-floor). The laminate's tiles are in pixels unless the surface sets --holo-print-unit: a surface sized by its own width (a card in cqw) sets it in that unit, so the print grows and shrinks with everything printed on it. FoilEdge is a one-pixel ring of foil on the rim, faint at rest and lit in the band under the hand. emblem is the glyph the prints repeat, your mark. The prints are lib/holo-prints, framework-free SVG, for a drawing that is not CSS too. Under reduced motion nothing follows the hand.
  • New: a security foil's spectrum as a shade, every stop at one OKLCH lightness and chroma so its strength holds as the hue turns. FOIL_STOPS and foilGradient(angle) for CSS.
  • Image no longer uses next/image, and image no longer depends on next: it is one <img> (no wrapper element), so className styles the picture itself and imageClassName is gone, put those classes in className. A picture filling a box is fill in a positioned parent, as before; fit="contain" fits the pixels and the blur alike. data-slot="image" and data-state are on the <img>.
  • Resizing is a loader. A Next site wraps its layout in NextImageLoaderProvider once (/_next/image for the site's own files, with the srcset next/image built); ImageLoaderProvider loader={…} sets your own, loader sets one picture's, and with none the source loads as it is. Told neither sizes, fill nor a width, a picture loads as it is too. A remote source loads as it is under nextImageLoader: write a loader for the hosts in your remotePatterns. imageRenditions returns the same srcset for elsewhere (a lightbox entry).
  • A picture with a blurDataURL (a static import has one) is painted over its blur, drawn as the image's own background from the first paint, instead of fading in. Without one it is held until it has loaded (read from the element too, so one cached before hydration is never left blank), then fades and sharpens in. priority asks for it first (preloaded, eager, never faded). Hidden on error.
  • Img is removed: Image is the plain picture now. Migration: rename Img to Image; a picture given no sizes, fill or width loads as it is, as Img did.
  • New: fallbacks, the sources to try in order when one fails, and minWidth, the narrowest a loaded picture may be to count as arrived (a host's stand-in for a size it never made is passed over).
  • The poster is one Image: optimizePoster goes through the site's loader, posterArrival="instant" is priority, and the blur is drawn under it either way.
  • The face arrives through Image: held until loaded, then faded in, hidden on error. Installing avatar now brings image.
  • Avatars arrive through Image: held until loaded, then faded in, hidden on error so the card's own tone shows. Installing github now brings image.
  • The author's avatar and the tweet's photos arrive through Image: held until loaded, then faded in, hidden on error so the card's own tone shows. Installing tweet now brings image.
  • The preview's image and favicon arrive through Image: held until loaded, then faded in, hidden on error so the card's own tone shows. Installing web-preview now brings image.
  • The thumbnail arrives through Image with YouTube's sizes as its chain (maxres, then hq, the 120 px grey YouTube sends for a maxres it never made passed over by minWidth): no frame of grey, no pop, and a thumbnail cached before hydration is never left blank. Installing youtube now brings image.
  • Installing trace no longer brings timeline, playback and the media libraries behind them: its axis depends on timeline-view alone, the arithmetic it always used. What it draws and how it is used are unchanged.
  • New: the window of time a timeline shows and the arithmetic that moves it (zoom at a point, pan stopping at the ends, ticks whose labels never touch, snapping in px), its own item so trace draws its axis without the timeline. timeline brings it as before.
  • Takes @adriangalilea/utils 10 (^10.0.0), the current major; nothing in it changed. The same for page-sensor, visits and stat: update the package in your project when you update them.
  • New: a country's flag drawn as inline SVG from its ISO code, the same on every system where a flag emoji is two letters on Windows. Two shapes, rect (3:2) beside a name and square (1:1) as a mark (bar-list's icon, size-4), sized in em on the text's baseline, a hairline over the edge so white and black fields keep their shape on either page. The name is Intl.DisplayNames' (countryName), the label and title by default; label="" when the name is written beside it. An unknown code throws: ask hasFlag where a country may be unset. The art is country-flag-icons (MIT). A server component; from a client component the whole set ships.
  • New: prose and feature take material and scheme like every surface: plain is the step up from the page they always stood on, glass is the glass material, from the prop or the nearest MaterialProvider. The surface is quote-surface.tsx, a small client part; the words and the arithmetic that sets them stay on the server. The card has no glass face, since it is its own link preview, and material="glass" on a card throws. Installing quote now brings material.
  • The card's portrait arrives through Image. Installing quote now brings image.
  • A change of tab moves as one: TabsBox's body glides to the new panel's height on the indicator's curve (and when a panel grows by itself), instead of resizing in one frame and moving what is under it; a panel that becomes the shown one arrives from the side the indicator travelled from (16 px, and up from nothing, over 220 ms), never on the first paint, none under reduced motion. The box's body now wraps its panels in a tabs-box-panels element, which carries the padding.
  • New: labels="active" on Tabs, for a row of many tabs that stays short: every tab shows its icon (a new prop on TabsTab) and only the active one its words, which open as the indicator slides to it and close on the tab you left, over the same time and curve. Closed words stay in the tab, so it keeps its accessible name. A tab with no icon under labels="active" throws.

telegram-profile

  • New: a Telegram profile as a card, from the facts t.me shows for a public handle: photo, name and check, handle, a channel's subscribers or a group's members, bio, and a button into Telegram; the whole card opens t.me. fetchTelegramProfile(handle) (lib/telegram-profile-data) reads them with no key; run it at a build or into a cache, never at render, and keep the facts days, since the photo's CDN address changes. A handle nobody has throws.
  • New: the server half of page-sensor, moved here from @adriangalilea/utils/visits so the page, the store and the views are one family: visitsRoute (the route a site's pages post to), readVisits, readVisit, readViews, eventStep, VISITS_SCHEMA. Installing page-sensor installs it. Migration: import from your copy (@/lib/visits) instead of @adriangalilea/utils/visits; the tables and the rows are the same.
  • Changed: DeploymentDetails draws only what the deployment carries. Before, every step's state was read off the status alone, so a deployment showed a passed summary, passed domains and waiting checks the host never reported, an empty picture, an empty settings section, and Share and Logs with nothing behind them. Now the picture is there only with preview, the settings only with recommendations, Share only with a unique domain, Logs and the duration only for a build the host saw, Visit only with a domain serving it.
  • Changed (breaking): the steps are DeploymentDetails' children, composed by the consumer from what it can report: DeploymentBuild (its defaultRunSummary moved here from DeploymentDetails), DeploymentSummary, DeploymentChecks, and DeploymentStep (value, title, state, aside) for any other step; each built-in one asserts its part is there, and a deployment carrying a build must show DeploymentBuild. Migration: <DeploymentDetails deployment={d} /> becomes <DeploymentDetails deployment={d}><DeploymentBuild /></DeploymentDetails>, adding DeploymentSummary and DeploymentChecks where the deployment has resources and checks; a domain-assignment step is a DeploymentStep with the state your host reports. stepsOf and DeploymentSteps are gone (buildState, outputState, checksState), and defaultOpen names sections by their value.
  • Changed (breaking): Deployment.build is optional: absent, the build ran where the host could not see it (no started, no duration either). A ready or failed deployment with a build and no start now throws; pass no build instead of an empty one. domains are the hosts serving this deployment now: an older one keeps only its own address.
  • New: data-view="/its/path" on a post shown whole on a page (a note in a feed) writes a view line naming that path once it is half on screen, once per page: read where it was shown, as a tweet in a timeline. ts-utils' visitsRoute (9.13) counts it as a view of that page, once per visit together with opening the page itself.
  • New: a site's pages written to the event stream (the utils contract's "Web pages") through ts-utils' logger, posted to the site's own route, where ts-utils' visitsRoute composes visits without cookies. Mount PageSensor once in the root layout with a service, the collector (a path such as /api/events, or a URL), the build and what a path's place is. A page shown is a page span, ended when the tab is hidden; a route change (read off the browser's Navigation API, any router), a return to the tab and a back/forward-cache restore each start a new one. Its end says which data-section parts came on screen and, on a load's first page, its vitals. A same-origin request is a fetch span with its traceparent on the request, so a server using ts-utils' log/next joins the page's trace. Never written: a query string, a URL, an IP, a user agent, a cookie, and a browser asking not to be tracked (Global Privacy Control, Do Not Track) gets nothing. currentPage() writes a named event, usePageReport() shows what this tab has written (a privacy page can show a visitor exactly that). The stream is written to the browser's console in development only (console overrides it), so a visitor's console shows nothing; it ships either way.
  • New: where people go on a site, from visits (each the places it went, in order, the referrer it landed from, and its page count when a store merges moves within a place, as ts-utils' readVisits does). TopPaths: the common paths, a visit's first four distinct places as coloured chips, a return being no new stop and what comes after a +n, with a bar, visits, share and pages per visit. PathTree: the paths folded by where visits landed, on ui's tree (landings open, the rest folded), each place with its visits, their share of the place before and how many left there. BeforeAndAfter: one place in the middle as a flow (a Sankey), each place on either side joined to it by a ribbon as thick as its visits, the middle node exactly as tall as the ribbons gathering into it; leaving is drawn as leaving, a faint ribbon of sparse crosses that bend with its curve, fading toward the side it leaves by with no place at its end; an arrival from outside the site (a referrer, a direct visit) as its mirror, chevrons on the same grid pointing in toward the place. Arriving is light (a ground of the text's ink, bright chevrons), leaving dark (a shadow below the page, dim crosses). The ribbons are drawn at their column's measured width, never stretched, and a visit is at most 24 px thick, so a handful of visits draws a handful of thin ribbons into a node just tall enough for its name, never a wall. A press on a place centres it, back returns to the one before, and a referrer, a direct arrival or the exit takes no press. A row under the hand is followed through the middle: on the other side each ribbon's part that is the followed visits stays at full strength in its own look (its colour, or its chevrons and crosses) while the rest of it recedes, and its number says how many of its own ("29 of 132" left after arriving from google.com), so where one source's visits went next, or where one destination's came from, reads at a glance (through and followed in the lib). Nothing moves while following: the counts keep the room "n of m" needs and the caption stands above the node. On the new charts shelf, with bar-list and sparkline. Funnel: steps in order, each step's visits and share of the first, and where the ones that dropped went instead. A step may be a named event in a place (lore:download, as ts-utils' eventStep writes it, read with stepOf): it is drawn apart from every page, a solid outline in its place's colour with a click mark, the place quiet and the event strong, and it takes its place's colour where colorOf gives it none. A place wears the colour colorOf gives it in every view, and faviconOf puts a site's icon before its name (a referrer's above all, so where people came from is known at a look), an image the consumer serves itself rather than asking a third party while someone reads. lib/user-flows-core works the views out of the visits, framework-free, and marks what is outside the site.
  • Changed: renderQuoteSvg draws its grain from one tile of noise repeated as a pattern, the tile grainLayer already used, instead of a turbulence filter over the whole frame. A rasterized still is unchanged; a still inlined in a page no longer recomputes the noise on every layout (five on one page in Safari: about five cores while the window resized, now about one and a half). To keep the grain at full strength inline, render it through QuoteStill (quote); a bare inline still's tile is resampled at most widths and the grain shows weaker than in the raster.
  • Fixed: the picture's dissolve banded, a strip of 8-bit steps where the portrait is nearly opaque, because the grain lies under the picture and fades out with it. The picture now carries its own faint dither inside the dissolve; it lifts a dark portrait by about one level.
  • New: GRAIN_FIT, the contract a page uses to put a still's noise on its own device pixels (the noise patterns, and the finer user space they are painted in).
  • Fixed: stills inlined on one page each keep their own ground. Their defs were all named glow, soften, fade, dissolve, so Chromium drew every still on the first one's ground; ids are now prefixed by what the defs hold (ag-quote-<hash>-glow). A consumer that referenced the old ids from outside the still must read the new ones from the markup.
  • New: QuoteStill, a still from renderQuoteSvg inlined in a page with its grain and dither fitted to the page's pixels on every resize. Migration: replace a dangerouslySetInnerHTML of the still with <QuoteStill svg={svg} />.
  • Fixed: command typechecks under @base-ui/react 1.8, which types a group of items with an index signature; with 1.8 installed, it failed to compile.
  • New: PortalTarget, where the popups inside a surface open, declared once. tooltip, popover, hover-card, menu, select, dialog and sheet read it as their default container (one given its own still goes there), so a surface that shows on its own (a player in fullscreen, where a popup on the body is invisible) says it once instead of handing a container to each popup. usePortalTarget() reads it for a popup of your own. A setting's tip in a fullscreen player, which no one could hand a container, now shows.
  • Removed: usePlayerLayer. PlayerLayer declares a PortalTarget, so every popup item inside a player opens in its layer with no container passed. Migration: drop container={usePlayerLayer()}; a popup of your own takes container={usePortalTarget()} from portal.
  • New: useOptionalTimebase(), the timebase around a component or null outside any owner, for a follower that also stands alone.
  • New: toggledChoice(offered, choice) in lib/captions, captions turned the other way, shared by the player's toggle and the wall's.
  • New: pinchInput(el, handlers) (ui/pinch-input), the hand read once for every viewer on the pinch ladder (a trackpad's pinch, Safari's gesture events, two fingers, a wheel that is no pinch, one pointer's drag); VideoPicture's zoom and PlayerGestures read it. A ctrl-wheel step is bounded the way the lightbox's is, so a mouse notch with ctrl nudges instead of taking a whole rung, and Safari's pinch acts on the player instead of zooming the page.
  • Changed: PopoverContent opens in the nearest PortalTarget (portal) when given no container; so do tooltip, hover-card, menu, select, dialog and sheet.
  • New: anchor on PopoverContent, for a panel that stands under something other than its trigger (a group of controls it serves), and createPopoverHandle, Base UI's handle: one popover opened by several triggers anywhere (<Popover handle>, <PopoverTrigger handle>).
  • New: pictures of a film at any instant. A StillSource says its grain (the finest step it tells apart, ms) and places an instant on the still it has at or before it: frameStills(asset.frames) names prepared frames at once (a media asset's, prepare-media's, a CDN's), addressStills(grain, last, address) any addressable tier, useSeekStills(src, { fps?, width }) decodes on the page (one hidden video seeked to each instant, one at a time; given no fps it times the film's frames). useStills([coarse, fine]) is the cache over a ladder: the prepared still paints at once, the decoded frame replaces it; what is on screen is asked first, a still under way is never stopped, a still refused or broken is not asked again and its stand-in stays. StillPicture draws the best one. The cache is framework-free (lib/film-stills-core).
  • New: the mosaic's keyboard. It is one Tab stop, the tile in hand ringed only when the keys moved it; the arrows move through the film (← → a tile, ↑ ↓ a row), past the view's edge paging onto the tile next to it in the film; Enter plays the tile in hand, + zooms into it, - and Esc out. Its tiles' buttons are the hand's and take no Tab stop. Back from the player, the instant it left off is a tick on that tile's line (no longer a ring round it) and that tile is in hand.
  • New: FilmWall, a film as a mosaic of itself in its player's box: every tile plays its own span, muted, in turns as many at once as the machine keeps up with (lib/ambient-budget, the gallery's), or only the tile under the hand with the walls' motion off (FilmWallMotion, a switch for the end of its bar, remembered); given stills, a tile shows its first still until its video presents a frame. Each tile is a VideoPicture with its progress on a MediaLine, its start on its left (only the last also says its end, on its right), its sound in its top-left corner and its zoom in the top-right under the hand. A tile's play hands the same box to player(start) at the instant it showed; FilmWallToggle in the player's bar hands it back at the zoom it was. Spread on a tile (a trackpad's pinch, Safari's, two fingers) and its span becomes the mosaic, one level a pinch; close to go out; zoomed, swipe sideways for the next stretch. The bar above the box says where it is and holds zoom out and the stretches before and after (disabled where they lead nowhere), the captions, the tiles' speed and your controls (children); keys + - 0 ← → Esc. The tiles of a level loop in step: one loop length for the level (loopOf), each tile at the speed that makes its span last it, starting together once all have a frame up. The tiles' speed is "auto" by default (FILM_WALL_SPEED, remembered): a level's loop lasts at least three seconds, so short tiles play slower; or one of the player's speeds. A zoom, a page or a jump is one continuous motion: the wall as it was, drawn once, grows (or slides, or fades) away while the new level grows out of where the tile was. Captions follow the choice every player shares (C); the tile under the hand is heard while the walls' sound is on (M). grid (default 3 × 3) and fit. The zoom state is framework-free (lib/film-zoom-core, any grid).
  • New: FilmMosaic, the strip for many films (one a row in a library): stills in a row, as many as fit the measured width (tile, the narrowest a tile is drawn) and never more than the stills tell apart, each starting on its own first frame, its start on its left and only the last saying its end. Resting on a tile plays its span muted (one video for the strip); a press hands the span to a timebase owner around it (the tile it plays lit, with its playhead), else onPlay, else plays inline (Esc gives the tiles back). One Tab stop, the arrows between tiles. view tiles a part of the film. FilmTile is the tile; the tiling is lib/film-tiles.
  • Changed: the playlist card opened from the bar (PlayerPlaylistButton) stays whole; its rows scroll inside it and fade their cut edges.
  • New: PLAYER_BUTTON, PLAYER_DISC and PLAYER_DISC_SIZE, the player's plain faces (a quiet bar button, a dark disc over the picture at the transport's sizes), for a control of your own that must read as the player's.
  • New: handVoice(setting), the voice of many videos playing muted (a gallery's cells, a wall's tiles): only the one under the hand is heard while the group's remembered sound is on. The gallery's sound is it now.
  • New: lib/ambient-budget (ambientBudget, ambientStart, AMBIENT_SAMPLE_MS), how many videos a page plays by themselves at once, in turns of one pass, as many as the machine keeps up with. The gallery's and the film wall's.
  • New: the budget also heeds the machine's load. Its frame counts said only whether the videos kept up, so a gallery grew its decoders until the browser dropped frames while everything else on the machine starved (a gallery taking files in froze a laptop with no frame dropped). watchPressure(budget) (ui/machine-pressure) hears the CPU's pressure where the browser says it (Chrome and Edge; elsewhere the frames decide alone): the budget never grows while it is serious, and while it is critical gives a decoder up at once and one more every window after, down to one. Galleries, loose cells and the film wall wire it themselves. A budget written by hand (AmbientBudget) implements pressure: (state, at) => void; one made with ambientBudget and handed to GalleryAmbient budget hears the machine only if you call watchPressure on it.
  • New: Verdict (inline) and VerdictBar (floating over a list, with the one before and after on ↑ ↓): like, neutral or dislike (always one), hidden and favourite. Keys from one table, VERDICT_KEYS: K U X W S, never repeated while held; inline they answer while the hand holds the card (within), the bar while the hand is in its list. Controlled: value, onValueChange(next, act); judge(value, act) is the rule.
  • New: span on Video: plays a part of the video round and round (its start, back there at its end, read on presented frames), resting at the span's start under rest="poster". Not with start or at.
  • New: VideoPicture's clicks={false}, for a picture that is part of something answering the hand itself: no play and pause on a click, no fullscreen on a double click, no pointer cursor.
  • New: useVideoElement(), the element of the VideoPlayback around, for what acts on the element itself.
  • New: usePoll(ask, { every, element, enabled, initial }), asking a server again on a cadence only while the tab is visible and element (if given) is on screen. every runs from the end of the last request, so it never asks twice at once. refresh() asks now; during a request it waits for it and asks once more, so it resolves with an answer asked for after the call. Returns data (kept through a failure), error, loading, updated and live. Unmounted, the request under way is aborted through its signal. The machine is framework-free in lib/poll-core (createPoll).
  • New: Stat, a dashboard's tile for one number, in parts written in the order you want them: StatLabel, StatValue (32 px, tabular, never wrapping; value={n} says a count short, "1.3B", with the exact count on hover and to a screen reader), StatChange, StatNote and StatChart (a 40 px slot a sparkline fills). StatChange takes a signed value, said as a signed share or in your words (children), toned by whether its direction is good (good: up, down for errors and latency, none), "better" or "worse" said to a screen reader. shareChange(previous, current) works out the share, null when there was nothing before. Alone it is a panel, plain or glass; inside a frame it draws none (frame overrides).
  • New: BarList, ranked things as horizontal bars (items: label, value, optional href and icon; label names the list). Each bar is its value's share of max, the largest value by default; the values sit in their own column in mono with tabular figures, through format. The order is yours. A zero is no bar; a negative value or one past max throws. Long labels are cut with an ellipsis and whole in their title; empty says what an empty list says. Renders in a server component, format included.
  • New: Sparkline, a series as a small line beside the number it explains (values, label). As wide as its container and 32 px tall unless className says otherwise; the stroke stays 1.5 px and the last value's dot round at any size, with nothing measured, so it is right on the first paint and renders in a server component. The scale takes in zero unless min and max say otherwise; area fills to zero. Empty draws nothing, one value is its dot alone. A screen reader hears the label and the series' first, last, lowest and highest values through format.
  • New: Status, how something stands in your word with a dot in its tone before it (tone: ok, warn, bad, info, neutral, the tokens' tones). pulse for work in progress, still with reduced motion. The dot is 0.7em of its words and the status sits on their baseline, so it lines up in a table, a readout, a sentence or a heading. A dot alone: put its word in sr-only. Renders in a server component.
  • New: RelativeTime, an instant said from now in a native <time>: "just now", "12s ago", "4m ago", "in 3d", and past dateAfter days (7) its date, with its year when it is not this one. dateTime holds the ISO instant and the title the whole date in the reader's zone. The reader's clock ticks only while the label is on screen in a visible tab, and only when its words change (every second, then every minute, and so on). precision="minute" says "just now" for the first minute; now says it from a given instant with no clock. The words are framework-free in lib/relative-time-core (relativeText, nextChange, dateText, fullText), usable on a server.
  • New: useOnScreen(element?), whether the page shows something now: the tab visible and, given an element, the element in the viewport (clipping by scrolling ancestors included). Pass the element itself, held by a callback ref (ref={setElement}). One IntersectionObserver and one document listener for the page. False on the server and until the first observation.
  • New: deployments as a host's dashboard draws them, from their facts (Deployment in lib/deployment-core: project, commit and author, branch, production or preview and whether current, queued / building / ready / error / canceled, created, started, duration, domains, a preview picture, and the build as @ag/telemetry lines and spans). The facts are asserted where they enter: a ready deployment without a duration, a building one with one, a current preview, two deployments with one id, a build line naming a span the build has not, each throws. A build's warnings and errors are counted from its lines, never stored.
  • DeploymentList: a 48 px row per deployment at fixed column widths (project, message, status with the build's length, environment, hash, branch, when, author), a link (href) or a button (onOpen). Narrower, the branch, the hash, the message, the environment and then the build's length go, read from the list's own width. Above it, Add Filter (every choice, counted among what the other filters keep) and a chip per filter (author, environment, status): dashed while unset, a suggestion naming its value that a press applies at once; solid once set, a press opening its choices and its × clearing it. filter and onFilterChange keep it on the page.
  • DeploymentDetails: the deployment page's body. Share, Logs, Visit with every domain under its chevron (one control, button's ButtonGroup), the consumer's verbs under ⋯ (actions); a picture beside the facts, domains (the rest behind +N), source; the steps as sections with their state: Build Logs (the log, its warnings and errors as toggles of the level shown, Run Summary laying the build's spans beside it as a trace, the two linked both ways), Deployment Summary (resources by kind), Deployment Checks, Assigning Custom Domains; links as cards at the foot. Times are said from now, else the reader's clock, ticking only while something builds.
  • Parts: DeploymentStatus (amber and pulsing only while building), EnvironmentBadge, CommitRef, BranchRef, ProjectMark, AuthorFace, StepMark. Plain or glass.
  • Changed: times are @ag/relative-time's, said as TIME_WORDS (just now for the first minute, the date from the third day: the same words). Without now, each label keeps the reader's clock and ticks while it is seen, where it used to wait for something to build; the title is the whole date in the reader's zone, not UTC. DeploymentStatus is a @ag/status, its dot now data-slot="status-dot". Removed: Ago and agoText; write <RelativeTime at={…} now={…} {...TIME_WORDS} />, or relativeText(at, now, TIME_WORDS) from lib/relative-time-core.
  • Changed: a ready or failed deployment whose build its host never saw (an upload built somewhere else, a Cloudflare version with no CI run in view) has neither started nor duration, and its duration is said "unknown". One without the other still throws.
  • New: ButtonGroup, buttons joined into one control: a split button (an action and its options under a chevron), or a run of actions. Plain, each button keeps its face, the corners where two meet square, solid faces divided by a hairline and outlines overlapping so their edges are one line. Glass, the group is one pane of glass and its buttons its parts, lit under the hand (a glass face has one corner radius, so two joined panes read as two pills touching). size sets a glass pane's pill radius.
  • New: chevron="start" on AccordionTrigger, a chevron before the heading turning right to down, and aside, what the section offers beside its heading (counts, buttons, its state), drawn in the heading's row after its button rather than inside it.
  • Changed: inside a frame already drawn (a panel: panel's InFrame), a glass accordion draws no pane of its own; its rows are the frame's. It now depends on @ag/panel.
  • Fixed: a parent that appears while the trace grows (a build still running gets its first child under a span) opens as the first ones did. The trace kept the parents open at mount, so a later one stayed folded with its children hidden. What is kept now is what was folded; defaultCollapsed is unchanged.
  • New: one request's spans as a waterfall. Flat spans in (id, parentId, name, service, from and to in ms, status, attributes), drawn as a row per span on one shared time axis under a ruler of ms ticks, bars coloured by service and errors in the bad tone; subtrees fold with the tree's keys, and a click or Enter shows the span's details as a readout in a region that never changes the trace's height (a column beside the waterfall from 48 rem of the trace's own width, a fixed band under it below that). The axis zooms on the timeline's view model: ⌘ or ctrl with the wheel or a pinch at the pointer, a sideways wheel pans, + - 0 and buttons, and a chosen span zooms to itself; bars the view cuts fade at its edge. The tree is asserted whole (one root, every parent present, a child inside its parent unless detached says why). Capped at visibleRows, or visibleRows="fill" to take the height it is given, windowed, with overlay thumbs and faded edges. Services take eight hues before one repeats. Plain or glass; inside a frame (a panel, a tabs box) it draws no box of its own, and frame overrides.
  • Linked to a log: lines (@ag/telemetry's entries, the log's own) are listed in a span's details, first under its name; a line under the hand there, or the one chosen (line), draws its instant across the waterfall as a playhead with a caret on the ruler, in its tone; a line chosen in the details calls onLineChange. A failed span's row is washed in the bad tone as the log washes an error line, rows square edge to edge so a run of them is one band; there is no ring round its bar. The chosen span's reach is a line along the ruler's foot. A span whose end was never written (status: "running") fades out where the stream ends. The span types (TraceSpan, TraceStatus) are @ag/telemetry's.
  • New: the lines and spans a log and a trace draw, framework-free: LogLine (time, level, verb, source, message, fields, the span it was written in, a span's own start or end and its length), RawLine (a line of a file that is no event, a runtime's panic, kept where it fell) and TraceSpan. A line names its span and a span holds its lines, so a log and a trace from one stream link both ways; the demo shows them side by side.
  • New: reading the stream itself, what go-utils, ts-utils and py-utils write (LOG_FORMAT=json): readStream(text) parses each line with @adriangalilea/utils' parseEvent (9.0.0, now a dependency), keeping lines that are no event; logEntries makes the log's lines (a span's start and end among them, the end with its length; the source a scope path, after its service when the stream mixes services); spanTrees pairs each span's start and end into a trace per root (a span never ended runs to the stream's last instant; a child past its parent by more than the millisecond's rounding, written by another service, is detached).
  • Changed: spanTrees runs a span never ended to the last instant the stream holds of ITS trace, not of the whole stream, so in a server's stream of many traces one lost end no longer stretches its trace by hours. A child of the same service that ends after its parent is detached too, saying why: it was not waited for (work handed off past a reply), or, still running, its end never reached the stream. Without that, Trace threw on both. One of the same service that starts before its parent still reaches Trace as it is, which throws.
  • New: InFrame and useInFrame(). A panel tells what it holds that a frame is already drawn round it, and an item that draws a box of its own when alone (log, trace) draws none inside one, so a box inside a box cannot happen by accident; the item's frame prop still decides when given. Wrap your own framed surface's content in <InFrame> for the same.
  • Added: a stream of log lines as a log view reads them: time, level, source and message on one column template, warn and error in the tokens' tones, a line's fields opened under it. It follows the tail while the reader is at the bottom; scrolling up stops it, holds what is in view and shows a jump to latest button counting what arrived. A lowest level shown, kept by the page or by the log's own choice in its head. Thousands of lines: only those near the view are mounted. Plain or glass. Inside a frame (a panel, a tabs box) it draws no box of its own; frame overrides.
  • Lines are @ag/telemetry's: five levels (trace the quietest), the outcome a verb says marked before the message (✓ success, ⨯ fail, ○ wait, ▶ ready, • step), an ended span's length after it, and lines that are no event shown as written whatever the level. Linked to a trace: a line names its span in a quiet chip that calls onSpanChange; span marks the lines of a chosen span; a press chooses a line (selected, onSelectedChange), and a line chosen from outside is shown whatever the level, brought to the middle of the view and held there.
  • New: ag-edge-fade-xy, the edge fade for what scrolls both ways (a code block, a tree, a table): the top and foot fade as ag-edge-fade-y does, the sides as ag-edge-fade does, each from its own axis of the scroll, with no scrollbar shown.
  • New: --edge-cover on ag-edge-fade-y and ag-edge-fade-xy, a length: a scroller whose top is covered by something pinned inside it (a box's head, a table's header row) keeps that much whole and fades below it, where the rows come out from under the cover. --edge-cover-x does the same for a pinned first column under ag-edge-fade-xy.
  • Fixed: an edge's fade grows over the first 1.5rem (--edge-fade) of scroll past it, in pixels. It grew over a tenth of the whole scroll, so a long scroller (a log of thousands of lines) faded fully only far from its ends and cut hard a few hundred pixels from them. ag-edge-fade, ag-edge-fade-y and ag-edge-fade-xy alike.
  • Fixed: a faded box whose content shrinks below it while scrolled (a filter, a tab switch) no longer keeps its top fade over a list with nothing above it. Chromium holds a scroll-driven fade at its last value when the box stops scrolling; edge-fade.ts, a new file of this item, restarts it. Migration: where you use an ag-edge-fade class, import ./edge-fade (it loads scrims.css) in place of ./scrims.css. The items here already do.
  • Changed: every item here that scrolls fades its cut edges or has a stated reason not to; none cuts its content hard at its edge. A fade is a mask and takes whatever its element paints, so each framed surface below now keeps its frame on an outer element and scrolls an inner one.
  • Changed: a block scrolls inside its frame both ways, its cut edges fading (ag-edge-fade-xy), with no scrollbar. A block given less height than its code (h-96, a pane's min-h-0 flex-1) scrolls down inside the frame; the bar stays. Only compact commands faded before, and full blocks showed a thin scrollbar.
  • Changed: a code block in a rendered body scrolls inside code's frame with code's fade (ag-edge-fade-xy).
  • Changed: the popup is a frame that stays whole, and its content scrolls in a new inner element, data-slot="dialog-body", which holds the padding and the gap and fades its cut edges. Before, the popup itself scrolled. Migration: a className on DialogContent that set padding, gap or the scroll (p-*, gap-*, overflow-*) now belongs on [data-slot=dialog-body]; size and place stay on DialogContent.
  • Changed: the same as dialog: the panel stays whole and its content scrolls in data-slot="sheet-body", which holds the padding and the gap and fades. Move padding, gap or scroll classes from SheetContent to [data-slot=sheet-body].
  • Changed: the popup is a frame that stays whole; the list inside it scrolls and fades its cut edges. Before, the popup scrolled with a hard cut.
  • Changed: the choices under a cell being edited (data-slot="table-choices", a text column's few repeated values) are a frame that stays whole, and the options scroll in an inner role="listbox" element that fades its cut edges. The frame keeps the slot; the listbox role, id and label moved to the inner element.
  • Changed: the tree scrolls both ways with its cut edges fading (ag-edge-fade-xy), with no scrollbar shown.

inspect

  • Changed: the table view's frame stays whole; the table scrolls inside it both ways and fades below its pinned header row (--edge-cover). The text view does the same inside its tinted frame.
  • Changed: the pill stays whole; too many choices for it scroll sideways inside it (data-slot="preview-picker-options"), the hidden side fading.
  • Changed: its words (the hover tip and what a screen reader hears) say what each state does over time, never "inherited", which read as a value saved here once: "follows lark: changes when lark does", "set here; lark says 16", and for a value set here that equals the one above, "set here, the same as lark: stays if lark changes", the one case that looks like following and is not. The × reads "follow lark again"; a clear is said as "workers: follows lark again, 16".
  • Fixed: Delete or Backspace on the × (or an ⌥-click) clears and hands the focus back to the control, as a press on the × did; it fell to the page when the × went.
  • Fixed: inside a Setting whose value is inherited, a named select posts "" with its form (nothing is set here); it posted its internal inherit marker.
  • Changed: a long list fades its cut edges where there is more (ag-edge-fade-y); it cut hard at the popup's edge.
  • Fixed: taking back a reaction nobody else gave keeps the focus in the row (on the pill now in its place, else the last, else the add button); it fell to the page with the pill.
  • Fixed: a controlled value changed from outside (a saved answer restored, a press the parent refused) shows as answered or not as it stands, with nothing half told; the stage kept what it was at mount.
  • Fixed: shortScore picks its unit after rounding (999,500 is "1m", not "1000k"), reads a down score as its up mirror, and throws on a score that is no whole number.
  • New: variant="info", the tokens' info tone (the current one, a suggestion waiting), beside success, warning and destructive.
  • Fixed: a status badge's words on a light surface are its colour deepened (lightness held at 0.55, as TONE_WORDS), so "current" in the info tone reads on its pale wash; on a dark surface, the colour itself.
  • Changed: count must be a whole number from 0; a negative or NaN count throws instead of drawing "-2" or "NaN".
  • Fixed: an aside that renders nothing (aside={stale && "12 s ago"} while not stale) is none: the top border runs to the corner instead of stopping short of an empty aside.
  • Changed: GroupBoxTone is gone; the tone is Tone from @ag/material, as readout's.
  • Changed: ReadoutTone is gone; the tone is Tone from @ag/material, where the toned words' colours (TONE_WORDS) now live once for readout and group-box.
  • New: Tone, and TONE_WORDS (a tone as words on a surface, deepened on a light one so it reads on white) in material; glassWords (coloured words lit along their foot on glass) in material-glass. Field, readout and group-box use them instead of three copies.
  • Changed: TabsBox's title is a heading above the box, outside it, and the two are one section named by it (data-slot="tabs-box-section"); with a title, className places the section, without one the box as before. It was a line inside the box above the strip.
  • Changed: a plain TabsBox's head is recessed, a step darker than the body in both schemes, so it reads as the box's chrome; in the line look the chosen tab is cut from the body, a folder's tab rising out of the strip (the body's colour, a hairline round its top, standing over the head's rule), in place of the lit stretch of rule. The action stands as low as the folder's tabs, on their labels' centre line. Glass is unchanged.
  • Changed: a plain TabsBox's frame is drawn by its head (the box's rounded top) and its body (the sides and foot), so a head pinned mid-box keeps the box's rounded top instead of a square strip between straight edges; it pins on a square of the page's ground.
  • Fixed: a capped TabsBox (maxHeight) no longer rubber-bands at its ends; the bounce past its top dragged the pinned head down with the rows and showed the empty box above it.
  • Changed: in the segment look a TabsBox's well sits as far from the head's ends as from its top (8 px at md), so its round end follows the box's corner; it stood at the body's 20 px.
  • New: --ag-tabs-box-under, where the pinned head ends: what a panel pins of its own (a table's header row) sticks at top-(--ag-tabs-box-under).
  • Fixed: TabsBox in glass keeps its corners when given a style: the consumer's style replaced the radius its body and back layer share with the edge.
  • New: TabsBox height, for panels whose content scrolls itself (a log, a trace): the box is that tall, the shown panel fills the body edge to edge and what it holds fills the panel; the box never scrolls. One of top, maxHeight and height, or it throws. What a box's panels hold is inside its frame (InFrame), so a log or a trace in it draws no second box.
  • Fixed: a capped TabsBox (maxHeight) no longer cuts its rows hard: they fade at the foot and where they come out from under the head. Capped, the panel draws the frame, outside the fade; pinned to the page, the head and body still do.
  • Fixed: a scroll-padding-top in percent is read as a share of the scroller's height (it was read as pixels), and a view too short for a thumb below what covers its top draws none instead of a thumb running backwards over the cover.
  • Fixed: a value that is no number (an average of no ratings, 0/0) or a count that is no whole number from 0 throws instead of drawing a full line. Inside a Setting at the root of its chain (nothing to inherit), Delete, 0, Home and a press on the value clear it to no rating, as anywhere else; they did nothing.
  • Fixed: a rating inside a hidden tab panel (kept mounted) draws its fractions right once the panel shows; it could collapse to too few stars.
  • New: right to left. Under dir="rtl" (on it or anywhere above) the line fills from its right, a half is a star's right half to the hand, and ArrowLeft gives more, ArrowRight less; Up and Down are unchanged. An average's fraction is cut from its right end too. Nothing changes left to right.
  • Changed: a thread's or a comment's reactions are drawn by reactions (its readout: a filled pill 28 px tall, the emoji, the count, read as "❤️, 1 reaction"), which github now installs. They were GitHub's outline pill, 24 px; an outline reads as a button, and this page cannot send a reaction. The github-reactions slot is gone: style the row as [data-slot=reactions].
  • Fixed: a video thumb's play mark is the triangle alone. It was a box clipped to a triangle with a drop-shadow filter on it, and some renderers drew the shadow round the unclipped box, a grey square behind the triangle; the triangle and its shadow are now one small picture.
  • Changed: SelectionBar stays one row. Its verbs keep the order they are given (their menu's, so their priority) and as many as the room holds stand in the bar; the rest are a "more" menu at its end, separators kept. The count and the clear always stand. The split is measured from the verbs' own widths and follows the room as it changes, where a long list used to wrap the bar onto a second row.
  • Changed: an icon is found by what it means, not only its name: lucide's own keywords for every icon (lib/lucide-keywords, from the lucide version installed), so "warning" or "danger" finds the alert icons. What is typed is looked for as text first, an icon named so ahead of one that only carries the word; letters scattered in order are shown only when nothing holds the text, under "nothing called that · letters in order". findItems and findIcons are that search.
  • New: NamedIcon, an icon drawn from its stored name where the whole set must not be carried: loaded when first drawn (lucide's one module per icon, Lucide Lab's only once a Lab name is drawn), kept once loaded, nothing drawn until it lands. A module that does not load is said in the console and asked for again every two seconds while drawn, never kept as a failure. It draws the icon each module exports, lucide's public shape, so it holds across lucide versions (reading its private drawing data drew empty icons under lucide-react 1.49, which renamed it). gallery-tag draws tags' icons with it; a mark says the icon it holds in data-icon.
  • Changed: the list takes the room the picker is given, up to 14rem as before: a picker set in a shorter box (className="h-48", a short popover) shrinks its list to fit instead of standing 260 px tall and spilling. Its cut edges fade where there is more.
  • Changed: the list stands on the best match as soon as something is typed, so Enter takes it; it stood on nothing, and Enter after typing "tawn" did nothing. Tab with something typed completes to that match too and keeps the focus in the field; with nothing typed Tab leaves, as before.
  • Changed: the popup is a frame that stays whole; the list inside it scrolls and fades its cut edges.
  • New: data-keys="held" on an element makes every instance inside it take a press only while the hand holds it (focus is in it, or it was the last thing pressed or focused in), never for the pointer resting over it and never page-wide. For a page of live examples, a gallery of cards each holding a real player or list: each works under the hand and the page keeps its own keys. Nothing changes where the attribute is absent.
  • New: a judgement on a line of stars, or any glyph handed in (glyph, an svg filled in the tone, a character greyed where empty), max of them (5). The value runs from 0 (no rating) to max and is drawn as that much of the line filled, cut on the glyph's own ink: 3.5 is three stars and a half, an average of 4.3 fills 4.3. step is 1 or 0.5.
  • New: a person's own is a slider (role="slider", "3.5 of 5 stars"): the hand previews where it is, a half on a star's left half; a press sets, a drag sets where it lets go, a press on the value it has clears. The arrows move by the step, PageUp and PageDown to the next whole star, Home (no rating) and End, a digit to its star, 0, Delete or Backspace clear. name posts it with a form ("" when none). readOnly holds it still, its Tab stop kept.
  • New: count makes it the average of that many ratings, a readout: one image to a screen reader ("4.3 of 5 stars, 120 ratings"), never a Tab stop, the number and the count beside the line.
  • New: at the input's heights (size); plain bare glyphs, or a pill of glass. Inside a field it is named and described by it, red when invalid, held still while read-only; inside a Setting a ghost (the filled glyphs hollow) of what it inherits, a press on that value sets it there and a clear goes back.
  • New: up or down with the score between them. Controlled, value (1, -1, 0) and score; a press reports the vote it makes, the other arrow's, or 0 when it takes yours back, so the handler moves the score with the press before any answer (score - vote + next). Each arrow a Button with aria-pressed; the score is said politely when it changes, the whole number to a reader and short to the eye (12.3k, format to change it). orientation vertical or horizontal, in a well (plain sunken, glass a pill) or compact, the arrows and the number alone; readOnly for a locked thread, kept in the Tab order and never dimmed.
  • New: "was this helpful?" and two thumbs, each a Button with aria-pressed. A side with reasons or a note opens its follow-up in place (the reasons as chips, a line of words, send); the same thumb again takes the answer back. onValueChange hears the thumb at once, onSend the whole answer once ({ value, reasons, note }), synchronously: the thanks is said politely in place and the focus goes back to the thumb. Plain a row on the page, glass a pane holding it.
  • New: emoji under a post with how many gave each. A pill is a toggle (aria-pressed, named "👍, 12 reactions"), offer adds a button opening the emoji you may give in a popover; the consumer flips mine and the count in onToggle. With no onToggle it is a readout, words and not buttons. Plain outline pills, quiet glass in glass.
  • New: a value that falls back to a default, a machine's over its model's over the fleet's, in one of three states: set here (value), inherited (value undefined, fallback: { value, from } applies), or locked here (locked names who sets it). <Setting label value fallback onValueChange>{(s) => <Switch checked={s.value} onCheckedChange={s.pin} />}</Setting>: the control inside reads it and draws what applies as a ghost while it is inherited, a dashed edge, a hollow mark, the same colour at less strength (glass: the material's own ghost). Where the value comes from is named on hover and focus, and to a screen reader as the control's description ("inherited from lark, not set here", "set here; lark says 16", "locked here, set by provisioning"); the control states the value that applies, never aria-checked="mixed".
  • New: the slot after the control holds × while the value is set here (under the hand and on focus, reachable by Tab, focus back on the control after it), a padlock while it is locked, nothing while it is inherited. ⌥-click on the control clears, so does Delete or Backspace where the control does not edit text with them (ownedHere decides), and an emptied number or text field; a clear is said in a polite live region. Locked is read-only, never disabled: the control keeps its Tab stop and is read out.
  • New: what a value control reads from the setting around it, the way it reads useField(): useSetting() (inherited, read-only, the id of the words that describe it, the clear, the fallback), settingProps(setting, describedBy) for a control's own props, and the ghost's one spelling, GHOST_EDGE (a dashed edge on ::before, plain) and GLASS_GHOST (--glass-ghost). Its own item, so a control reads its setting without installing the slot and the keys.
  • New: inside a Setting whose value is inherited, a ghost of the value that applies: the thumb hollow where the value puts it, the track at a fifth of its colour and dashed (glass: the material's ghost). Described by the setting's words; read-only while it is locked (data-readonly, the default cursor).
  • New: inside an inherited Setting, a ghost: the box dashed and unfilled, the tick in the primary over a fifth of it (glass: the material's ghost). Described by the setting's words, read-only while it is locked.
  • New: inside a Setting, Input and InputGroup are empty while the value is inherited and their placeholder is the value that applies, drawn as a value, not a hint (the words' ink a step back where a hint's is fainter, the well dashed; glass: the material's ghost); emptied, the field clears what is set here. Read-only while the setting is locked, described by its words.
  • New: readOnly: the thumb holds still under the hand and the keys, keeps its focus, is read out (aria-readonly); never dimmed. A field's read-only and a locked setting's hold it the same way.
  • New: aria-describedby of its own, joined with a field's and a setting's words.
  • New: inside an inherited Setting, a ghost: the thumb hollow in its tone, the fill at a third (glass: the material's ghost on the thumb, its dot hollow).
  • New: a group under a name, the name on its edge. A hairline box whose name stands in a gap in its top border at the left, and an aside at the right for when it was written or a count, so a page holds many dense groups that read as an instrument panel. The gap is real, nothing is painted under the name, so it reads over a picture or a gradient: the border is four pieces laid on the box's grid round the name and the aside, no measuring and no fieldset. role="group" named by its name. tone (ok, warn, bad) puts the whole group in that tone, its edge and its name. In glass there is no cut, since a gap would break the lit rim: the name and the aside stand on the pane's front surface (the material's GlassFront), a strip of glass across the top with its own blur and one lit hairline as its inner edge, and what the group holds lies on the body under it. A tone is the glass's stain, on the front and the body alike.
  • New: names and their values, read at a glance, as a description list: <Readout> of <ReadoutRow label="…">value</ReadoutRow>. The names in the words' voice a step back, the values in one column after them in mono with tabular figures, so separators and digits line up down the column. A value is any node (a badge, a link, a field). tone (ok, warn, bad) colours a value, deepened on a light ground so it reads on white, and a screen reader hears the tone's word after it. Under 24 characters wide, a container query on its own width, each name sits over its value. It draws no surface: it reads in the text's colour on whatever holds it, plain or glass.
  • New: a month to pick a day from, or a range of days (mode="range": the first press one end, the second the other, the band following the hand between them, Escape letting go). The WAI-ARIA date grid: one Tab stop, the arrows by day and week, PageUp and PageDown by month (a year with Shift), Home and End to the week's edges, Enter or Space chooses; the keys are answered on the grid through ownedHere, so a page's shortcut never also hears them.
  • New: the value is a civil date string, "2026-10-11", or { from, to }, never a Date: a Date is an instant, and the same day picked in Madrid is the day before in UTC once it is serialized. toDay(date) and dateOf(day) cross to and from a Date where an API wants one; a form posts the value by name as the day or an ISO interval (2026-10-03/2026-10-09).
  • New: today dotted, the months either side dimmed, six weeks always so it keeps its height, days disabled refuses or outside min and max reachable but not chosen. The month, the weekdays and the digits are the locale's (locale, default the reader's), on the Gregorian calendar the grid is drawn in; the week starts where the locale's does unless weekStart says. labels for the words that are not dates. Inside a field it is named and described by it and holds still while read-only. Plain or glass; it draws no surface of its own.
  • New: lib/calendar-core, the arithmetic, framework-free: day numbers counted in UTC, so a step never slips across a change of the clocks; month grids, months held to their last day, ranges, the keys' steps, the locale's week start and words.
  • New: a day, or a range of days, chosen from a field: the select's field face at its three heights, showing the choice in the locale's words (format), opening the calendar in a popover. The keyboard lands on the chosen day or today, Enter chooses and the field takes the focus back; a range closes once both ends are chosen. Inside a field it is the field's control (named by the label with the choice, described, red when wrong, not opening while read-only). Plain or glass; container for a layer that covers the page.
  • New: panes that share a container, side by side or one above the other (ResizablePanelGroup, ResizablePanel, ResizableHandle, shadcn's names, no dependency). Sizes are shares of the room from 0 to 1, never pixels or percents (a percent screams): defaultSize, minSize, maxSize, a pane without a size taking an even part of what is left. A handle drags under a captured pointer and pushes the next pane once the nearest is at its least; focused, the arrows move it by step, Home and End as far as it goes, Enter or a double-click puts it back where the default layout has it. layout and onLayout hold the layout outside. The handle is the separator's line, withHandle adds a grip, plain or glass. lib/resizable-core is the arithmetic.
  • New: the picture behind a surface decides its scheme. scheme="backdrop" (on a control, or a provider round many) is answered by the nearest picture that can be read: each control wears the scheme the part of the picture under it asks for, light glass with dark words over a bright sky, dark glass over the ground, and each glass as thick as its words need over what is under it and no thicker (tintFor: dark glass enough to bring the brightest part under it down for white words, light glass enough to bring the darkest up for dark ones; set as --glass-thickness). <Backdrop src> puts a picture behind a section and reads it; useBackdropSource(video or image, { fit }) makes any media a source. A scheme named on a control or a provider pins it. Sampled small and only while on screen; a cut holds a second before a visible surface flips, a strobe flips nothing, a hidden surface changes at once. lib/backdrop-core holds the arithmetic (lightness in CIE L*, an area's reading, the verdict).
  • New: a source over several pictures, useBackdropSource([…images]): photographs gliding under the same controls, a surface over two of them read across both (pictures of one source that overlap scream). AdaptiveGlass is Glass (server-safe, told its scheme) as a surface that reads its own, for a piece of glass of your own over a picture. A canvas is a source too (a comparison stage, a chart): its owner calls the source's refresh() after drawing.
  • New: BackdropGroup, controls that read as one (a row of buttons) read the picture as one: the band under the group decides every piece's scheme, so a pill over the sky no longer turns light beside circles over rock that stay dark. Its glass's thickness and rim are the band's too. The player's transport reads as one row.
  • Fixed: <AdaptiveGlass fluid> wears data-material="glass" and data-scheme like any glass.
  • New (experimental): <AdaptiveGlass fluid> chooses no scheme and reads no source: the glass is the picture under it, frosted, drained of colour and then pushed in contrast by the compositor (backdrop-filter, in that order, so a coloured picture never clips to a burned hue), under a faint white veil that lifts its black to a deep grey, so it is light where the picture is light and dark where it is dark along the picture's own shapes; its words are white blended by difference against it, dark on the light parts and light on the dark inside one word. It follows anything under it, a playing video and the page included, on the frame it moves. Its words must be elements (a span, an icon), and nothing between it and the picture may isolate (a transform, opacity, a filter), or the frost sees only what is inside that layer.
  • Changed: glass keeps its words at one contrast, 7:1 (WCAG's AAA), against the part of the picture least in their favour, white words on the dark face and dark ones on the light (WORDS, from which TINT's targets are worked out). The faces had a lightness each, which held white words at about 5:1: dark glass over a white rock let it through as a washed mid-grey. It is charcoal now, and light glass is a touch thinner.
  • Fixed: a rim's strength is read from what is right under it, the picture seen through the glass's face (underRim), not the picture alone. Dark glass over a white sky is only as light under its rim as its words allow, and its rim was made stronger for the sky's sake: a hard white line on a dark face. Over the same sky it now carries about 0.8 where it carried 1.1.
  • Fixed: a picture and the glass over it arrive as one movement. The picture showed on load under glass still wearing the empty page's scheme, which then snapped to the reading a beat later; <Backdrop> now shows its picture once it has been read, fading in over 300 ms while the glass eases to its reading on the same frames. A source says when it has been read (useBackdropSource(…).read), for an owner drawing its own picture to do the same.
  • New: a reading carries the area's mean colour (Reading.color, sRGB, averaged in linear light so a mix of light and dark is the grey the eye sees), and a source sets it on each surface it reads as --glass-ground, the colour its rim picks up.
  • Changed: a source also looks the frame after any pointer move, so a surface dragged over the picture is read as it goes.
  • Changed: a source looks only when something can have changed: a frame of a playing video, the picture loading, a scroll, a resize, a pointer move, a transition or an animation starting or ending (chrome rising on a transform). A still picture under still controls costs nothing, where a look every 500 ms ran for as long as a source was on screen. --glass-ground is written in OKLCH at the grain the eye tells apart (oklchGrain: steps of 0.02 in lightness and chroma, 10° of hue), so a colour gliding under a glass writes a new value a few times a second instead of on nearly every frame, each write restarting the glass's ease.
  • New: each surface a source reads gets its rim's sheen from the lightness under it (sheenOf, from 0.55 over black to 1.1 over white), continuously: less edge light over a dark picture, more over a light one, so the edge reads about the same on both.
  • Changed: it follows the picture instead of trailing it. A playing video is read about every frame (it was every 150 ms), the surfaces are looked at the frame anything moves (a sample, a scroll anywhere, a resize) instead of every 300 ms, and a visible change holds 180 ms (it was a second) before it turns, still long enough that a strobe turns nothing. The turn eases over 300 ms (the glass's background, words and light), where it cut.
  • New: the front surface, a strip of glass in front of what a glass holds, across its top or along its foot: it blurs what passes under it (--glass-front-blur, 12px), wears a face of its own (--glass-front-color, else the page's background at 55% hued by the stain), takes its glass's corners on its side, and its inner edge is one straight hairline lit by the rim's catch. <GlassFront at="top"> lays it over a glass, --glass-front-height tall, for the glass's content to pad by the same length; glassFront.top and glassFront.bottom are the surface alone, for an element that places itself (a list pinned with sticky top-0 over content scrolling under it).
  • New: --glass-ghost (0 to 1, registered, eased, not inherited), the glass's ghost state: a value that applies but is not set here (setting). A glass rim is light and cannot be dashed, so at 1 the glass itself is thinner (its body a third as thick), its rim and glow under half as bright, its stain a third as strong.
  • Fixed: an outline glass lost its ring for the length of every hover and every leave, and a pressed glass its stained rim: the glass transitioned its shadow, and Chrome draws a shadow layer in the text's colour as transparent for the whole of such a transition. The shadow is no longer transitioned; its light eases through the properties it is made of (--glass-rim, the sheen, the ground's light), so the hover still eases.
  • New: scheme="backdrop", the fourth way a scheme is decided (beside the page's, light and dark): by the picture behind the surface, through backdrop. useSurfaceScheme(scheme, ref) is a surface's scheme read by its own element, with the ref its root takes; BackdropScope makes a source the answer inside it.
  • Fixed: under scheme="backdrop" every surface is read by the part of the picture under it, not by the whole picture's average: input (and InputGroup), textarea, select (its field, and its list with it), toggle-group, tabs (the list; a control in a panel asks for itself), switch, checkbox, radio-group, slider, number, progress, edge-arrows (each arrow), scroll-thumb (each thumb), alert, accordion, skeleton, field's FieldSet, form's FormSummary, people-picker, tree, toast (each toast), thumb-strip, popover, menu (Menu and DropdownMenuContent), dialog, sheet, tooltip, timeline, edition-compare, and the player's speed, captions, sound, fullscreen, playlist button and foot. A surface that holds controls (an alert, a fieldset, a group's well) hands them the scheme it reads. PlayerChrome hands down the scheme asked rather than the one it reads, so each part over a picture reads the frame under itself; before, every part inside it wore the whole frame's.
  • Changed (breaking): what a surface wears is WornScheme (auto, light, dark), what it may be asked for is Scheme (those and backdrop): schemeOf, glassVariants and Glass take a WornScheme, and material-glass and liquid-glass export WornScheme where they exported Scheme. Migration: import Scheme from material for a prop, WornScheme for a value already resolved (useScheme's return).
  • New: --glass-thickness, the opacity of the face's own tint (0.7 light, 0.6 dark unless set), which a backdrop source sets to what the words over its picture need.
  • Changed: the light glass frosts harder (14 px where it was 5, its tint a little thicker and more saturated): its dark words stood on the picture's texture showing through, and over the same picture it read worse than the dark glass. The dark glass is unchanged. Set --glass-blur to keep a thinner frost.
  • Changed: the glass's edge is a specular catch of light, not an outline: brightest at the corner facing the light (top left), nothing along the sides; at the far corner the dark face has a fainter catch of light and the light face the curve's refraction, a faint dark band (a white catch on light glass over a white sky showed no edge at all); the drop shadow parts it from the page. A hairline all the way round and a dark one along the foot drew a grey, slightly thick line round every glass ("the opposite of a fine specular reflection").
  • Changed: the rim reads against its ground. --glass-sheen (registered, eased) multiplies every part of the rim's light (the ring, the hairline, the inner light) beside --glass-rim, the glass's own state: the dark face carries 0.8 of it, the same white edge having read several times stronger on dark glass than on light ("overpowering"), and a glass over a picture is given its own by the picture under it (backdrop). Set --glass-sheen to choose.
  • Changed (breaking): the glass's tone is now scheme, the colour scheme a control stands in: auto (the page's), dark (dark whatever the page is: glass over a picture) or light (light glass and words whatever the page is). MaterialProvider carries it beside material (<MaterialProvider material="glass" scheme="dark">, each kept from the provider around when unnamed), and useScheme(prop) reads it. schemeOf(scheme) is the one class a surface wears for it: dark is the theme's own class, so the page's colour variables flip on the surface and inside it (words, icons and nested controls read on it without ink of their own, and the browser's own parts, scrollbars and native lists, go dark with color-scheme). glassVariants takes scheme instead of tone, and its dark half follows the scheme, not only the page. GlassTone is Scheme. Migration: rename every glass tone= to scheme= and GlassTone to Scheme; a glass you meant dark over a picture, set scheme="dark" on it or on a provider round the picture, since no control defaults to dark any more (dialog, tooltip, edge-arrows and thumb-strip did); reinstall tokens for the scheme-dark: and scheme-light: variants.
  • Fixed: a dark glass on a light page took the light half of its face: <Button material="glass" tone="dark" variant="secondary"> drew white words on pale glass, because its faces asked the page (dark:) and not the glass. Every face now asks the scheme (scheme-dark:).
  • New: GlassEdge (from material-glass), glass as an edge only over media that runs to it: the inner light and the specular rim, no mat. It was glass-display's, so an edge round a picture installed a video engine; it is the material's now, re-exported from liquid-glass. Migration: import GlassEdge from material-glass (or liquid-glass), not from glass-display.
  • Changed: every glass edge is one edge. Its light is defined once (glassLight: the catch, the far corner, the light inside, per face) and drawn once (glassEdge: a hairline on ::before for the catch, one on ::after for the far corner, the same fall mirrored). A control's glass, GlassEdge round media, GlassPane's edge and a bar's chosen segment (glassLift, tabs and toggle-group) all wear it, where media frames and panes drew their own SVG rim with numbers of their own and the segments a flat top line. Each hairline is an inset shadow shaped by a gradient mask, which stays as fine round a curve as along a side; the masked ring drew heavier on the curves in Chrome. GlassEdge takes scheme. An element wearing glass leaves both pseudo-elements to its edge. glassVariants({ edge: false }) is the body alone, glassVariants({ edge: "only" }) the edge and its shadow with no body (no fill, no backdrop filter, nothing inside it standing on glass); glassShapes is glass's box alone.
  • Changed (breaking): a picture's reading of the rim is --glass-ground-sheen, and --glass-sheen is worked out on each glass from it (unset, the face's own: 1 light, 0.8 dark). Before, the dark face's own 0.8 overrode what a BackdropGroup set, so dark glass in a group never took the picture's sheen. Migration: set --glass-ground-sheen where you set --glass-sheen.
  • Changed (breaking): glass takes a colour one way, the STAIN: --glass-stain (any colour) at --glass-stain-amount (20% unless set). tint/tintAmount set them, a parent can set them for every glass inside, and a control's states set them (on: a pressed button, a checked box; invalid: input, textarea, select, switch, checkbox and radio now stain their body as well as their rim). The glass works out its hues from the stain in CSS (--glass-hue, --glass-hue-deep, the rim's ink), so a colour set by a class is drawn exactly like one set by a prop. Migration: --glass-hue and --glass-hue-amount set by hand are --glass-stain and --glass-stain-amount; rimInk and deepInk are gone (read the hues in CSS).
  • New: the rim picks up the colour of what the glass stands over. Over a picture a backdrop source reads (backdrop), each glass is given the picture's colour under it (--glass-ground), and its rim's light is white leaning toward that colour, made light and clear, by --glass-ground-share (35% unless set): a red rock's edge warms, a fjord's cools. It eases as the ground changes. Over no read picture the rim stays white.
  • New: the bend, the picture bent under a curved edge, steepening into it as a curved surface does: barely touched until the last few pixels, then bent hard at the rim, with a crest of light lifting the picture into the edge's fine line by a share of that line's own light (--glass-rim), so line and bend are one light. bendLayers(radius, bend) gives its layers (a faint softening, a harder bend, a hard press at the rim, the crest), each a GLASS_BEND element with its filter and its mask, the box's own rounded outline blurred, so it follows the rim round every corner. Place them over the picture inside its rounded clip: outside, the blur read the page beyond the corners and smudged them dark. glass-display's frames carry it (bend, 0.6).
  • Changed: glass ON glass (a field in a glass panel, a chosen segment in a glass bar, glass inside a GlassPane) carries --glass-on-glass (0.65 unless set) of its tint and --glass-on-glass-rim (0.85 unless set) of its edge's light: what is under it is the outer glass, already frosted and tinted, and a full tint and edge over it read as a flat, washed-out slab with a doubled rim, where half of each read as faint under the soft edge. A glass BODY above it tells it: every glass body sets --glass-body (glassVariants does), what is inside inherits it, and each glass asks its parent. data-material is the face's and says nothing about what a control stands on: read as "stands on glass", it drew radio items, a slider's and a progress's track, a tab list, a date picker in a plain field, a people picker's chips, a tree's chosen row and the player's transport at on-glass strength while they stood on the picture. Glass you lay on something that is glass without being a body above it (content beside a pane's back surface) sets --glass-body: 1 on what holds it.
  • Fixed: a glass inside another glass took the outer one's rim (--glass-rim is inherited): a rail or bar drawn with rim={0} put out the edge of every glass on it that did not set its own. Every glass starts at its own rim, 1; a control's states and rim set it from there. A bare edge (GlassEdge, a pane's) still follows the glass it belongs to.
  • New: LOOKS, a whole way of drawing the glass's light (glassLooks), worn by a glass or by anything round several (inherited, so a page can wear one) while each glass keeps its own state. apple is the edge macOS renders on an app's icon, set by eye beside those icons: a crisp half-pixel line at 1.65× the light, a 2 px light under it, and the body shaded across from the light's corner, neutral grey, lighter top left. For glass among app icons; a look stays off the rest of a page, where apple read as too much.
  • New: the edge's knobs a look is made of, each inherited: --glass-rim-depth (how deep the light under the rim reaches, 4px unless set), --glass-look-rim (a look's multiple of the edge's light, beside --glass-rim), --glass-shade (how much the body is shaded across, 0 unless set; it goes with the rim, so a flat sheet with rim={0} has none).
  • Changed: Glass's children are its content and nothing else: its edge is drawn on the element (glassVariants), the lines on its pseudo-elements, the glow an inset shadow under the content. A trailing layer it appended for the edge broke space-y-*, divide-* and :last-child (cards gained 4 px at the foot), was invalid inside as="ul" and impossible with a void element. Content held to the edge covers the glow where it reaches (within about 3 px of the edge); keep a picture a few pixels in.
  • Changed (breaking): every glass states its corner radius ONCE, as a length, --glass-radius, which draws its corners (glassVariants and glassEdge wear rounded-(--glass-radius)) and places its edge's light. The catch is brightest where the shape's edge begins, (√2 − 1) × the radius along the light's line from the box's corner, and falls over 120 px (to 27.5% at 0.4375 of it), never past the middle of the line, where the far corner's begins. Measured as shares of the box's diagonal, the light peaked at the shape only for a circle: a pill button's catch began at 57% of its light and a wide field's at 81%, round glass of other sizes (a round skeleton, telegram-chat's 34 to 36 px glass) showed only its tail, and the fall ran 6 px on an icon button and 25 px on a wide field. The fall is the one set by eye on frames over pictures (within 4 px on a 372 × 209 frame of radius 20, within 2 px on a 40 px circle); a pill and a wide field now carry more of their light. Every control states its radius (a pill's is half its height); glassShapes state theirs, a round shape (circle, pill, bar) sized by --glass-size with its radius half of it; GlassEdge's radius sets it; a sheet against the screen squares the corner it presses there (--glass-corner, --glass-corner-far). --glass-radius is not inherited: a glass that states none draws square with its light a whole ring. A frame's bend follows a look's light as its line does. Migration: on a glass face of your own, replace its rounded-* with [--glass-radius:…] (a theme radius as --theme(--radius-2xl), a pill as half its height); size a Glass circle, pill or bar with [--glass-size:3rem], not size-12 or h-12.
  • Fixed: a glass face's transition has one owner, the glass. A control wrote its own transition-[…] on the same element, and tailwind-merge kept only the later of the two. Written before the glass, the control's motion was dropped: the glass tooltip, menu's list, select's list, dialog, popover's and hover-card's popups and scroll-thumb opened and closed with no fade or scale. Written after, the glass's was: input, textarea, select's field, checkbox, switch, radio-group, edge-arrows, slider's thumb, number, sheet and toast cut from one scheme to the other instead of easing, and number's rim jumped. A control's own motion now goes in through --glass-moves (whole items of transition, opacity_150ms_ease-out,…), and the glass eases its body, words, shadow, rim (--glass-rim, so a hover or a focus eases on every glass) and light over --glass-change (300 ms on the material's ease unless --glass-change-duration or --glass-change-ease say otherwise). Migration: on a glass face of your own, move a transition-*, duration-* and ease-* into --glass-moves.
  • Changed: the edge has two strengths, the line's (--glass-strength, with the ground's sheen) and the glow's under it (--glass-glow, without), so the glow is one declaration for both faces. The dark face's glow carried shares a quarter higher to undo its own sheen of 0.8, which was wrong as soon as a picture set the sheen (backdrop). At rest the dark glow is 29% and 15% of the rim's light (it was 29.6% and 14.8%), the light face's unchanged.
  • Fixed: a state's light wins over the glass's own. glassStyle (and a consumer's style) writes the light inline, which beat every state class: a pressed destructive glass button kept its red stain instead of the on state's, a tint given to a glass beat its invalid state. Every state that sets the light (--glass-rim, --glass-rim-color, --glass-stain, --glass-stain-amount under a hover, a focus, on, invalid) is important now, in button, checkbox, switch, radio-group, input, textarea, select, number, slider, edge-arrows and scroll-thumb (scripts/examples/glass-faces.tsx refuses one that is not). Migration: on a glass face of your own, end such a state with ! (hover:[--glass-rim:1.5]!); to override one of ours, write yours with ! too.
  • Changed: what the glass does to the picture behind it is one property, --glass-backdrop, a whole backdrop-filter in its order (a blur of --glass-blur and the face's saturation unless set). A fluid glass's frost sets it, where its inline filter left the body's blur and saturation classes composing a filter that never applied. material installs tokens (its faces' dark half is scheme-dark:; installed alone it had no dark face). GlassStyle types --glass-backdrop, --glass-moves, --glass-change-duration and --glass-change-ease.
  • Changed: GlassStyle no longer types --glass-bend, which nothing read (a frame's bend is GlassFrame's bend). The edge's knobs (--glass-rim-width, --glass-rim-softness, --glass-rim-depth, --glass-stain-amount) are resolved once on each glass and read by that one name, their defaults written once.
  • Changed: --glass-sheen and --glass-ground-light are registered as not inherited: every element that draws with them works them out itself, so while they ease (a picture under the glass changing) only those elements are restyled, never everything inside a glass. Set --glass-ground-sheen on a group to reach the glass inside it, as before.
  • Changed: the tint's default amount is 20% (was 12%), set by eye with the tint sample's knobs. The textarea's invalid rim is as bright as the other fields' (it lacked the 2.2).
  • Changed: the edge is SOFT, the one set by eye on frames over pictures, and every glass wears it: a whole-pixel line blurred by one (--glass-rim-width 1px, --glass-rim-softness, new, 1px unless set) that melts into a soft light about 4 px deep inside it, brighter on the lit side, the gradient that reads as the thickness of a curved edge. It is about a quarter of the light the controls' edge had, which was a crisp half pixel with a lit line along the top and read as a white line ("too white", "too intense") beside the frames. --glass-strength is --glass-rim × the ground's sheen × glass on glass, with no factor of its own; a control's states still raise --glass-rim the same multiples. Migration: a glass whose edge you want as before sets --glass-rim-width: 0.5px, --glass-rim-softness: 0px and rim about 4.
  • Changed: a status is stained as any tint is, glassStyle({ tint }) at the tint's own amount, its colour said again on its mark (an icon, a title), never on all its words: a destructive button, a status badge, an alert and a toast each set their own (30%, 30%, 22%, 20%), and the louder ones read far greener or redder than a tinted glass beside them. stain, briefly the one amount, is gone. Migration: stain(color) is glassStyle({ tint: color }).
  • New: success and warning variants beside destructive, the alert's words for a status: in plain its colour washed under its words, in glass the glass tinted it as any tint is and its icon in it, the words the glass's own. A status badge was hand-stained with glassStyle.
  • New: count, a number as the badge's content, drawn small enough to sit beside a label's words on their centre line (a tab's, a menu item's): a circle while one digit, a pill past it, its digits tabular. <TabsTab value="alerts">alerts<Badge variant="warning" count={3} /></TabsTab>. A count given children as well throws.
  • Changed: a scroller whose top is covered (a pinned head over what scrolls) says so with scroll-padding-top, and the vertical thumb's track starts below it, where it ran over the head.
  • Changed: the glass segment's chosen tab is lit by the glass's own edge (glassLift), where it wore a flat line along its top.
  • Changed (breaking): a TabsPanel mounts the first time its tab is shown and stays mounted after, hidden while another tab is shown, so what was typed in it, its scroll and anything it fetched survive a switch, and a panel never shown never mounts. mount says when: "seen" (the default), "active" (only while shown, the old behaviour) or "always" (from the start). Migration: keepMounted is gone, write mount="always"; a panel that must start over on every visit takes mount="active"; content that polls or subscribes and should stop while hidden asks useTabsPanelActive(). TabsPanel no longer takes render: it draws its own element to hold the content back until first shown.
  • New: useTabsPanelActive(), whether the panel round it is the one shown; it throws outside a TabsPanel.
  • New: TabsBox, the tabbed container: one surface (plain a Panel, glass built as GlassPane is), its title on its own line in the panel's title voice, then a strip holding the tabs and an action on one centre line, then the TabsPanels, which keep their mount behaviour. Title, strip and body share the panel's inset, a line list's first label on that edge too; the strip is 40, 48 or 56 px by size. The strip pins while the body scrolls under it: against the page, below what the page passes as top (top="var(--header-height)"), or inside the box when maxHeight caps it (the box then scrolls itself and wears the scroll thumbs). Plain, the head is a solid strip of the card's colour as it is seen (the card laid over the page's ground), so rows passing under it are hidden; glass, the box is a pane whose body lies on a back layer and the strip is the material's front surface (glassFront: its own blur and face, one lit hairline as its inner edge, as GlassPane and the glass group-box wear), what passes under it seen only as light, the glass's edge drawn over both. Both looks, segment and line (the strip's rule its line). tabs now installs panel and scroll-thumb.
  • Changed: the ‹ › chevrons at the ends of the bar are on by default. Migration: a field that should stay the value and its hairline alone takes chevrons={false}; a chevrons you pass already can go.
  • New: readOnly: shown and read out, never changed (no typing, drag, step or wheel; the value still takes focus). Never dimmed, that is disabled. A field's read-only and a locked setting's hold it the same way.
  • New: aria-describedby of its own, joined with a field's and a setting's words.
  • New: inside an inherited Setting, a ghost (the well unfilled and dashed, the digits a step back until typed over; glass: the material's ghost); an emptied field, committed, clears what is set here and shows what it inherits at once.
  • New: readOnly, shown and never changed here: a press changes nothing, the options keep their Tab stop and the arrows still move between them, never dimmed (that is disabled). A field's read-only and a locked setting's hold it the same way (data-readonly).
  • New: inside an inherited Setting, the mark on the option that applies is a ghost (a dashed outline plain, the lit pane at the material's ghost in glass), and pressing that option sets it there: the press on the chosen option is reported then, and only then (elsewhere one of one keeps it to itself, as before). Described by the setting's words beside a field's.
  • Fixed: a single choice given defaultValue (uncontrolled) moves its highlight to the option pressed; it stayed on the first choice while another option read as chosen. Pressing the chosen option again keeps it chosen, as with value; it used to leave the group with nothing chosen.
  • Changed: the glass group's chosen option is lit by the glass's own edge (glassLift), where it wore a flat line along its top. With multiple, each pressed option's lift is the same element the single choice glides, under its words.
  • Changed: the glass arrow is 32 px, the plain one's size and the icon button's, where it was 36: the two materials differ in face, never in size.
  • Changed: a browser scrollbar wherever one appears is thin, its thumb the scroller's own words' colour at 25% on a clear track, so it follows the scheme and reads over a picture as on the page; the browser's grey bar and white track read as broken. The registry's long scrollers keep their own thumbs (scroll-thumb). Migration: reinstall tokens.
  • New: scheme-dark: and scheme-light:, an element in the dark or the light scheme, which is the NEAREST one named: its own .dark or .light, else the closest surface above it, else the page's theme class. .dark and .light set --ag-scheme, inherited, which the variants read through a container style query (Chrome, Safari 18, Firefox 151). A selector cannot say nearest: a control with no scheme of its own inside a light glass on a dark page drew its dark face. Tailwind's dark: asks only about ancestors, so a surface that is dark itself could not style its own face with it. .dark sets color-scheme: dark, .light color-scheme: light. Migration: reinstall tokens.
  • Fixed: a roving control the keys moved to (an option, a tree row, a tab, a radio) keeps its focus ring while it holds tabindex="-1", a frame or for good; only a script's focus target that is no control (a dialog, a stage, a scroller) goes unringed. Migration: reinstall tokens.
  • Changed: the glass outline button draws its own faint ring all the way round, in its words' colour: an outline is a line round the shape, which the glass's edge, now a specular catch at a corner and along the top, no longer is.
  • Fixed: the glass outline button (and badge's) is the glass's edge with no body (edge: "only"): it kept a backdrop filter (saturating the picture under a transparent body) because only the blur and the fill were reset.
  • Changed: under scheme="backdrop" each button is read by the part of the picture under it, not the whole picture (and badge and panel the same; a panel's content wears the panel's scheme).
  • Changed: a toggle says its state one way: aria-pressed on a fixed name, or with on/off the name itself changing and no aria-pressed (which said it twice: "following, pressed"). Its pressed look follows data-pressed.
  • Fixed: a loading button's name stays its label (it was "loading": the label hidden with visibility left the accessibility tree); the spinner is hidden from assistive tech, aria-busy says it.
  • Fixed: a link in the button's look (render={<a href="…" />}, or a router's <Link href>) stays a link: Base UI's button gave it role="button", so a screen reader announced the link as a button. It wears the same face and takes no button keys. A link cannot be disabled, loading or pressed; asked to be, it throws.
  • Changed (breaking): a face alone, installing nothing else (it pulled the lightbox and the whole video engine with it, 22 items, into every install, a picker's included) and no longer a client component. full is gone: a face that opens its portrait is avatar-portrait. avatarLook and AvatarImage are the look, for a face of your own. Migration: an Avatar with full becomes AvatarPortrait (npx shadcn add @ag/avatar-portrait); without full, nothing changes.
  • New: a face worth a closer look, the avatar that opens the full portrait alone in a lightbox of its own, captioned with the person's name (what Avatar's full did). It brings the lightbox; quote uses it for an author with a portrait.
  • Changed (breaking): no live region by default. A callout of the page is read with the page; live makes one that appears because something happened say so (role=alert when destructive, role=status otherwise). Migration: add live where an alert is shown after an action.
  • Removed (breaking): isKeys. kbd is a client module (it reads the material), so a server component calling isKeys to choose between keys and code failed to prerender. Migration: keys(text) !== null from lib/keys, which runs anywhere.
  • New: inside a Setting, the list leads with an entry for "inherit": the fallback's words, "lark's, follows it" under them, chosen while nothing is set here; choosing it clears. Inherited, the field is a ghost (its well dashed and unfilled, the words a step back; glass: the material's ghost); read-only while the setting is locked; described by its words beside a field's. A fallback that is none of the options screams.
  • Fixed: its list's opening and closing motion stands still under reduced motion.
  • New: container, where its list is drawn, as popover and menu take: a select inside a player passes usePlayerLayer() so its list shows in fullscreen.
  • New: selectFieldFace(material, size), the select's field face, for anything else that opens from a field (date-picker's trigger wears it).
  • Fixed: its opening and closing motion stands still under reduced motion (a sheet's too).
  • Changed (breaking): DIALOG_MOTION is one motion per material (DIALOG_MOTION[m]), the frame it carried (scrolling, the outline, the start and end opacity) no longer in it. In glass the popup's fade and scale went missing (see material). Migration: write DIALOG_MOTION[material] where you wrote DIALOG_MOTION, and keep overflow-y-auto overscroll-contain outline-none on the popup yourself.
  • Fixed: the dropdown's opening motion stands still under reduced motion.
  • New: DropdownMenuLinkItem, a dropdown row that is a link (Base UI's link item): href, or render for a router's own (<Link href />), in the rows' face; it shuts the menu when followed.
  • New: a command palette, <Command open onOpenChange groups />: a dialog holding a field and the actions it finds, in groups under headings, each action a label, an optional glyph, the keys it stands for drawn as keycaps (hint, as menu draws them) and other words that find it (keywords). Typing filters with fuzzy, the matched letters lit and the best match first and lit, so Enter runs it; arrows walk the list while the field keeps focus (Base UI's autocomplete: a combobox, its listbox, aria-activedescendant); Esc empties what is typed, then closes; a shortcut drawn on a row runs it while open, unless the field edits with it (⌘X, ⌘⌫); empty is said when nothing matches. The action runs once the dialog has shut and given focus back. You open it: it registers no page-wide key. container for a player's layer; plain or glass (material).
  • New: shadcn's hover card on Base UI's preview card (HoverCard, HoverCardTrigger, HoverCardContent): what is behind a link, a person's card behind their name or a page's preview behind its address, shown while the hand rests on the link or the keyboard is on it. It opens after 600 ms and closes 300 ms after the hand leaves (delay, closeDelay on the trigger), staying open while the hand crosses to it; Esc and an outside press close it. The trigger stays a link (render for a router's own). Arrowless, container for a player's layer, plain or glass (material on HoverCardContent, or a provider round it), what is inside in the same material.
  • New: where a page sits, <Breadcrumb crumbs max />: the path of pages above it, each crumb a link (href, or render for a router's own <Link />), the last the current page (aria-current="page", never a link), chevrons between (separator), in a nav named for a screen reader. Given max, a long path keeps its first crumb and its last max - 1 and folds the middle into ⋯, a dropdown of the crumbs it hides. It draws in its surface's currentColor and takes no scheme; material and container reach the ⋯ button and its dropdown.
  • New: the bundle installs command, hover-card, breadcrumb, calendar, date-picker, resizable, group-box, readout, setting, rating and vote (Setting, Rating and Vote from the one import too).
  • Changed: the value controls read their field themselves. number, slider, select, checkbox, switch, radio-group and toggle-group, inside a field, take its wiring (described by its description and error, invalid, required, disabled), are named by its label, and hold still while the form sends (no change taken, the focus kept, never disabled), so binding one is only c.value and c.onValueChange. A slider's or a number's error reached nothing before, and only text fields held still. What you pass explicitly still wins; spreading c.wiring keeps working.
  • Fixed: a check that throws, or an async schema that rejects, no longer leaves the form submitting forever (the spinner turning and the fields read-only for good). It is a state, failed (its reason, by path; "" for the rules), said at the field as "could not be checked", listed in FormSummary with "ask again", and the status line's retry asks it again. submit answers { ok: false, reason: "unchecked", paths } and always ends with submitting false. form.recheck(path) asks one again ("": the rules).
  • Fixed: a submit sends exactly the values it checked. It decides on the values at the press, every check answered for those values (asked again where a change cut it short), so a value typed while it settles or sends, autosave included, is never sent unchecked; it is the next change.
  • New: checks by row, a path pattern ("tracks.*.title": each track's title its own check); a check's answer moves with its row, so a reordered row is not asked again.
  • New: FieldControl gives c.input only where the value is text: spreading it on a number's field no longer compiles (it wrote a string into the number).
  • New: FormLeaveGuard, a dialog (stay, leave) before a link in the page leaves changes not saved, and useLeaveGuard() for a dialog of your own. Migration: guard now covers only the browser's own prompt (a reload, a closed tab); a link pressed in the page asked through window.confirm, and now asks only where <FormLeaveGuard /> (or your hook) is inside the <Form>. The item now pulls in dialog.
  • Changed: one change notifies once (a keystroke used to wake every subscriber three times); FormSummary re-renders only when what it lists changed, orders its problems by where the fields stand without reading the page while it renders, and its live region is polite and always in the page (hidden while empty), so a failed submit's focus on the first field is heard first and the summary after. useFormState takes an equality (deepEqual for a slice that builds a list). A reset gives the loaded rows back their own keys, so nothing they hold remounts and the focus stays.
  • New: fieldRow, fields side by side on one grid line (a sign-up in a row): every field's name and control on one row and what each says on the next (subgrid), so one field's error opens under it without moving the controls beside it, and the words' row keeps one line from the start, so the first error fills room already there and the card round the row never grows. A Field is two rows now, its name and control, then its description and error.
  • Changed: what a control reads from its field (useField, FieldWiring, FieldContextValue) lives in field-context, its own part, re-exported here, so a value control reads its field without installing the field's layout. New there: fieldProps(field, { named }), the field's wiring as a control's own props (named by the label where a <label for> cannot reach it), and heldStill(field, change), a change dropped while the field is read-only.
  • New: useField() carries invalid and readOnly (a form sending) beside the wiring, the whole contract a value control reads to sit in a field or a form with only its value bound; Field takes readOnly (read by the control, never spread as an attribute) and marks itself data-readonly.
  • Fixed: the plain field editor's tools no longer show through a layer laid over the page (a full view, a dialog). Wordgard ranks its own panels at z-index 300, and nothing in the plain field kept that number inside the editor; the well is now the stacking context for everything Wordgard draws, in every face.
  • Changed (breaking): the editor looks like markdown. variant="page", the default, has no box: the document at the size of where it is placed (no pinned 15 px), set as markdown sets it, its first block flush with the top, its measure yours (className, as markdown's is); the tools are a quiet row in the registry's voice (the alpha ladder's quiet tier, a wash under the hand, a wash when on, never a solid fill, no grey bar, a hairline under it) that stays at the top while a long document scrolls (sticky in its scroll container, the caret scrolled clear of it); focus is the tokens' keyboard ring round the document. Migration: an editor in a form (a comment, a message, a proposal's body) takes variant="field" to keep a bounded box.
  • New: variant="field", a field in a form: the textarea's well (its surface and hairline, brightening under the hand and with the keyboard's focus) round the tools and the document, at the textarea's size (13 px, its padding), in both materials (material, scheme, from a MaterialProvider round it): in glass a well of the one glass material, its rim lit by the hand and the keyboard. invalid stains it red and sets aria-invalid on the document.
  • New: rows, the lines it is tall when empty (5 on a page, 3 in a field). Migration: --editor-min-height is gone; set rows.
  • Changed: Wordgard's menus (a block's kinds, a table's verbs) are menu's plain popup with its rows, its dialogs and the link's address on the registry's washes and hairlines; the / menu is menu's popup and rows, glass in a glass editor. A quote is markdown's (a wash, the quote voice, the mark in the gutter), where Wordgard's grey bar and indent showed through.
  • Changed: the uploads say what they are by name. The list is pictures going up (it was Image uploads), each bar <file> going up, its retry try <file> again and its cancel cancel <file> (they were retry and cancel, the same for every file). Migration: a test or a script that finds them by name uses the new names; a bar is a progressbar role, not a <progress> element.
  • Fixed: the first and last blocks sit flush with the body's edges, as MARKDOWN meant: each tag's own margin was the more specific rule, so a page opening on a heading started 1.5em down, and a body opening on a paragraph 1em down. The editor's document wears the same fix.
  • Fixed: a picture in the server's HTML stayed blank until the page hydrated, however early it arrived (a cached video poster, there in 0.4 s, waited seconds on a heavy page), because only React could mark it ready. Image and Img hold nothing in the first paint: the browser paints each picture as it lands, and once the page is live a picture still on its way fades and sharpens in as before.
  • New: one voice on a page at a time, for every item that sounds. claim(key, silence) takes the page's one floor (the voice that held it is silenced: each of its claimers' silence is called) and returns the release; claims under the same key are one voice and never silence each other; the floor frees with the voice's last claim. holding() and subscribe() say who holds it. useAudible(key, silence) is a component's claim (speak, hush, released on unmount); useHolding() re-renders with the holder.
  • New: a video asked to play (by the hand, by sight, by the page it follows) shows its poster under falling water (the registry's water) until its first frame is up, the picture then coming in over the ebbing water. A manual video's own controls say it is loading, so it has none. The item now pulls in water.
  • Changed: what sits over a VideoPicture (a player's chrome, a zoom finder, their menus and tooltips) wears the scheme the frame under each part asks for, read live (backdrop), where it was always dark: light glass over snow, dark over night. A video whose pixels cannot be read (another origin without crossOrigin and CORS) keeps it all dark. Migration: scheme="dark" on the chrome, or a provider round it, pins it dark.
  • Fixed: a VideoPicture with no aspect drew nothing: its view sizes itself to the stage, so the video could not give the stage its height. The stage takes the video's own shape (its width and height) when no aspect is given.
  • Changed: a video starting with sound claims the page's one floor (audible) instead of a list of its own videos, so it also silences a gallery cell or anything else that sounds, and is silenced by them.
  • New: voice on Video and VideoPlayback, the key its sound speaks under: share it with what the video plays for (a reading's), and the two are one voice.
  • Changed (breaking): one face's shots are shots, never a track (the registry's time vocabulary keeps "track" for none of its data): VideoCrop's track prop is shots, and shotAt, framed and useInShot take shots. Migration: <VideoCrop track={…}> becomes <VideoCrop shots={…}>; positional calls are unchanged.
  • Changed: VideoPicture stands what sits over it in the dark scheme (a MaterialProvider scheme="dark" round its children), so the player's chrome and any control over the picture read light in either theme without saying so. The item now pulls in material.
  • New: loading as water, for any component. What has not come yet lies under it, what has come surfaces. calm (waiting its turn): the content softened, a little of its colour drained, a faint dark grey over it. falling (on its way): drops fall in that water, broad swells travelling out from the middle and bending the content as they pass, then the next drop. The water rises and ebbs over half a second, never cutting, so a video surfacing over it crosses its ebb. It is an SVG filter on the content itself, nothing laid over it, the grey only where something is drawn. <Water state> wraps anything (a picture, a card, a panel); useWater(state) dresses an element you cannot wrap (a ref, a style, and the filter to render beside it). No drop falls under reduced motion. The gallery's cells wear it for motion's waiting and loading.
  • Fixed: what is under water keeps its rounded corners. A filter is drawn after the element's own clip, so the blur spread past rounded corners into soft square ones; the water now clips its result to the element's corners, read from the element.

scroll-fade

  • Changed (breaking): no blur strip lies over the ends any more; the content fades itself out at an end that has more to show. Inside glass (a backdrop root of its own) the strip could not blur what passed under it and drew a hard dark band across the top of a column once a row scrolled beneath. Migration: drop topFog, it is gone.
  • Changed: the scroller takes its surface's corners and no longer reaches 4 px past its own sides. Put it straight inside the surface with no padding between them (p-0 on a Glass card) and the rows run into the surface's rounded edge as they fade, instead of being cut by a flat line inside it; its side padding is yours (className).
  • New: the one drawing of how far a video or a clip has played: a track, what of it has loaded lighter on it, and the played part solid from the start to where it is now, its edge the position. It draws in the text's colour (white over a picture, a player's accent); moments or chapters go in as children, placed with lineShare; style places a line (one chapter of a longer bar). The gallery's cells draw their strip with it, and player's bar each of its chapters, so the two say progress the same way. player now pulls in media-line.
  • Fixed: toasts show again outside fullscreen. With no element fullscreen and no container, the stack was handed to Base UI's portal as null, which it reads as "wait for a container", so nothing was ever drawn; it now goes to the document's body.
  • Changed: one Toaster per page. Every toast() goes through one manager, so a second Toaster drew and announced each toast twice; mounting a second one now throws. Migration: keep the one in your layout, remove any other.
  • New: container on Toaster. While an element is fullscreen the stack is drawn inside it, so a toast raised during a fullscreen video shows over it; a layer that covers the page without the browser's fullscreen (a player filling the window) is passed as container.
  • Fixed: toast.promise's outcome stays as long as the Toaster's timeout says, not a fixed 5 s.
  • Changed: a loading toast's spinner turns for 30 s at most, then holds still while the toast stays.
  • Changed: the action and the dismiss are Buttons (secondary, and a ghost icon) in the toast's own material and scheme, so a glass toast's verb is glass too. toast now installs @ag/button.
  • New: who a key belongs to, page-wide. An item that answers keys on the page registers with useKeys(onKey, { root, own, modal }) (or registerKeys from an effect) instead of listening on the document, and one listener hands each press to one of them: never a press a focused control owns (ownsKeys, below), a modal (an open lightbox) before anything behind it, else the instance holding focus, then the one under the pointer, then the one last pressed in, then the newest; the first whose onKey returns true takes the press (and its default). A player and a gallery on one page no longer both answer Space; two players no longer throw. useEscape(onClose, { active, root }) (or openLayer) is the Escape ladder: Esc closes the innermost open layer only, and only when nothing inside it took that Esc (a menu open over it closes first). ownedHere(event, root, own) and keyTargetOf read the same rule for a widget's own onKeyDown, typingIn(target) for a clipboard handler.
  • New: an instance or a layer whose root is inside an inert subtree hears no key: inside an inert picture of a demo (a catalogue card's preview), a player, a search or a layer no longer takes the page's Space, / or Esc.
  • New: ownsKeys(press, target), the one rule for whether a press belongs to what it landed in, so a page-wide shortcut leaves it alone: already taken (defaultPrevented); typed into a field (every key a field edits with, ⌘ and ⌃ editing combinations included, but not Escape, Tab or a page's ⌘K); a key a widget answers itself by its role (Space on a button, switch or checkbox; arrows in tabs, radios, a slider; navigation, activation, Escape and typeahead in a list, menu, tree or grid). KeyPress and KeyTarget are its facts; key-router reads them off the page.
  • New: capOf(press): a press's cap, where ⌥ made another character of a letter (⌥F is "ƒ" on a Mac) the physical key (code) standing in. Press takes code, and pressed matches ⌥ combinations on a Mac.
  • Changed: pressed lets a symbol be typed as the layout types it: + with ⇧ held matches "+", ? matches "?". A named key keeps ⇧ as a modifier (⇧← is not ←).
  • New: ariaKeyshortcuts(shortcuts): shortcuts in this notation as aria-keyshortcuts names them (Shift+ArrowLeft, Space, Plus), each once; anything that is not keys throws.
  • Changed: a focused separator (a resizable's handle, a window splitter) owns its arrows, Home, End and Enter, so a page's shortcut leaves them to it.
  • Fixed: until the medium's length is known (a video before its metadata, a page rendered on the server), the bar is one empty track. It drew its chapters cut against a length of 1 ms, each ending before it started; the chapter skip buttons stay off until then too.
  • Fixed: in glass, the bar's time under the hand is a small chip over the hovered instant again; it was a strip as wide as the bar, laid over the controls above it.
  • Changed: PlayerKeys answers through key-router: a press a focused control owns never reaches it (Space on a switch toggles the switch alone, an arrow in tabs moves the tab alone, Space on a focused button presses the button instead of being swallowed), and with several players the one holding focus, under the pointer or last pressed answers alone. A second PlayerKeys on a page no longer throws.
  • Changed (breaking): the parts take material (plain or glass) and scheme, as every base control does, each their own prop or the nearest MaterialProvider's: PlayerTransport, PlayerSpeed, PlayerCaptions, PlayerSound, PlayerFullscreen, PlayerFoot, PlayerBar, PlayerPlaylistButton and PlayerZoomFinder. Their glass faces are the base Button, DropdownMenu, Popover and Tip in glass, so the item now pulls in button and popover. PlayerChrome takes material and gives it to every part inside (<PlayerChrome material="glass"> is a whole player in glass, its tooltips and menus too), and PlayerFoot and PlayerBar do the same for what they hold. The playlist card is the popover item, no longer a raw Base UI popover. PlayerTransport's fill works in both faces. The glass twins in liquid-glass are gone. Migration: - GlassPlayerTransport, GlassPlayerSpeed, GlassPlayerCaptions, GlassPlayerSound, GlassPlayerFullscreen and GlassPlayerFoot are PlayerTransport, PlayerSpeed, PlayerCaptions, PlayerSound, PlayerFullscreen and PlayerFoot inside <PlayerChrome material="glass"> (or each with material="glass"). - GlassPlayerPlaylist is PlayerPlaylistButton in glass; GlassPlayerZoomFinder is <PlayerZoomFinder material="glass" />, and PlayerZoomFinder's frame and edge props are gone (the material draws both faces). - Delete glass-player.tsx from your copy.
  • Changed (breaking): the chrome's dark comes from the picture, not from PlayerChrome: VideoPicture (video) stands what sits over it in the dark scheme, so the chrome, its parts and their popups are light ink in either theme with no part saying so. Migration: over a picture of your own (no VideoPicture), give PlayerChrome scheme="dark" or put a <MaterialProvider scheme="dark"> round it; a dark class you set on the chrome is no longer needed.
  • Changed: GlassPane draws its front surface with the material's GlassFront instead of its own copy; its props, its --glass-pane-front-* knobs and how it looks are unchanged (every computed style of the pane samples is identical). It needs the material that ships GlassFront, which an update of liquid-glass installs with it. GlassFront, GlassFrontAt and glassFront are re-exported here.
  • Changed: GlassPane's edge is GlassEdge, the glass's own, and its front surface's hairline is lit by the same catch; the back surface is the glass body alone (glassVariants({ edge: false })), its shadow the face's own.
  • New: GlassPaneStyle, a pane's style with its own knobs typed (--glass-pane-front-height, --glass-pane-front-blur).
  • Changed: a GlassPane whose light changes (a card lifting under the pointer) eases it on its edge and its front surface's hairline, the two elements that draw it, instead of on the pane, where the inherited rim restyled everything the pane holds on every frame (about half the style work over the ease, measured on a pane holding 60 elements).
  • Changed (breaking): the player's glass parts are the player's own (player), so the collection no longer installs a player, a button, a menu or a tooltip, and GlassEdge is the material's, re-exported here. Migration: GlassPlayerTransport, GlassPlayerSpeed, GlassPlayerCaptions, GlassPlayerSound, GlassPlayerFullscreen, GlassPlayerFoot, GlassPlayerPlaylist and GlassPlayerZoomFinder are gone (see player); delete glass-player.tsx from your copy.
  • Changed (breaking): the collection is what glass makes that no base control is and that needs no media: GlassPane and GlassGlide, beside the material's re-exports. It installs material and tokens and nothing else (it pulled 21 items, a player and a video engine among them, for a pane). The display round media is its own item, glass-display. Migration: GlassDisplay, GlassFrame, DisplayPlayback, DisplayVideo and GlassDisplayProps are installed by glass-display (npx shadcn add @ag/glass-display); the file and its imports are unchanged.
  • New: the floating glass display round any media as its own item, out of liquid-glass: GlassFrame (looked at in place), GlassDisplay (a door that lifts and plays what is inside while looked at), DisplayPlayback and DisplayVideo. Its edge is the material's GlassEdge, and it uses video for DisplayVideo.
  • Changed: its edge is the glass's own edge, the one every control wears, where it drew a fainter rim of its own; the shared edge was set by eye on these frames. rim is a multiple of the glass's own, 1 unless set. Being looked at raises it a quarter, the line, its soft body and the bend together, one light.
  • Changed (breaking): radius is px, a number (default 20), where it was any CSS length read off the laid-out box once: a percentage parsed as pixels, and the bend's outline never followed a radius that changed. Migration: radius="1.25rem" is radius={20}.
  • Fixed: the bend and the line are one light on both faces. The bend lifted the picture by the rim and the look alone while the line also carried the ground's sheen and glass on glass, so on the dark face (sheen 0.8) the bend lifted 1.25× its line; both read the line's strength (--glass-strength) now. On the light face the frame is unchanged. Only the softening and the crest, which lift by the light, wear it and ease it; the two layers that only blur no longer run a transition.
  • Changed: its keys go through key-router: a press a field or a control owns is never an answer, and a second instance on a page no longer throws (the one under the hand or holding focus answers). Migration: keyboard={false} on extra instances is no longer needed.
  • Changed (breaking): ThermalMoment, what a frame draws at an instant, is ThermalState. Migration: rename the type.
  • Fixed: the grid's keys and its copy and paste leave a textarea or a select inside a cell alone (only inputs and editable elements were), and a control inside a cell (a row's verb) keeps its own keys; a press from a portal is never the grid's.
  • Changed: a row's own control (an action's button), a field and a press from a portal (the row menu) keep their keys; the tree's keys no longer stop only at an input.
  • New: useTree (ui/tree-state, shipped with tree), the tree without its drawing: expansion (controlled, uncontrolled or remembered), lazy children, the filter, selection, the roving focus, the window of rows mounted, rename and the WAI-ARIA keys, over lib/tree-core. Tree draws its row grid on it; a list of another shape can draw on it too.
  • New: layer on Tree, a column at the row's end and what pressing it means, made of the rows each render: its column, whether a press on the row is its own, its keys, its menu verbs first, the word a row reads as (TreeRowState.mark) and its data attributes on the row. TreeApi and TreeLayer are its contract.
  • Changed (breaking): the include and exclude marks and the lens leave the tree; they are the gallery's semantics and a plain tree no longer carries them. Gone from Tree: marks, onMarksChange, onMark, orWith, pressMarks; from ui/tree: MarkSign, pressMark, markEntries, JoinBadge, OrBadge; from lib/tree-core: Mark, MarkState, MarkChange, MarkEntry, markStates, setMark, toggleMark and the lens (Lens, NO_LENS, markLens, flipJoin, lensText, lensTerms, lensMarks, lensOf, lensEmpty). The install is lighter by those files (the tree's own source 2,033 lines before, 1,665 after); the items it pulls are the same 17. Migration: a tree without marks changes nothing. A tree with them installs tree-marks and passes its layer: marks={m} onMarksChange={set} pressMarks becomes layer={useTreeMarks({ marks: m, onMarksChange: set, pressRows: true })} (onMark and orWith move into the same call), and the names above are imported from ui/tree-marks and lib/tree-marks-core.
  • New: the include, exclude and neither marks on a tree as a layer of tree (layer={useTreeMarks({ marks, onMarksChange })}), its own item, installed with npx shadcn add @ag/tree-marks: what Tree's marks did, pressed the same way (a click only this row, ⌘ adds, ⇧ a range, ⌥ excludes, ⌘⌥ adds as or; Space, + and - from the keys), pressRows where the rows are the marks, onMark and orWith for a lens. lib/tree-marks-core holds markStates, setMark, toggleMark and the lens; pressMark, markEntries, MarkSign and JoinBadge give a list that is not a tree the same gestures.
  • Fixed: the answers the number keys give stand in the readout under the timeline, each with its key, where there is room for all of them; under the row's name they ran over the rows below.
  • Changed: EDIT_KEYS is the action table the timeline resolves its keys through (resolveEdit), not only a legend: each row { keys, actions, does, held?, repeats?, shown? }, its keys in lib/keys notation ("j", "shift+left", "1"…"9"). EDIT_SHORTCUTS is the valid aria-keyshortcuts derived from it (the ruler's was glyphs). Migration: a legend reading row.keys as display text reads row.shown ?? row.keys and draws them with Kbd.
  • Fixed: K held stops making J and L frame steps when the window loses the keyboard (⌘Tab away with K down), not only on K's keyup; a control inside the timeline (a zoom button's Space) keeps its keys.
  • Fixed: a claim on a painted row (past a few hundred in view) can be pressed. A press finds its claim from the row's index (the instant under the hand, the row and line under it, a binary search), never from the element under it, and the selection names the claim by its id. 10,000 presses over 10,001 claims: the same answers as a scan of every claim, 133 claims read a press where a scan reads all of them.
  • Changed: a painted row repaints only when what it shows changed (its claims, the view, its size, its lines, the ink of its scheme), reallocates its canvas only when its size changed, and reads its ink once per scheme, not per paint. A drag selecting a span, a hand skimming the rows and the playhead dragged along the ruler repaint nothing (three painted rows of 10,000 claims, a replayed session: 459 paints before, 99 now).
  • Changed (breaking): the timeline is one focusable widget, role="application" named by label, with aria-keyshortcuts from EDIT_KEYS; the ruler is a drawing, no longer a role="slider" whose arrows changed rows and whose digits edited claims. The row in hand is its active descendant (named with its claims in view and its answers), or the claim chosen in it by a press or X; every claim is named by its row's question, its answer, when (timecode) and whose. A number key's answer, an in, an out, a repeat and the way back are said in a polite live region. Migration: focus the timeline itself ([data-slot=timeline], or by role application and its name) where you focused the ruler; a test that found the slider finds the application.
  • New: scheme on Timeline, the colour scheme it stands in (material).
  • Changed (breaking): lib/timeline-rows' ClaimIndex keeps one list per length class (classes) and byId instead of starts and reach, so a claim as long as the medium no longer makes every view and press walk from the first claim. New: claimAt(index, ms, slopMs, line?), countIn(index, view), and repaintOf(was, next) with Painting. Migration: code reading starts or reach reads visibleIn or claimAt instead.
  • Fixed: an open lightbox answers its keys in the bubble phase on its own element, after the controls inside it, and is a modal on key-router so nothing behind it hears a key: a consumer's popup or field opened over a slide gets its arrows, Enter and Escape (the document capture-phase listener stopped every key before it could reach them), and a field or a button's Space inside it is theirs (ownedHere). Its keyup listener moves out of the capture phase.
  • Fixed: a player filling the window (where the browser refused fullscreen) leaves on Esc as a rung of key-router's Escape ladder: an Esc that closes a menu open inside it (the speed, the captions) no longer also leaves the fill.
  • New: CaptionLines and CAPTION_SIZE in media-captions, where caption lines stand and how big they are: a full 42-character line spans 60% of the box's width, the box being the picture. The player's captions and transcript's TranscriptCaptions both stand in it, so the one caption look is one size too. Migration: TranscriptCaptions sized itself text-sm; it now sizes against the box it spans, so place it across the picture (a className like absolute inset-x-4 bottom-16), not in a narrow box.
  • Removed: GlassButton, GlassToggle, GlassBadge, GlassCard, glassButtonVariants and their types (GlassButtonProps, GlassButtonSize, GlassButtonCorners, GlassToggleProps), and GlassTip and GlassTooltipContent (glass-tooltip). The collection keeps what no base control is: GlassPane, GlassGlide, the display and the player's glass parts, and the material's re-exports. Migration: - GlassButton is Button material="glass", its heights one step down the button's ladder: sm (32 px) is md, md (40) is lg, icon-sm is icon, icon is icon-lg; its 48 px lg and icon-lg are lg/icon-lg with h-12/size-12. as="a" is render={<a />}; a tint is style={glassStyle({ tint })}. - GlassToggle is Button material="glass" pressed, with its on and off. - GlassBadge is Badge material="glass" (a tint through glassStyle on style); GlassCard is Panel material="glass". - GlassTip is Tip material="glass", GlassTooltipContent is TooltipContent material="glass". - A MaterialProvider material="glass" round a section does it for every control inside. Delete glass-tooltip from your copy.
  • Changed: the player's glass parts are the base Button, DropdownMenu and Tip in glass, so liquid-glass now pulls in button and menu, and no longer class-variance-authority.
  • Removed: GlassInput and GlassSubmit (glass-input), GlassMenu and its parts (glass-menu), GlassSegmented (glass-segmented). Migration: GlassInput is Input material="glass" (its validation is the form's: form with any Standard Schema, FormSubmit for the button); GlassMenu, GlassMenuTrigger, GlassMenuContent, GlassMenuItem, GlassMenuLabel, GlassMenuSeparator are menu's DropdownMenu parts with material="glass" on the content; GlassSegmented is ToggleGroup material="glass" with a ToggleGroupItem per option. Delete the three files from your copy.
  • Removed: GlassDialogContent and GlassNumber, now the dialog's and the number field's own glass faces. Migration: <GlassDialogContent> is <DialogContent material="glass"> (dark by default, as before), <GlassNumber size="md"> is <NumberField material="glass" size="lg"> (the field now takes the button's heights, 28/32/40 px).
  • Changed: the glass material (glassVariants, Glass, GlassProps, GlassStyle, rimInk, deepInk) lives in material's material-glass.tsx and is re-exported from liquid-glass, so imports from liquid-glass keep working. liquid-glass.css is gone: its one rule (--glass-rim registered, so the rim's light eases) is material-glass.css. Migration: reinstall liquid-glass (it pulls material) and delete your old liquid-glass.css. New: glassStyle(light), a glass's rim and tint as the custom properties glassVariants reads, for an element that wears the glass itself.
  • New: GlassPlayerCaptions, the captions control in glass; GlassPlayerFoot measures its room as PlayerFoot does.
  • Changed: the bar's verbs (BarVerb) and its clear are the base Button, ghost, at the bar's height: one button everywhere, and selection pulls in button.
  • New: one selection for every set that is shown. The kernel that was gallery's lib/gallery-selection (only, toggle, range, all, area, target, inOrder, shownOf, inRange) plus actsOn, what a verb asked of an item acts on: the shown selection when the item is in it, else that item alone. SelectionBar is a selection's verbs as a bar: the count, what the filter hides, the same MenuEntry list a right click opens with each verb's keys drawn beside it, clear and Esc. The gallery and the table pin it to the foot of their view, the same place however short or long the set.
  • Changed: its docked ways out are Button material="glass"; it pulls in button, no longer liquid-glass.
  • Changed: its glass is the material's (material-glass); it pulls in material, no longer the whole liquid-glass collection.
  • New: the menu's second shape, the dropdown a control owns, composed as parts the way shadcn's are: DropdownMenu, DropdownMenuTrigger (render makes any button the trigger), DropdownMenuContent (material, tone, side, align, container), DropdownMenuItem (destructive), DropdownMenuRadioGroup and DropdownMenuRadioItem (a choice among the rows, checked), DropdownMenuLabel, DropdownMenuSeparator. Never modal, at least as wide as its trigger, its rows reading the content's material: the list of verbs' two faces.
  • New: material (and tone for glass): plain, the page's popover; glass, the material's glass, its rows lit in the text's own colour, a destructive row lit in tone-bad, its keys glass too. A text hint takes the text's colour, so it reads on either.
  • New: the menu of verbs that was the tree's row menu, now its own item for anything that asks: a right click, a ⋯, ⇧F10, a selection's actions. MenuEntry is the one shape of a verb; its hint may list several inputs, alternatives drawn side by side (["double-click", "enter"]). A key a menu draws is a key it answers to: pressed while the menu is open, the verb runs (a highlighted entry keeps Enter and Space). verbFor(entries, press, platform) finds the verb a press names, so the set behind a menu answers the same keys while it is shut. components installs and exports it too.
  • Changed: PlayerSpeed and PlayerCaptions open the menu's dropdown (player now pulls in menu), and SpeedItems and CaptionsItems are its radio rows, in whatever material the menu holding them is. Migration: their item class prop is gone; put them inside a DropdownMenuContent and style the rows there.
  • New: PlayerCaptions, beside the speed: with one language a button that turns captions on and off, with several a menu of off and each language. CaptionsItems are its rows for any menu face. C turns captions on and off under PlayerKeys where the medium has some (PlayerKeysCard's captions lists it). PlayerIcon.captions.
  • New: PlayerFoot measures the room it takes at the picture's foot (useFootRoom, --ag-player-foot on the picture), which the captions rise by.
  • Changed: useFullscreen is never null, and PlayerFullscreen and GlassPlayerFullscreen always show: where the browser has no fullscreen (an iPhone's Safari), the player fills the window.
  • Changed: PlayerGestures' pinch takes the same ladder, at the lightbox's pinch-to-close distance (the fingers closed to 0.75 of their spread, or spread to its inverse) and once a pinch, instead of after 60 px either way at any moment.
  • New: sections that open in place, shadcn's parts on Base UI's accordion (Accordion, AccordionItem, AccordionTrigger, AccordionContent): one open at a time or several (multiple), each heading a real heading and a real button, its chevron turning, the panel growing to its own height and back, still under reduced motion. Plain rows parted by hairlines, or one pane of glass parted by lit lines (material).
  • New: a panel sliding in from an edge (shadcn's sheet), the dialog's logic: Sheet is dialog's root, modal; SheetContent takes side (right, left, top, bottom) and material; SheetTrigger, SheetClose, SheetHeader, SheetTitle, SheetDescription, SheetFooter are the dialog's parts. Plain, the popover surface with a hairline on its inner edge; glass, a pane rounded only where it meets the page.
  • New: DIALOG_BACKDROP, the dimmed page behind a modal one, exported for the sheet; it now stacks at the popup's layer (z-50).
  • New: material on DialogContent (or a MaterialProvider round it): plain, the page's popover surface; glass, the material's glass with a heavier blur, dark by default (tone). Everything inside the dialog wears its material.
  • New: material on ScrollThumbs: plain, the text's colour at a low alpha; glass, a sliver of the material's glass whose rim lights under the hand. With scroll-fade it is this registry's scroll area: shadcn's ScrollArea is a scroller wearing these, so there is no second scrollbar item.
  • New: the scrollbar of a long scroller, overlay thumbs instead of the browser's bar: NO_SCROLLBAR on the scroller, ScrollThumbs as its first child. They take no room, show while it scrolls or with the pointer near their edge, drag, and come in both axes. The gallery's own thumb, made an item; the table wears it too.
  • New: the bundle installs the whole base set, each control live on its page and re-exported from the one import: material, button, badge, label, textarea, separator, spinner, skeleton, panel, checkbox, radio-group, switch, select, toggle-group, tabs, field, form, popover, toast, alert, accordion, sheet and scroll-thumb, beside what it held.
  • New: aria-describedby on the field, so a form's description and error are read with it.
  • New: one of a list, on Base UI's select: the list opens over the field with the chosen row under the hand, typing jumps to a row, a hidden input submits it. Given as data, the way menu takes its verbs: options flat or in named groups (SelectGroup), each option with an optional icon, description, text (what typing finds it by when its label is not plain text) and disabled; the chosen row is ticked. The field is the input's well at the input's heights (sm, md, lg), red when aria-invalid; plain lists on the page's popover surface, glass on a glass pane. Where we part from shadcn: data rather than seven parts to compose.
  • New: aria-labelledby and aria-describedby, so a form's label names it (aria-labelledby={c.labelId}) and its description and error are read with it.
  • New: a segmented control on Base UI's toggle group (one Tab stop, the arrow keys between options). One of them by default, always one once chosen, a highlight gliding to it; multiple for a set, each pressed option lit on its own. ToggleGroup + ToggleGroupItem, at the input's heights. Plain is a sunken well with the chosen lifted out of it; glass a bar of glass with a lit pane (what liquid-glass's GlassSegmented was).
  • New: who someone is, one person, chip-input's single-choice partner. Named, their face and short name as one chip (pressed it changes, onClear behind its ×); unnamed, a field that finds anyone by any of their names and offers someone new of what was typed when onCreate is given. Choosing is saving. A guess rides under it to confirm (onPick) or refuse (onRefuse). People are lib/person records with an id (Pickable); a promise from any handler dims the picker until it settles. PersonFace draws a face, its picture or its initial, at the avatar's sizes.
  • New: material (and tone): plain, the step surface, the input's well and the page's popover; glass, a pill of the material's glass for the chip and the field, its list a pane of it. With tone="dark" it takes the dark theme's ink, its list too. Its field is the registry's Input in the picker's material.
  • New: a medium's time as rows under one ruler, following any timebase (a video's, a sound's, a drawn clip's). Timeline takes rows (claims, { kind: "claims", claims, answers? }, drawn as bands and pins, a person's solid and a machine's outlined, overlapping ones stacked; curves, { kind: "curve", points }, drawn as ridges that keep every peak), the timebase's outline as rows of its divisions (finer ones by levels), a filmstrip of stills from a source callback, and onAnswer, where a person's answer to a row's question (1-9) over the selection or at the playhead is handed out for the consumer to keep as a claim. The view zooms at the hand from the whole medium to a dozen frames and pans, stopping at the ends; the ruler's labels never touch, reading timecode on frames close in; an overview shows the view in the whole. A press seeks, a drag selects (snapping to every edge, the outline and the playhead; ⌥ held, free), a press on a claim selects it. An editor's keys while it has focus, never the watching player's: J K L shuttle (again, faster; K held steps a frame), ← → a frame, ⌥ the next edge, ⇧ the selection's end, ↑ ↓ the row, I O in and out, X the claim under the playhead, R repeats the selection through the timebase's loop, B back to where you were (any big move leaves the way back on the ruler), + - 0 zoom (EDIT_KEYS lists them). Only the claims in view are drawn, and past a few hundred a row is painted on a canvas. Plain or glass (material). The arithmetic ships with it and runs anywhere: lib/timeline-view (zoom, pan, ticks, snap), lib/timeline-rows (claims in view by binary search, stacking, curve decimation, spans over a threshold), lib/timeline-shuttle (the shuttle, the way back, the key table).
  • New: what a control is made of, plain or glass. Every base control takes material and draws that face over one behaviour; MaterialProvider material="glass" sets it for everything inside it (a page, a panel, a player), and the prop on one control overrides it there. useMaterial(prop) resolves it for a control of your own. The item also holds the glass material itself (material-glass: glassVariants, Glass, glassStyle, rimInk, deepInk, server-safe), so a control's glass face pulls in the material, not the whole glass collection.
  • New: material on TooltipContent, Tip and TooltipHint: plain, the page's popover colours; glass, the material's glass (tone, dark by default), a pill for a line. The keys inside wear the same material. Under a MaterialProvider material="glass" every tooltip is glass.
  • Removed: TooltipPopup and TOOLTIP_FACE (a face is now chosen by material), and TooltipHint's tip prop. Migration: <TooltipHint tip={GlassTip}> is <TooltipHint material="glass">.
  • New: Tip takes className for its surface, so a tip's content can shape what it sits on (gallery-tag's PERSON_CARD_SURFACE).
  • New: material (plain, a well of the text's colour; glass, a pill of the material's glass, soft corners over a strip, its rim brighter on focus) and size (sm, md, lg: the button's 28, 32, 40 px with one row).
  • Removed: bare. Migration: a field that brought its own surface takes material instead.
  • New: material on Kbd, KbdCap and KeysText: plain, a key with a deeper foot; glass, a small key of the material's glass. Inside a glass tooltip or menu they are glass on their own. The file is now a client module (it reads the material from context).
  • New: material, its keys in either material; on glass the joiner and the pointer glyph take the text's own colour.
  • Fixed: the double-click glyph drew its ripples beside the right button, reading as a right double-click. They now come off the left button, on the left.
  • New: material: plain, a hairline and a solid knob; glass, a channel of the material's glass and a knob of glass with the tone's own dot at its heart, its rim catching more light under the hand and while it is held.
  • Changed: the marks are set into the track, 2 px stops inside its rounded ends, instead of dots hanging under it; the ones the fill has reached go under it, so what is left ahead is what the dots say.
  • New: material: plain, a groove of the page's foreground; glass, a channel of the material's glass, the fill and the words in the text's own colour.
  • New: material (and tone, dark by default) on EdgeArrows and EdgeArrow: plain, a bare glyph; glass, the glyph on a disc of the material's glass. The file is now a client module.
  • New: material (and tone, dark by default): plain, on whatever holds it; glass, a bar of the material's glass with the counter inside, the active thumb ringed in the rim's light.
  • New: shadcn's popover on Base UI (Popover, PopoverTrigger, PopoverContent, PopoverTitle, PopoverDescription, PopoverClose): anchored to its trigger, arrowless, focus moved in and given back, Esc and an outside press close it. material on PopoverContent: plain, the page's popover surface; glass, the material's glass (tone). What is inside wears the same material.
  • New: toasts with sonner's API over Base UI's toast. <Toaster /> once, then toast(), toast.success, toast.warning, toast.error, toast.loading, toast.promise(p, { loading, success, error }) and toast.dismiss(id?), from anywhere, inside React or out. A description, an action ({ label, onClick }), a timeout (0 keeps it), an id that replaces a toast in place. The stack sits at the window's foot (position: bottom-end, bottom-center), the newest in front, the rest peeking behind and opening into a list under the hand while their timers wait; swipe down or sideways to dismiss; limit (3). Announced politely, an error at once; still under reduced motion. Each kind one colour (tone-ok, tone-warn, tone-bad). material on the Toaster: the page's popover surface, or the material's glass stained with the kind's colour.
  • New: a callout in the page's flow (shadcn's alert): kind default, success, warning, destructive, each one colour everywhere (its icon and title in it, a faint wash, a fainter edge), the description in the text's colour; an icon per kind or your own (icon, null for none), an action at the end. role="alert" only when destructive. material: a wash on the page, or a pane of the material's glass stained with the kind's colour.
  • New: material (and tone): plain, rows lit in the foreground's tiers; glass, the chosen row a pane of the material's glass, the others lit under the hand. With tone="dark" (over a picture) the tree takes the dark theme's ink. Its row menu wears the same material.
  • New: branchIds(roots, children, depth) stops depth levels down, for a tree that opens part of the way on its first look.
  • Changed: the icon column sizes and dims only a glyph placed straight in it; a mark in a wrapper of its own (a tag's coloured icon, a face) keeps its own size and colour, and fades only on a dimmed row.
  • Changed: a multi-selection tree selects through the selection kernel (lib/selection, now a dependency), as the gallery and the table do: ⌘ toggles, ⇧ is a range in row order from the last press, ⌘⇧ adds a range to what is selected, ⌘A takes every row that can be selected, and Esc (new) lets go of them all. selected and onSelectedChange are unchanged.
  • New: rowMenu receives a third argument, the ids its verbs act on: the selection in row order when the row is in it, else the row alone.
  • Changed: RowMenu and TreeMenuEntry moved to menu as Menu and MenuEntry, same props and shape. Migration: import { Menu, type MenuEntry } from "@/components/ui/menu", rename RowMenu to Menu and TreeMenuEntry to MenuEntry. The popup's data-slot is menu (it was tree-menu).
  • Changed: marked rows are pressed like files are selected. A click marks only this row (its includes replaced; pressed again while it is the only one, none; excludes stay), ⌘-click adds it or takes it back, ⇧-click marks every row from the last one pressed, ⌥-click excludes (it was ⌘-click), ⌘⌥-click adds as "or" (it was ⌥-click). pressMark excludes on ⌥.
  • Changed: onMark hears a whole gesture: (changes: MarkChange[], only: boolean), only when the includes start from none. Migration: onMark={(id, mark, how, clear) => …markLens(lens, id, mark, how.or, clear)} becomes onMark={(changes, only) => changes.reduce((l, c) => markLens(l, c.id, c.mark, c.or, c.clear), only ? { ...NO_LENS, exclude: lens.exclude } : lens)}.
  • Changed: storageKey is now remembered, a Setting<readonly string[]> from remembered: the consumer says how long the expansion lives. Migration: storageKey="x" becomes remembered={setting("x", { initial: [], keep: "always", parse: isStrings })}; what was kept under the old key is not read.
  • New: a button in either material. shadcn's variants (default, secondary, outline, ghost, destructive, link) at the input's heights (sm 28, md 32, lg 40 px, and icon-sm, icon, icon-lg), so a field and its button sit flush. Plain: the page's primary for default, the foreground's alpha tiers for the quiet ones, tone-bad for destructive. Glass: the collection's glass, its rim lighting up under the hand and a press swelling it on a spring; outline is the rim alone, ghost nothing until the hand comes, destructive a glass stained red. pressed makes it a toggle (aria-pressed, worn as light, never a change of size); loading keeps its width and focus and turns a spinner in its middle; render makes it a link (Base UI's button underneath).
  • New: on and off, a toggle's two faces held in one cell at the wider's width, so pressing it never moves what sits beside it (what GlassToggle did, in either material).
  • New: a small label in metadata's voice (mono, lowercase): default, secondary, outline, destructive, plain or a sliver of glass; render makes it a link.
  • New: what a field is called, a real <label>, dimmed beside or around a disabled field; on glass its lettering catches the rim's light.
  • New: a hairline across or down (Base UI's separator): the page's hairline plain, the glass's lit line in glass.
  • New: text of more than a line in the input's well (red when aria-invalid, the input's three type sizes), growing with what is written from rows to maxRows, then scrolling; the browser sizes it where it can (field-sizing: content) and it is measured where it cannot. autosize={false} keeps it rows tall. Plain, or a well of glass.
  • New: work under way, a ring with a turning arc in the text's colour (sm, md, lg), still with reduced motion, a status a screen reader hears once by its label; on glass the arc catches the rim's light.
  • New: the shape of something still coming, breathing slowly while it loads, still with reduced motion, hidden from a screen reader; plain or a pane of glass.
  • New: a card (shadcn's Card; card is a family name here, card-gallery and card-link are their own items): a surface holding one thing, with title, description, an action at the head's end and a footer, all props. Plain the page's card surface, glass the collection's glass card (what liquid-glass's GlassCard was).
  • New: one control and what is said about it, plain or glass: its label (htmlFor), a description and an error under it (aria-describedby, aria-invalid), required (a mark for the eye, aria-required for a screen reader), an aside at the label's end, orientation above or beside. The contract is plain props: children as a function is handed the wiring to spread on the control's focusable element, and useField() gives the same to a control that draws none of its own. The error's row opens and closes in place. FieldSet groups fields under a legend: a panel of the page, or a pane of glass.
  • New: a form on its own engine (lib/form-core, framework-free and runnable in bun) over any Standard Schema (zod, valibot, arktype). useForm({ initial, schema, validate, checks, onSubmit, optimistic, autosave }) makes it; form.Field binds a field by name, typed (the name autocompletes, the value is the type at that path), handing its control c.input for a text field or c.value, c.onValueChange and c.wiring for any other. Errors are said once a field is left or a submit was tried, then on every change, so fixing one clears it at once. checks are async per field: debounced, the stale one aborted, settled before a submit. A server's answer ({ errors: { path: message } } or Standard Schema issues, and { error } for the whole form) lands on its fields and clears with the next change. What changed is deep against the loaded values; a save makes the sent values the loaded ones (setInitial for a save elsewhere); optimistic takes the send as done and rolls back with the reason. form.FieldArray keys each row and moves its marks and messages with it (append, insert, remove, move). Form focuses the first field to fix on a failed submit, submits on ⌘↵ or Ctrl↵ from anywhere in it, holds the fields read-only while sending (never disabled, so the focus stays), and asks before leaving with changes not saved (guard). FormSummary lists every problem, each a way to its field, in a region a screen reader hears; FormStatus says unsaved, saving, saved or why it failed, with a retry; FormSubmit turns while sending; FormReset discards. Plain or glass through material. The settings demo binds every base control through the one field contract: a select, a switch, a checkbox, radios and a segmented choice beside the text, number and slider.
  • New: material and tone on Input and InputGroup. Glass is a pill of glass at the same heights, its rim lit while you type and red when the value is wrong (aria-invalid), the start and end slots quieter in the glass's own ink. The input now depends on material. GLASS_WELL is the glass well, beside WELL.
  • New: a yes or no ticked in a box, on Base UI's checkbox (a hidden input a form submits, Space ticks it). indeterminate draws a dash, for a box that stands for a group only partly ticked. With children the box and its words are one label, the words pressing it too. size (sm, md, lg), red when aria-invalid or judged by a Base UI Field. Plain fills with the page's primary when ticked; material="glass" is a small pane of glass whose rim lights and takes the text's colour when ticked.
  • New: one of a few, each shown, on Base UI's radio group (the arrow keys move the choice, one Tab stop, a hidden input a form submits). RadioGroup sets size, material and orientation (vertical or a row) for every Radio in it; a radio with children is one label with its words. Red when the group is aria-invalid. Plain a sunken circle filled with the primary, glass a bead lit by the text.
  • New: on or off at once, on Base UI's switch: a track and a thumb that slides across on a slight spring, sm, md, lg, with children one label with its words, red when aria-invalid. Plain is the field's well tone, the primary when on; glass a track of glass lit by the text, the thumb a bright bead.
  • New: views of one thing, one shown at a time, on Base UI's tabs (arrow keys, Home and End, each panel named by its tab), the indicator sliding to the active tab. Tabs (with variant, size, material), TabsList, TabsTab, TabsPanel. segment (the default) lifts the active tab out of a well; line lights its stretch of a rule. Plain or glass.
  • New: captions on VideoPlayback, the languages a video is captioned in, a WebVTT file each ({ src, lang, label, default? }). The browser parses and loads the shown file and never draws it; VideoPicture draws its lines over the picture in the shared caption look, sized by the picture's width (a full 42-character line spans 60% of it), and lifts them above the chrome's foot while the chrome shows. A bar preview shows that instant's line. A playlist item passes its own. Another origin needs crossOrigin and CORS headers, as the video does.
  • Changed: a pinch on a picture that zooms (VideoPlayback with zoom) climbs one ladder, one rung a pinch. In the page, a spread fills the screen. Filling it, a spread zooms in at the hand, and a close at fit leaves. Zoomed, a pinch zooms, down to fit and no further. Until a rung is taken the picture follows the fingers. Leaving fullscreen, by any means, returns the picture to fit. Where the browser has no fullscreen, the picture zooms in place as before.
  • Fixed: the picture following a pinch toward fullscreen or out of it, and its return to whole, name no zoom ("fit" no longer flashes over a picture nobody zoomed).
  • Fixed: a pinch let go short of its rung returns the picture whole. Settled on its nearest stop, a spread short of fullscreen left a picture of another shape than its frame zoomed at its fill.
  • New: useMediaCaptions reads a media element's captions (lib/captions: the files offered, which shows, the lines loaded) and useCaptions hands them to the controls. Whether captions show and in which language is one choice for every player, kept with the volume where the owner remembers it; never chosen, a file marked default shows. Caption (and CAPTION) is the look of a caption line over a picture, one for every caption drawn.
  • New: lib/picture-zoom exports the ladder both use, pinchRung and rungTaken, with PINCH_PER_WHEEL and PINCH_SLOP.
  • Changed: toggleFullscreen fills the window when the browser refuses its fullscreen. The browser grants fullscreen only to a press (a click, a key, a finger lifted), and a trackpad's pinch is none, so a pinch asking for the screen was refused. The part is fixed over the page edge to edge, the page's scroll held, Escape leaving. New: screenHolder() (what holds the screen, either way) and onScreenChange(fn); ask these instead of document.fullscreenElement. pinchRung drops canFill: the screen is always there to take.
  • Changed: PictureZoom.pinch takes { start, quiet } where it took a positional start, and fit takes { quiet }; a quiet move names no zoom. Migrate pinch(s, at, true) to pinch(s, at, { start: true }).
  • Changed: volume, mute and speed are kept by remembered (PLAYBACK, exported from media-timebase); what was kept under the old key is not read.
  • New: kind: "audio" (its width and height its picture's, 0 when it has none), average (the mean colour, #rrggbb), and frames: { every, count, src }, stills along a clip, frame i at i × every seconds at src with {i} replaced. isMediaAsset checks them. The asset is written to be the contract an app preparing with AVFoundation emits too.
  • New: the input may be a path or a URL ({ url, headers }), besides bytes; video and sound stream to ffmpeg, so span: { from, to } reads only that part of a long source. frames: { every, width } makes stills along the clip, audioOnly keeps the sound alone (AAC in MP4, its picture as the poster), maxSide caps a video's longest side (1920), and every asset carries average. mediaAsset resolves the frames' {i} address. A Buffer input behaves as before.
  • Fixed: average is the true mean of every pixel; a 1×1 resize only estimated it and could miss by several steps in a channel. An app preparing with AVFoundation (CIAreaAverage) now gets the same colour for the same picture.
  • New: Lucide Lab's icons (about 350, drawn to lucide's rules: the horse's head, animals and things lucide's main set leaves out) beside lucide's, by name; a name both have is lucide's. Needs @lucide/lab.
  • New: iconsNamed(words), the icons named like a thing, best first, a plural read as its singular.
  • New: short, the name where room is short (a caption's speaker, a chip, a mention in running text), and shortName(person), which falls back to name.
  • Fixed: a picture that fails to load turns transparent instead of drawing the browser's broken-image glyph and its alt text over the frame; what stands behind shows, and the alt stays for a screen reader.
  • New: a YouTube video as a picture until it is asked for: its thumbnail, title and channel, a play mark; pressed, YouTube's player (youtube-nocookie) takes its place and plays, so a page of videos costs pictures, not players. A Short stands upright. youtube-data reads the video from any link YouTube hands out (watch?v=, youtu.be, shorts, embed, live, a start as t=1m30s) and its title and channel from YouTube's oEmbed at authoring time: mise youtube <url>.
  • New: a quotation is the quote item's prose weight (its surface, its mark, the quote voice), and its last line names who said it: — Ada Lovelace (or --, -), a link around the name taken as theirs, what follows a comma as where it was said. components.blockquote draws it your way. A bare blockquote (the editor's) wears the same look through MARKDOWN and MARKDOWN_VARS (the quote's mark as a mask); wear MARKDOWN_VARS beside MARKDOWN wherever you draw the tags yourself.
  • New: a link alone on its line is the thing it points to: embeds(url) returns what to draw (a post on X through tweet, a Telegram post through telegram-chat, from facts you keep), and a YouTube link is a youtube video by default. The item now uses quote and youtube.
  • New: --ag-markdown-link gives links a colour (a site's accent) and their hairline and hover wash follow it; nothing else changes. Set it where the look is worn (className="[--ag-markdown-link:var(--accent)]"), on a renderer or the editor. Any other rule is overridden by a class of the same kind in className (a later text-sm, [&_p]:my-0, [&_h2]:border-0 wins); the demo shows all three ways.
  • Changed: Markdown renders anywhere, a server component included: the text is parsed on the server and no parser ships to the browser, and components and transform may be server functions. Highlighted moved to ui/markdown-code (the one part that needs the browser); import it from there.
  • New: components.code draws inline code (given its text), for code that spells a shortcut drawn as keys.
  • New: a markdown text drawn as a page (<Markdown>{text}</Markdown>) and the look of one (MARKDOWN, rules on plain tags in em, so a body set small is the same page made small). The serif voice for h1 to h3, links underlined by a hairline that turns to a highlighter's wash under the hand, inline code on a chip, code blocks highlighted in the browser in code's frame (Highlighted), quotes in the quote voice, tables with small uppercase heads and hairline rows, tasks as boxes, footnotes at the foot; one colour at the alpha ladder's tiers throughout. Parsed by lib/markdown-core (CommonMark, GitHub's extensions, front matter), shared with the editor. Nothing is set as HTML but Shiki's markup, and a link or image is drawn only where its URL is safe (safeHref, safeSrc: the web, mail, a phone, the page, the site; never javascript: or data:), so text anyone wrote is safe to draw. It composes: components swaps how a link or an image is drawn (given checked URLs), transform changes the tree first, and mapText rewrites text outside code and links (autolinking a ticket number or a mention).
  • Changed: the prose weight is sized in em of the page around it, one step up (1.125em), its padding, mark and attribution with it: identical at a 16 px page, and the same quote made small in a page set small.
  • Changed: the lightbox's code loads only for a face that can be opened (full), lazily, the plain face standing in until it has. A page of faces, or a quote with none, no longer carries the viewer.
  • Changed: pictures going up are a strip: each one small as it was dropped, a hairline filling as it goes, "preparing" while the server makes its renditions, the reason when it failed, retry and cancel as icons. EditorJob carries preview. The messages are lowercase and say what happened.
  • New: the / menu. In an empty paragraph / offers the blocks the document holds (text, headings 1 to 3, bulleted and numbered lists, a quote; in a markdown file a code block and a divider), each with the keys that make it; what follows the slash narrows them by name or a word for them (/h2, /ol, /hr), ↑ ↓ walk them, Enter or Tab turn the paragraph into the one marked, a press takes one, Esc leaves the slash as typed. A slash anywhere else is a slash. The item now uses menu (its keys are drawn the menu's way).
  • New: a web address typed, then a space, links itself; the sentence's closing punctuation stays out of the link.
  • New: the block and mark keys of Google Docs: ⌘⌥1 to ⌘⌥6 a heading (pressed again, back to a paragraph), ⌘⌥0 a paragraph, ⌘⇧7 a numbered list, ⌘⇧8 a bulleted one, ⌘E code, and in a markdown file ⌘⇧X struck and ⌘⌥8 a code block. Wordgard's own keys stay.
  • New: a web address pasted over chosen words makes them its link.
  • New: list keys as document apps have them: Tab nests an item under the one above (joined to a list already there), ⇧Tab brings it back out to follow its parent, the items after it coming along nested; ⌘] and ⌘[ the same. Tab in a code block indents two spaces. Enter on an empty item leaves the list. Elsewhere Tab is left alone.
  • New: markdown typed becomes what it means, in either format. Blocks: # to ###### , > , - , 1. , three backticks, --- (the last two where code blocks and rules exist). Inline, on the closing delimiter: **strong**, __strong__, *em*, _em_, ` code , ~~struck~~, [words](url)`. Plain text pasted that reads as markdown arrives converted; any other text as text.
  • Changed: list items hold blocks in the HTML format too (<li><p>…</p></li>), so a typed - makes a list and lists nest. Consumers that render the HTML get a paragraph inside each item.
  • Changed: the document wears markdown's look (MARKDOWN), so what is written reads as it will be read: the serif headings, the hairline links, tight lists, tables without a grid, code in a hairline frame, source kept verbatim shown quiet and dashed. The editor's own typography rules are gone from editor.css; its box and toolbar stay.
  • New: format="markdown" reads and writes a markdown file instead of HTML: defaultValue is the file's text and onChange hands back the text. Code blocks (with their language), rules, struck text, nested lists and tables join the editor; anything it cannot hold as itself (raw HTML, front matter, footnotes, reference links and definitions, task lists, aligned tables, titled links) is kept as its source in a verbatim block and written back untouched. Written in one style (- bullets, * emphasis, fenced code). New dependencies: the mdast parser and printer with GFM and front matter. onChange's argument is now named for either format; HTML stays the default.
  • New: choice columns. A text column that repeats a few values (a status, a category) is read as choosing among them, or a column declares its options ([] reads it as free text). Its cells wear the value as a badge, and an edit offers the values as a list that narrows as you type (↑ ↓ walk it, Enter or Tab take one), with what was typed as the last entry, so a new value is chosen like any other.
  • New: optimistic edits. When onEdit returns a promise the cells show the new text at once, faded, until it settles; a rejection rolls them back, marks them and says why at the table's foot (the error's message). onEdit's type is now (edits) => void | Promise<unknown>.
  • New: staged. Edits wait instead of being asked for: each changed cell carries a corner mark and what it was, ⌘Z and ⇧⌘Z step through them before the consumer's history, and a bar at the foot applies them as one onEdit call (or ⌘S) or discards them.
  • New: density="spacious" (40 px rows) beside compact (24) and comfortable (32).
  • New: ⌘F finds in the table: a bar over its top right corner, matching cells washed, the cursor on the first match as you type, Enter and ⇧Enter to walk them, Esc to close.
  • New: a header dragged sideways moves its column, a line showing where it lands; the column menu's move left and right work in every table. Through onMoveColumn when the consumer keeps the order (a CSV's columns are its data), else the table holds it as a view.
  • New: a table to read and to edit. Thousands of rows windowed, the header and the first column sticky, columns sorted by their kind (read from the text: numbers right-aligned in tabular figures, dates, yes/no as a check that flips), resized from their edge, renamed and moved, lettered A, B, C. Cells as a spreadsheet's: a cursor walked by the keys and edited in place, a block grown with ⇧ or a drag that is copied, pasted into (one value fills it), cleared and filled down or right (⌘D, ⌘R), ⌘ with an arrow to the edge of the data, ⌘G to an address typed in the corner box (B9, 9, B, B2:D5, as sheets' :B9); every cell one gesture changed is handed back in one call (onEdit(edits)), and ⌘Z / ⇧⌘Z reach onUndo / onRedo. Rows selected the one way (selection) from their numbers, ⇧Space or Space, with their verbs on a right click and on the selection bar. A column may draw its own face (render) and a row its tint; the sticky parts fill with --ag-table-ground (the page colour unless set) for a table on another surface. CsvTable edits a CSV's text: the text in, the text out on every change, rows and columns inserted, duplicated, moved and deleted, every change a step of its own undo history.

table-data

  • New: choicesOf(values), the values a text column chooses among (two to eight, filled at least four times, each used one and a half times on average), or null for free text.

selection-bar

  • New: BAR, BAR_RULE and BarVerb, the bar's body, its rule and a verb with its keys, for any other bar a set raises at its foot (the table's unsaved changes) to read as the same family.
  • New: pressed(text, press), whether a key press is the combination a text names ("mod+backspace"), exactly its modifiers and its key.
  • Fixed: an icon in a translucent colour (its own text-x/55, or one inherited from a dimmed button or label) came out brighter where its strokes meet, the alpha stacking stroke by stroke. Every lucide icon (.lucide), every svg drawn in the text's colour (stroke="currentColor", as hand-drawn icons are written; parts filled or stroked in it by their own attribute too), and any of your own given ag-icon, now draws its strokes opaque and takes the colour's alpha over the whole icon, evenly. Icons drawn opaque are untouched. Needs relative colours (Safari 18, Chrome 119, Firefox 128); older browsers draw as before.
  • New: what a person set, kept, and given back to its default when it should be. setting(key, { initial, keep, parse }) defines one: its default, how what is kept is checked, and how long it lives ("tab" for the tab's session, "always" until changed, { idle } until untouched that long). useRemembered gives the value, a setter and a reset; recall, remember and reset work outside React. Every reader of a setting agrees, other tabs follow, the server renders the default. A kept object is laid over its default, so a field added later takes the default's; what fails its parse is said and dropped.
  • Changed: start and a followed at (play="follow") seek to the middle of the asked millisecond (seekSeconds), so an instant that is a frame's name shows that frame; the follow tolerance compares whole ms.
  • New: VideoPicture takes inPicture, drawn on the picture itself and moving with its zoom (boxes in shares of the frame), filling the picture's own box inside the stage whatever the stage's shape (an aspect stage, fullscreen); children stays still over the stage. The zoom takes frame(rect): a rectangle of the picture brought close, eased, from lib/picture-zoom's frameView.
  • New: VideoPicture takes fullscreen: the picture with its chrome over it is what fullscreen fills (the button, F, a double click, a pinch), not the player's whole root, for a page laid out around the player; while it is fullscreen its popups open inside it, and otherwise on the page, so a menu taller or wider than the picture (a narrow vertical video) is never cut by its edge. toggleFullscreen fills the root's part marked data-fullscreen, else the root.
  • Changed: the chrome over a picture shows while the picture is awake, not while it is hovered: a pointer moving over it, or resting on a control or a popup, and it fades 1.5 s after the hand stops (fullscreen too, where the pointer never leaves the picture and the chrome never went). The cursor hides with it while it plays. A stage wears data-awake; what shows on hover over it reads group-data-[awake]/picture:, never group-hover/picture:. AudioPicture the same (media-timebase's useStageAwake). Migration: a consumer's own group-hover/picture: becomes group-data-[awake]/picture:.
  • New: a timebase plays from a sixteenth to sixteen times its speed (RATE_MIN, RATE_MAX, clampRate in timebase), what browsers' media elements take; it was held to 0.25–4. The player's own speed menu and swipe keep a person's range.
  • New: setRate(rate, { remember: false }), a speed a page sets as it plays (slowing into a moment) without it being remembered as the person's speed in a VideoPlayback or AudioPlayback that remembers settings.
  • New: setRate(rate, { pitch: "follows" }), the sound's pitch falling and rising with the speed instead of held: what a speed that keeps changing needs (holding the pitch anew on each change stops WebKit's clock for a moment each time). "held" stays the default.
  • Changed: every instant a timebase reports is a whole ms. Playback's clock walks in fractions and writes at rounded; a VideoPlayback reports the presented frame by its name (the first whole ms inside it, frameMs), an AudioPlayback its currentTime rounded (msOf), and both read duration the same way. A preview is whole too. A consumer comparing at against a float it computed itself now sees integers.
  • Changed: a seek lands on the frame named by the instant. VideoPlayback and AudioPlayback seek the element to the middle of the asked millisecond (seekSeconds), so a seek to a frame's name never shows the frame before it, for every seek and not only a step. A step lands on the next frame's name instead of its middle. The frame rate is still measured from the decoder's own times, never from the rounded names.
  • New: PlayerLayer takes active; false hands its popups to the layer around it.
  • Changed: the frame and timecode readouts read whole-ms instants through the new frameAt, so a frame shows the same number however the instant was reached (a step, a seek, a click on the bar).
  • Changed: a narrow PlayerBar (under 30rem, a vertical video's player) gives the bar its own row above the buttons, instead of squeezing it between them and running the last button off the edge.
  • New: PlayerZoomFinder takes corner (top right by default, or any other), so a page that knows where the picture's subject is keeps the finder off it.
  • New: PlayerFullscreen (and GlassPlayerFullscreen in glass-player): fullscreen and back as a button in the bar, the corners pointing the way it goes; the same flip as F and a double click, over useFullscreen. Absent where the browser has no fullscreen.
  • Changed (breaking): PlayerZoomFinder sizes itself as a share of the stage, share (default 0.2: a fifth each way, 4% of the picture, whatever its shape), in place of a fixed width in px, which took a third of a tall picture. Migration: width={200} becomes a share, share={0.3}.
  • New: a field whose value is a list of chips, the text after them a search. Focused with nothing typed it lists every option in its groups; typing finds them fuzzily (lib/fuzzy), the letters it matched lit. options picks from a set; free makes what is typed a chip of its own (Enter, a comma, or leaving the field), the set only suggesting, and a chip pressed goes back to the text. Keyboard throughout, on Base UI's combobox and input's well (WELL, now exported).
  • Changed (breaking): a one-line word is drawn solid by default (look="solid"): filled in its ink with the word in the ground's colour (--ag-media-ground, else the page's background), legible on any picture; a ghost solid chip is quieter (75%), never translucent. That is the chip on the page's own ground (a list, a rail, a panel); over a picture it is look="glass", a dark translucent ground with the word in its ink. The outline look is gone, and --ag-media-scrim with it. Migration: drop look="outline".
  • Fixed: a badge's word sat a pixel high, touching the top of its frame, wherever the badge did not start on a whole pixel. Every word is trimmed to its capitals (text-box: trim-both cap alphabetic) so the box centres the capitals in any font, and the one-line badge puts its baseline on a whole device pixel from its top edge, so frame and word snap together. The sticker's lower band centres its word in the band and the frame's bottom stroke, the shape the eye reads as one fill. Firefox, without text-box, keeps the previous centring.

inspect

  • New: Inspect, any nested data as a tree you can read. A value (objects, arrays, Map, Set, Date, cycles named) or a source text read as JSON or XML (attributes as @name, repeated elements numbered). Each field a row drawn by kind: quoted strings, numbers, booleans, null, a colour with its swatch, a link, a date with how long ago; a closed branch previews its contents and size; a branch over 100 children opens in ranges of 100. Fuzzy search over keys and values, open to a depth or all, the whole as formatted text, and a table for a list of records (an SQL result). Every row copies its value or its path ($.tags["a/b"].yes[0], /root/item[2]/@id). annotate adds a word only the consumer knows. inspectValue, inspectXml and parseSource give the node model alone.
  • New: fuzzy matching for every text search. A query's letters are found in order, not only side by side ("ppl smlg" finds "a person smiling"), case and accents folded, every word ANDed in any order. Runs and word starts score first, then earlier and shorter. fuzzy(query, text) gives the score and the lit ranges, fuzzyFilter(items, query, keys) ranks items by one text or several, segments(text, ranges) splits a text for drawing.
  • New: Picker, a search over things drawn small (icons, emoji, marks): the consumer's items (id, label, keywords, group, node), suggested first while nothing is typed, then every item by group; typing ranks whole words over word starts over insides. Enter takes the first match, Esc empties the search, none offers the no-choice. Its search is Search over fuzzyFilter. IconPicker is it over every lucide icon, stored by lucide's own name ("Flame"); lucideIcon(name) draws one back. It carries lucide's whole set.
  • New: Input, a text field that reads as one: a sunken well (a faint fill and a hairline ring inset) that brightens under the hand and on focus, red when aria-invalid, at the button's heights (size sm, md, lg). InputGroup puts start and end (an icon, a unit, a key, a clear button) inside the well. components installs and exports it.
  • New: Shortcut, an input drawn as what the hands do: keys, a pointer action, or both. input="mod+click" (or keys={["mod"]} pointer="click") draws the ⌘ cap, a quiet "+" and a mouse with its left button lit. Pointer actions: click, double-click, secondary-click (alias right-click), middle-click, drag, scroll, hover, long-press, always last; keys alone work too (mod+shift+k, shift+f10). The keys are kbd's caps; mod is ⌘ on a Mac and Ctrl elsewhere, and off a Mac modifiers read as words (Ctrl, Alt, Shift). The pointer glyph is bare, at the caps' height and centre, never a cap. A screen reader hears words ("Command click"). size="sm" for a menu row or a tooltip; platform draws for a given system. shortcut(text) parses without drawing, PointerGlyph is the glyph alone.
  • New: mod (⌘ on a Mac, ⌃ elsewhere) in combo and keys, which take an optional platform ("mac" by default, so nothing changes for existing calls). platformOf(navigator.platform), capLabel(cap, platform) for a cap as Windows and Linux write it, and modifiers(tokens) for modifiers held without a key.
  • New: keyName(cap, platform) names ⌥ "Alt", ⌘ "Windows", ↩ "Enter" and ⌫ "Backspace" off a Mac; the Mac's names stay the default.
  • New: KbdCap, the single cap every shortcut is drawn with; platform="other" prints it as Windows and Linux do (Ctrl, Alt) with the matching title.
  • New: ThumbStrip, a reel's thumbnails in one row, the lightbox's top strip on its own. items ({ id, src?, width, height, label, video? }), index, onJump(i), height (32 px by default; the strip is 16 px taller), counter for "2 / 14" before it. The active thumb is lit and kept centred instantly, by the strip's own scroll, so no ancestor moves. Thumbs are sized by their aspect at that height, capped at 2:1; an item without src shows its label; video marks a poster. The edges fade only when the row overflows, with no scrollbar.
  • New: EdgeArrows, ‹ and › at the left and right edges of the stage they sit in, vertically centred: onPrev, onNext, hasPrev, hasNext. Quiet at rest, a tier clearer while the stage is hovered, the title tier under the hand. At an end the arrow dims and a press does nothing, and it stays focusable. EdgeArrow is one arrow, for a stage that names its own (label, keys, tooltip).
  • Changed: the strip and the arrows are thumb-strip and edge-arrows, installed with it. Nothing changes on screen. The .ag-lb-strip, .ag-lb-thumb and .ag-lb-nav classes are gone: a stylesheet that reached them targets [data-slot="thumb-strip"], [data-slot="thumb-strip-thumb"] and [data-slot="edge-arrow"].
  • New: a dialog on Base UI with shadcn's part names, in two places: <Dialog place="center"> over a dimmed, held page, and <Dialog place="side">, a panel at the right edge beside a live page (not modal, and a press on the page leaves it open). DialogSection titles a group of controls inside it.
  • New: a slider with snap points. marks are named under the track; snap="marks" chooses only them (equal room per choice), snap="near" pulls the hand onto one within pull, snap="free" keeps them as landmarks. behind draws under the track in the slider's own coordinates.
  • The marks are small dots under the track, as macOS draws a slider's stops, not lines across it; the thumb is a solid knob with a soft shadow, no translucent halo.
  • New: NumberField, a number a person sets, in place of <input type="number">: the value with its unit after it, always a dot decimal. Drag sideways on the value or its handle to scrub (pointer locked; alt fine, shift coarse; marks hold the value as the hand passes; Esc reverts); a click types. Typing reads .25, 250ms, 1m 30s, 0,25, 1,120, 1.2k, 80% and 0.25 * 2; Enter commits, Esc reverts, leaving commits. Arrows step (shift ten, alt a tenth), page keys go mark to mark, the wheel only while focused. unit={SECONDS} keeps the value in whole ms and shows seconds. A known range draws a hairline filled to the value.
  • New: chevrons, ‹ › at the two ends of the range bar with the track between them: each press moves to the previous or next mark, or by the step when there are no marks (shift ten steps, alt a tenth); held, it repeats. Their strip is always present with them (an open range shows the chevrons with nothing between), and a field with chevrons is 44 px tall.
  • Changed: the field is laid on a grid. Its padding is a left and a right inset: the value (with its unit) always ends on the right inset, the handle and the mark's name start on the left inset (the name keeps the widest name's room, so moving between marks moves nothing), and the bar's track runs inset to inset, or between the chevrons, whose ink sits on the insets, at equal gaps. The field is 32 px tall without a bar and 44 px with one (6 px above and below in both). GlassNumber takes its height from its padding the same way, with soft corners instead of a pill when it has a bar.
  • Changed: the range bar is a slider of its own. Hovered, focused or in use it grows in a reserved strip (taller track, a thumb, taller ticks, the mark under the hand named below it) and back to a hairline at rest, moving nothing; a press on it sets the value where it lands, a drag carries it, Esc puts it back. A field with a bar is now 40 px tall (32 px without one) to hold that strip.
  • Fixed: the parsing and the arithmetic live in lib/number-core. Named lib/number beside ui/number.tsx, shadcn's install rewrote number.tsx's import of it to the ui file. Migration: install number again with --overwrite and delete the old lib/number.ts.
  • New: Tree, one tree for files and places, a layers panel and a tag hierarchy. Every row sits on one grid (depth, a chevron gutter a leaf keeps empty, the icon, the label, the trailing meta), so rows at one depth start at one x and a child's chevron sits under its parent's icon. The WAI-ARIA tree pattern with a roving tabindex, type-ahead and *; the focus ring for the keyboard only. expanded and selected controlled or not (selection="single" | "multi" | "none"), storageKey remembers expansion; loadChildren with isBranch for lazy children; filter keeps a match's ancestors, opened; actions with TreeAction (a pinned one stays visible), onRename (F2, double-click), onMove for drag with a before, inside or after line, canDrop to narrow it; marks and onMarksChange for include, exclude, neither, a branch reading mixed when its subtree disagrees: a click toggles include, ⌘-click (Ctrl elsewhere) toggles exclude, never a cycle; pressMarks makes the whole row the mark (a filter rail, where ⌘-click is exclude rather than multi-select). Right-click, a ⋯ on hover or focus, the context-menu key or ⇧F10 open the row's menu anchored to the row: "exclude" or "include again" first with its ⌘-click shown, then rowMenu's entries (TreeMenuEntry). content draws the label column as something richer than its text (a chip); reserve keeps the mark and ⋯ columns empty on a tree without them, so stacked trees share one column system. density, guides, and a ref with expandAll, collapseAll, focus. Windowed: only the rows in view are mounted.
  • Fixed: the core is lib/tree-core. Named lib/tree beside ui/tree.tsx, shadcn's install rewrote tree.tsx's import of it (and gallery-filters') to the ui file, which then imported itself. Migration: install tree again with --overwrite and delete the old lib/tree.ts.
  • New: lib/tree-core, the arithmetic, framework-free: flatten, filterTree, rollup (a known whole count can stand for its subtree), markStates, setMark, toggleMark, fromPaths (a tree from slash paths), moveNodes, canMove, navigate, typeahead.
  • New: a lens with a grammar in lib/tree-core: Lens ({ groups, exclude }: groups ANDed, OR inside a group, excludes AND NOT after), NO_LENS, markLens (a mark gesture, or joining the last group), flipJoin (an "and" merges two groups, an "or" splits one), lensText (people (beach | mountains) -color/red), lensTerms, lensMarks, lensOf (plain marks as a lens), lensEmpty. On Tree, onMark(id, mark, { or }, clear) hears the gesture whole in place of onMarksChange: with it, ⌥-click includes as or and the row menu offers "include as or · ⌥ click".
  • New: FileIcon and iconFor (a file's icon by mime type or extension, a folder open or closed), kindOf, and ICONS (disk, offline disk, phone, camera, cloud, trash, star, tag, layers, group, eye, lock and more).
  • Changed (same day): a row menu's hint is drawn with shortcut: write it as an input ("mod+click", "shift+f10") and it shows as keys and a pointer glyph; any other text shows as it is. The built-in exclude entry's hint is mod+click and include again's click. Installs shortcut.
  • New: nestable(node): which rows take a dropped row inside them (the middle half of the row); by default a branch does and a leaf does not. A row about to take one is lit as a target.
  • Changed (same day): every row menu entry has an icon (required: the eye finds a verb by its glyph). An unmarked row's mark column is empty, a faint + only under the hand; + and − stay for included and excluded. A dimmed or disabled node's whole subtree is dimmed or disabled.
  • Changed (same day): the filter matches fuzzily (lib/fuzzy) and lights the matched letters, not only a contiguous substring.
  • New: a row's gestures for any list of markable rows, the tree's own: pressMark (click includes, ⌘ excludes, each pressed again takes it back), markEntries (the mark verbs a row menu starts with, their presses drawn), MarkSign (the + and − glyph) and RowMenu (the menu itself), whose anchor takes a point (client px) as well as an element, so a context menu opens where the click was. A list that is not a tree wears the same gestures with them.
  • Fixed: a row menu entry runs once the menu has closed and given focus back to the row. It ran on the click, so an entry that opened a dialog closing on focus out (a side dialog) lost its focus to the row a moment later and shut as it opened, some of the time.
  • New: the base controls in one install and one import: dialog, slider, number, tree, tooltip, progress, kbd, input, shortcut, thumb-strip and edge-arrows.
  • New: GlassDialogContent, the dialog in glass (dark by default, a heavier blur than a menu's), at the dialog's place.
  • New: GlassNumber, the number field in glass, at the button's pill and heights.
  • Fixed: GlassPlayerZoomFinder draws the picture to its edge, the glass only that edge; it had a mat of glass (p-1) round the picture, a thick border. New: GlassEdge (glass-display), the edge alone over media that runs to it, which GlassFrame and the finder wear.
  • New: the door to seconds, the only place a timebase's ms meet a media element's seconds: msOf(seconds) (rounded to nearest), frameMs(seconds) (a decoder's presented frame as its name, the first whole ms inside it), secondsOf(ms) and seekSeconds(ms) (the middle of that millisecond, where a seek aims). secondsLabel(ms, digits) says a stretch or a setting in seconds for a person (3.1 s).
  • Changed: frameTime(n, fps) returns frame n's name, the first whole ms inside it (ceil(n * 1000 / fps); frame 1 at 30000/1001 is 34), no longer its middle: what a step lands on and what a cut stores. frameAt(ms, fps) reads the frame presenting at ms + 0.5, with no tolerance, so every name reads back as its own frame. TimebaseState.at, duration and preview are documented as whole ms.
  • Changed: eta is ms, like every time in the registry, and timeLeft(ms) takes ms. A caller passing seconds now reads a thousandth of the time left.
  • New: how far along a piece of work is, said three ways at once: a bar, the count (done of of, in its unit) and the time left (eta, said as a person says it: timeLeft). state: running (the fill eases forward under a sheen), paused (striped where it stopped, to go on from), done, failed (the bad tone). of null while it is not known: a stretch sweeps the track. action sits at the row's end for a stop or a resume. A progressbar with its values; still under reduced motion.

moment-lane

  • New: what a model or a person says about a recording, over its time. MomentLanes, inside a player, draws a lane per question on one axis: its curve (series, a value 0..1 at each instant) as a filled strip, its moments as bars (a model's outlined, a person's solid) and the playhead through every lane; a click plays from that moment, a hand resting on a lane previews it (the shared hover intent), onRemove puts a × on each label. Data in, monochrome by alpha.
  • New: MomentCurve, one answer drawn on the player's own bar (a PlayerBar face), as YouTube draws its most replayed: the curve on the bar's axis, so a peak is exactly where to click; the played part whole, the hovered stretch between, the rest faint, the moments marked along its foot. also reads it against other answers (other questions, another model's answer to the same one), each a line in its own dash (LINE_DASHES) on the same axis. threshold draws where its moments are cut, a hairline across the curve.
  • New: momentSnaps(moments), a curve's moments as PlayerBar snaps (where each starts and ends). MomentCurve's moment marks are pressable: a press plays from the moment's start.

talking heads

  • New: VideoPlayback sound: the video's spectrum handed down, so a halo round its face hears it. VideoPicture at aspect="1" and rounded-full is the face, PlayerTransport only={["play"]} its play and pause.
  • New: VideoCrop and CropShot: a piece of the picture drawn live, one person's face out of a shot that holds several, following a track of where the face is and when; every face in a scene moves, the listening ones too. useInShot says when the camera is elsewhere (an orb stands in).
  • New: floating: a thing that leaves its place to stay with the reader. Pulled magnetically between its spot in the page and its place on the window as you scroll, never cut by the window's edge, carried by a spring; it sticks to spots a page offers (corners and edge middles by default) and stays where it is let go elsewhere.
  • New: where a drawing of a sound gets the sound, one door for every player and every drawing. useSoundTap(element, readable) taps a media element once, after a press, connecting only once its context runs; soundReadable(src, crossOrigin) says whether its spectrum may be read; SoundSourceProvider / useSoundSource hand the analysers and whether it plays to anything inside; TimebaseSoundSource says it plays while the timebase around it plays. lib/sound-scale is the one analysis scale. AudioPlayback uses it; your own drawing reads useSoundSource.
  • New: a preview is heard by the drawings. While a timeline previews a moment (a word hovered, the bar hovered), TimebaseSoundSource hands down that moment's sound instead of the held one: the element's own recording, decoded once on the first preview, a quarter second at the moment looped into analysers that never reach the speakers (usePreviewSound). A halo round a face moves with the moment under the hand. useSoundTap returns the tap's context and element beside its analysers, and TimebaseSoundSource takes the tap as sound.

shade, aurora

  • New: shade, a colour effect kept apart from what is drawn with it: a Shade is a name and the colour x (0..1) along a travel from a base colour; solid keeps the base, shadeTable makes a travel's steps for a canvas.
  • New: aurora, the palette an aurora borealis travels (yellow-green, green, aqua, blue, violet, magenta), entered from any colour at its own place and travelling span of it, turning back at the end, never wrapping. aurora(span) is the shade; auroraColor, auroraHue, placeOf the parts.
  • New: a conversation the way every messaging app draws it: a card per message, the sender's face and name, a run of one sender grouped (face and name once), the time, your messages on your side, days between days, photos and quoted replies. Neutral by default and made to be skinned: look bubble, card or plain, density comfortable or compact, each person in their colour. Chat is the preset (messages and people in, you a person among them named by self, grouping and sides computed; align sides, start or alternate); where a message sits and whose it is are separate (side, own), so your messages wear your colour on any side and a guest moved to your side keeps theirs; the parts are the API (ChatThread, ChatMessage, ChatDay, ChatFace, ChatSender, ChatTime, ChatPhoto, ChatReply), each taking className and a look="none" that keeps the structure (a list of listitems, data-side, data-continued, data-tail) and drops every visual class, so a skin owns every pixel.
  • Change: drawn with chat's parts: the thread, each message's row and bubble, the day, the sender's name, the photo and the quoted reply. It looks and behaves exactly as before; the thread is now announced as a list of messages, each a listitem. Installing it now also installs chat.
  • Change: the profile video is a video that loops on sight. Pulls @ag/video.
  • New: color, the person's own colour, for surfaces that tell people apart by colour (a chat's senders, a transcript's speakers). Absent, such a surface picks one from PERSON_PALETTE by order of appearance (personColors), one palette so every surface tints the same people the same.
  • New: shadcn's tooltip on Base UI: TooltipProvider, Tooltip, TooltipTrigger (render where shadcn's Radix one takes asChild, delay where it takes delayDuration) and TooltipContent, installing over shadcn's own file so components written for it keep working. On top: Tip wraps a control and names it in one line, with the keys that do the same drawn as keys; TooltipHint is a small ⓘ beside a label for explaining something that is not a control; TooltipContent takes keys and card (richer content laid out as it comes). It waits 200 ms on a control (80 ms on a hint), opens neighbours at once after the first, and never opens on touch. liquid-glass has GlassTooltipContent and GlassTip.
  • New: an option's hint: shown on hover of that option, its label dotted underneath as the affordance, and read with it by a screen reader (aria-description).
  • New: where focus came from, so the focus ring answers the keyboard only. A click gives a button focus, and the next key pressed anywhere (space, a player's keys, an arrow on a page that listens to keys) made the browser ring whatever was last clicked. Mounted once in the root layout, <FocusSource /> marks focus a press gave (data-focus-pointer), which the ring skips however many keys follow; focus the keyboard moved (Tab, arrows through a group, Home, End) is never marked, so it is always ringed. Without it mounted, nothing changes.
  • Breaking: one play replaces mode and playOn. Migrate: mode="player" (or no mode) is play="manual" (the default); mode="preview" is play="hover"; playOn="visible" is play="visible"; playOn="visible-once" is play="once". The preview's pause button is pauseButton (it was controls in preview mode); controls now means only the browser's own bar.
  • New: play="follow": the page decides with playing, and optionally at (ms), the instant it must show (a stage whose clock moves the video). It stays unseen until it has shown that instant, the poster standing in, and stays seen while it seeks on.
  • New: rest: what a stopped video shows, "hold" (the frame it reached, default) or "poster" (back to the start and the poster). Preview covers used to rewind to the poster; pass rest="poster" to keep that.
  • New: one video plays with sound at a time: one starting with sound pauses the other. Its media is released when it leaves (it used to keep playing its sound detached), and its source is set by an effect so a remount restores it.
  • New: VideoPlayback and VideoPicture: a video's decoder as the owner of a timebase, so the player's parts drive a real video. The instant is the frame the browser presented, seeks coalesce, steps add up, a preview holds and resumes, range loops a span, chapters divide the bar, a refused autoplay plays muted until the first press, and volume, mute and speed are remembered. play lets it play itself on sight ("visible") or on hover with the same controls.
  • New: automatic videos load 1200px before they are seen (a play on a cold file flashed a half-drawn picture), their sound is one decision for the page (unmute one and the others follow, one voice at a time; they used to be muted for good), and with sound on they pause when the window loses focus and play on when it returns. An unoptimized poster's blurDataURL is painted under it in the first paint.
  • New: width and height are optional (without them the frame fills a parent you size), optimizePoster={false} draws any-origin posters without Next's optimizer, posterArrival="instant" puts such a poster in the first paint, and start (ms).
  • Fix: a rounded video's corners are smooth in Chrome. The video rounds its own corners, and nothing stays painted under it once it shows a frame: the placeholder ground and the poster used to show round its edge as a light, stepped rim.
  • New: a play="follow" video's sound is the page's decision like an automatic one's: it starts muted, and a refused play falls back to muted instead of failing silently in a browser that refuses sound.
  • New: VideoPlayback's zoom: pinch (a trackpad's or two fingers) or ctrl-scroll at a point, + - 0 with PlayerKeys, and while zoomed a drag, a scroll or one finger looks around (PlayerGestures stands down: the pinch zooms instead of going fullscreen, a drag pans instead of scrubbing). PlayerZoomFinder shows where: the frame in small, live, the part on screen outlined, a press or drag moving there; over the picture's corner, or anywhere on the page with floating={false}. liquid-glass has GlassPlayerZoomFinder.
  • New: VideoPlayback's onEnded, and VideoPicture's aspect: a stage of one shape, a video of another shape fitting inside it whole, so a playlist's player keeps its size between videos.
  • Change: volume, mute and speed are remembered under one key for every medium, ag-media-playback (was ag-video-playback): a person's volume is theirs, not a video's. A volume remembered before this update is forgotten once.
  • New: clip-rounded, for a rounded frame that cuts what it holds smoothly, whatever it holds: Chrome cuts a child with a layer of its own (a video, a playing stage) to a parent's rounded corners without anti-aliasing; through a mask the cut is smooth and nothing beneath leaks. Use it with the frame's radius (rounded-xl clip-rounded). Not on a frame whose content uses backdrop blur that must see past it (a glass pane's buttons): a mask makes the frame a backdrop root.
  • Change: the focus ring's base rule and Tailwind's focus-visible: variant (redefined here, so every focus-visible: utility follows) skip an element marked data-focus-pointer. Mount focus-source to make the ring keyboard-only in practice. A marked element also loses the browser's own default outline, which still drew once a key (Escape closing a popover) made the browser call the focus visible.
  • Change: DisplayVideo is a video following its display: playing while looked at, holding its frame after. Pulls @ag/video.
  • Fix: a display's corners stay smooth while what it holds plays (a video, a stage, phones): the body is cut with clip-rounded. They stepped in Chrome, the page's fill showing round them.
  • Change: the popover's clip is a video following the scene's clock, its still in the first paint as before. Pulls @ag/video.
  • Fix: the popover's corners and a frameless stage's corners stay smooth while the clip plays (clip-rounded).
  • Change: a gif plays on sight and loops, as X plays one (it waited for a click); a video keeps the browser's own bar. Both are video, which this item now pulls. The media grid's rounded corners stay smooth over a playing video (clip-rounded).
  • Change: a slide's video is a video with the browser's bar; the engine keeps its keys. Pulls @ag/video.
  • Breaking: playhead is now playback. The item holds the clock and Playback, so it is named for them; usePlayhead keeps its name (where the playhead is). Migrate: install @ag/playback, change imports from ui/playhead to ui/playback, remove your playhead.tsx. Every item that depended on @ag/playhead (terminal, macos, telegram-chat, liquid-glass, player) now pulls @ag/playback.
  • Breaking: <Playhead at={p}> is <Playhead progress={p}>. It is a timebase the page holds; moving it from inside (a bar placed under it) throws instead of silently doing nothing.
  • New: useTimebase() (its controls move the clock: play, pause, seek, step, jump, preview, loop, speed) and useTimebaseState(select) (a slice of the clock, re-rendering only when that slice changes). Content still reads usePlayhead, and now re-renders only when its instant moves rather than with its whole subtree.
  • New: useOutline() and OutlineProvider: how the clip is cut, beside the clock rather than in it. Playback and Player provide it from the clip.
  • New: autoPlay={false} for a clip whose person decides when it plays.
  • New: useMediaTimebase(element, options), a media element as the owner of a timebase, and useStageClicks, a stage's click to play and double click to fullscreen. VideoPlayback is a thin user of the hook and AudioPlayback (audio) another, so a video and a sound seek, step, preview, loop, remember and start the same way. The clock is read off the element: a video's is the frame it presented and runs on between frames at the element's speed (never past the next frame's time), so a still over a soundtrack at 1 fps tells a transcript every word, not one time a second; a sound's is its current time once a display frame while it plays. A media element whose metadata arrived before the page hydrated (an <audio> rendered on the server starts loading at once) is read as it attaches, its length, start and autoPlay included.
  • New: useHoverIntent(show, end), the one rule every timeline scrubs by: hovering shows a moment only once the hand has meant it (resting or slow for 120 ms, not while it passes over fast or the page scrolls it by), then follows the hand at once; a press is intent whatever came before. PlayerBar and the transcript's words use it, so travelling over either never flashes the moments under it.
  • New: a picture's zoom (usePictureZoom, lib/picture-zoom). Zooming in stops first at fill (the picture just covers the stage, its bars gone), then goes on freely; zooming out catches at fill, then fit. Under a pinch each stop is a detent the hand pushes through; keys and buttons step to the stops exactly. Every change is named in the player's flash pill ("fill", "2.4×") and to a screen reader.
  • New: PlayerLayer and usePlayerLayer: where a player's popups go. Portalled into the player's own box, a popup was one more child of its layout and opening one pushed the page down; portalled to the page's body, it vanished in fullscreen. The layer is out of the flow and inside the fullscreen element, and the speed menus use it too. It is the popups' containing block (contain: layout), so opening one (the playlist card, the speed menu from the keyboard) never scrolls the page to its foot, and it carries its own z-50.
  • New: PlaylistProvider and usePlaylist: media played one after another. The player is keyed by the item, starts it when continuing (reached by watching or picking), and calls advance on its end; loop goes from the last back to the first.
  • New: the one clock an instant is read from and the contract everything that shows or moves time speaks: one owner (a clip's own clock, a video's decoder), any number of followers that never know which. What a timebase can do beyond play, pause and seek is stated, never assumed: fps when it knows its frames, volume (null without sound) with setVolume and toggleMute, setRate where speed can change, and root(), the element its owner draws in. Also the frame arithmetic: frameAt, frameTime (the middle of a frame, so a step of one always moves), frameCount, timecode, clockTime, measureRate (a frame rate from presented frames, snapped to the standard rates). Framework-free; times are ms.
  • New: the outline. Division (a start and a title), Level (one depth: chapters, scenes, shots), Outline (coarse to fine), outlineOf(clip), divisionSpans, divisionAt. clip.chapters is unchanged and is the first level; finer ones go in clip.levels. Nesting is computed from starts, never stored.
  • New: clip.fps, for a clip told in frames: its timebase steps by them.
  • New: PlayerBar, the bar alone over any timebase, for compositions beyond the preset. Player is now a Playback, the content and a PlayerBar; its props are unchanged.
  • New: the rest of a player, each a part over the timebase around it and shown only where the timebase can do it: PlayerKeys (page-wide, one per page: space and K play; ← → step a frame on a paused picture and jump 5 s otherwise; J and L jump 5 s; shift jumps 30 s; , . step a frame; Home, End, F fullscreen, M mute; presses add up), PlayerGestures (on whatever it is placed in: swipe to scrub, vertical for volume on the left half and speed on the right, pinch for fullscreen, only while wholly on screen), PlayerChrome (the controls' layer over a picture, shown on hover, focus, pause or preview) holding PlayerTransport (the centre cluster: previous, jump back, play, jump forward, next) and PlayerFoot (the bar on a scrim), PlayerSpeed (a menu of speeds, opening under the hand), PlayerSound (mute, the volume opening above it). Each is logic once (useTransport, useSpeed, useSound) under its face; liquid-glass has the glass faces. The key table and the swipe grammar are framework-free in lib/player-input.
  • New: the bar reads elapsed time on its left and the length on its right; a click on the length shows what is left (-0:02), a click on the elapsed side moves through the clock, the frame beside it (54 · 0:02) and the editors' timecode (00:00:02:04), the last two where the timebase knows its frames (time picks the first). Every readout holds the width of its widest value (PlayerReadout), so nothing on the row moves while it counts.
  • Change: hovering the bar holds the clock while it shows that instant (it used to keep running underneath), and letting go of the bar plays from there (a click or drag on a paused player used to leave it paused). A horizontal swipe on the content keeps the play state.
  • New: PlayerBar snaps, more points the hand is pulled onto while it scrubs, beside the chapter starts, each naming itself in the tip when it lands there (PlayerSnap: at, label). Letting go on one plays from exactly there.
  • New: PlayerBar's face: a drawing the timeline shows instead of its chapter bar (a SoundTimeline, or anything that reads the timebase and fills the width), its height the face's own. The track stays the slider, with the same keys, hover preview, chapter snap and time above the pointer.
  • New: PlayerTransport and GlassPlayerTransport only: just the buttons a small stage needs. GlassPlayerTransport fill: one button (only={["play"]}) the stage's whole size, its icon grown with it, so a round face is itself play and pause. Its glass is clear (no blur, no tint, its rim and a shadowed icon), so a paused frame stays seen under it.
  • New: the transport steps aside while a moment is previewed (the bar or a word under the hand): play and pause never cover the frame being looked for. The chrome around it stays.
  • New: the controls name themselves on hover: the transport's buttons ("back 10 s", play with its key), the bar's play button, and each end of the bar (what a click shows next). A key is named only where PlayerKeys listens on the page. The transport's buttons are data once (useTransportButtons) for both faces.
  • New: inside a PlaylistProvider, the transport's previous and next go through its videos instead of the chapters (previous a few seconds in restarts the one playing), and they are listed to pick from (poster, length, the name whole, the one playing lit): PlayerPlaylist beside or under the player, PlayerPlaylistButton as a card over the picture from a button in the bar (opening on hover, the speed menu's timings, with no tooltip naming the button first; a press or the keyboard still opens it), PlaylistRows for a face of your own; liquid-glass has GlassPlayerPlaylist. P and N are previous and next for PlayerKeys. PlayerKeysCard draws the keys as a legend, for a TooltipHint card.
  • New: a question's answers as keys and buttons: yes and no, or a choice on the number keys, skip and undo, every press shown on its button, and a receipt that reveals a reference answer only after yours. For grading, labelling, review and flashcards; one listens to the keyboard per page.

playhead, glass-display

  • Fix: one contract for every display: looked at, it plays on from the frame it stands on; looked away, it holds that frame. A page-driven Playback (playing={false}) used to wind back to its start, so a card resting on its last frame played on to the end after the pointer left and one resting mid-story rewound; DisplayVideo paused and cut back to its first frame. Now both stop where they are, and a held video stays on screen instead of fading back to its poster.
  • New: mediaCdn(host), the serving half of prepare-media: renditions cut at the edge on request by Cloudflare's transformations, on the host you name. image(src, { width, quality }) strips metadata and picks the browser's format at the width shown; frame(src, second, width) is a video's exact frame at a second, the picture macos's still wants. What is known ahead of time stays prepare-media's (files, once); what is known only when a page draws is this.
  • New: still, a still of a surface's clip at a second ((src, second, width) => url, from wherever the page's media can be cut, a CDN's frame extraction). A popover first drawn at any second of its clip (a card resting on its poster frame mid-story) shows that very frame from the first paint while its video loads, where it used to wait empty. Without it, only a popover opened at its clip's start shows whole.
  • Fix: a popover opened at its clip's start is whole from the first paint. Its poster is the clip's first frame, so while the story wants that second the poster is the right picture and stands until the video takes over; it used to be hidden like any other poster, and the popover hung empty for the half second the video took to load. At any other second the clip still waits unseen until it is on its frame.
  • Fix: an app's surface (the popover hanging from its menu bar glyph) is in the server's frame, where it hangs. It is placed by CSS anchor positioning against the status item, one anchor name per stage, where it used to wait for a measurement after hydration, so the first frame had no popover and the next had one.
  • Fix: the surface's clip stays unseen until it stands on the scene's second, then dissolves in. Its poster, another moment of the app, used to show first and then jump to the right frame (a card resting on its poster frame flashed two states of the app).

liquid-glass (glass-display)

  • Fix: a display's corners are clean in Chrome. Its scrim is its dark gradient alone, no backdrop blur (Chrome clips a backdrop blur to rounded corners without anti-aliasing, so the foot corners stepped; Safari drew them clean), and its rim is drawn whole inside the edge, one pixel wide at half the light, outside the clipped body, where a rounded mask used to cut it.
  • New: GlassFrame, the display's frame alone: media to the edge, the glass only that edge, scrim, caption and cover as before, and nothing that moves or answers the pointer. For media looked at in place (a hero's picture, a video with its own controls), where a lift under the pointer moved the control being reached for. GlassDisplay is now that frame plus the look and lift, for displays that are doors (an index card); its props are unchanged.
  • New: AmbientLight, broad soft light behind a page that does not band. A CSS gradient on a dark ground falls through a handful of 8-bit levels and shows each as a ring, and grain laid over it afterwards cannot remove rings already in the pixels. Here the ground and each glow (color, centre x/y, reach rx/ry as fractions of the box, strength) are mixed per pixel in floating point on a smooth fall (a smoothstep on the squared distance: a broad, flat shoulder easing out toward the reach, a wash rather than a hotspot), a triangular noise of one level is added, and only then is the pixel rounded. Drawn into a canvas at half the box's resolution; colours may use var().
  • The light is in the page's first paint: an inline script after the canvas paints it while the page is still being parsed (the stylesheets have loaded, the box has its size), so it never fades in over a bare ground after hydration. The component paints it again only when the client mounted it, on a resize or on a theme change. There is no fade (data-lit).

scroll-fade

  • New: ScrollFade, a capped scroller that ends in a fade instead of a clip (a kanban's column, a list in a panel). The content fades itself out at its edges (ag-edge-fade-y) and a thin colourless blur (--scroll-fade, 0.75rem) lies across each end, always, at rest as mid-scroll; the scroller's padding is deeper than it, so the rows at either end of the scroll stand clear of it. No script. Nothing paints a coat, so on glass the pane's body and edge run on beneath it. topFog={false} when a pane's front surface is the top edge.

liquid-glass (demo)

  • New: the demo page shows GlassPane: a front surface across the top in its own colour with a list passing under it, and tinted cards with a clear front surface at the foot.
  • New: ag-edge-fade, a class for anything that scrolls sideways (a command, a row of icons): no scrollbar drawn, and the side with more to show fades out (--edge-fade, default 1.5rem), read from the scroll; nothing fades where nothing scrolls. code uses it for its compact row, so it depends on @ag/scrims.
  • New: ag-edge-fade-y, the edge fade for a vertical scroller (a kanban column, a capped list): its top and foot fade to nothing as the scroll says there is more, read from the scroll itself and with no colour of its own, so it sits on glass or any surface without a band. No script.

playhead, liquid-glass (DisplayPlayback)

  • Fix: a clip the page plays (playing, a display under the pointer) plays on from the frame it rests on, to the end and round again from the top, and winds back to its rest at the rewind's pace when the page stops it. It used to cut to the first frame on hover and back to the rest frame on leave, so a card resting on its best frame jumped to the story's opening the moment a pointer touched it (or rested on it as the page loaded).
  • New: at, the moment (ms after the page's navigation) before which something on screen at load may not arrive. It runs on the document's clock, so several pieces on a page keep their order however long each takes to hydrate. Past that moment, or for what scrolls into view later, it arrives at once.
  • Fix: anything on screen when the page loads, even by a sliver, arrives at once. It waited for 20% of itself to be inside the window minus its bottom tenth, so a row at the fold stayed blank until the reader scrolled.
  • Fix: an open hinged MacbookFrame (lid set, as macos sets it) is the same drawing as a flat one. Its lid stood in front of the base even fully open, so the lid's rounded foot and its shadow lay over the base's top edge; it now stands behind the base until it starts to close, then comes in front.

focus (tokens, and every item with a control)

  • New: one focus ring for everything, defined in @ag/tokens: a fine line (--focus-ring-width, 1.5px) just off the shape's edge (--focus-ring-offset, 3px), in the content's own colour at --focus-ring-alpha (45%), following the corner radius. It answers the keyboard only (:focus-visible); a click or a tap shows nothing. A base rule gives it to every focusable element, so no control and no link inside an embed falls back to the browser's default ring; focus-ring is the utility for drawing it on another element than the one focused (a field's glass, a carousel's card). Elements a script focuses (tabindex="-1": dialogs, stages, scrollers) are never ringed. copy, card-gallery, preview-picker, media-spec, image, video, liquid-glass, player and lightbox dropped their own rings (2px --ring, outline-2) for it and now depend on @ag/tokens.
  • Fix: Player's bar draws no box around itself when focused. Under the keyboard it answers as it does under the pointer: the bar thickens and the chapter you are in lights. A click on the bar followed by a key (space to pause) used to put a heavy grey ring around the whole bar.
  • Fix: GlassInput drops the outline it drew round its glass on focus. A text field is focus-visible to a click as much as to Tab, so the ring showed for everyone; the field's focus is its rim catching more light, as before.
  • Fix: GlassSegmented's options take the shared ring, drawn inside the bar.
  • New: GlassPane, a pane of glass in two surfaces with one edge round both. The back surface is the pane's body, what its content lies on; the front surface (front) is a strip of glass in front of the content, across the top or along the foot (frontAt), part of the pane: it takes the pane's corners on its side, has one straight inner edge lit as the rim is, and blurs what passes under it, so content running beneath it shows through only as light. Each surface takes its own colour, backColor and frontColor, set on that surface alone, so a pane inside a pane (a card on a board) keeps its own; tint hues the whole pane. The pane is built in layers (back, content, front, edge), its root carrying no blur, so the front surface can blur what passes beneath it and the whole edge, rim and inner glow, stays above everything inside. The front's height (frontHeight, 3rem) reaches the content as --glass-pane-front-height to pad by, its blur is --glass-pane-front-blur (12px). A board's columns under their names, a card over its footer.
  • New: a tinted glass works out its own edge from the tint (rimInk): the rim and inner glow are lit in the same hue made vivid and readable on dark (lightness at least 0.78, chroma doubled up to 0.2), while the body keeps only a hint. One tint is all a glass needs; rimColor still overrides. Exposed as --glass-rim-ink for content to write in the same colour. Tinted buttons and panes say their hue at the edge instead of a white rim leaning faintly toward it.
  • Fix: a dark glass given a hue takes it at depth (deepInk, --glass-hue-deep: the rim ink's hue and chroma, lightness held at 0.5 or below), not in the light form its edge wears. The light form mixed into a dark body read as a washed grey haze, worst with a pale tint and a generous tintAmount; at depth the body saturates. The edge keeps the light form; light glass keeps it in its body too.
  • Fix: every glass control shows the same pointer, the hand, whether it is a link or a button (GlassButton, GlassToggle, GlassSegmented's options). A button showed the arrow beside a link showing the hand.
  • Fix: a glass's light eases with its movement. The rim's brightness (--glass-rim) is a registered number (liquid-glass.css), so it transitions: a GlassPane lifting under the pointer brightens its edge over the same 500ms and ease as the lift (GlassDisplay's), the light trailing the movement by 40ms both ways, and a button's rim rises on its own spring. It used to jump to full brightness while the glass was still moving.
  • New: GlassBadge, a small label of glass (a stack's tool, a status, a version) in metadata's voice; tint stains it a status's colour, as makes it a link.
  • New: GlassMenu (glass-menu.tsx), a dropdown made of glass on Base UI's menu, its parts named as a dropdown's: GlassMenuTrigger (render makes any button the trigger), GlassMenuContent, GlassMenuItem (destructive turns its words and the glide red), GlassMenuLabel, GlassMenuSeparator. It never locks the page's scroll, opens at least as wide as its trigger with a soft button's corners, and one highlight glides between its rows. The collection now depends on @base-ui/react.
  • New: GlassSubmit (in glass-input.tsx), the button that sends a form of fields: locked (dimmed, a dull rim) until the form is valid, still pressable; pressed early it shakes and every wrong field says why, the first taking focus; unlocking, what it holds nudges forward once. Reduced motion skips both movements.
  • New: GlassCard, a card of glass with a title and a description above its content.
  • New: the rim catches the glass's colour: white leaning toward tint three times as strongly as the body, or exactly rimColor (--glass-rim-color). Untinted glass is unchanged.
  • Fix: GlassInput has the rim every other glass control has (an input draws no ::before; the glass is now a wrapper around a bare input). Its message hangs on a small tag of red-rimmed glass from the field's left edge, readable over any background, where bare red text vanished; wrong, the red is on the rim and the body only faintly red, since red mixed deep into dark glass reads brown. It checks as you type and tells the form (so form:valid holds), still speaking only once you leave or submit; the browser's own rules speak when a schema or function is satisfied.
  • Fix: GlassGlide appears and leaves where the row is, fading; it glides only between rows while shown, where it used to travel from or toward the top.
  • New: GlassInput (glass-input.tsx), a text field made of glass at the button's heights and corners, its focus the rim's light and the glass's fine line, never a coloured border. It validates the way the page does: schema (any Standard Schema: zod, valibot, arktype), validate (value to message), or the browser's own rules and messages, plus error from outside; it judges on leaving the field, then on every keystroke once it has spoken; wrong, the glass takes a faint red and the message sits under the field without moving anything, announced and tied to it. GlassButton takes corners, pill (default) or soft.
  • New: GlassGlide (glass-glide.tsx): one highlight for a whole list, gliding to the row under the pointer or holding focus (a menu's items, a rail's links); a row's --glide-tone colours it, blending from row to row, and without one it is a faint neutral. Pressed glass controls swell a little on a spring; GlassToggle given on and off holds the wider's width; focus is the glass's own fine line.
  • New: Img (image): a plain <img> that arrives as Image does (held clear and a little blurred until decoded, then fading and sharpening in), for a picture the optimizer should not touch; it reads an image that finished before hydration. The display's video poster uses it.
  • New: liquid-glass is the glass collection, one item of several files: the material (Glass), GlassButton (three sizes and three icon-only circles, a <button> or anything as makes it, a link most often), GlassToggle (controlled pressed, so a form's submit button can be one with no client code; on, it wears the text's colour faintly in its glass with a brighter rim), GlassSegmented (glass-segmented.tsx: one choice among a few in a glass bar, a highlight gliding to the chosen option, a radio group with roving focus and the arrow keys), and GlassDisplay (glass-display.tsx, below). The collection now depends on @ag/scrims, @ag/playhead and @ag/clip, for the display.
  • New: GlassDisplay, a floating glass display around any media (an image, a video, any component): the media fills it edge to edge and the glass is only its edge: liquid-glass's inner light, and its specular rim (the same 135° light) drawn as an SVG stroke so the corners stay smooth, where a masked ring steps. radius sets the corners for the box, its clip and the rim at once. caption sits at its foot on the scrims scrim (scrim, scrimHeight), cover makes the whole display one target (a link or a button), rim sets the edge's light. Looked at (the pointer on it, or focus inside it) it lifts: it rises 2px on a smooth ease-out, its shadow deepens and its rim light comes up, and it never resizes; it is its own compositing layer, moved as a finished picture, so the hairline, the clip and the scrim's blur are never redrawn between pixels. Reduced motion keeps the light and the shadow. Its scrim reaches past the foot, so the blur's own bright edge is never in view. What is inside plays while it is looked at: DisplayPlayback for any clip on the playhead, DisplayVideo for an uploaded video over its poster, useDisplayHover for anything else.
  • New: rim and rimWidth on Glass tune the edge: rim is the rim's and inner reflections' brightness as a multiple of the default (1), rimWidth its thickness (default 0.5px). Also as CSS, --glass-rim and --glass-rim-width. Unset, the glass is exactly as before.
  • New: tint and tintAmount on Glass stain the glass a colour, faintly: the colour is mixed into the tone's own tint (tintAmount, 0 to 1, default 0.12), never replacing it. As CSS, --glass-hue and --glass-hue-amount, so a parent (a brand's page, a project's subtree) stains every glass inside it. Unset, the glass is exactly as before.

liquid-glass · display

  • Fix: a GlassDisplay under the pointer waits for the page to stop scrolling (150 ms still, wheel or scroll) before it counts as looked at, and then wakes without the pointer moving; a pointer resting on a display when the page loads wakes it too. A page scrolled under a still pointer made the browser report an enter on every display it crossed, so each lifted and started its clip as it passed, and the scroll lost its frames to them. Touch never counts as hover.
  • New: --player-gap sets the gap between the stage and its controls (default 1rem), for a page that spaces its parts by its own step.
  • Fix: the play button's icon starts on the stage's edge (the button is pulled out by the room its circle leaves around the icon), so the controls line up with the stage and a caption under them; the chapter line follows the track.
  • Fix: a compact command too long for its row scrolls on one line with no scrollbar drawn over it; the side with more of the command fades out, read from the scroll (the end at the start, the start at the end, both between), and a command that fits has no fade. Its row sizes to the whole command where there is room: the scroller's basis was 0, so a fit-content row thought the command had no length.
  • New: a demo page: scrub a moment on a track and the MacBook frame draws it.

macos · player

  • Fix: the shut lid's "critical heat" alert belongs to the stretch it was raised in: it stays while that lid stays shut, and closing the lid again starts clean. It stayed for the rest of the story, so a later sleep for another reason (the battery floor, hours after the Mac cooled) showed the old heat as if it were why.
  • Fix: a menu bar app's popover arrow is drawn as macOS draws it, 36 by 13 points, straight sides near 45° with a small round tip and small fillets into the panel, in the panel's own colour, its outline continuing the panel's border with no seam across its base. It was a 22 by 10 tent. The panel hangs 3 points lower to make room.
  • New: captions={false} on Macos shows only the Mac, no caption line under it. Breaking: Player's captioned is now chapters, "line" (default, the chapter on screen under the bar) or "hover" (only under the pointer); captioned becomes chapters="hover".
  • New: runsOn on Macos: past the story's end the machine keeps running while the stage is on screen, its fans turning on at their last speed and its sensors drifting, so a hero never ends on a still frame. Off by default. thermalAt adds the fans' turns past the last key in one step, so running on costs the same every frame.
  • New: what a title shows, beside what a file carries. AgeBadge is one reusable disc per age (AGES: all, 7, 12, 13, 15, 16, 17, 18): the age in a round mark of its band's colour (ageBand: green to 7, amber 12 to 16, red from 17), kept whatever the tone because the colour is the meaning. RatingChip draws a title's rating ({ board: "ES", value: "16" }) as the disc of the age its board admits, ratingAge(rating) (US R reads 17, ES 16 reads 16, a guidance PG reads 0); the board's own word is the title and the accessible name, never a flag beside it; a value stating no age draws its word. AdvisoryChip draws one category of a title's advisory (nudity, violence, profanity, substances, frightening) with its glyph, its name above the poster rung, and its severity as a three-step meter; none is three empty steps, a known zero, and an absent category is unknown. MediaSpec takes rating and advisory and draws them after the cut; omit and adornments know both kinds. Inks: --ag-media-rating, --ag-media-advisory. The five glyphs are tabler (MIT).
  • New: how hot a machine runs, over time, framework-free. A ThermalReading is what the machine reports (each part's °C, the case's skin, each fan's share of its range); a track keys readings and view depths in ms, and thermalAt(track, ms) folds it to the one moment a frame draws: readings glide from one to the next, a depth fades in DEPTH_MS, and each fan's turns are its rate added up, so a fan speeding up never jumps. fanTurnRate(share) is the one rule for how fast a fan is drawn turning.
  • New: MacbookFrame shows the machine's heat with thermal (a reading, t, turns, depth). Depth 1 is the skin, the base's face as a thermal camera sees the case, ice where it is cool and ember as it warms. Depth 2 is inside, flat in the base's face: the chip, memory, ssd and battery cells as hairlines, each fan a blower seen edge-on with its slats turning, and the air as a fine fluid in at the sides, through the fans, over the parts and out along the hinge, warming over the chip. The skin and the body's outer details recede as the insides come in. A shut lid's face warms with the same skin, as a bag heats both halves. t is the playback clock, never a story clock running faster.
  • New: a thermal step: a reading (thermal), a view depth (depth), or both. Readings glide into one another over the time between them (an author's @ms); a depth change is given time to be watched. thermalKeys(timeline, clock) is the scene's track. While the view shows, the camera holds the whole machine.
  • New: a scene's thermal steps drive the MacBook's heat view at the playhead, on the same clock as the lid, the camera and the captions, so a story walks from the menu bar into the machine and back with no cut. On a display or the bare screen the scene plays the same, its heat unshown.
  • New: GitHub drawn from its facts, no API and no token at render. Github draws whatever a github.com URL is: a repository's pinned card (description, topics, language, stars, forks, license), an issue or pull request (state in GitHub's colours, labels in their own, the diff and branches), one comment with its thread on a line above it and GitHub's own sanitized markdown (syntax colours included), a profile with ContributionCalendar, the last year in GitHub's five greens, light and dark. lines cuts a long body with a fade and a link to the rest. Every card opens its place through card-link. fetchGithub(url, token) in github-data reads the facts once with the author's token.
  • Reactions under an issue, a pull request and a comment, GitHub's eight in GitHub's order. commits lists a repository's latest commits or a pull request's last ones (newest first, up to five kept in the facts): author, message, short sha, day, each a link. An organization's avatar is a rounded square, a person's round. Breaking for kept facts: GithubRepo.commits, GithubThread.reactions and GithubComment.reactions are required; refetch facts written before today.
  • The whole post opens the post on X (card-link), and a quoted post opens itself; the post lightens a step under the pointer. The mark and the date stay the keyboard's way there.
  • New: a post on X drawn from its facts, with no widget script and no request at render. The author with the verified badge (blue, business gold, government grey; businesses in a rounded square), the text with mentions, hashtags and links, X's grid for one to four pictures, video with its poster, a quoted post drawn small inside, a link card or X article through web-preview's x style, the date and the like and reply counts. fetchTweet(url) in tweet-data reads the facts once, from the endpoint X's own embed reads; keep them beside the page.
  • New: a public channel post as a chat of one. fetchTelegramPost(url) in telegram-chat-post reads t.me's embed page into the facts and postScript(post) makes them the chat's own script, drawn with frame="none" backdrop text="page" composer={false} frozen. Its pictures are on signed CDN paths that expire: download them beside the page.
  • New: backdrop keeps Telegram's chat background (screen colour, gradient, a gutter) under a frameless chat instead of the page's. text="page" sets a frameless chat's words at the page's own size with bubbles capped at a reading measure (--tg-measure, 60 characters), for a post inside prose; the default scaled is unchanged.
  • Fix: code blocks wrap instead of scrolling sideways inside the bubble.
  • New: day on a message draws the client's day separator before it ("23 July 2024"); a bubble's foot reads in the client's order, views, edited, then the hour. postScript(post, { timeZone }) writes the day and the hour in that zone (UTC when omitted), since a page rendered ahead of time has no reader's zone to use.
  • New: href makes the whole chat open a place (a post's permalink, through card-link), with received bubbles tinting under the pointer. In a channel, the channel's name links to the channel.
  • New: a channel chat kind, labelled like a group with the channel's name in the accent. Messages take rich (formatted text: bold, italic, underline, strike, inline code, spoilers, links and code blocks with their language), photo (edge to edge at the head of the bubble) and foot (the time, linked when given an href, the views and edited). A message with both text and rich throws.
  • New: a person as every surface that shows one needs them (Person: the name, the @handle, the picture, its animated video), defined once by the page and handed to each component, so the same person wears the same face in a chat and on a Mac's lock screen. telegram-chat's ChatProfile extends it with bot; macos takes one as user.
  • New: stereo 3D on the picture axis. STEREOS (sbs, half-sbs, tab, half-tab, mvc, and 3d when the layout is unnamed), stereo on PictureChip and MediaSpec, drawn as one 3D badge after the range, with the layout in the axis label ("1080p · 3D half side-by-side"). New: stereoLabel; pictureLabel takes stereo as an optional fourth argument. A flat picture passes nothing and renders as before.
  • Fix: the resolution sticker's lower band shows its word ("ULTRA HD", "FULL HD"); it was drawn in the band's own colour and read as an empty panel.
  • Fix: gold at vivid, and every gold badge at sm, draws its metal and letters; both were empty boxes.
  • vivid keeps the chip in its own ink: the frame goes to the full ink and the glow grows, and the word no longer turns white inside a coloured frame. A consumer that relied on vivid reading white sets the kind's ink to white.
  • The poster rung's scrim is a plain fill, no backdrop blur: a grid of posters re-blurred every badge on every animated frame.
  • A lockup's word stands at the font's cap height: DOLBY's capitals are TrueHD's beside it, and IMAX and ULTRA HD no longer tower over the row. Each lockup's size comes from its measured cap (the manifest, see MarkArt.cap), not from its width, and uses the CSS cap unit. Breaking: MarkArt.em is gone (a symbol stands at 1em, a lockup at 1cap / cap); Mark's em prop still overrides. New: markArt(id, form).
  • Fix: the ultra-hd mark's "HD" is visible. The source artwork drew it in hairlines under a pixel wide at any chip size; it is redrawn at a light weight in the same place, so ULTRA, the spacing and the mark's size are unchanged.
  • New: a coding agent's session as data (AgentEntry: a prompt, a reply, a tool call with its result; AgentWork: how long the turn and the request in flight have run in the world's seconds, the output tokens so far, and screen time in ms, facts each skin words and animates itself; AgentFinished: how long the finished turn took), the props every agent skin takes (AgentViewProps), formatElapsed, elapsed time the way both CLIs say it, and entryTokens / requestTokens / formatTokens, the output a turn counts up (each reply and tool call its thinking and words, a request nothing until its thinking is over).
  • New: anything drawn from a moment, as the facts a driver moves through it by: Clip (its length in ms and its Chapters), progressAt and spans; chapterSpan(clip, first, last) for the stretch an act tells, and progressWithin(clip, span, local) for a card scrubbed across only its part of a clip. The content's lib makes the clip, a driver moves through it (the player by the clock, a scroll stage by scroll, a still at one moment), the component draws the frame at a progress. One contract, so any content plays in any driver.
  • New: Chapter.code, a title that is code as typed (a terminal's command), for a driver to draw in mono.

playhead

  • New: where a clip is and where it is going, with one clock walking the first to the second while the content is on screen and the tab is visible. Every driver sets the target: Playback plays a clip once in view with no controls (start where it stands first, delay before it plays), the player's bar aims and seeks, and a scroll stage's cue (a span to be in, heading to its end, from chapterSpan) aims it act by act, so scroll and a bar move one playhead and whichever moved last wins. An aim travels (forward at the clip's pace, backward at 3x, so scrolling back an act unwinds it) and a seek jumps; reduced motion lands every aim at once. usePlayback(clip, options) is the clock for a driver of your own. Playhead provides the moment and usePlayhead(progress) reads it as { at, driven }: an explicit progress wins (a still, driven: false), else the driver's (driven: true, it can change at any moment), and a component placed where nothing moves it throws. So a page composes plain elements, <Playback clip={clip}><Terminal session={s} /></Playback>, from a server component too.
  • New: playing, for a page that decides when a clip plays (a card under the pointer): true plays it from the top, false returns it to where it rests (start, a poster frame). Set, it replaces playing once on screen.
  • New: usePlayhead(…).playing (and Playhead's playing), true while time runs forward at the clip's pace, for content with a clock of its own (a video) to run alongside and hold still otherwise. usePlayback returns it too.
  • Breaking: the terminal draws and no longer plays itself. The in-view autoplay and duration are gone: place it inside <Player clip={sessionTimeline(parseSession(session)).clip}>, or pass a fixed progress for a still.
  • New: size, the type size as any CSS length (default 13px); the chrome is in em and scales with it. A box the terminal must fill passes stillSize().
  • The window parts from the page: a hairline in its own accent at low alpha (TerminalPalette.edge) and a soft, low shadow, so its near-black no longer melts into a dark page. A frame that fills a box of its own (a card) drops both with its className.
  • sessionTimeline(…).clip: the session as a clip, one chapter per command titled with it, as typed (no $), marked code.
  • New: STILL, the still's frame and type in px (the defaults of renderSessionSvg), and stillSize(), the still's type as a share of its width in cqw, so a live terminal in an inline-size container draws the frame the still draws.
  • A scroll stage drives any clip a player plays: an act cues the playhead (<Player cue={chapterSpan(clip, k)}>, so scroll and the bar share one story), and a beat's progress scrubs a card directly (progressWithin). clipBeats and clipProgress, shipped earlier today, are gone with that.
  • Breaking: the chat draws and no longer plays itself. from, until, duration and afterlifeDelay are gone, each a driver's job now: wrap it in <Playback clip={chatClip(script)}> (with start={clip.chapters[k].start} for a story that opens at message k, delay for a phone that waits its turn), in a Player, or cue it from a scroll stage (cue={chapterSpan(clip, k)} is the old until); a fixed progress is a still. frozen stays, for captures with nothing decorative moving.
  • chatClip(script): the chat as a clip, one chapter per message at its natural pace titled who and what ("Melon: you have to watch this"), then the afterlife (reactions, late messages) as a last chapter, later: it scrubs and replays like the story, and a still at 1 shows it whole. storyEnd(script) is where the story ends in it.
  • Breaking: from="conversation" is gone with the chat's own clock. summaryClip(script) is the summary's clip and conversationStart(script) now returns where the conversation starts in it (ms): <Playback clip={summaryClip(s)} start={conversationStart(s)}><TelegramSummary script={s} /></Playback>.
  • New: a player for any clip (<Player clip label><Terminal session={s} /></Player>: a macos stage, a terminal, a chat; it moves the content's playhead). It is Playback with a bar and takes the same start, delay and cue, so a scroll stage and the bar drive one story. The chapter on screen is named on a reserved line under the bar; captioned leaves that to content that captions itself (a macos scene). It plays once in view; a thick timeline under the content, cut into its chapters, fills in the accent. Hovering the bar shows that exact frame, with the chapter and the time above the pointer, and a pointer near a chapter's start is pulled onto it (a marker stands at the boundary, the tooltip says so). A click or a drag seeks without losing the play state, a horizontal swipe scrubs, and the bar is a slider: space plays and pauses, ← and → move between chapters, Home and End go to the ends.
  • A code chapter (a command) is drawn in mono, on the chapter line and in the bar's tooltip.
  • A click anywhere on the content plays and pauses, as on a video; a control inside the content (a button, a link, a reaction pill) keeps its own click.
  • New: keyboard shortcuts as data. keys("⌃⌥⌘A"), keys("ctrl+alt+cmd+a") and keys("← →") split any notation into the keys pressed, in the Mac's glyphs and order; anything that is not keys (awake hotkey, 2h) is null, so a renderer can tell a shortcut from a command. withKeys(text) finds the shortcuts inside a sentence.
  • New: keyboard shortcuts drawn as keys. <Kbd keys="⌃⌥⌘A" /> in any notation (it throws on a string that is not keys), <KeysText> for a sentence with its shortcuts drawn as keys, and isKeys for a renderer choosing between keys and code. Nested <kbd>, as HTML defines a key combination.
  • New: Claude Code's terminal UI drawn from an agent-session: the welcome box, your prompts as full-width grey bars (the only tinted lines, as in the real CLI), ⏺ replies and tool calls with their ⎿ results, the working line as the CLI draws it (✢ Shimmying… (1s), then (5s · thinking), (14s · still thinking), (25s · thinking more), the stage in the verb's colour, and ↓ 2.9k tokens once the turn has output: the spinner breathing through · ✢ ✳ ✶ ✻ ✽, a light band sweeping the verb, a new verb from Claude Code's own list on every request, all a pure function of time), the grey line a finished turn leaves (✻ Sautéed for 8m 37s), the prompt box. Reads like a terminal: from the top while it fits, the oldest lines scrolled away once it does not.
  • New: OpenAI Codex CLI's terminal UI drawn from an agent-session: the header box, › prompts, • replies and Ran commands with their └ output, the working line, the ─ Worked for 2h 14m rule a finished turn leaves, the composer. Same terminal reading as claude-code.
  • New: a menu bar app performed on a Mac screen. <Macos timeline art alt /> draws the menu bar with the app's own glyph, the macOS 26 capsule battery (no percentage) and the clock, each item in the system's own padded box so the icons sit evenly apart, the app's real menu (check column, key equivalents, submenus opening to the side with room), a terminal driving it, notification banners, the keyboard shortcut as keycaps, a pointer that travels to whatever the story clicks with the pressed mouse button shown beside the keycaps, and captions, with any shortcut in them drawn as keys (@ag/kbd). A story can open a coding agent in the terminal (agents={{ claude: ClaudeCode, codex: Codex }}: the page supplies the skins, so this item depends on none of them), and the camera closes in on a glyph before the chord or right-click that flips it. device="macbook" puts it on device-frame's MacBook and closes the lid for real when the scene does, and while it is shut the dark panel shows the time passing, large; device="display" puts the panel to sleep instead. A camera closes in on the menu bar corner while a menu or banner is up or the glyph has just changed, as far as the measured thing allows. It draws the frame at a moment and nothing moves it on its own: place it inside <Player clip={sceneClock(timeline).clip}> to play it, or pass a fixed progress for a still. The height never changes while it plays: the caption line reserves the story's tallest caption. The screen is scaled by CSS, not a measure, so the server's HTML is already the opening frame: no blank panel before hydration. accent colours the menu highlight and the terminal.
  • New: the lock screen. A world with locked fades macOS's lock screen in over everything: the wallpaper dimmed, the date and a large rounded glass clock, the person the Mac belongs to (user, an @ag/person the page supplies: their face through @ag/avatar and their name, macOS's silhouette without a picture) over a glass password field with Touch ID's hint, Wi-Fi and the battery at the top right. Locking closes an open menu or surface, as macOS does, and the camera never closes in on a glyph behind it.
  • New: the app's surface. A surface step hangs the app's own rendered popover (a clip in art.surfaces: its video, poster, size in points and background) from its glyph at true size, slid inside the screen's edge with its arrow still on the glyph, and plays it in step with the story: it plays while the playhead plays (playing) and holds at the exact second on a pause, a scrub or a still. Opened by the author it is a click on the glyph; the status item stays pressed while it is open, and the camera stays wide. A surface open at the story's first instant is simply there, no fade, so a story held at its start shows it, and the camera never closes in over it, not even for a glyph the same opening frame sets.
  • New: a desktop picture of its own, an original in the spirit of macOS 26's (Apple's are theirs), on the desktop and the lock screen.
  • The time and the date are the viewer's own unless the story sets them: the menu bar and the lock screen read the viewer's clock (English macOS: "9:41 AM", "Tuesday, September 30"). The server's frame shows no time rather than a wrong one, the slot keeps its width so nothing it measures moves, and the browser fills it in at hydration.
  • The terminal is as wide as the story's widest line (in the font's own ch), between the classic window and the screen's margins, so no line wraps; a story that types nothing shows no terminal, and Finder leads the menu bar.
  • New: MacbookFrame has a hinge. lid="open" or lid="closed" and the frame moves the lid itself, on its own curve, whenever the value changes; a number (0 open .. 1 shut) puts it exactly there with no motion, for a caller that owns time (a scrubbed or filmed timeline). The lid's face turns in true perspective (the eye at the hinge, at the distance where a shut lid spans exactly the base's width) and its thickness is painted where that same camera puts it: the lid's own outline raised by the projected thickness, so it follows the rounded corners, is nothing when open, grows as the lid comes down, and shut is the closed laptop's front resting on the base. All of it is sin()/cos() of one number, so a transition, a scrub and a filmed frame are the same geometry. Without lid the frame renders as before, flat.
  • New: the timeline a macos stage plays, written by the app itself: a scene script played through the app's real engine compiles to steps (world, glyph, command, output, menu, hover, press, key, banner, caption), so every word and pixel on stage is the app's. awake's awake-scene is the first compiler. The lib owns pacing and nothing else, and paces for reading: every step says what it asks of the viewer (a line to read, a menu to scan, the lid to watch close), and before the script's next act the player waits until all of it has been taken in, while the app's reactions follow their cause at once. An agent's prompt sets it working, as the CLIs do, until the story says it is done. What happens behind a shut lid takes no screen time: the world's clock jump plays as a time-lapse (lapseAt: the clock and battery run from before to after, the time gained counts up), and when the lid opens the viewer is given time to read what the agent did while it was shut, then the banners posted meanwhile. sceneClock(…).clip is the story as a clip, its chapters, one per caption, starting where the thing each tells begins; the caption line shows the chapter from that moment, so a player's timeline and the stage always name the same chapter. sceneClock lays the steps out in milliseconds, and frameAt(timeline, clock, ms) is a pure fold to the whole stage at one instant, so scrubbing, stills and autoplay are one function.
  • New: WorldState.locked (the lock screen; LOCK_MS), WorldState.date, the surface step and Art.surfaces (Surface); Frame.surface and Frame.lockSince. chill's chill --demo scene is the second compiler, the first to hang its own rendered popover.
  • Fix: a step the author paused before (delay) ends the opening frame, whatever its kind: a glyph that flips a second into the story flips a second in, not at the first frame.
  • New: SceneClock.opening, how many steps the opening frame is. Fix: a surface open in the opening frame keeps the camera wide from 0; a glyph set earlier in that frame used to zoom in over the popover and leave its pointer out of the shot.
  • Breaking: WorldState.clock is optional. A story that sets it in every world step keeps its story time (time-lapses, an agent's hours); one that never sets it has no time passing and shows the viewer's; setting it in some steps and not others throws.
  • New: the media format vocabulary as typed chips wearing the real marks. PictureChip, SoundChip, TierChip, LangChip, CutChip and the composed MediaSpec strip take values (resolution, range, audio, tier, lang, cut), never label strings. The item ships two files: the chips and media-spec-marks.tsx, the artwork inlined as single-ink SVG (Dolby Vision, Dolby Atmos, TrueHD, Dolby Digital and Plus, dts, DTS-HD MA, FLAC, Opus, Blu-ray, Ultra HD Blu-ray, DVD, IMAX, the Spain flag, plus HDR10 / HDR10+ and tabler's 4K / 8K / HD / SD badges kept as references). A chip is ONE mark standing frameless; a drawn badge (a rounded box with the word, to the HDR10 badge's proportions) stands in for every value with no artwork (a resolution as the disc-case badge, "4K" over "ULTRA HD"; HDR10, HDR10+, HLG, channels, Remux, WEB-DL, DTS:X, AAC, a language, a cut). Each axis is a group of one to three chips ([resolution] [range], [object] [codec] [channels], [disc] [Remux], [flag] [name]); at md the lockups, at sm the brand symbols and the small badges. emphasis is a ladder of light named for its look (ghost · plain · lit · vivid: opacity, resting, a soft glow in the badge ink, a stronger glow at full brightness; a step never adds a shape), trailing is a slot after an axis and adornments fills it per axis on the strip; what either means is the consumer's. langLabel(tag, locale) names a language: a small variety table first (es-ES Castilian, es-419 Latin American Spanish), else the language subtag's display name through Intl.DisplayNames with the region left to the flag, else the tag; LangChip takes a label override. tone="brand" paints only marks, in their official colour; a near-black brand keeps the ink. tone="gold" is the disc-case sticker: near-black boxes with a metallic gradient on stroke and letters and the brand marks in ink beside them (flat mid gold at sm); the metal is retuned through --ag-media-gold-hi, -mid, -lo and -glint. Mark takes a paint (any CSS background, applied through a mask). TierChip takes resolution so a 2160p disc wears the Ultra HD Blu-ray mark. No dependencies: with nothing set every chip is monochrome in the surrounding text colour; a consumer colours it through --ag-media-picture, -sound, -tier, -lang, -cut and -scrim. Every chip carries data-kind, data-facet, data-emphasis, data-size and data-mark. Mark, MARKS, the label functions and the vocabulary tuples are exported.
  • Breaking: the prepare-media-cli.ts file is no longer part of the item; call prepareMedia from your own script. tsx leaves the item's dependencies with it.
  • Breaking: playback takes only "boomerang". "forward" named the default and chose nothing; leave the option unset for a video prepared as it plays.
  • Breaking: the options message field and the inline-results popup it drew are removed. A choice is shown as sent messages, one per form, the way the language gallery does; remove options from scripts when updating.
  • frame="none" no longer carries reset rules for phone chrome. A bare frame draws no shell, so there was nothing for them to undo.
  • Removed, nothing read them: the --tg-phone, --tg-phone-band, --tg-phone-chamfer, --tg-phone-button, --tg-lift, --tg-island and --tg-border custom properties. device-frame draws the phone and owns those colours. --tg-message-gap is internal now: the gap between messages is the client's geometry, not a knob.
  • A story scrubbed back below completion rewinds its afterlife with it: reactions and late messages land again when it completes again.
  • A profile video loops while on screen, as the client does; it had stopped after one play.
  • Once mounted, only the live layout stays mounted: the pinned stage below lg or under reduced motion, the stacked alternative above it, is unmounted instead of hidden, so a hidden stage's phones, observers and timers no longer run.
  • Removed, nothing set it: the --stage-align custom property. A pinned stage centres its scene.
  • The lightbox trigger carries its anchor-content lint exemption in the source, so a consumer's copy no longer diverges to add it.
  • Breaking: the upload prop is read once, at mount, alongside the document and the schema. Changing it on a mounted editor no longer takes effect; give the editor a new key to remount it with a different uploader.

Installation and updates

  • Only the items that draw with the studio tokens (code, reveal, quote, lightbox) depend on @ag/tokens. Install it explicitly once; the README's first command does.

Registry website

  • Registry requests and command copies are rate limited per address before they reach the collector; the lab pages are marked noindex; the home page states that visits are counted with Vercel Web Analytics.

Removed surface nothing used

  • scrims: the topProps and bottomProps props. Style the overlays through className and the scrim-top / scrim-bottom slots.
  • preview-picker: optionClassName; theme-toggle: the fallback prop (an unresolved control shows system) and the THEMES export.
  • quote-card: the faceShare, faceFeather and blockAt still options; the fusion and the block position are the module's constants, as the card's own geometry requires.
  • lightbox-motion: the QUICK tuning; lightbox-motion's springStep and settled and lightbox-wheel-phase's released read are internal now, the binder never decides on a release.
  • terminal-session: mix, BLACK, WHITE, LAND_MS, BLANK_MS are internal.
  • tokens: --dur-4; three durations remain. reveal now declares its dependency on tokens instead of carrying literal fallbacks.
  • Standalone AnimatedImage previews receive keyboard focus without requiring an overlay control.
  • Added a Next.js image component with reserved dimensions, prepared blur previews, and a short reveal after decoding. It supports self-hosted Next optimization, custom loaders, reduced motion, and rendering without JavaScript. className styles the frame; imageClassName styles the pixels.
  • Isolated internal pixels from prose typography. Placeholders now receive blur and share the final image's fit and position. Use className="w-full" for full-width articles; dimensions reserve proportions rather than forcing a crop.
  • Added AnimatedImage in image-animation: poster-first native animation on hover/focus, visibility on touch, explicit pause, and reduced-motion support. Stopping returns to the poster; native GIFs do not offer frame-accurate seeking.
  • AnimatedImage controls are now opt-in. Add controls to retain the play/pause button; the default preview has no overlay control.
  • Forwards its native video element ref. sizes describes responsive poster width instead of assuming every video occupies an 800px column.
  • Added native playback and silent cover previews with posters, explicit pause, visibility handling and race-safe hover changes. playOn="visible" supports article covers; normal players retain browser controls, sound and caption tracks.
  • Cover previews are unadorned by default. Opt into a play/pause button with controls.
  • playOn="visible-once" plays a cover once on arrival, holds its final frame, and replays on a fresh hover/focus. With loop, those later interactions loop until the pointer/focus leaves; the initial arrival still plays only once.
  • Added a storage-independent metadata contract and response validator.
  • Removed the unused glass(shape, tone) alias. Use glassVariants({ shape, tone }) when applying glass styles without Glass.
  • Added shared Node preparation and a CLI. Preserves original image bytes, measures orientation, retains transparency in previews, and generates posters. Video is opt-in and requires FFmpeg/ffprobe. The caller uploads returned files; preparation never writes to a database or chooses a storage provider.
  • playback: "boomerang" (CLI --boomerang) explicitly prepares GIFs or videos of 0.1–10 seconds into silent forward/reverse MP4s for the same video component.
  • Added a typed uploader contract, multipart transport, and a React hook with cancellation, retries, progress, preparation status and local preview cleanup.
  • Toolbar uploads share paste/drop pending-upload protection, retry and cancellation. Publishing cannot race a newly started upload.
  • A file that is not an image is refused before it uploads, as a job error with the file's name. A cancelled toolbar upload rejects Wordgard's promise instead of handing the dialog an empty URL.
  • Added a Wordgard editor with scoped styles, HTML in/out and an injected uploader. Paste/drop uploads follow document edits, preserve batch order, offer retry/cancel, and prevent publishing pending uploads. Set images={false} for text-only formats.
  • Responsive images now open from their loaded currentSrc, avoiding a second thumbnail download when the browser selected a different rendition. The photo demo uses local originals and the new image loading treatment.
  • Dismissal clicks no longer bubble through the portal to a containing card.

Registry website

  • Connected production website visits to Vercel Web Analytics. Installed components contain no analytics; registry requests are not reported as completed installs.
  • Added an Updates page and component-specific release notes next to installation commands. Update instructions and previews are available on every component page.
  • Playback responds when reduced-motion preferences change while mounted.
  • Fixed resize-observer feedback during focus fitting. Viewport scroll animations now stop when the chat unmounts, and autoplay pauses while the browser tab is hidden. No prop changes are required.
  • Breaking: removed the unused scrub and numeric replay props. Drive animation explicitly with progress for scroll-linked playback, including reverse movement. Omit progress for one-shot autoplay. Autoplay now runs at its configured pace; page scrolling no longer changes its speed. Remove the old props when updating.
  • Fixed focus-fit sizing oscillating between adjacent pixel widths at some viewport heights. No API migration is required for this fix.
  • Added fit="focus" with viewport="container" to size a phone around its focused exchange and bottom frame. The parent must provide a definite height.

Installation and updates

  • Local contributor installs now use shadcn for the full dependency graph, file placement, CSS, and updates. The helper no longer manually copies files afterward.
  • Fresh-install and update checks cover standalone and workspace consumers, including dependency files, styles, and preservation of the consumer's cn utility.