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-runUse --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 --overwriteReview 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.
parseFeedreads RSS 2.0 and Atom (a blog, a YouTube channel's uploads, a podcast) and runs anywhere;feedOffinds the feed a page names;readFeedfetches one on a server throughpublic-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).domainsonvisitsRouteandvisitHits: 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 carryownerandself;visits_owner_viewscounts, 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_hitkeeps themVISIT_DAYS,visits_owner_hitsthe 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_bstays).- Migration: a store provisioned before today runs
VISITS_MIGRATIONSonce (twoALTER TABLE visits_page), THENVISITS_SCHEMA, which adds the new tables and indexes. A site that runsVISITS_SCHEMAon 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.isPublicAddressis the check alone. Server-only; depends onundici.
- New:
UnfurlandhostOf, moved out ofweb-preview-unfurl, plushrefOf(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: importUnfurlandhostOffromlib/web-preview-factsinstead oflib/web-preview-unfurl.
unfurlfetches throughpublicFetch: 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-factsand@ag/public-fetch.
framingandlivePagefetch throughpublicFetchtoo.
- 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.
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 amailto:link or the bare address.
- A place on the same site opens in the same tab, through the site's router when it is given (
navigate), no longer always a new tab; ⌘ or Ctrl opens a new tab, as a link does. Another site's still opens in a new tab.
- 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 glassPanel) 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 installedcountry-flag-iconsand prerendered for every flag at build, so the CDN serves/flags/3x2/ES.svgand no function runs. Migration:shadcn add @ag/flag --overwritebrings the route; a site serving/flagsalready 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/ 1now. Updateapp/tokens.css(shadcn add @ag/tokens --overwrite).
- New:
TabsPanels, a track theTabsPanels 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 controlledvaluework 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 thePanel(or theTabs). - 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. Amount="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,
TabsPanelsruns to the panel's edge: the track reaches over the body's padding, read off the body itself (p-5, or whatever itsbodyClassNamemakes 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.Panelmarks the element whose padding is its body's (data-panel-pad). TabsPanelstakesonTrace: 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), andtabsswipes on it. - Breaking:
SLIDE_GAPis removed. The track reads the room between slides from its own CSSgap(the lightbox's stays 32px inlightbox.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 byswipeGain: 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 inCARRY_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_LEANandswipeGiveare removed;SlideTrack.wheel(dx, handBack, coasting)takes the wheel phase's read and no speed (the track measures its own), andSlideTrack.glideTo(i)none either. - A hand back on the trackpad inside the last swipe's coast is a new swipe: the wheel phase's
startbreaks a spent gesture, beside the surge and the turn, so a second flick the same way is no longer swallowed as tail.SlideTrack.wheeltakes it ashandBack. lightbox-wheel-phaseopens a new stream when a delta turns to the other axis (AXIS_TURN, inside a stream alreadyAXIS_HELDalong 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.whysays why a stream opened (first,silence,interrupted,turned).lightbox-wheel-phaseends a stream afterSWIPE_ENDof 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.createSlideTracktraces every decision with the engine's whole state when itsdebugis 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 asspan/play/hold, andcontent({ 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.StoryCardsets a chapter's words (title,body,action,aside) beside itsscene, on eitherside. A device stands taller than its card: the story keeps room round its cards (ScrollStory'soverflow, a length) and a card's scene reaches into it (StoryCard'soverflow), never cut by the deck's edge.StoryTitleis 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.
picker="labels"shows each card's mark with its label in words;icons(the default) is the mark alone, as before.
- New:
ContactsApp, a picture of iOS 26's Contacts app drawn from a .vcf's text, for anIphoneFrame's screen:view="import"is the sheet iOS raises over an opened .vcf (pressedlights 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'sstyle: the status bar's ink and the ground under it, which the frame draws. Fields are read bycontact'slib/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 withscrollas 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, neveroverflow: 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'sTouch; done swells while pressed (data-pressinground the app), as iOS 26's glass does.- If you installed it earlier today:
schemeis gone, the poster takes the photo's colour; passaverageto bothContactsAppandcontactsAppScreen.
- The MacBook's display clips with
clip-pathinstead ofoverflow: hidden. Its corners are uneven (rounder at the top), and Chrome clips such a box through a mask of its own, which made a glassPanelon 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 (anIphoneFrame'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-bottomas well as--ag-browser-top.- A typed address can start pasted (
{ kind: "type", pasted, text }): the start oftextlands whole at once and only the rest is typed, a route added to an address known by heart (👤.to/adapasted,/ghtyped). Apastedthat does not starttextthrows. BrowserCapturePagedraws a captured site (mise browser-shot) from its manifest as it is: the picture for the scheme it stands in,childrenlaid over it in the page's coordinates. The manifest's type islib/browser-capture, shipped withbrowser:BrowserCapture,foundOn,middleOf, andatFound(capture, name), the style that sits a box on a thing--findfound (plain values, so a server component computes it).browser-shotwrites each picture's served address (--served, else what followspublic/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).--phonecaptures the mobile layout,--find name=selectorrecords where things are.- A site's captures as code:
lib/browser-capture'sCapturePlandeclares them (each page'surl,phone,width,tall, the things it must show asfind,optionalfor a site that refuses headless browsers), andmise capture <plan.ts>in this checkout makes them: every page in both schemes into the plan's folder underpublic/, oneCaptureManifestthe 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 mustfindfails the run, which catches a capture of a deploy still rolling out. - A
reloadstep ({ 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 inBrowserSession'spages("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'saverageis (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).Idedraws one instant (project,files,active,open,dirty,caret,explorer);IdeSessionplays an edit (steps) on the playhead, its clip fromcodeTimeline(files, steps), anddurationholds its last frame to a longer shared clock (a browser on the same Mac reloading after the save). The code is coloured bycode's highlighter in the browser, each character as it reads in the finished file. Ide'shighlightmarks lines ascodespells them ("6,10","1,4-6"), withcode's own wash and bar, for a still that points at what to paste.blinkblinks the caret as a focused editor's does (none under reduced motion); a playingIdeSessionholds it on, as VS Code does while keys land.ide.cssships beside it for the blink's keyframes.Ide'scolours: 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_THEMESis gone fromide: it isVSCODE_THEMESin 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), atx,yin 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, asmacos's pointer shows a Mac's clicks.
- New: an edit of a project's files as steps (
open,typeat alineandcolumnor at the caret,save, each with adelay), 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.ideandterminal-editordraw 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:wis typed and answered. It plays the samecode-sessionsteps aside:<Terminal alt="…"><TerminalEditor files={…} steps={…} /></Terminal>.
children: a full-screen program drawn in the window in place of asession(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, anduseCodeColours, the same read in the browser.CODE_THEMESis the one spelling of code's two themes.codeColoursruns 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).useCodeColourstakesgivencolours 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 (wrapgoes 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).StepChevronis the chevron alone, a step a press and repeating while held, never taking focus. "stepper" now finds this item;numberis "number field".
- Its ‹ › are
stepper'sStepChevron(the same press, hold and repeat); their slots arestep-previousandstep-next, where they werenumber-previousandnumber-next.numbernow depends onstepper.
valueis optional: givedefaultValueand the slider keeps its own, saying each change (onValueChange); givevalueand 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).Browseris controlled (url,address,editing,load,back,forward,scroll,tabs);BrowserSessionplays a visit (start,steps:type,go,back,scroll;pagesby address) on the playhead, its clip frombrowserTimeline;BrowserLiveshows a site live in an iframe, or its unfurl where the site refuses framing. Whether a site frames is read from its headers bymise live-page <url>(livePageinlib/browser-framing), since a browser cannot tell a refused frame from a loaded one. macostakes windows aschildren, laid out in its stage's pixels on the same playhead, andfront, the app named in the menu bar:<Macos front="Safari" …><BrowserSession className="absolute …" … /></Macos>.fullshows 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 throughURL, which turned a host in other scripts into punycode (👤.to read xn--mq8h.to).linkmakes 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'slinkedpasses 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>.jsonwith each one'sMediaAsset(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 infallbacksis tried. A host whose network is blocked hangs rather than fails (a minute or more), so the chain never moved on; withpatienceit 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.
Panelis 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.headandfoot: front surfaces across its top and along its bottom (a tab list, a board's column names, a card's buttons),headHeightandfootHeighttall (3rem), told to what it holds as--panel-headand--panel-foot. In glass each is the material's front (glassFront), plain a solid strip a step darker than the card.actionstands at the head's end. A head and atitletogether throw: name a panel with a head in it, or with a heading above it.maxHeightandheight: 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).pinpins the head to the page while a growing panel scrolls under it, and--panel-underis where what pins under the head (a table's header row) pins.glideeases the body to a new height.overlays the fronts over content that scrolls by itself (a board's columns, each aScrollArea 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 glassbackColor,frontColor,tint,tintAmount,rim,rimWidth,rimColor.usePanelHead()andusePanelBody()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.
- The
cardstyle is aPanel: plain or glass (material, else the nearestMaterialProvider;scheme), the picture running to its edge, and inside a frame already drawn it draws no box (framedecides when given). AclassNamethat stripped its border and fill to sit in a glass box is no longer needed: passmaterial="glass", or put it in the box.telegramandxare unchanged. It now depends onpanelandmaterial, 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 onpanel.
TabsBoxis removed: tabs own no container. Compose them withPanel, 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 onTabs;tabs={…}becomeshead={<TabsList>…</TabsList>};maxHeight,height,actionandspillgo onPanel;topispin;--ag-tabs-box-underis--panel-under; atitleis a heading above the panel. A box that grew and glided takesglide.- A
TabsListin 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.
tabsno longer depends onscrims,scroll-fadeorscroll-thumb.
- New:
ScrollArea, the one capped scroller, replacingscroll-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.thumbadds overlay thumbs, laid over it from beside it so its fade never fades them.coverandcoverEndare 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.rendermakes 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. Adata-slotgiven to it is kept. - Every capped vertical scroller in the registry is one now, so they end alike:
dialogandsheet's bodies (their slotsdialog-bodyandsheet-bodykept),select's,people-picker's andchip-input's lists,table's choices,icon-picker, thegallerytoolbar's settings,gallery-filters' sections,file-view's text,player's playlist,commandandcountry-picker. Each of these items now depends onscroll-area.icon-pickerhands the scroll on to the page at its ends, where it used to hold it.
- 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 thepy-5it had. A panel that scrolls is<Panel maxHeight>or<Panel height>.
GlassPaneis removed: a glass container is<Panel material="glass">. Migration:front={…}ishead={…}(frontAt="bottom":foot={…}),frontHeightisheadHeightorfootHeight,--glass-pane-front-heightis--panel-heador--panel-foot,frontClassNameisheadClassNameorfootClassName;radius,backColor,frontColor,spill,tint,tintAmountandrimkeep their names. The panel pads what it holds (bodyClassName="p-0"for none).GlassSpillis 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,widthpx in (10), and takes the sameradiusasGlassEdge, any CSS length. A glassPanelwears it by default, withspill={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),
ScrollThumbslays 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-topand-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 explicitmaterialstill wins.
ag-edge-fade-ykeeps 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 (framedecides as before), so in glass it has the panel's spill and an edge over its content.
- Its frame is
Panel's (framedecides as before). A glass log is no longer clipped to its corners, which cut its own shadow.
- A glass
FieldSetis a glassPaneldrawn as the fieldset, named by its legend (aria-labelledby).
- A glass accordion standing alone is a glass
Paneldrawn 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.
useFormField(form, path, label?)binds one path with noFieldround it: value, change, leaving, the error when it is due, checking, read-only while sending, andanchor, a ref callback that places the control for the summary.inputspreads on a text control, asc.inputdoes. For a control that draws its own label and error (a name written on a card, a cell in a table).form.Fieldis 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.
inspectis nowdata-view, a noun for what it is: data, viewed as a tree or as text. Migration: install@ag/data-viewand deleteinspect.tsx;InspectisDataViewer(notDataView, which is JavaScript's own),InspectPropsDataViewerProps,InspectNodeDataNode,InspectKindDataKind,inspectValuedataOf,inspectXmldataOfXml, and the slotsinspect-*aredata-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.itemNamenames an array's item beside its index (0 githubfor{ 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.
DialogContenttakesbare: 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.backdropadds classes to the dim behind a centred one (a stronger dim, a blur).
Codetakesbare: no frame, no bar and no copy of its own, for a frame that holds it (a tab of aPanelshowing an example's code, its copy in the panel's head).Codetakesmaterial:"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 aMaterialProvider, 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.
InputGrouptakes aprefix: text the value continues (github.com/,@), joined to it with no gap, so the two read as one address.startstays for an icon, set a little apart. A field that showed a prefix throughstartshowed a gap inside the address; move that text toprefix.
- 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 withoverwhen 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 assuggested. Arrows walk the list, Enter picks.CountryPickeris the panel;CountryPickerPopoveropens it from your own trigger and shuts on a pick. It carries every flag's art, about 180 KB (45 KB gzipped):LazyCountryPickerPopoverkeeps that off the page's first load, fetching it as soon as its trigger is drawn so it is there before it is pressed (preloadCountryPickerto fetch it earlier).
- New: a security hologram for any rounded surface.
HoloLightmeasures the hand over what it holds, andHoloLeaninside it leans a few degrees toward it (pose="flat", or"turned"beside a headline);Hololays a print (laminate,webornone) 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.floorkeeps 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 incqw) sets it in that unit, so the print grows and shrinks with everything printed on it.FoilEdgeis a one-pixel ring of foil on the rim, faint at rest and lit in the band under the hand.emblemis the glyph the prints repeat, your mark. The prints arelib/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_STOPSandfoilGradient(angle)for CSS.
Imageno longer uses next/image, andimageno longer depends onnext: it is one<img>(no wrapper element), soclassNamestyles the picture itself andimageClassNameis gone, put those classes inclassName. A picture filling a box isfillin a positioned parent, as before;fit="contain"fits the pixels and the blur alike.data-slot="image"anddata-stateare on the<img>.- Resizing is a loader. A Next site wraps its layout in
NextImageLoaderProvideronce (/_next/imagefor the site's own files, with the srcset next/image built);ImageLoaderProvider loader={…}sets your own,loadersets one picture's, and with none the source loads as it is. Told neithersizes,fillnor awidth, a picture loads as it is too. A remote source loads as it is undernextImageLoader: write a loader for the hosts in yourremotePatterns.imageRenditionsreturns 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.priorityasks for it first (preloaded, eager, never faded). Hidden on error. Imgis removed:Imageis the plain picture now. Migration: renameImgtoImage; a picture given nosizes,fillorwidthloads as it is, asImgdid.- New:
fallbacks, the sources to try in order when one fails, andminWidth, 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:optimizePostergoes through the site's loader,posterArrival="instant"ispriority, and the blur is drawn under it either way.
- The face arrives through
Image: held until loaded, then faded in, hidden on error. Installingavatarnow bringsimage.
- Avatars arrive through
Image: held until loaded, then faded in, hidden on error so the card's own tone shows. Installinggithubnow bringsimage.
- 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. Installingtweetnow bringsimage.
- 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. Installingweb-previewnow bringsimage.
- The thumbnail arrives through
Imagewith YouTube's sizes as its chain (maxres, then hq, the 120 px grey YouTube sends for a maxres it never made passed over byminWidth): no frame of grey, no pop, and a thumbnail cached before hydration is never left blank. Installingyoutubenow bringsimage.
- Installing
traceno longer bringstimeline,playbackand the media libraries behind them: its axis depends ontimeline-viewalone, 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
tracedraws its axis without the timeline.timelinebrings it as before.
- Takes
@adriangalilea/utils10 (^10.0.0), the current major; nothing in it changed. The same forpage-sensor,visitsandstat: 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 andsquare(1:1) as a mark (bar-list'sicon,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 isIntl.DisplayNames' (countryName), the label and title by default;label=""when the name is written beside it. An unknown code throws: askhasFlagwhere a country may be unset. The art iscountry-flag-icons(MIT). A server component; from a client component the whole set ships.
- New:
proseandfeaturetakematerialandschemelike every surface: plain is the step up from the page they always stood on,glassis the glass material, from the prop or the nearestMaterialProvider. The surface isquote-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, andmaterial="glass"on a card throws. Installingquotenow bringsmaterial. - The card's portrait arrives through
Image. Installingquotenow bringsimage.
- 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 atabs-box-panelselement, which carries the padding. - New:
labels="active"onTabs, for a row of many tabs that stays short: every tab shows itsicon(a new prop onTabsTab) 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 noiconunderlabels="active"throws.
- 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/visitsso 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. Installingpage-sensorinstalls it. Migration: import from your copy (@/lib/visits) instead of@adriangalilea/utils/visits; the tables and the rows are the same.
- Changed:
DeploymentDetailsdraws 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 withpreview, the settings only withrecommendations, Share only with auniquedomain, 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(itsdefaultRunSummarymoved here fromDeploymentDetails),DeploymentSummary,DeploymentChecks, andDeploymentStep(value,title,state,aside) for any other step; each built-in one asserts its part is there, and a deployment carrying a build must showDeploymentBuild. Migration:<DeploymentDetails deployment={d} />becomes<DeploymentDetails deployment={d}><DeploymentBuild /></DeploymentDetails>, addingDeploymentSummaryandDeploymentCheckswhere the deployment hasresourcesandchecks; a domain-assignment step is aDeploymentStepwith the state your host reports.stepsOfandDeploymentStepsare gone (buildState,outputState,checksState), anddefaultOpennames sections by theirvalue. - Changed (breaking):
Deployment.buildis optional: absent, the build ran where the host could not see it (nostarted, nodurationeither). A ready or failed deployment with a build and no start now throws; pass nobuildinstead of an empty one.domainsare 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 aviewline 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'
visitsRoutecomposes visits without cookies. MountPageSensoronce in the root layout with aservice, thecollector(a path such as/api/events, or a URL), thebuildand what a path'splaceis. A page shown is apagespan, 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 whichdata-sectionparts came on screen and, on a load's first page, its vitals. A same-origin request is afetchspan with itstraceparenton the request, so a server using ts-utils'log/nextjoins 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 (consoleoverrides 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'
readVisitsdoes).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,backreturns 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 (throughandfollowedin 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, withbar-listandsparkline.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'eventStepwrites it, read withstepOf): 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 wherecolorOfgives it none. A place wears the colourcolorOfgives it in every view, andfaviconOfputs 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-coreworks the views out of the visits, framework-free, and marks what is outside the site.
- Changed:
renderQuoteSvgdraws its grain from one tile of noise repeated as a pattern, the tilegrainLayeralready 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 throughQuoteStill(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 fromrenderQuoteSvginlined in a page with its grain and dither fitted to the page's pixels on every resize. Migration: replace adangerouslySetInnerHTMLof the still with<QuoteStill svg={svg} />.
- Fixed:
commandtypechecks under@base-ui/react1.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,dialogandsheetread it as their defaultcontainer(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.PlayerLayerdeclares aPortalTarget, so every popup item inside a player opens in its layer with nocontainerpassed. Migration: dropcontainer={usePlayerLayer()}; a popup of your own takescontainer={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)inlib/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 andPlayerGesturesread 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:
PopoverContentopens in the nearestPortalTarget(portal) when given nocontainer; so dotooltip,hover-card,menu,select,dialogandsheet. - New:
anchoronPopoverContent, for a panel that stands under something other than its trigger (a group of controls it serves), andcreatePopoverHandle, Base UI's handle: one popover opened by several triggers anywhere (<Popover handle>,<PopoverTrigger handle>).
- New: pictures of a film at any instant. A
StillSourcesays 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 nofpsit 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.StillPicturedraws 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); givenstills, a tile shows its first still until its video presents a frame. Each tile is aVideoPicturewith its progress on aMediaLine, 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 toplayer(start)at the instant it showed;FilmWallTogglein 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) andfit. 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), elseonPlay, else plays inline (Esc gives the tiles back). One Tab stop, the arrows between tiles.viewtiles a part of the film.FilmTileis the tile; the tiling islib/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_DISCandPLAYER_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) implementspressure: (state, at) => void; one made withambientBudgetand handed toGalleryAmbient budgethears the machine only if you callwatchPressureon it.
- New:
Verdict(inline) andVerdictBar(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:
spanonVideo: 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 underrest="poster". Not withstartorat. - New:
VideoPicture'sclicks={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 theVideoPlaybackaround, 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 andelement(if given) is on screen.everyruns 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. Returnsdata(kept through a failure),error,loading,updatedandlive. Unmounted, the request under way is aborted through its signal. The machine is framework-free inlib/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,StatNoteandStatChart(a 40 px slot a sparkline fills).StatChangetakes a signedvalue, said as a signed share or in your words (children), toned by whether its direction is good (good:up,downfor 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 (frameoverrides).
- New:
BarList, ranked things as horizontal bars (items:label,value, optionalhrefandicon;labelnames the list). Each bar is its value's share ofmax, the largest value by default; the values sit in their own column in mono with tabular figures, throughformat. The order is yours. A zero is no bar; a negative value or one pastmaxthrows. Long labels are cut with an ellipsis and whole in their title;emptysays what an empty list says. Renders in a server component,formatincluded.
- New:
Sparkline, a series as a small line beside the number it explains (values,label). As wide as its container and 32 px tall unlessclassNamesays 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 unlessminandmaxsay otherwise;areafills 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 throughformat.
- 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).pulsefor 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 insr-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 pastdateAfterdays (7) its date, with its year when it is not this one.dateTimeholds 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;nowsays it from a given instant with no clock. The words are framework-free inlib/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 (
Deploymentinlib/deployment-core: project, commit and author, branch,productionorpreviewand whethercurrent,queued/building/ready/error/canceled, created, started, duration, domains, a preview picture, and the build as@ag/telemetrylines 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.filterandonFilterChangekeep it on the page.DeploymentDetails: the deployment page's body. Share, Logs, Visit with every domain under its chevron (one control, button'sButtonGroup), 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;linksas cards at the foot. Times are said fromnow, 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 asTIME_WORDS(just now for the first minute, the date from the third day: the same words). Withoutnow, 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.DeploymentStatusis a@ag/status, its dot nowdata-slot="status-dot". Removed:AgoandagoText; write<RelativeTime at={…} now={…} {...TIME_WORDS} />, orrelativeText(at, now, TIME_WORDS)fromlib/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
startednorduration, and its duration is said "unknown". One without the other still throws.
- New:
chevron="start"onAccordionTrigger, a chevron before the heading turning right to down, andaside, 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;
defaultCollapsedis unchanged. - New: one request's spans as a waterfall. Flat spans in (
id,parentId,name,service,fromandtoin 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,+-0and 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 unlessdetachedsays why). Capped atvisibleRows, orvisibleRows="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, andframeoverrides. - 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 callsonLineChange. 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) andTraceSpan. 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;logEntriesmakes 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);spanTreespairs 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, isdetached). - Changed:
spanTreesruns 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 isdetachedtoo, saying why: it was not waited for (work handed off past a reply), or, still running, its end never reached the stream. Without that,Tracethrew on both. One of the same service that starts before its parent still reachesTraceas it is, which throws.
- New:
InFrameanduseInFrame(). 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'sframeprop 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;
frameoverrides. - Lines are
@ag/telemetry's: five levels (tracethe 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 callsonSpanChange;spanmarks 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 asag-edge-fade-ydoes, the sides asag-edge-fadedoes, each from its own axis of the scroll, with no scrollbar shown. - New:
--edge-coveronag-edge-fade-yandag-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-xdoes the same for a pinned first column underag-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-yandag-edge-fade-xyalike. - 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 anag-edge-fadeclass, import./edge-fade(it loadsscrims.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'smin-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: aclassNameonDialogContentthat set padding, gap or the scroll (p-*,gap-*,overflow-*) now belongs on[data-slot=dialog-body]; size and place stay onDialogContent.
- 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 fromSheetContentto[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 innerrole="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.
- 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
Settingwhose 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
valuechanged 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:
shortScorepicks 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:
countmust be a whole number from 0; a negative or NaN count throws instead of drawing "-2" or "NaN".
- Fixed: an
asidethat 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:
GroupBoxToneis gone; the tone isTonefrom@ag/material, as readout's.
- Changed:
ReadoutToneis gone; the tone isTonefrom@ag/material, where the toned words' colours (TONE_WORDS) now live once for readout and group-box.
- New:
Tone, andTONE_WORDS(a tone as words on a surface, deepened on a light one so it reads on white) inmaterial;glassWords(coloured words lit along their foot on glass) inmaterial-glass. Field, readout and group-box use them instead of three copies.
- Changed:
TabsBox'stitleis a heading above the box, outside it, and the two are onesectionnamed by it (data-slot="tabs-box-section"); with a title,classNameplaces 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. Theactionstands 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 atmd), 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 attop-(--ag-tabs-box-under). - Fixed:
TabsBoxin glass keeps its corners when given astyle: the consumer's style replaced the radius its body and back layer share with the edge. - New:
TabsBoxheight, 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 oftop,maxHeightandheight, 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-topin 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
countthat is no whole number from 0 throws instead of drawing a full line. Inside aSettingat 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"), whichgithubnow installs. They were GitHub's outline pill, 24 px; an outline reads as a button, and this page cannot send a reaction. Thegithub-reactionsslot 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:
SelectionBarstays 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".findItemsandfindIconsare 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 indata-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 thetone, a character greyed where empty),maxof them (5). The value runs from 0 (no rating) tomaxand 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.stepis 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.nameposts it with a form ("" when none).readOnlyholds it still, its Tab stop kept. - New:
countmakes 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 aSettinga 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) andscore; 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 aButtonwitharia-pressed; the score is said politely when it changes, the whole number to a reader and short to the eye (12.3k,formatto change it).orientationvertical or horizontal, in a well (plain sunken, glass a pill) orcompact, the arrows and the number alone;readOnlyfor a locked thread, kept in the Tab order and never dimmed.
- New: "was this helpful?" and two thumbs, each a
Buttonwitharia-pressed. A side withreasonsor anoteopens its follow-up in place (the reasons as chips, a line of words, send); the same thumb again takes the answer back.onValueChangehears the thumb at once,onSendthe 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"),offeradds a button opening the emoji you may give in a popover; the consumer flipsmineand the count inonToggle. With noonToggleit 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 (valueundefined,fallback: { value, from }applies), or locked here (lockednames 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, neveraria-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 (
ownedHeredecides), 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) andGLASS_GHOST(--glass-ghost). Its own item, so a control reads its setting without installing the slot and the keys.
- New: inside a
Settingwhose 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,InputandInputGroupare 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-describedbyof 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
asideat 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'sGlassFront), 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 throughownedHere, so a page's shortcut never also hears them. - New: the value is a civil date string,
"2026-10-11", or{ from, to }, never aDate: aDateis an instant, and the same day picked in Madrid is the day before in UTC once it is serialized.toDay(date)anddateOf(day)cross to and from aDatewhere an API wants one; a form posts the value bynameas 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
disabledrefuses or outsideminandmaxreachable 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 unlessweekStartsays.labelsfor the words that are not dates. Inside afieldit 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 afieldit is the field's control (named by the label with the choice, described, red when wrong, not opening while read-only). Plain or glass;containerfor 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 bystep, Home and End as far as it goes, Enter or a double-click puts it back where the default layout has it.layoutandonLayouthold the layout outside. The handle is the separator's line,withHandleadds a grip, plain or glass.lib/resizable-coreis 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-coreholds 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).AdaptiveGlassisGlass(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'srefresh()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>wearsdata-material="glass"anddata-schemelike 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 bydifferenceagainst 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 whichTINT'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-groundis 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-heighttall, for the glass's content to pad by the same length;glassFront.topandglassFront.bottomare the surface alone, for an element that places itself (a list pinned withsticky top-0over 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,lightanddark): by the picture behind the surface, throughbackdrop.useSurfaceScheme(scheme, ref)is a surface's scheme read by its own element, with the ref its root takes;BackdropScopemakes 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(andInputGroup),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'sFieldSet,form'sFormSummary,people-picker,tree,toast(each toast),thumb-strip,popover,menu(MenuandDropdownMenuContent),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.PlayerChromehands 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 isScheme(those andbackdrop):schemeOf,glassVariantsandGlasstake aWornScheme, andmaterial-glassandliquid-glassexportWornSchemewhere they exportedScheme. Migration: importSchemefrommaterialfor a prop,WornSchemefor 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-blurto 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-sheento choose. - Changed (breaking): the glass's
toneis nowscheme, the colour scheme a control stands in:auto(the page's),dark(dark whatever the page is: glass over a picture) orlight(light glass and words whatever the page is).MaterialProvidercarries it besidematerial(<MaterialProvider material="glass" scheme="dark">, each kept from the provider around when unnamed), anduseScheme(prop)reads it.schemeOf(scheme)is the one class a surface wears for it:darkis 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 withcolor-scheme).glassVariantstakesschemeinstead oftone, and its dark half follows the scheme, not only the page.GlassToneisScheme. Migration: rename every glasstone=toscheme=andGlassTonetoScheme; a glass you meant dark over a picture, setscheme="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); reinstalltokensfor thescheme-dark:andscheme-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(frommaterial-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 fromliquid-glass. Migration: importGlassEdgefrommaterial-glass(orliquid-glass), not fromglass-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::beforefor the catch, one on::afterfor the far corner, the same fall mirrored). A control's glass,GlassEdgeround 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.GlassEdgetakesscheme. 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);glassShapesis glass's box alone. - Changed (breaking): a picture's reading of the rim is
--glass-ground-sheen, and--glass-sheenis 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 aBackdropGroupset, so dark glass in a group never took the picture's sheen. Migration: set--glass-ground-sheenwhere 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/tintAmountset 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-hueand--glass-hue-amountset by hand are--glass-stainand--glass-stain-amount;rimInkanddeepInkare 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 aGLASS_BENDelement 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(glassVariantsdoes), what is inside inherits it, and each glass asks its parent.data-materialis 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: 1on what holds it. - Fixed: a glass inside another glass took the outer one's rim (
--glass-rimis inherited): a rail or bar drawn withrim={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 andrimset 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.appleis 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, whereappleread 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 withrim={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 brokespace-y-*,divide-*and:last-child(cards gained 4 px at the foot), was invalid insideas="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 (glassVariantsandglassEdgewearrounded-(--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);glassShapesstate theirs, a round shape (circle, pill, bar) sized by--glass-sizewith its radius half of it;GlassEdge'sradiussets it; a sheet against the screen squares the corner it presses there (--glass-corner,--glass-corner-far).--glass-radiusis 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 itsrounded-*with[--glass-radius:…](a theme radius as--theme(--radius-2xl), a pill as half its height); size aGlasscircle, pill or bar with[--glass-size:3rem], notsize-12orh-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 glasstooltip,menu's list,select's list,dialog,popover's andhover-card's popups andscroll-thumbopened 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,sheetandtoastcut from one scheme to the other instead of easing, andnumber's rim jumped. A control's own motion now goes in through--glass-moves(whole items oftransition,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-durationor--glass-change-easesay otherwise). Migration: on a glass face of your own, move atransition-*,duration-*andease-*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'sstyle) 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-amountunder 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.tsxrefuses 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 wholebackdrop-filterin its order (a blur of--glass-blurand 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.materialinstallstokens(its faces' dark half isscheme-dark:; installed alone it had no dark face).GlassStyletypes--glass-backdrop,--glass-moves,--glass-change-durationand--glass-change-ease. - Changed:
GlassStyleno longer types--glass-bend, which nothing read (a frame's bend isGlassFrame'sbend). 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-sheenand--glass-ground-lightare 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-sheenon 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-width1px,--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-strengthis--glass-rim× the ground's sheen × glass on glass, with no factor of its own; a control's states still raise--glass-rimthe same multiples. Migration: a glass whose edge you want as before sets--glass-rim-width: 0.5px,--glass-rim-softness: 0pxandrimabout 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)isglassStyle({ tint: color }).
- New:
successandwarningvariants besidedestructive, 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 withglassStyle. - 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
TabsPanelmounts 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.mountsays when:"seen"(the default),"active"(only while shown, the old behaviour) or"always"(from the start). Migration:keepMountedis gone, writemount="always"; a panel that must start over on every visit takesmount="active"; content that polls or subscribes and should stop while hidden asksuseTabsPanelActive().TabsPanelno longer takesrender: 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 aTabsPanel. - New:
TabsBox, the tabbed container: one surface (plain aPanel, glass built asGlassPaneis), itstitleon its own line in the panel's title voice, then a strip holding thetabsand anactionon one centre line, then theTabsPanels, which keep theirmountbehaviour. 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 bysize. The strip pins while the body scrolls under it: against the page, below what the page passes astop(top="var(--header-height)"), or inside the box whenmaxHeightcaps 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, asGlassPaneand the glassgroup-boxwear), what passes under it seen only as light, the glass's edge drawn over both. Both looks,segmentandline(the strip's rule its line).tabsnow installspanelandscroll-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}; achevronsyou 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 isdisabled. A field's read-only and a locked setting's hold it the same way. - New:
aria-describedbyof 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 isdisabled). 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 withvalue; 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. Withmultiple, 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: reinstalltokens. - New:
scheme-dark:andscheme-light:, an element in the dark or the light scheme, which is the NEAREST one named: its own.darkor.light, else the closest surface above it, else the page's theme class..darkand.lightset--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'sdark:asks only about ancestors, so a surface that is dark itself could not style its own face with it..darksetscolor-scheme: dark,.lightcolor-scheme: light. Migration: reinstalltokens. - 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: reinstalltokens.
- 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.
fullis gone: a face that opens its portrait isavatar-portrait.avatarLookandAvatarImageare the look, for a face of your own. Migration: anAvatarwithfullbecomesAvatarPortrait(npx shadcn add @ag/avatar-portrait); withoutfull, 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'sfulldid). It brings the lightbox;quoteuses it for an author with a portrait.
- Changed (breaking): no live region by default. A callout of the page is read with the page;
livemakes one that appears because something happened say so (role=alertwhen destructive,role=statusotherwise). Migration: addlivewhere an alert is shown after an action.
- Removed (breaking):
isKeys. kbd is a client module (it reads the material), so a server component callingisKeysto choose between keys and code failed to prerender. Migration:keys(text) !== nullfromlib/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 passesusePlayerLayer()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_MOTIONis 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 (seematerial). Migration: writeDIALOG_MOTION[material]where you wroteDIALOG_MOTION, and keepoverflow-y-auto overscroll-contain outline-noneon the popup yourself.
- 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 withfuzzy, 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, ⌘⌫);emptyis 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.containerfor 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,closeDelayon the trigger), staying open while the hand crosses to it; Esc and an outside press close it. The trigger stays a link (renderfor a router's own). Arrowless,containerfor a player's layer, plain or glass (materialonHoverCardContent, or a provider round it), what is inside in the same material.
- New: the bundle installs
command,hover-card,breadcrumb,calendar,date-picker,resizable,group-box,readout,setting,ratingandvote(Setting,RatingandVotefrom the one import too).
- Changed: the value controls read their field themselves.
number,slider,select,checkbox,switch,radio-groupandtoggle-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 onlyc.valueandc.onValueChange. A slider's or a number's error reached nothing before, and only text fields held still. What you pass explicitly still wins; spreadingc.wiringkeeps 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 inFormSummarywith "ask again", and the status line's retry asks it again.submitanswers{ ok: false, reason: "unchecked", paths }and always ends withsubmittingfalse.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:
FieldControlgivesc.inputonly 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, anduseLeaveGuard()for a dialog of your own. Migration:guardnow covers only the browser's own prompt (a reload, a closed tab); a link pressed in the page asked throughwindow.confirm, and now asks only where<FormLeaveGuard />(or your hook) is inside the<Form>. The item now pulls indialog. - Changed: one change notifies once (a keystroke used to wake every subscriber three times);
FormSummaryre-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.useFormStatetakes an equality (deepEqualfor 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. AFieldis two rows now, its name and control, then its description and error. - Changed: what a control reads from its field (
useField,FieldWiring,FieldContextValue) lives infield-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), andheldStill(field, change), a change dropped while the field is read-only. - New:
useField()carriesinvalidandreadOnly(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;FieldtakesreadOnly(read by the control, never spread as an attribute) and marks itselfdata-readonly.
- Fixed: the plain
fieldeditor'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) takesvariant="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 aMaterialProviderround it): in glass a well of the one glass material, its rim lit by the hand and the keyboard.invalidstains it red and setsaria-invalidon the document. - New:
rows, the lines it is tall when empty (5 on a page, 3 in a field). Migration:--editor-min-heightis gone; setrows. - 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 wasImage uploads), each bar<file> going up, its retrytry <file> againand its cancelcancel <file>(they wereretryandcancel, the same for every file). Migration: a test or a script that finds them by name uses the new names; a bar is aprogressbarrole, not a<progress>element.
- Fixed: the first and last blocks sit flush with the body's edges, as
MARKDOWNmeant: 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.
ImageandImghold 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'silenceis 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()andsubscribe()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 inwater. - 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 withoutcrossOriginand CORS) keeps it all dark. Migration:scheme="dark"on the chrome, or a provider round it, pins it dark. - Fixed: a
VideoPicturewith noaspectdrew 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 (itswidthandheight) when noaspectis 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:
voiceonVideoandVideoPlayback, 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'strackprop isshots, andshotAt,framedanduseInShottakeshots. Migration:<VideoCrop track={…}>becomes<VideoCrop shots={…}>; positional calls are unchanged. - Changed:
VideoPicturestands what sits over it in the dark scheme (aMaterialProvider 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 inmaterial.
- 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.
- 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-0on aGlasscard) 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;styleplaces a line (one chapter of a longer bar). The gallery's cells draw their strip with it, andplayer's bar each of its chapters, so the two say progress the same way.playernow pulls inmedia-line.
- Fixed: toasts show again outside fullscreen. With no element fullscreen and no
container, the stack was handed to Base UI's portal asnull, which it reads as "wait for a container", so nothing was ever drawn; it now goes to the document's body. - Changed: one
Toasterper page. Everytoast()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:
containeronToaster. 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 ascontainer. - Fixed:
toast.promise's outcome stays as long as the Toaster'stimeoutsays, 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.toastnow 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 })(orregisterKeysfrom 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 whoseonKeyreturns 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 })(oropenLayer) 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)andkeyTargetOfread the same rule for a widget's ownonKeyDown,typingIn(target)for a clipboard handler. - New: an instance or a layer whose root is inside an
inertsubtree 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).KeyPressandKeyTargetare 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.Presstakescode, andpressedmatches ⌥ combinations on a Mac. - Changed:
pressedlets 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 asaria-keyshortcutsnames 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:
PlayerKeysanswers 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 secondPlayerKeyson a page no longer throws. - Changed (breaking): the parts take
material(plainorglass) andscheme, as every base control does, each their own prop or the nearestMaterialProvider's:PlayerTransport,PlayerSpeed,PlayerCaptions,PlayerSound,PlayerFullscreen,PlayerFoot,PlayerBar,PlayerPlaylistButtonandPlayerZoomFinder. Their glass faces are the baseButton,DropdownMenu,PopoverandTipin glass, so the item now pulls inbuttonandpopover.PlayerChrometakesmaterialand gives it to every part inside (<PlayerChrome material="glass">is a whole player in glass, its tooltips and menus too), andPlayerFootandPlayerBardo the same for what they hold. The playlist card is thepopoveritem, no longer a raw Base UI popover.PlayerTransport'sfillworks in both faces. The glass twins in liquid-glass are gone. Migration: -GlassPlayerTransport,GlassPlayerSpeed,GlassPlayerCaptions,GlassPlayerSound,GlassPlayerFullscreenandGlassPlayerFootarePlayerTransport,PlayerSpeed,PlayerCaptions,PlayerSound,PlayerFullscreenandPlayerFootinside<PlayerChrome material="glass">(or each withmaterial="glass"). -GlassPlayerPlaylistisPlayerPlaylistButtonin glass;GlassPlayerZoomFinderis<PlayerZoomFinder material="glass" />, andPlayerZoomFinder'sframeandedgeprops are gone (the material draws both faces). - Deleteglass-player.tsxfrom 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 (noVideoPicture), givePlayerChromescheme="dark"or put a<MaterialProvider scheme="dark">round it; adarkclass you set on the chrome is no longer needed.
- Changed:
GlassPanedraws its front surface with the material'sGlassFrontinstead 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 thematerialthat shipsGlassFront, which an update ofliquid-glassinstalls with it.GlassFront,GlassFrontAtandglassFrontare re-exported here. - Changed:
GlassPane's edge isGlassEdge, 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'sstylewith its own knobs typed (--glass-pane-front-height,--glass-pane-front-blur). - Changed: a
GlassPanewhose 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, andGlassEdgeis the material's, re-exported here. Migration:GlassPlayerTransport,GlassPlayerSpeed,GlassPlayerCaptions,GlassPlayerSound,GlassPlayerFullscreen,GlassPlayerFoot,GlassPlayerPlaylistandGlassPlayerZoomFinderare gone (seeplayer); deleteglass-player.tsxfrom your copy. - Changed (breaking): the collection is what glass makes that no base control is and that needs no media:
GlassPaneandGlassGlide, beside the material's re-exports. It installsmaterialandtokensand 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,DisplayVideoandGlassDisplayPropsare installed byglass-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),DisplayPlaybackandDisplayVideo. Its edge is the material'sGlassEdge, and it usesvideoforDisplayVideo. - 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.
rimis 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):
radiusis 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"isradius={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, isThermalState. Migration: rename the type.
- Changed: the
shortcutthat focuses it goes through key-router: typed into a field, "/" stays a character there, while a ⌘ combination (mod+k) now reaches it from a field too; a lone-character shortcut matches however the layout types it. - Changed: the shortcut's place on the page is the field itself, so a search inside an
inertsubtree leaves the shortcut to the page's own, and the one under the pointer goes first.
- 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 withtree), the tree without its drawing: expansion (controlled, uncontrolled orremembered), lazy children, the filter, selection, the roving focus, the window of rows mounted, rename and the WAI-ARIA keys, overlib/tree-core.Treedraws its row grid on it; a list of another shape can draw on it too. - New:
layeronTree, 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.TreeApiandTreeLayerare 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; fromui/tree:MarkSign,pressMark,markEntries,JoinBadge,OrBadge; fromlib/tree-core:Mark,MarkState,MarkChange,MarkEntry,markStates,setMark,toggleMarkand 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 installstree-marksand passes its layer:marks={m} onMarksChange={set} pressMarksbecomeslayer={useTreeMarks({ marks: m, onMarksChange: set, pressRows: true })}(onMarkandorWithmove into the same call), and the names above are imported fromui/tree-marksandlib/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 withnpx shadcn add @ag/tree-marks: whatTree'smarksdid, pressed the same way (a click only this row, ⌘ adds, ⇧ a range, ⌥ excludes, ⌘⌥ adds as or; Space, + and - from the keys),pressRowswhere the rows are the marks,onMarkandorWithfor a lens.lib/tree-marks-coreholdsmarkStates,setMark,toggleMarkand the lens;pressMark,markEntries,MarkSignandJoinBadgegive 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_KEYSis 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_SHORTCUTSis the validaria-keyshortcutsderived from it (the ruler's was glyphs). Migration: a legend readingrow.keysas display text readsrow.shown ?? row.keysand draws them withKbd. - 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 bylabel, witharia-keyshortcutsfromEDIT_KEYS; the ruler is a drawing, no longer arole="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 roleapplicationand its name) where you focused the ruler; a test that found the slider finds the application. - New:
schemeonTimeline, the colour scheme it stands in (material). - Changed (breaking): lib/timeline-rows'
ClaimIndexkeeps one list per length class (classes) andbyIdinstead ofstartsandreach, 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), andrepaintOf(was, next)withPainting. Migration: code readingstartsorreachreadsvisibleInorclaimAtinstead.
- 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:
CaptionLinesandCAPTION_SIZEin 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'sTranscriptCaptionsboth stand in it, so the one caption look is one size too. Migration:TranscriptCaptionssized itselftext-sm; it now sizes against the box it spans, so place it across the picture (aclassNamelikeabsolute inset-x-4 bottom-16), not in a narrow box.
- Removed:
GlassButton,GlassToggle,GlassBadge,GlassCard,glassButtonVariantsand their types (GlassButtonProps,GlassButtonSize,GlassButtonCorners,GlassToggleProps), andGlassTipandGlassTooltipContent(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: -GlassButtonisButton material="glass", its heights one step down the button's ladder:sm(32 px) ismd,md(40) islg,icon-smisicon,iconisicon-lg; its 48 pxlgandicon-lgarelg/icon-lgwithh-12/size-12.as="a"isrender={<a />}; a tint isstyle={glassStyle({ tint })}. -GlassToggleisButton material="glass" pressed, with itsonandoff. -GlassBadgeisBadge material="glass"(a tint throughglassStyleonstyle);GlassCardisPanel material="glass". -GlassTipisTip material="glass",GlassTooltipContentisTooltipContent material="glass". - AMaterialProvider 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,DropdownMenuandTipin glass, soliquid-glassnow pulls inbuttonandmenu, and no longerclass-variance-authority. - Removed:
GlassInputandGlassSubmit(glass-input),GlassMenuand its parts (glass-menu),GlassSegmented(glass-segmented). Migration:GlassInputisInput material="glass"(its validation is the form's:formwith any Standard Schema,FormSubmitfor the button);GlassMenu,GlassMenuTrigger,GlassMenuContent,GlassMenuItem,GlassMenuLabel,GlassMenuSeparatorare menu'sDropdownMenuparts withmaterial="glass"on the content;GlassSegmentedisToggleGroup material="glass"with aToggleGroupItemper option. Delete the three files from your copy. - Removed:
GlassDialogContentandGlassNumber, 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 inmaterial'smaterial-glass.tsxand is re-exported fromliquid-glass, so imports fromliquid-glasskeep working.liquid-glass.cssis gone: its one rule (--glass-rimregistered, so the rim's light eases) ismaterial-glass.css. Migration: reinstallliquid-glass(it pullsmaterial) and delete your oldliquid-glass.css. New:glassStyle(light), a glass's rim and tint as the custom propertiesglassVariantsreads, for an element that wears the glass itself. - New:
GlassPlayerCaptions, the captions control in glass;GlassPlayerFootmeasures its room asPlayerFootdoes.
- Changed: the bar's verbs (
BarVerb) and its clear are the baseButton, ghost, at the bar's height: one button everywhere, andselectionpulls inbutton. - New: one selection for every set that is shown. The kernel that was
gallery'slib/gallery-selection(only, toggle, range, all, area, target, inOrder, shownOf, inRange) plusactsOn, what a verb asked of an item acts on: the shown selection when the item is in it, else that item alone.SelectionBaris a selection's verbs as a bar: the count, what the filter hides, the sameMenuEntrylist 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 inbutton, no longerliquid-glass.
- Changed: its glass is the material's (
material-glass); it pulls inmaterial, no longer the wholeliquid-glasscollection.
- Changed:
PlayerSpeedandPlayerCaptionsopen the menu's dropdown (playernow pulls inmenu), andSpeedItemsandCaptionsItemsare its radio rows, in whatever material the menu holding them is. Migration: theiritemclass prop is gone; put them inside aDropdownMenuContentand 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.CaptionsItemsare its rows for any menu face.Cturns captions on and off underPlayerKeyswhere the medium has some (PlayerKeysCard'scaptionslists it).PlayerIcon.captions. - New:
PlayerFootmeasures the room it takes at the picture's foot (useFootRoom,--ag-player-footon the picture), which the captions rise by. - Changed:
useFullscreenis never null, andPlayerFullscreenandGlassPlayerFullscreenalways 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:
Sheetis dialog's root, modal;SheetContenttakesside(right, left, top, bottom) andmaterial;SheetTrigger,SheetClose,SheetHeader,SheetTitle,SheetDescription,SheetFooterare 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:
materialonDialogContent(or aMaterialProviderround 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:
materialonScrollThumbs: 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_SCROLLBARon the scroller,ScrollThumbsas 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-describedbyon 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
menutakes its verbs:optionsflat or in named groups (SelectGroup), each option with an optionalicon,description,text(what typing finds it by when its label is not plain text) anddisabled; the chosen row is ticked. The field is the input's well at the input's heights (sm,md,lg), red whenaria-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-labelledbyandaria-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;
multiplefor 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'sGlassSegmentedwas).
- New: who someone is, one person, chip-input's single-choice partner. Named, their face and short name as one chip (pressed it changes,
onClearbehind its ×); unnamed, a field that finds anyone by any of their names and offers someone new of what was typed whenonCreateis given. Choosing is saving. Aguessrides 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.PersonFacedraws a face, its picture or its initial, at the avatar's sizes. - New:
material(andtone): 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. Withtone="dark"it takes the dark theme's ink, its list too. Its field is the registry'sInputin 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).
Timelinetakesrows(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 bylevels), afilmstripof stills from a source callback, andonAnswer, 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_KEYSlists 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
materialand 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:
materialonTooltipContent,TipandTooltipHint: 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 aMaterialProvider material="glass"every tooltip is glass. - Removed:
TooltipPopupandTOOLTIP_FACE(a face is now chosen bymaterial), andTooltipHint'stipprop. Migration:<TooltipHint tip={GlassTip}>is<TooltipHint material="glass">. - New:
TiptakesclassNamefor its surface, so a tip's content can shape what it sits on (gallery-tag'sPERSON_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) andsize(sm,md,lg: the button's 28, 32, 40 px with one row). - Removed:
bare. Migration: a field that brought its own surface takesmaterialinstead.
- New:
materialonKbd,KbdCapandKeysText: 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 thetone'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(andtone, dark by default) onEdgeArrowsandEdgeArrow: plain, a bare glyph; glass, the glyph on a disc of the material's glass. The file is now a client module.
- New:
material(andtone, 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.materialonPopoverContent: 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, thentoast(),toast.success,toast.warning,toast.error,toast.loading,toast.promise(p, { loading, success, error })andtoast.dismiss(id?), from anywhere, inside React or out. Adescription, anaction({ label, onClick }), atimeout(0 keeps it), anidthat 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).materialon 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):
kinddefault,success,warning,destructive, each one colour everywhere (its icon andtitlein it, a faint wash, a fainter edge), the description in the text's colour; an icon per kind or your own (icon,nullfor none), anactionat 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(andtone): 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. Withtone="dark"(over a picture) the tree takes the dark theme's ink. Its row menu wears the same material. - New:
branchIds(roots, children, depth)stopsdepthlevels 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.selectedandonSelectedChangeare unchanged. - New:
rowMenureceives 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:
RowMenuandTreeMenuEntrymoved tomenuasMenuandMenuEntry, same props and shape. Migration:import { Menu, type MenuEntry } from "@/components/ui/menu", renameRowMenutoMenuandTreeMenuEntrytoMenuEntry. The popup'sdata-slotismenu(it wastree-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).
pressMarkexcludes on ⌥. - Changed:
onMarkhears a whole gesture:(changes: MarkChange[], only: boolean),onlywhen the includes start from none. Migration:onMark={(id, mark, how, clear) => …markLens(lens, id, mark, how.or, clear)}becomesonMark={(changes, only) => changes.reduce((l, c) => markLens(l, c.id, c.mark, c.or, c.clear), only ? { ...NO_LENS, exclude: lens.exclude } : lens)}. - Changed:
storageKeyis nowremembered, aSetting<readonly string[]>fromremembered: the consumer says how long the expansion lives. Migration:storageKey="x"becomesremembered={setting("x", { initial: [], keep: "always", parse: isStrings })}; what was kept under the old key is not read.
- New: a small label in metadata's voice (mono, lowercase):
default,secondary,outline,destructive, plain or a sliver of glass;rendermakes 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 fromrowstomaxRows, then scrolling; the browser sizes it where it can (field-sizing: content) and it is measured where it cannot.autosize={false}keeps itrowstall. 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 itslabel; 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;
cardis a family name here,card-galleryandcard-linkare their own items): a surface holding one thing, withtitle,description, anactionat the head's end and afooter, all props. Plain the page's card surface, glass the collection's glass card (what liquid-glass'sGlassCardwas).
- New: one control and what is said about it, plain or glass: its label (
htmlFor), adescriptionand anerrorunder it (aria-describedby,aria-invalid),required(a mark for the eye,aria-requiredfor a screen reader), anasideat the label's end,orientationabove or beside. The contract is plain props:childrenas a function is handed the wiring to spread on the control's focusable element, anduseField()gives the same to a control that draws none of its own. The error's row opens and closes in place.FieldSetgroups 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.Fieldbinds a field by name, typed (the name autocompletes, the value is the type at that path), handing its controlc.inputfor a text field orc.value,c.onValueChangeandc.wiringfor 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.checksare 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 (setInitialfor a save elsewhere);optimistictakes the send as done and rolls back with the reason.form.FieldArraykeys each row and moves its marks and messages with it (append,insert,remove,move).Formfocuses 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).FormSummarylists every problem, each a way to its field, in a region a screen reader hears;FormStatussays unsaved, saving, saved or why it failed, with a retry;FormSubmitturns while sending;FormResetdiscards. Plain or glass throughmaterial. 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:
materialandtoneonInputandInputGroup. 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 onmaterial.GLASS_WELLis the glass well, besideWELL.
- New: a yes or no ticked in a box, on Base UI's checkbox (a hidden input a form submits, Space ticks it).
indeterminatedraws a dash, for a box that stands for a group only partly ticked. Withchildrenthe box and its words are one label, the words pressing it too.size(sm,md,lg), red whenaria-invalidor 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).
RadioGroupsetssize,materialandorientation(vertical or a row) for everyRadioin it; a radio withchildrenis one label with its words. Red when the group isaria-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, withchildrenone label with its words, red whenaria-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(withvariant,size,material),TabsList,TabsTab,TabsPanel.segment(the default) lifts the active tab out of a well;linelights its stretch of a rule. Plain or glass.
- New:
captionsonVideoPlayback, 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;VideoPicturedraws 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 needscrossOriginand CORS headers, as the video does. - Changed: a pinch on a picture that zooms (
VideoPlaybackwithzoom) 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:
useMediaCaptionsreads a media element's captions (lib/captions: the files offered, which shows, the lines loaded) anduseCaptionshands 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 markeddefaultshows.Caption(andCAPTION) is the look of a caption line over a picture, one for every caption drawn. - New:
lib/picture-zoomexports the ladder both use,pinchRungandrungTaken, withPINCH_PER_WHEELandPINCH_SLOP. - Changed:
toggleFullscreenfills 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) andonScreenChange(fn); ask these instead ofdocument.fullscreenElement.pinchRungdropscanFill: the screen is always there to take. - Changed:
PictureZoom.pinchtakes{ start, quiet }where it took a positionalstart, andfittakes{ quiet }; a quiet move names no zoom. Migratepinch(s, at, true)topinch(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"(itswidthandheightits picture's, 0 when it has none),average(the mean colour,#rrggbb), andframes: { every, count, src }, stills along a clip, frameiati × everyseconds atsrcwith{i}replaced.isMediaAssetchecks 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, sospan: { from, to }reads only that part of a long source.frames: { every, width }makes stills along the clip,audioOnlykeeps the sound alone (AAC in MP4, its picture as the poster),maxSidecaps a video's longest side (1920), and every asset carriesaverage.mediaAssetresolves the frames'{i}address. A Buffer input behaves as before. - Fixed:
averageis 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), andshortName(person), which falls back toname.
- 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-datareads the video from any link YouTube hands out (watch?v=, youtu.be, shorts, embed, live, a start ast=1m30s) and its title and channel from YouTube's oEmbed at authoring time:mise youtube <url>.
- New: a quotation is the
quoteitem'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.blockquotedraws it your way. A bare blockquote (the editor's) wears the same look throughMARKDOWNandMARKDOWN_VARS(the quote's mark as a mask); wearMARKDOWN_VARSbesideMARKDOWNwherever 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 throughtweet, a Telegram post throughtelegram-chat, from facts you keep), and a YouTube link is ayoutubevideo by default. The item now usesquoteandyoutube. - New:
--ag-markdown-linkgives 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 inclassName(a latertext-sm,[&_p]:my-0,[&_h2]:border-0wins); the demo shows all three ways. - Changed:
Markdownrenders anywhere, a server component included: the text is parsed on the server and no parser ships to the browser, andcomponentsandtransformmay be server functions.Highlightedmoved toui/markdown-code(the one part that needs the browser); import it from there. - New:
components.codedraws 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 bylib/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; neverjavascript:ordata:), so text anyone wrote is safe to draw. It composes:componentsswaps how a link or an image is drawn (given checked URLs),transformchanges the tree first, andmapTextrewrites 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.
EditorJobcarriespreview. 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 usesmenu(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 fromeditor.css; its box and toolbar stay. - New:
format="markdown"reads and writes a markdown file instead of HTML:defaultValueis the file's text andonChangehands 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 averbatimblock 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
onEditreturns 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 oneonEditcall (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
onMoveColumnwhen 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 reachonUndo/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.CsvTableedits 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.
- 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.
- New:
BAR,BAR_RULEandBarVerb, 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 givenag-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).useRememberedgives the value, a setter and a reset;recall,rememberandresetwork 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:
startand a followedat(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:
VideoPicturetakesinPicture, 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 (anaspectstage, fullscreen);childrenstays still over the stage. The zoom takesframe(rect): a rectangle of the picture brought close, eased, from lib/picture-zoom'sframeView. - New:
VideoPicturetakesfullscreen: 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.toggleFullscreenfills the root's part markeddata-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 readsgroup-data-[awake]/picture:, nevergroup-hover/picture:.AudioPicturethe same (media-timebase'suseStageAwake). Migration: a consumer's owngroup-hover/picture:becomesgroup-data-[awake]/picture:.
- New: a timebase plays from a sixteenth to sixteen times its speed (
RATE_MIN,RATE_MAX,clampRateintimebase), 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 aVideoPlaybackorAudioPlaybackthat 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 writesatrounded; aVideoPlaybackreports the presented frame by its name (the first whole ms inside it,frameMs), anAudioPlaybackitscurrentTimerounded (msOf), and both readdurationthe same way. A preview is whole too. A consumer comparingatagainst a float it computed itself now sees integers. - Changed: a seek lands on the frame named by the instant.
VideoPlaybackandAudioPlaybackseek 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:
PlayerLayertakesactive; 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:
PlayerZoomFindertakescorner(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(andGlassPlayerFullscreenin 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, overuseFullscreen. Absent where the browser has no fullscreen. - Changed (breaking):
PlayerZoomFindersizes 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 fixedwidthin 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.
optionspicks from a set;freemakes 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 islook="glass", a dark translucent ground with the word in its ink. Theoutlinelook is gone, and--ag-media-scrimwith it. Migration: droplook="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, withouttext-box, keeps the previous centring.
- Fixed: icons dimmed by a translucent text colour painted each stroke at that alpha, so they doubled where lines cross. They are drawn in the full colour and faded whole (their opacity).
- New:
Inspect, any nested data as a tree you can read. Avalue(objects, arrays, Map, Set, Date, cycles named) or asourcetext 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).annotateadds a word only the consumer knows.inspectValue,inspectXmlandparseSourcegive 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:
Search, the search field: the input's well with a magnifier, a clear button once typed, Esc empties it, an optional page-wide focus key (shortcut="/"), Enter and ↓ handlers.Highlightdraws the letters a match lit, from ranges or a query.
- New:
Picker, a search over things drawn small (icons, emoji, marks): the consumer's items (id,label,keywords,group,node),suggestedfirst 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,noneoffers the no-choice. Its search isSearchoverfuzzyFilter.IconPickeris 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 whenaria-invalid, at the button's heights (sizesm, md, lg).InputGroupputsstartandend(an icon, a unit, a key, a clear button) inside the well.componentsinstalls and exports it.
- New:
Shortcut, an input drawn as what the hands do: keys, a pointer action, or both.input="mod+click"(orkeys={["mod"]} pointer="click") draws the ⌘ cap, a quiet "+" and a mouse with its left button lit. Pointer actions:click,double-click,secondary-click(aliasright-click),middle-click,drag,scroll,hover,long-press, always last; keys alone work too (mod+shift+k,shift+f10). The keys arekbd's caps;modis ⌘ 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;platformdraws for a given system.shortcut(text)parses without drawing,PointerGlyphis the glyph alone.
- New:
mod(⌘ on a Mac, ⌃ elsewhere) incomboandkeys, which take an optionalplatform("mac"by default, so nothing changes for existing calls).platformOf(navigator.platform),capLabel(cap, platform)for a cap as Windows and Linux write it, andmodifiers(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),counterfor "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 withoutsrcshows its label;videomarks 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.EdgeArrowis one arrow, for a stage that names its own (label, keys, tooltip).
- Changed: the strip and the arrows are
thumb-stripandedge-arrows, installed with it. Nothing changes on screen. The.ag-lb-strip,.ag-lb-thumband.ag-lb-navclasses 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).DialogSectiontitles a group of controls inside it.
- New: a slider with snap points.
marksare named under the track;snap="marks"chooses only them (equal room per choice),snap="near"pulls the hand onto one withinpull,snap="free"keeps them as landmarks.behinddraws 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 itshandleto scrub (pointer locked; alt fine, shift coarse;markshold the value as the hand passes; Esc reverts); a click types. Typing reads.25,250ms,1m 30s,0,25,1,120,1.2k,80%and0.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).
GlassNumbertakes 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. Namedlib/numberbesideui/number.tsx, shadcn's install rewrotenumber.tsx's import of it to the ui file. Migration: installnumberagain with--overwriteand delete the oldlib/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 trailingmeta), 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.expandedandselectedcontrolled or not (selection="single" | "multi" | "none"),storageKeyremembers expansion;loadChildrenwithisBranchfor lazy children;filterkeeps a match's ancestors, opened;actionswithTreeAction(apinnedone stays visible),onRename(F2, double-click),onMovefor drag with a before, inside or after line,canDropto narrow it;marksandonMarksChangefor include, exclude, neither, a branch reading mixed when its subtree disagrees: a click toggles include, ⌘-click (Ctrl elsewhere) toggles exclude, never a cycle;pressMarksmakes 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, thenrowMenu's entries (TreeMenuEntry).contentdraws the label column as something richer than its text (a chip);reservekeeps the mark and ⋯ columns empty on a tree without them, so stacked trees share one column system.density,guides, and arefwithexpandAll,collapseAll,focus. Windowed: only the rows in view are mounted. - Fixed: the core is
lib/tree-core. Namedlib/treebesideui/tree.tsx, shadcn's install rewrotetree.tsx's import of it (and gallery-filters') to the ui file, which then imported itself. Migration: installtreeagain with--overwriteand delete the oldlib/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,orjoining 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. OnTree,onMark(id, mark, { or }, clear)hears the gesture whole in place ofonMarksChange: with it, ⌥-click includes as or and the row menu offers "include as or · ⌥ click". - New:
FileIconandiconFor(a file's icon by mime type or extension, a folder open or closed),kindOf, andICONS(disk, offline disk, phone, camera, cloud, trash, star, tag, layers, group, eye, lock and more). - Changed (same day): a row menu's
hintis drawn withshortcut: 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 ismod+clickand include again'sclick. Installsshortcut. - 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) andRowMenu(the menu itself), whoseanchortakes 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:
GlassPlayerZoomFinderdraws 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, whichGlassFrameand 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)andseekSeconds(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 framen'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 atms + 0.5, with no tolerance, so every name reads back as its own frame.TimebaseState.at,durationandprevieware documented as whole ms.
- Changed:
etais ms, like every time in the registry, andtimeLeft(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 (
doneofof, in itsunit) 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).ofnull while it is not known: a stretch sweeps the track.actionsits at the row's end for a stop or a resume. Aprogressbarwith its values; still under reduced motion.
- 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),onRemoveputs a × on each label. Data in, monochrome by alpha. - New:
MomentCurve, one answer drawn on the player's own bar (aPlayerBarface), 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.alsoreads 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.thresholddraws where its moments are cut, a hairline across the curve. - New:
momentSnaps(moments), a curve's moments asPlayerBarsnaps (where each starts and ends).MomentCurve's moment marks are pressable: a press plays from the moment's start.
- New:
VideoPlaybacksound: the video's spectrum handed down, so a halo round its face hears it.VideoPictureataspect="1"androunded-fullis the face,PlayerTransport only={["play"]}its play and pause. - New:
VideoCropandCropShot: 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.useInShotsays 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/useSoundSourcehand the analysers and whether it plays to anything inside;TimebaseSoundSourcesays it plays while the timebase around it plays.lib/sound-scaleis the one analysis scale.AudioPlaybackuses it; your own drawing readsuseSoundSource. - New: a preview is heard by the drawings. While a timeline previews a moment (a word hovered, the bar hovered),
TimebaseSoundSourcehands 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.useSoundTapreturns the tap'scontextandelementbeside its analysers, andTimebaseSoundSourcetakes the tap assound.
- New:
shade, a colour effect kept apart from what is drawn with it: aShadeis a name and the colourx(0..1) along a travel from a base colour;solidkeeps the base,shadeTablemakes 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 travellingspanof it, turning back at the end, never wrapping.aurora(span)is the shade;auroraColor,auroraHue,placeOfthe 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:
lookbubble, card or plain,densitycomfortable or compact, each person in their colour.Chatis the preset (messages and people in, you a person among them named byself, grouping and sides computed;alignsides, 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 takingclassNameand alook="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 installschat. - Change: the profile video is a
videothat 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 fromPERSON_PALETTEby 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(renderwhere shadcn's Radix one takesasChild,delaywhere it takesdelayDuration) andTooltipContent, installing over shadcn's own file so components written for it keep working. On top:Tipwraps a control and names it in one line, with the keys that do the same drawn as keys;TooltipHintis a small ⓘ beside a label for explaining something that is not a control;TooltipContenttakeskeysandcard(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-glasshasGlassTooltipContentandGlassTip.
- 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
playreplacesmodeandplayOn. Migrate:mode="player"(or no mode) isplay="manual"(the default);mode="preview"isplay="hover";playOn="visible"isplay="visible";playOn="visible-once"isplay="once". The preview's pause button ispauseButton(it wascontrolsin preview mode);controlsnow means only the browser's own bar. - New:
play="follow": the page decides withplaying, and optionallyat(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; passrest="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:
VideoPlaybackandVideoPicture: 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,rangeloops a span,chaptersdivide the bar, a refused autoplay plays muted until the first press, and volume, mute and speed are remembered.playlets 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
blurDataURLis painted under it in the first paint. - New:
widthandheightare 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, andstart(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'szoom: pinch (a trackpad's or two fingers) or ctrl-scroll at a point,+-0withPlayerKeys, and while zoomed a drag, a scroll or one finger looks around (PlayerGesturesstands down: the pinch zooms instead of going fullscreen, a drag pans instead of scrubbing).PlayerZoomFindershows 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 withfloating={false}.liquid-glasshasGlassPlayerZoomFinder. - New:
VideoPlayback'sonEnded, andVideoPicture'saspect: 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(wasag-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 everyfocus-visible:utility follows) skip an element markeddata-focus-pointer. Mountfocus-sourceto 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:
DisplayVideois avideofollowing 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
videofollowing 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
videowith the browser's bar; the engine keeps its keys. Pulls@ag/video.
- Breaking:
playheadis nowplayback. The item holds the clock andPlayback, so it is named for them;usePlayheadkeeps its name (where the playhead is). Migrate: install@ag/playback, change imports fromui/playheadtoui/playback, remove yourplayhead.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()(itscontrolsmove the clock: play, pause, seek, step, jump, preview, loop, speed) anduseTimebaseState(select)(a slice of the clock, re-rendering only when that slice changes). Content still readsusePlayhead, and now re-renders only when its instant moves rather than with its whole subtree. - New:
useOutline()andOutlineProvider: how the clip is cut, beside the clock rather than in it.PlaybackandPlayerprovide 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, anduseStageClicks, a stage's click to play and double click to fullscreen.VideoPlaybackis a thin user of the hook andAudioPlayback(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,startandautoPlayincluded. - 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.PlayerBarand 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:
PlayerLayerandusePlayerLayer: 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 ownz-50. - New:
PlaylistProviderandusePlaylist: media played one after another. The player is keyed by the item, starts it whencontinuing(reached by watching or picking), and callsadvanceon its end;loopgoes 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:
fpswhen it knows its frames,volume(null without sound) withsetVolumeandtoggleMute,setRatewhere speed can change, androot(), 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.chaptersis unchanged and is the first level; finer ones go inclip.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.Playeris now aPlayback, the content and aPlayerBar; 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) holdingPlayerTransport(the centre cluster: previous, jump back, play, jump forward, next) andPlayerFoot(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-glasshas the glass faces. The key table and the swipe grammar are framework-free inlib/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 (timepicks 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:
PlayerBarsnaps, 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'sface: a drawing the timeline shows instead of its chapter bar (aSoundTimeline, 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:
PlayerTransportandGlassPlayerTransportonly: just the buttons a small stage needs.GlassPlayerTransportfill: 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
PlayerKeyslistens 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):PlayerPlaylistbeside or under the player,PlayerPlaylistButtonas 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),PlaylistRowsfor a face of your own;liquid-glasshasGlassPlayerPlaylist.PandNare previous and next forPlayerKeys.PlayerKeysCarddraws the keys as a legend, for aTooltipHint 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.
- 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 itsstart, so a card resting on its last frame played on to the end after the pointer left and one resting mid-story rewound;DisplayVideopaused 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 ofprepare-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 picturemacos'sstillwants. What is known ahead of time staysprepare-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).
- 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 andcoveras 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.GlassDisplayis 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, centrex/y, reachrx/ryas 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 usevar(). - 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).
- 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.
- 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.codeuses 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.
- 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(lidset, asmacossets 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.
- 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-ringis 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,playerandlightboxdropped 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:
GlassInputdrops 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,backColorandfrontColor, set on that surface alone, so a pane inside a pane (a card on a board) keeps its own;tinthues 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-heightto 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. Onetintis all a glass needs;rimColorstill overrides. Exposed as--glass-rim-inkfor 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 generoustintAmount; 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: aGlassPanelifting 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;tintstains it a status's colour,asmakes 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(rendermakes any button the trigger),GlassMenuContent,GlassMenuItem(destructiveturns 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(inglass-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 atitleand adescriptionabove its content. - New: the rim catches the glass's colour: white leaning toward
tintthree times as strongly as the body, or exactlyrimColor(--glass-rim-color). Untinted glass is unchanged. - Fix:
GlassInputhas 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 (soform:validholds), still speaking only once you leave or submit; the browser's own rules speak when a schema or function is satisfied. - Fix:
GlassGlideappears 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, pluserrorfrom 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.GlassButtontakescorners,pill(default) orsoft. - 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-tonecolours it, blending from row to row, and without one it is a faint neutral. Pressed glass controls swell a little on a spring;GlassTogglegivenonandoffholds the wider's width; focus is the glass's own fine line. - New:
Img(image): a plain<img>that arrives asImagedoes (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 anythingasmakes it, a link most often),GlassToggle(controlledpressed, 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), andGlassDisplay(glass-display.tsx, below). The collection now depends on@ag/scrims,@ag/playheadand@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.radiussets the corners for the box, its clip and the rim at once.captionsits at its foot on thescrimsscrim (scrim,scrimHeight),covermakes the whole display one target (a link or a button),rimsets 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:DisplayPlaybackfor any clip on the playhead,DisplayVideofor an uploaded video over its poster,useDisplayHoverfor anything else. - New:
rimandrimWidthonGlasstune the edge:rimis the rim's and inner reflections' brightness as a multiple of the default (1),rimWidthits thickness (default 0.5px). Also as CSS,--glass-rimand--glass-rim-width. Unset, the glass is exactly as before. - New:
tintandtintAmountonGlassstain 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-hueand--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.
- Fix: a
GlassDisplayunder 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-gapsets 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
compactcommand 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.
- 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}onMacosshows only the Mac, no caption line under it. Breaking:Player'scaptionedis nowchapters,"line"(default, the chapter on screen under the bar) or"hover"(only under the pointer);captionedbecomeschapters="hover". - New:
runsOnonMacos: 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.thermalAtadds 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.
AgeBadgeis 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.RatingChipdraws 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.AdvisoryChipdraws 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;noneis three empty steps, a known zero, and an absent category is unknown.MediaSpectakesratingandadvisoryand draws them after the cut;omitandadornmentsknow 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
ThermalReadingis 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, andthermalAt(track, ms)folds it to the one moment a frame draws: readings glide from one to the next, a depth fades inDEPTH_MS, and each fan'sturnsare 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:
MacbookFrameshows the machine's heat withthermal(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.tis the playback clock, never a story clock running faster.
- New: a
thermalstep: 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
thermalsteps 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.
Githubdraws 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 withContributionCalendar, the last year in GitHub's five greens, light and dark.linescuts a long body with a fade and a link to the rest. Every card opens its place throughcard-link.fetchGithub(url, token)ingithub-datareads 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.
commitslists 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.reactionsandGithubComment.reactionsare required; refetch facts written before today.
- New: a whole card that opens a place, the way an embedded post opens the post. A click anywhere opens
hrefin a new tab, except on links, buttons, form and media controls, or when it ends a text selection; in nested cards the innermost wins. The card is not a tab stop: the keyboard reaches the place through a real link inside it.
- 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)intweet-datareads 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)intelegram-chat-postreads t.me's embed page into the facts andpostScript(post)makes them the chat's own script, drawn withframe="none" backdrop text="page" composer={false} frozen. Its pictures are on signed CDN paths that expire: download them beside the page. - New:
backdropkeeps 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 defaultscaledis unchanged. - Fix: code blocks wrap instead of scrolling sideways inside the bubble.
- New:
dayon a message draws the client's day separator before it ("23 July 2024"); a bubble'sfootreads 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:
hrefmakes the whole chat open a place (a post's permalink, throughcard-link), with received bubbles tinting under the pointer. In a channel, the channel's name links to the channel. - New: a
channelchat kind, labelled like a group with the channel's name in the accent. Messages takerich(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) andfoot(the time, linked when given anhref, the views and edited). A message with bothtextandrichthrows.
- 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'sChatProfileextends it withbot; macos takes one asuser.
- New: stereo 3D on the picture axis.
STEREOS(sbs,half-sbs,tab,half-tab,mvc, and3dwhen the layout is unnamed),stereoonPictureChipandMediaSpec, drawn as one3Dbadge after the range, with the layout in the axis label ("1080p · 3D half side-by-side"). New:stereoLabel;pictureLabeltakesstereoas 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 atsm, draws its metal and letters; both were empty boxes. vividkeeps 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, seeMarkArt.cap), not from its width, and uses the CSScapunit. Breaking:MarkArt.emis gone (a symbol stands at 1em, a lockup at1cap / cap);Mark'semprop still overrides. New:markArt(id, form). - Fix: the
ultra-hdmark'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, andentryTokens/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 itsChapters),progressAtandspans;chapterSpan(clip, first, last)for the stretch an act tells, andprogressWithin(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.
- 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:
Playbackplays a clip once in view with no controls (startwhere it stands first,delaybefore it plays), the player's bar aims and seeks, and a scroll stage'scue(a span to be in, heading to its end, fromchapterSpan) 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.Playheadprovides the moment andusePlayhead(progress)reads it as{ at, driven }: an explicitprogresswins (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):trueplays it from the top,falsereturns it to where it rests (start, a poster frame). Set, it replaces playing once on screen. - New:
usePlayhead(…).playing(andPlayhead'splaying), 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.usePlaybackreturns it too.
- Breaking: the terminal draws and no longer plays itself. The in-view autoplay and
durationare gone: place it inside<Player clip={sessionTimeline(parseSession(session)).clip}>, or pass a fixedprogressfor a still. - New:
size, the type size as any CSS length (default13px); the chrome is in em and scales with it. A box the terminal must fill passesstillSize(). - 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$), markedcode.- New:
STILL, the still's frame and type in px (the defaults ofrenderSessionSvg), andstillSize(), the still's type as a share of its width incqw, 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).clipBeatsandclipProgress, shipped earlier today, are gone with that.
- Breaking: the chat draws and no longer plays itself.
from,until,durationandafterlifeDelayare gone, each a driver's job now: wrap it in<Playback clip={chatClip(script)}>(withstart={clip.chapters[k].start}for a story that opens at message k,delayfor a phone that waits its turn), in aPlayer, or cue it from a scroll stage (cue={chapterSpan(clip, k)}is the olduntil); a fixedprogressis a still.frozenstays, 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 andconversationStart(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 isPlaybackwith a bar and takes the samestart,delayandcue, so a scroll stage and the bar drive one story. The chapter on screen is named on a reserved line under the bar;captionedleaves 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
codechapter (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")andkeys("← →")split any notation into the keys pressed, in the Mac's glyphs and order; anything that is not keys (awake hotkey,2h) isnull, 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, andisKeysfor 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 tokensonce 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 andRancommands with their└output, the working line, the─ Worked for 2h 14mrule 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 fixedprogressfor 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.accentcolours the menu highlight and the terminal. - New: the lock screen. A world with
lockedfades 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/personthe page supplies: their face through@ag/avatarand 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
surfacestep hangs the app's own rendered popover (a clip inart.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:
MacbookFramehas a hinge.lid="open"orlid="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 issin()/cos()of one number, so a transition, a scrub and a filmed frame are the same geometry. Withoutlidthe frame renders as before, flat.
- New: the timeline a
macosstage 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'sawake-sceneis 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(…).clipis 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.sceneClocklays the steps out in milliseconds, andframeAt(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, thesurfacestep andArt.surfaces(Surface);Frame.surfaceandFrame.lockSince. chill'schill --demo sceneis 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.clockis 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,CutChipand the composedMediaSpecstrip take values (resolution,range,audio,tier,lang,cut), never label strings. The item ships two files: the chips andmedia-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]); atmdthe lockups, atsmthe brand symbols and the small badges.emphasisis 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),trailingis a slot after an axis andadornmentsfills 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-ESCastilian,es-419Latin American Spanish), else the language subtag's display name throughIntl.DisplayNameswith the region left to the flag, else the tag;LangChiptakes alabeloverride.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 atsm); the metal is retuned through--ag-media-gold-hi,-mid,-loand-glint.Marktakes apaint(any CSS background, applied through a mask).TierChiptakesresolutionso 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,-cutand-scrim. Every chip carriesdata-kind,data-facet,data-emphasis,data-sizeanddata-mark.Mark,MARKS, the label functions and the vocabulary tuples are exported.
- Breaking: the
prepare-media-cli.tsfile is no longer part of the item; callprepareMediafrom your own script.tsxleaves the item's dependencies with it. - Breaking:
playbacktakes only"boomerang"."forward"named the default and chose nothing; leave the option unset for a video prepared as it plays.
- Breaking: the
optionsmessage 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; removeoptionsfrom 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-islandand--tg-bordercustom properties.device-framedraws the phone and owns those colours.--tg-message-gapis 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
lgor under reduced motion, thestackedalternative 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-aligncustom property. A pinned stage centres its scene.
- The bleed fade is as wide as the reserved rail, capped at 2rem, so a rail that resolves to zero (a custom property below its breakpoint) fades nothing.
- 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
uploadprop 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 newkeyto remount it with a different uploader.
- 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 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.
scrims: thetopPropsandbottomPropsprops. Style the overlays throughclassNameand thescrim-top/scrim-bottomslots.preview-picker:optionClassName;theme-toggle: thefallbackprop (an unresolved control showssystem) and theTHEMESexport.quote-card: thefaceShare,faceFeatherandblockAtstill options; the fusion and the block position are the module's constants, as the card's own geometry requires.lightbox-motion: theQUICKtuning;lightbox-motion'sspringStepandsettledandlightbox-wheel-phase'sreleasedread are internal now, the binder never decides on a release.terminal-session:mix,BLACK,WHITE,LAND_MS,BLANK_MSare internal.tokens:--dur-4; three durations remain.revealnow declares its dependency ontokensinstead of carrying literal fallbacks.
- Standalone
AnimatedImagepreviews 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.
classNamestyles the frame;imageClassNamestyles 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
AnimatedImageinimage-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. AnimatedImagecontrols are now opt-in. Addcontrolsto retain the play/pause button; the default preview has no overlay control.
- Forwards its native video element ref.
sizesdescribes 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. Withloop, 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. UseglassVariants({ shape, tone })when applying glass styles withoutGlass.
- 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.
- 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
scruband numericreplayprops. Drive animation explicitly withprogressfor scroll-linked playback, including reverse movement. Omitprogressfor 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"withviewport="container"to size a phone around its focused exchange and bottom frame. The parent must provide a definite height.
- 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
cnutility.