gridsmith-ui 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +969 -0
- package/README.md +21 -5
- package/dist/charts.d.ts +2 -2
- package/dist/charts.js +58 -43
- package/dist/charts.js.map +1 -1
- package/dist/chat.js +4 -3
- package/dist/chat.js.map +1 -1
- package/dist/{chunk-CYN5O567.js → chunk-2CKSSWYG.js} +9 -5
- package/dist/chunk-2CKSSWYG.js.map +1 -0
- package/dist/chunk-3BRQRKDO.js +1 -0
- package/dist/chunk-3BRQRKDO.js.map +1 -1
- package/dist/{chunk-RWKCAKN3.js → chunk-5TUVX4B2.js} +6 -5
- package/dist/chunk-5TUVX4B2.js.map +1 -0
- package/dist/{chunk-X2HEV7FB.js → chunk-64TXZ4L7.js} +23 -33
- package/dist/chunk-64TXZ4L7.js.map +1 -0
- package/dist/chunk-7Q46JXSK.js +1 -0
- package/dist/chunk-7Q46JXSK.js.map +1 -1
- package/dist/chunk-CRQ6KCRD.js +1 -0
- package/dist/chunk-CRQ6KCRD.js.map +1 -1
- package/dist/{chunk-ZYVXV4WC.js → chunk-EKBWWA6P.js} +53 -6
- package/dist/chunk-EKBWWA6P.js.map +1 -0
- package/dist/chunk-HP7WUZPK.js +1 -0
- package/dist/chunk-HP7WUZPK.js.map +1 -1
- package/dist/{chunk-MYIJNVTK.js → chunk-IXR4UEF4.js} +5 -4
- package/dist/chunk-IXR4UEF4.js.map +1 -0
- package/dist/chunk-M2QRNBDQ.js +1 -0
- package/dist/chunk-M2QRNBDQ.js.map +1 -1
- package/dist/chunk-MD2EFMES.js +1 -0
- package/dist/chunk-MD2EFMES.js.map +1 -1
- package/dist/chunk-NRHIYS5W.js +1 -0
- package/dist/chunk-NRHIYS5W.js.map +1 -1
- package/dist/chunk-OTCEJN64.js +1 -0
- package/dist/chunk-OTCEJN64.js.map +1 -1
- package/dist/chunk-OYNUU346.js +78 -0
- package/dist/chunk-OYNUU346.js.map +1 -0
- package/dist/{chunk-EX64MN2U.js → chunk-PO65I3MQ.js} +4 -3
- package/dist/chunk-PO65I3MQ.js.map +1 -0
- package/dist/chunk-QM4SJKWV.js +1 -0
- package/dist/chunk-QM4SJKWV.js.map +1 -1
- package/dist/{chunk-KWLNEH53.js → chunk-R5NFCEZ3.js} +10 -7
- package/dist/chunk-R5NFCEZ3.js.map +1 -0
- package/dist/chunk-SFY2KMHG.js +1 -0
- package/dist/chunk-SFY2KMHG.js.map +1 -1
- package/dist/{chunk-HXVXJUGV.js → chunk-TJEV7YAY.js} +38 -10
- package/dist/chunk-TJEV7YAY.js.map +1 -0
- package/dist/chunk-WH2FUFAC.js +1 -0
- package/dist/chunk-WH2FUFAC.js.map +1 -1
- package/dist/chunk-XVB55M3N.js +1 -0
- package/dist/chunk-XVB55M3N.js.map +1 -1
- package/dist/chunk-ZI3OQDET.js +1 -0
- package/dist/chunk-ZI3OQDET.js.map +1 -1
- package/dist/data-grid.d.ts +1 -1
- package/dist/data-grid.js +5 -3
- package/dist/data-grid.js.map +1 -1
- package/dist/editor-mentions.js +2 -1
- package/dist/editor-mentions.js.map +1 -1
- package/dist/editor.js +3 -2
- package/dist/editor.js.map +1 -1
- package/dist/{feather-VFWLAILO.js → feather-FNBQ74RR.js} +10 -17
- package/dist/feather-FNBQ74RR.js.map +1 -0
- package/dist/{heroicons-H3Z7DFCF.js → heroicons-DODCSYWD.js} +9 -8
- package/dist/heroicons-DODCSYWD.js.map +1 -0
- package/dist/icons.d.ts +31 -4
- package/dist/icons.js +2 -1
- package/dist/icons.js.map +1 -1
- package/dist/index.d.ts +418 -65
- package/dist/index.js +1893 -838
- package/dist/index.js.map +1 -1
- package/dist/kanban.js +3 -2
- package/dist/kanban.js.map +1 -1
- package/dist/{lucide-MR32KM4G.js → lucide-ZWE3EPGP.js} +9 -8
- package/dist/lucide-ZWE3EPGP.js.map +1 -0
- package/dist/{phosphor-ABQQL5N5.js → phosphor-DIHPA4AJ.js} +9 -8
- package/dist/phosphor-DIHPA4AJ.js.map +1 -0
- package/dist/resizable.js +7 -5
- package/dist/resizable.js.map +1 -1
- package/dist/roots.css +25 -7
- package/dist/{tabler-ANUSY4D2.js → tabler-D3VYTSGI.js} +9 -8
- package/dist/tabler-D3VYTSGI.js.map +1 -0
- package/dist/tokens.css +11 -8
- package/dist/{use-pagination-BnKOnBdz.d.ts → use-pagination-BxcRlNQG.d.ts} +6 -0
- package/dist/utils.d.ts +275 -1
- package/dist/utils.js +875 -1
- package/dist/utils.js.map +1 -1
- package/dist/virtualized.d.ts +1 -1
- package/dist/virtualized.js +5 -3
- package/dist/virtualized.js.map +1 -1
- package/dist-static/tokens.css +11 -8
- package/package.json +5 -3
- package/dist/chunk-CYN5O567.js.map +0 -1
- package/dist/chunk-EX64MN2U.js.map +0 -1
- package/dist/chunk-HXVXJUGV.js.map +0 -1
- package/dist/chunk-KWLNEH53.js.map +0 -1
- package/dist/chunk-MYIJNVTK.js.map +0 -1
- package/dist/chunk-RWKCAKN3.js.map +0 -1
- package/dist/chunk-X2HEV7FB.js.map +0 -1
- package/dist/chunk-ZYVXV4WC.js.map +0 -1
- package/dist/feather-VFWLAILO.js.map +0 -1
- package/dist/heroicons-H3Z7DFCF.js.map +0 -1
- package/dist/lucide-MR32KM4G.js.map +0 -1
- package/dist/phosphor-ABQQL5N5.js.map +0 -1
- package/dist/tabler-ANUSY4D2.js.map +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
# gridsmith-ui changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased (0.27.0)
|
|
4
|
+
|
|
5
|
+
The calendar review (Sprint 25.19), dates on server-rendered pages (Sprint 25.20), the owner's review of the themes page with icon packs that build on Next.js (Sprint 25.21), and components a Next.js server component can render (Sprint 25.22). Small actions beside text are links; the Stepper's steps open when they are reached, are greyed while they can't start yet, and are a plain progress list when nothing opens them; events repeat, in the iCalendar standard's rule, with a field to set it, the Calendar drawing every occurrence and a dialog asking whether an edit is for one or the series; weeks start on the visitor's first day; fields rest on the soft edge again, with a strong edge kept for the controls whose edge is their state; a server-rendered page writes its dates in the visitor's form from the first paint and hydrates without a mismatch; an app hands the library its theme's icon pack, so every bundler builds it; a Next.js page can stay a server component, its helpers from `gridsmith-ui/utils`. It adds components (`RecurrencePicker`, `RecurrenceScopeDialog`, `LocaleProvider`), helpers (`lib/recurrence`, `useFormatLocale`, `useHydrated`, `useBrowserTimeZone`, `registerIconPack`, and the server-safe helpers on `gridsmith-ui/utils`), types (`Recurrence`, `Weekday` and the rest), props (`weekStartsOn` on DatePicker, a RadioGroup option's `reveal`, the shell's `canNavigateBack`) and a token (`--ds-control-edge-strong`), and changes behaviour and defaults (the Stepper's steps, the week's first day, the fields' edges, what a server-rendered page draws before it has loaded, an icon pack other than Lucide, the minimal header, the shell's height), which breaks some 0.26.0 code and tests: a 0.x minor release, 0.27.0, says so, and an app pinned to `^0.26.0` doesn't reach it by itself. The upgrade note at the end says what an app on 0.26.0 does.
|
|
6
|
+
|
|
7
|
+
**The Stepper's three kinds of step.**
|
|
8
|
+
- **A step already reached opens from its number, backwards or forwards,** when the page passes `onStepChange`. A step marked `disabled` is greyed, its name ends "cannot start yet", and it has no hover, so it is neither drawn nor announced as something to press; its label reads quieter too. A Stepper with no `onStepChange` only shows progress: the current step is marked (`aria-current="step"`) and none is a button, where each was a button that did nothing. A Stepper's full form now draws its steps as an ordered list in its Progress nav (`nav > ol > li`), so a screen reader hears how many there are and which one this is; nothing on screen moves, and the compact bar a phone-narrow header shows keeps its "Step 2 of 4" line. A disabled step's name gains ", cannot start yet": a test that found `useFlowSteps`' disabled step by its exact name adds it.
|
|
9
|
+
- **`useFlowSteps`** marks the steps past the furthest reached `disabled`, so its Stepper greys them; every step reached opens, forwards as well as back. `flow.stepper.onStepChange` is optional now (undefined when the project only goes forward): code that calls it itself checks it first.
|
|
10
|
+
- **The project's going back, carried by the shell:** `AppShell`'s `preferences` takes `canNavigateBack` (the project's "Allow step navigation back"), and `useFlowSteps` reads it. Off, a flow's step list only shows progress, `flow.back` is undefined (no Back link) and `flow.goTo` ignores a step behind. On, the default, nothing changes. A shell made before 0.27.0 has no `canNavigateBack`, and behaves as on.
|
|
11
|
+
|
|
12
|
+
**Repeating events.**
|
|
13
|
+
- **RecurrencePicker (new)** is the field for how something repeats: "Does not repeat", the quick choices for its date (every day, every weekday, every week on its day, every month on its nth weekday, a fourth one in a month's last seven days as the fourth and as "the last" and a fifth as "the last", as Outlook and Google offer, every month on its date, and on the last day when it is one, every year on its anniversary), then "Custom…", which opens right under the choice: every n days, weeks, months or years, the days as pills in the week's order, a month's date or nth weekday, and when it ends (never, on a date, after a number of times), each shown under its option. A plain summary sits under the field, read with it ("Every week on Tuesday and Thursday, until 30 November 2026"). In a FormWrapper its errors ("Choose at least one day", "Enter the end date", "The end date must be after the event starts") reach the error summary and the errors `onSubmit` receives, so a page that saves only when they are empty doesn't save. A quick choice follows the event's date when it moves; a custom repeat stays as set, and its choices name its own day. A value no quick choice gives (a rule from another app, a quick choice the date has moved away from) opens Custom, so what shows is always the value. An end date is the end of its chosen day, so another app reading the rule keeps that day's occurrence; clearing it keeps "On a date" chosen and asks for the date.
|
|
14
|
+
- **The rule is the iCalendar standard's** (RFC 5545's RRULE) in the subset Outlook's and Google's menus write, so events move between apps: `lib/recurrence`'s `parseRRule` and `toRRule` read and write it (`readRecurrence` takes either form), `describeRecurrence` puts it in words, `recurrencePresets` gives the quick choices, `occurrences` lists an event's dates over a range (a list of upcoming events shows each date with it), `occurrenceOn` gives the occurrence on one date (given the page's events, an occurrence changed on its own included) and `sameRecurrence` compares two rules. A rule stored as JSON comes back with its `until` as a string, which every helper reads as the date it was. The event's own start is always its first occurrence, even when the rule's end comes before it (RFC 5545). The types are exported: `Recurrence`, `RecurrenceFrequency`, `WeekOfMonth`, `RepeatingEvent`, `Occurrence`, `RecurrencePreset`, `RepeatChange`, `RecurrencePickerProps`, `RecurrenceScope`, `RecurrenceScopeDialogProps`.
|
|
15
|
+
- **Where it differs, on purpose.** An occurrence keeps its wall-clock time and its number of days when the clocks change; a time that doesn't exist on the night they go forward moves forward by the hour the clocks skip (01:30 becomes 02:30 in London), as calendars show it, where RFC 5545 would drop that date. Day 31 skips the months without one, as RFC 5545 and Google do; Outlook moves it to the month's last day.
|
|
16
|
+
- **The Calendar draws repeats.** An event's `repeat` (the picker's rule, or an RRULE string from another app) and `exceptions` (the dates left out, as day keys) are drawn in every view, the neighbouring months' days of the month view included; the card names the schedule. Each occurrence handed to `eventActions` and `onEventClick` has its own id ("<id>::<date>") and `series: { id, date }`. A rule the Calendar can't read draws the event once, its card saying it repeats. A field of your own named `recurrence` (as some apps already pass) is left alone: the Calendar reads only `repeat`.
|
|
17
|
+
- **One repeat or the series.** RecurrenceScopeDialog (new) asks "This event", "This and following events" or "All events". An edit that changes how it repeats passes `repeatChanged`, and "This event" isn't offered (one occurrence can't repeat on its own), and `edited={countEdited(events, occurrence)}`, so the dialog says how many events changed on their own the choice undoes. The data side is the library's: `saveOccurrence(events, occurrence, scope, changes, newId)` and `deleteOccurrence(events, occurrence, scope)` take the page's events, the event the user opened and the form's fields (only what differs from that event applies), and return `{ events, save, remove }`: the list as it now is, or the events to write and the ids to delete. "This event" makes the occurrence an event of its own, its date left out of the series, linked to it by `series` (the series' id and the original date, as RFC 5545's RECURRENCE-ID links an edited instance); "This and following" ends the series the day before and starts a new one at the date, keeping its share of a COUNT unless the repeat changed; "All events" saves a changed repeat, and a moved time or day moves the series' start and its rule with it (a weekly rule's days, a month's day or nth weekday, a year's month, each excluded date to the moved rule's own date), so every earlier date stays. An occurrence changed on its own stays its series' (the owner's decision of 3 October 2026): opened again, it asks again; the series' later changes reach it, except what it changed itself (a field, another day, other times); deleting the series, or the dates from one on, deletes it; and a changed repeat removes it, as Outlook does, its date going back to the new rule. When a deletion removes the event that had focus, focus goes to its day (the month) or its hour (the week), never to the page; once focus has a place of its own, the Calendar leaves it there.
|
|
18
|
+
- **RadioGroup** options take `reveal`, shown under the option while it is chosen (GOV.UK's conditional reveal), never inside the radio; the arrows of a field it reveals stay that field's.
|
|
19
|
+
|
|
20
|
+
**Weeks start on the visitor's first day.** The Calendar, the Gantt (`useTimeScale`), DatePicker and DateRangePicker start the week on the first day of the visitor's region: from their browser's languages, then the page's language, then Monday. An American visitor now sees Sunday first in the Calendar and the Gantt, which started on Monday, and a British visitor Monday first in the date pickers, which started on Sunday; a visitor from Egypt sees Saturday first. A page's own `weekStartsOn` wins, and now takes any day, Sunday 0 to Saturday 6 (the `Weekday` type, exported); DatePicker takes it too, where Sunday was fixed. A Gantt or `useTimeScale` given its own `locale` starts the week where that language is spoken. On a server-rendered page the Calendar is drawn once the browser has the page, in the browser's week; a Gantt drawn on the server starts on the request's day (with `LocaleProvider` given the header), else Monday, and the browser's day applies once the page has hydrated; a browser whose `Intl` has no week information (Firefox) reads the region from CLDR's table.
|
|
21
|
+
|
|
22
|
+
**Softer field edges.** A field's and a control's resting edge (`--ds-input-border`) is the theme's soft border again, the colour of its cards and dividers, as it was before 0.23.0's stronger edges: Input, Textarea, the selects, Combobox, the date and time pickers, Checkbox and RadioGroup at rest, FileUpload's zone, the editor and the chat composer. A hover is a step stronger, as before. The controls whose edge shows their state keep the strong edge, in a token of its own, `--ds-control-edge-strong` (new): an off switch's outline and knob, and the Calendar's neutral events; the checked colours are unchanged. A SegmentedControl's selected segment takes a secondary button's edge (below). Your `gridsmith-tokens.css` carries the edges: upgrade, then regenerate it. 0.27.0 over an earlier stylesheet draws the off switch's outline in the soft edge until it is regenerated (still seen, not at 3:1); a stylesheet written from 0.27.0 on is for 0.27.0 or later, since an earlier library draws the Calendar's neutral events with the field edge, too faintly on it in dark modes and in presets with their own edges, so the platform's prompts to regenerate say to upgrade first. On the platform, the deploy changes every project's stylesheet, and the dashboard's own fields take the soft edge too.
|
|
23
|
+
|
|
24
|
+
**The Calendar.**
|
|
25
|
+
- **An event card's long list of attendees** shows the theme's focus ring when the keyboard reaches it, drawn inside its edge and clear of the avatars, which still line up with the card's title; the browser's own ring showed there.
|
|
26
|
+
- **The year view's month names are buttons:** each is a Tab stop, in month order, named with its year ("Jan 2026"), with the theme's ring drawn inside its edge; Enter or Space opens the month, with focus on its grid's day once that month is drawn (a page that sets `date` a moment later is waited for). They opened only by a click.
|
|
27
|
+
- **The year view stays quick with repeats:** it groups the year's events by day once, where it looked every event up for each of its days.
|
|
28
|
+
|
|
29
|
+
**Small actions beside text are links.** A ghost button lines up by its box, so it stays among other buttons; a small action on its own beside plain text is a link, its words on the line at rest and on hover. SplitView's Back on a phone (alone at the start of its header) and AddAnother's Remove (under its fields, or beside them at their middle) are links now, where they were ghost buttons; AddAnother's Remove column is as wide as the link's word, so the headings still sit over their fields.
|
|
30
|
+
|
|
31
|
+
**Dates on server-rendered pages (Sprint 25.20).** A Next.js or Remix page is drawn on the server and again as the browser hydrates it; every date and number the library wrote could differ between the two (the visitor's language, their time zone), and React then threw the server's HTML away.
|
|
32
|
+
- **`LocaleProvider` (new)** takes the page's language and the visitor's, from the request, at the app's top: `<LocaleProvider lang={LANG} acceptLanguage={…}>`, its `lang` required, one constant giving it and `<html lang>` so the two never differ. The server then writes every date and number in the visitor's form of the page's language ("6 Oct 2026" for a British visitor to an English page), and the browser hydrates with the same. Next.js reads the header in its root layout (`(await headers()).get('accept-language')`) and renders the provider through a client file, `app/providers.tsx` (an app with one of its own adds the provider inside its `Providers`); reading the request renders each route per request. Remix reads it in its root loader, whose `shouldRevalidate` returns false so it runs for the server's render alone; in SPA mode React Router 7.2 or later runs that loader at build time, with no request, so the provider has `lang` alone. `get_app_shell` writes both. A Vite app is drawn in the browser alone and doesn't need it. Without it a server-rendered page still hydrates cleanly: the server and the hydrating browser write `en`, and the visitor's form follows straight after. A visitor's tag is read as its language, script and region, from the header's first twenty languages, and each cache the library keys by locale keeps a few dozen entries, so a server's memory doesn't grow with each new header.
|
|
33
|
+
- **The week's first day** comes from the request too: with the header, a Gantt of calendar days drawn on the server starts on the visitor's day from the first paint (Sunday for an American, Monday for a Briton).
|
|
34
|
+
- **What depends on the visitor's time zone,** which no request carries, is drawn once the browser has the page, holding its place until then: the Calendar (its days are the visitor's: today's month or week with no date, and a given `date`, a Date being an instant whose day is the visitor's); the Repeat field's words (its start's weekday and date, an end date); a Gantt whose plan or `today` has times (a plan of calendar days, "2026-03-03", draws on the server, its axis labels once hydrated); a date field's value and today; chat and timeline times, their lines kept; and `useRelativeTimeFormat()`'s text, "" until then. A Vite app draws everything at once, as before.
|
|
35
|
+
- **Server and browser date data differ** (each runtime's CLDR: "5–11 Oct" against "5 – 11 Oct", "Sep" against "Sept"), which no setting can make agree: the calendar days a server names (a Gantt's rows and region) keep the server's words when hydrating.
|
|
36
|
+
- **A dark sidebar renders on a server:** its portal, which read the document while rendering, is drawn once the browser has the page; every page of a server-rendered app with a dark sidebar answered with an error.
|
|
37
|
+
- **New hooks:** `useFormatLocale()`, the locale the library writes in on both sides, for an app's own dates and numbers; `useHydrated()`, whether the browser has the page yet, before a local time or today; `useBrowserTimeZone()`, the visitor's zone once hydrated (undefined before).
|
|
38
|
+
- **Helpers take a locale:** `formatRelativeTime(time, { locale })`, `formatFullTime(time, locale)`, `describeRecurrence(rule, start, locale)`, `recurrencePresets(start, locale)`, `timeZoneOffset(zone, at, locale)` and `categoryKind(labels, locale)`, each optional; left out, each reads the browser as before (`categoryKind` English and the runtime's, the browser's and the page's languages, the others the page's language in the visitor's form), and on a server, where there is no page, English (`en`), never the machine's own language (Sprint 25.22's review).
|
|
39
|
+
- **The engine** flags a Next.js root layout or a Remix root without the provider, and a Next.js root whose own `Providers` never get the visitor's languages (`gs:locale-provider-missing`, a warning naming 0.27.0).
|
|
40
|
+
|
|
41
|
+
**The themes page review, and icon packs on Next.js (Sprint 25.21).**
|
|
42
|
+
- **An icon pack comes through the app's own import.** An app whose theme draws Feather, Heroicons, Phosphor or Tabler registers it once, at the top of its shell, before any `preloadPack`: `registerIconPack('feather', () => import('react-icons/fi'))`, from `gridsmith-ui/icons`. The library itself names only Lucide, which every app installs as the fallback, so a bundler that resolves every import while it builds (Next.js, webpack and Turbopack alike) meets only the pack the app installed: every Next.js app on 0.26.0 failed `next build`, and `next dev` answered its pages with an error, unless all five packs were installed. A pack drawn without its registration draws Lucide's icons, and in development the console names the line to add; registered later, it draws its own. `get_app_shell` writes the line, and the engine warns on a file that draws such a pack and never registers it (`gs:icon-pack-not-registered`, naming 0.27.0).
|
|
43
|
+
- **Feather's missing icons are Lucide's.** Feather has no sparkle, strikethrough, numbered list, quote, superscript, subscript, highlighter or drag grip, and other Feather glyphs stood in for them (a star for the assistant's sparkle, a slash for strikethrough, chevrons for superscript and subscript). A Feather theme now draws Lucide's for those eight, on the same grid and stroke, Lucide being Feather's continuation. The table is Feather's own table, where its grid stood in.
|
|
44
|
+
- **The header in the floating block.** A floating header that doesn't snap, with its breadcrumbs contained, sits in one card with the breadcrumbs, as the snapping header does before the page scrolls: it drew a second card of its own inside that one (a bordered box for the default and minimal headers, an inset band for the colourful one), since `PageContainer` took the composition over on 14 September. The tabs-integrated header leaves 24 px under its tabs, as the snapping header and the default header with tabs do; it left 16.
|
|
45
|
+
- **The minimal header shows no description,** static or snapping: it's minimal for a reason, and it grew from 66 to 90 px with one. `description` is still accepted, so no code breaks; a page that needs its description shows uses the default header.
|
|
46
|
+
- **A SegmentedControl's selected segment is a kind of button:** it takes a secondary button's surface and edge (`--ds-border-default`), as its frame takes a button's edge, where it kept the darker 3:1 edge the fields gave up. Its state shows as the raised surface on the muted track, with its shadow and its label's stronger colour.
|
|
47
|
+
- **An Avatar's status dot sits 2 px further in** at every size: from the avatar's bottom and right edges, −2 px small, 0 medium and 2 px large.
|
|
48
|
+
- **The shell's height is the dynamic viewport alone** (`h-dvh`), which an app's own height class replaces: a shell given `h-full` inside a frame was the window's height, and ran under whatever followed the frame. A phone browser's toolbar still hides nothing.
|
|
49
|
+
- **The engine** points a hand-built search row above a `CardGrid` at TableToolbar, as it did above a Table, however long the row's markup, and leaves a search box inside a dialog, a drawer or a popover alone (`gs:table-toolbar-pattern-expected`); it flags an aside that pads itself with an inline style as it did with padding classes (`gs:splitview-aside-padded`).
|
|
50
|
+
|
|
51
|
+
**Next.js server components (Sprint 25.22).**
|
|
52
|
+
- **The components render in a server component.** Every file of the package's component entries opens with `"use client"`, so a Next.js page left as a server component (the App Router's default) imports and renders them, passing plain data: text, numbers, dates, elements. A function (`onClick`, `onChange`, a column's `render`) needs a client component of the app's own, as in any Next.js app, and so does a page that calls the library's hooks. A server page doesn't read the library's constants or contexts either (it hands one on as it is), and renders a compound part by its own name: `Tabs.List`, `Tabs.Trigger`, `Tabs.Content`, `Collapsible.Trigger` and `Collapsible.Content` are exported as `TabsList`, `TabsTrigger`, `TabsContent`, `CollapsibleTrigger` and `CollapsibleContent` too. Before, a server page that imported the library failed `next build` ("createContext is not a function"), so every page using it had to be a client component.
|
|
53
|
+
- **A server page's layout composes as a client page's.** A server component hands each client component among its children on as a lazy reference, so the library's own checks of a child (PageContainer's PageHeader, Card's header and title, EmptyState's icon, a table header's badges, ChartContainer's chart) now read through it; otherwise the page drew without its theme's placement, contained breadcrumbs or a card's badge in place, with no error.
|
|
54
|
+
- **The helpers a server calls come from `gridsmith-ui/utils`,** which isn't marked and needs neither React nor a browser: the repeat rules (`parseRRule`, `toRRule`, `readRecurrence`, `occurrences`, `occurrenceOn`, `describeRecurrence`, `recurrencePresets`, `sameRecurrence`, `nthOf`, `saveOccurrence`, `deleteOccurrence`, `countEdited`), calendar days and date ranges (`dayOf`, `dateOfDay`, `endDayOf`, `timeExtent`, `formatDateRange`, `TIME_ZOOMS`), time zones (`canonicalTimeZone`, `timeZoneOffset`) and a chart's label kind (`categoryKind`), beside `cn`, with their types (`RecurrenceScope` and `CategoryKind` among them; the charts entry exports `CategoryKind` too). They are the same functions the main entry exports for client code; imported from it into a server component (a page, a route handler, a server action), they are client references, which Next.js refuses to call. On a server a Date is read in the server's time zone: what the visitor reads in their own, a relative time or a repeat's days, is the components' to draw, so a server page hands `RelativeTime` and the Calendar the data (the relative-time formatters stay on the client entries). A calendar day ("YYYY-MM-DD") or a day index is the same everywhere. With no locale given the helpers write English on a server, not the machine's own language; **`requestLocale(lang, acceptLanguage)`** (new, on `utils`) gives the locale `LocaleProvider` writes in for the request, so a server page's own words read as the components' beside them.
|
|
55
|
+
- **`LocaleProvider` can sit in the root layout itself,** now that a server component renders it; `app/providers.tsx`, as `get_app_shell` writes it, works as before.
|
|
56
|
+
- **The engine** knows server code by its file's name (an App Router page, layout, route handler or metadata route without `'use client'`) or by what it shows (`'use server'`, `server-only`, `next/headers`, a route's server exports; an async default export that renders, or a route handler's methods, as a guess), and flags there a helper imported from a client entry (`gs:server-helper-from-client-entry`), another of the library's values called or read (`gs:client-value-on-server`), and a function handed to one of its components (`gs:function-prop-on-server`): an error where the file is surely server code, a warning on a guess, each naming 0.27.0. Such a page builds and fails only when it renders, because the root layout that reads the request renders every route per request. The engine no longer flags `LocaleProvider` rendered by a root layout. `get_app_shell`'s Next.js notes say what a server page can hand the components, and that the icon pack's registration stays in the shell, a client file.
|
|
57
|
+
- **Vite and Remix apps** build as before: their bundlers ignore the marker. Next.js 15 or later (the first to take React 19).
|
|
58
|
+
|
|
59
|
+
**Also.** The core entry, `dist/index.js`, measures about 844 kB (844,362 bytes; 788,464 in 0.26.0), most of the growth the repeat rule, its field and the edit helpers, and the locale threading of Sprint 25.20; its size gate rises to 850 kB. The release gate fails a build that imports any icon pack but Lucide. The package now ships this changelog, `CHANGELOG.md`, each release ending with its upgrade note for agents.
|
|
60
|
+
|
|
61
|
+
### Upgrade note for agents (0.26.0 → 0.27.0)
|
|
62
|
+
|
|
63
|
+
Upgrade `gridsmith-ui` to `^0.27.0` (an app pinned to `^0.26.0` never reaches it), then:
|
|
64
|
+
- **The tokens.** Upgrade first, then regenerate the theme's tokens (`get_tokens`), every app: they write `--ds-control-edge-strong` in every mode, which an off switch's outline and knob and the Calendar's neutral events draw in. Until then, 0.27.0 over a 0.26.0 stylesheet draws the off switch's outline in the soft resting edge: still seen, not at 3:1. Never regenerate them on 0.26.0: its neutral events read the field edge, soft on the new stylesheet.
|
|
65
|
+
- **Steps.** A Stepper whose numbers open steps passes `onStepChange` and marks every step not reached yet `disabled`, and its handler opens every step it's given; `useFlowSteps` does both, so a flow on it changes nothing. Remove a guard that ignores a click on some steps (`if (index <= reached) setStep(index)`): mark those steps `disabled` instead. The engine flags the guard (`gs:stepper-click-ignored`). A Stepper that only shows where the user is drops a handler that does nothing: with no `onStepChange` its steps are list items, each number an image named for its step ("Step 2: Schedule"), the current one marked `aria-current="step"`. A test that found those steps as buttons finds them by that name, as images. `flow.stepper.onStepChange` may be undefined now: code that calls it itself checks it first.
|
|
66
|
+
- **Going back, from the shell.** A shell generated before 0.27.0 doesn't carry the project's "Allow step navigation back", and behaves as allowed. When the project's is off, add `canNavigateBack: false` to AppShell's `preferences` literal (the engine's shell check warns and names the value).
|
|
67
|
+
- **Repeats.** Replace a Repeat select of your own (`'none' | 'daily' | 'weekly' …`) with `<Field label="Repeat"><RecurrencePicker value={repeat} onChange={setRepeat} start={start} /></Field>`, store its value on the event's `repeat`, and remove the code that copied a repeating event across dates: pass the series and the Calendar draws every occurrence. An event field of your own named `recurrence` is not read: convert it to `repeat` (`'weekly'` is `{ freq: "weekly" }`, which follows the event's day). Saving an edit to an occurrence (it carries `series`), or deleting one, asks first: `<RecurrenceScopeDialog verb="Save" repeatChanged={…} …/>`, or `verb="Delete"` for a deletion. Then store what `saveOccurrence(events, occurrence, scope, changes, newId)` or `deleteOccurrence(events, occurrence, scope)` returns (`setEvents(change.events)`, or write `save` and delete `remove`), keeping each event's `series` field when you store it: an occurrence changed on its own is linked to its series by it. Pass the dialog `edited={countEdited(events, occurrence)}`. Never filter or split the events yourself, and never store the occurrence spread with the changes as a new event, which starts a second series. An Edit with an address of its own carries `event.series?.id ?? event.id` in the path and the occurrence's date in the navigation's state, and finds the occurrence with `occurrenceOn(series, date, events)`. A list of upcoming events shows each date of a repeat with `occurrences(event, from, to)`, and `toRRule(rule, { allDay })` writes the rule for another app. The engine flags a hand-built repeat choice (`gs:recurrence-hand-built`).
|
|
68
|
+
- **Weeks.** Remove a `weekStartsOn` set only for one language (`weekStartsOn={1}` for British English): the visitor's region decides now. Keep one where the product fixes the week (a payroll week). A test that read a grid's first column, a weekday header or Home and End by name builds the expected day from the visitor's region, or stubs `navigator.languages`: jsdom's is American English, so the Calendar and the Gantt now start on Sunday there. A test in a real browser (Playwright, Vitest's browser mode) gets the machine's language: pin the browser's (`--lang=en-GB`) or build the expected day from it.
|
|
69
|
+
- **Small actions.** A ghost Button with words on its own in a SectionHeading's, PageHeader's or CardHeader's `actions` (an Edit, a Change, a View all) becomes `<Button variant="link">`; ghost buttons among other buttons stay as they are. The engine flags the lone ghost (`gs:ghost-alone-beside-text`). A test that found SplitView's phone Back or AddAnother's Remove by their ghost look (`data-variant="ghost"`) finds a link.
|
|
70
|
+
- **Server-rendered apps (Next.js, Remix).** Add `LocaleProvider` at the app's top, as `get_app_shell`'s `locale.patch` says, merging into the app's own files and never replacing them. Next.js: in `app/providers.tsx`, add `LocaleProvider` inside the app's `Providers` (or create the file from `providers_snippet`); in `app/layout.tsx`, keeping the rest, make `RootLayout` async, read `const acceptLanguage = (await headers()).get('accept-language') ?? undefined` and wrap `{children}` in `<Providers lang={LANG} acceptLanguage={acceptLanguage}>`, with `const LANG = 'en'` giving `<html lang={LANG}>` too; a static export (`output: 'export'`) can't read the request, so there `Providers` gets `lang` alone. Remix: export a root `loader` returning `{ acceptLanguage: request.headers.get('accept-language') ?? undefined }` (or add it to the root loader there is) and `shouldRevalidate()` returning false, read it in `Root` with `useLoaderData`, and wrap what `Root` returns in `<LocaleProvider lang={LANG} acceptLanguage={acceptLanguage}>` (in SPA mode React Router 7.2 or later runs the loader at build time, so the provider gets `lang` alone; before 7.2, add no loader). `LocaleProvider`'s `lang` is required. Nothing breaks without the provider. On a server-rendered page the Calendar, the Repeat field's words, a Gantt with times, date fields' values, and chat and timeline times now appear just after the page loads, each holding its place; `useRelativeTimeFormat()` returns "" until then, so a `useMemo` that formats with it lists it among its dependencies. TimezoneSelect: `const browserZone = useBrowserTimeZone()` at the component's top, the state from the stored zone, `value={zone ?? browserZone}`, and save and check `zone ?? browserZone`, in place of `useState(saved ?? browserTimeZone())`. An app's own date text takes its locale from `useFormatLocale()`, and waits for `useHydrated()` before a local time, today or a day from a Date. `renderToString`, in a test or in the browser (a print view), writes the server's side: English without the provider, and those parts held back.
|
|
71
|
+
- **Field edges.** Nothing to change. A page that set `--ds-input-border` itself to soften the fields can drop it; one that relied on the 3:1 resting edge for a control of its own reads `--ds-control-edge-strong`.
|
|
72
|
+
- **Icon packs, every app whose theme's pack isn't Lucide.** Add the registration at the top of the shell file, as `get_app_shell`'s `icon_pack.patch` says, and change nothing else: `registerIconPack` added to the shell's import from `'gridsmith-ui/icons'`, then, below the imports and before any `preloadPack`, `registerIconPack('feather', () => import('react-icons/fi'))` (Heroicons `'@heroicons/react/24/outline'`, Phosphor `'@phosphor-icons/react'`, Tabler `'@tabler/icons-react'`). Without it the icons draw from Lucide, and the engine warns (`gs:icon-pack-not-registered`). A Next.js config that mapped the packs it doesn't install to an empty module (`resolve.alias`) can drop that. A test that renders a Feather `IconProvider` registers the pack in its setup, as the shell does.
|
|
73
|
+
- **Headers.** Nothing to change. A page on the minimal header that needs its description seen moves to the default header. A test that found a minimal header's description, or a header's own card inside the floating block, finds neither now.
|
|
74
|
+
- **The shell's height.** Nothing to change. A shell given its own height with `!important` (`className="!h-full"`) can drop the `!`; a test that found the shell by its `h-screen` class finds `h-dvh`.
|
|
75
|
+
- **Next.js pages.** Nothing breaks: a page marked `'use client'` works as before. A page that only renders the library's components with data (no hook, no function, no constant read) can drop its `'use client'` and stay a server component, rendering a compound part by its own name (`TabsList` for `Tabs.List`); server code that calls the library's helpers (a page, a route handler, a server action) imports them from `'gridsmith-ui/utils'`, passing `requestLocale(LANG, acceptLanguage)` as their locale, and hands `RelativeTime` its times rather than formatting them. The engine flags each of these on the server. A root layout may render `LocaleProvider` itself.
|
|
76
|
+
|
|
77
|
+
## 0.26.0 — 2 October 2026
|
|
78
|
+
|
|
79
|
+
The Calendar (Sprint 25.18). An event opens one card, with the page's buttons; events draw in the theme's supporting colours; an event across days is one bar; a day or an hour starts an event, by pointer and by keyboard; the week view fills the page and opens at the working day; "now" is the accent; the Calendar's and the date pickers' dates and times follow the page's language. It adds props (the Calendar's `eventActions`, the time `onDayClick` gives, an event's `attendees`, a Combobox option's `keywords`), types (`CalendarEvent`, `CalendarEventAttendee`, `CalendarColour`, `CalendarEventAction`, `CalendarDayClickDetail`, `EventCategory` and the rest, now exported) and tokens (`--ds-accent-bg`, `--ds-accent-text`), and changes behaviour and defaults (the card replaces the hover card, the week fills, the default event colours, the formats), which breaks some 0.25.0 code: a 0.x minor release, 0.26.0, says so, and an app pinned to `^0.25.0` doesn't reach it by itself. The upgrade note at the end says what an app on 0.25.0 does.
|
|
80
|
+
|
|
81
|
+
**One card per event.**
|
|
82
|
+
- **A click, Enter or Space on any event opens its card:** the month's bars, the all-day row, the week's timed events, a day's "+N more" list and a phone's day list. The card names the event's type, when it is (an all-day event across days names its days), where (with the theme's `map-pin`, a curated icon now), its description and its attendees by name, and holds the page's buttons. Escape, a press outside, a second press on the event or Tab past either end closes it; another event moves it; focus goes into it, on the first of its buttons that isn't destructive, and back to the event; an action that removes the event (Delete) leaves focus on its day. The hover card and the native tooltip are gone: hover, click and tooltip together were three surfaces for one thing.
|
|
83
|
+
- **`eventActions`** puts the page's buttons in the card, `[{ label, onClick, variant?, iconLeft? }]` (where Edit goes), or a function of the event for buttons that depend on it. The card closes and gives focus back to the event before an action runs, so a drawer Edit opens returns focus there.
|
|
84
|
+
- **`onEventClick`** is now for a page that opens a surface of its own: the card is then not shown.
|
|
85
|
+
- **An event's `attendees`** (`[{ name, initials? }]`) show in its card. The event types are exported: `CalendarEvent`, `CalendarEventAttendee`, `CalendarEventAction`, `CalendarDayClickDetail`, `CalendarColour`, `EventCategory`, `CalendarProps`, `CalendarView`, `CalendarSize`.
|
|
86
|
+
|
|
87
|
+
**Events in the theme's supporting colours.**
|
|
88
|
+
- **An event draws in its type's readable pair, as a rota's shifts do:** the supporting colour's pale fill, its deep text, and its colour on the edge (a phone's dot in it too). Every theme's pair passes 4.5:1; an event's title in its own colour on a tint of it was under that in most themes (the accent, the default Work, in 23 of 26 preset-modes).
|
|
89
|
+
- **A type names its colour:** `categories={{ meeting: { color: 2, label: "Meetings" } }}`, a supporting colour by number (1 to 5, as Badge's palette colours), `"accent"` or `"neutral"`. A supporting colour's token written as a colour (`"var(--ds-palette-2-bold)"`) is read as that colour, so a page that passed one draws its pair with no edit, and a legend can draw the same string as it is; so is a chart colour, which is the accent or a supporting colour by another name (`"var(--ds-chart-1)"` the accent, `"var(--ds-chart-3)"` supporting colour 2); any other colour, and a number the theme has no supporting colour for, draws on the neutral pair, the colour on its edge only.
|
|
90
|
+
- **The defaults:** Work is the accent; Personal, Meeting, Deadline, Travel and Social the five supporting colours (Deadline is no longer red: a status colour means a status). Social had no colour at all: its token didn't exist.
|
|
91
|
+
- **The accent's pair**, `--ds-accent-bg` and `--ds-accent-text`, is made by the supporting colours' own step and written into every theme's tokens. A token file from before 0.26.0 lacks it: `roots.css` then reads it from the accent itself where the browser reads relative colours (current Chrome, Edge, Firefox and Safari), and elsewhere an accent event draws on the accent's tint with the primary text colour, readable but paler; regenerating the tokens writes the theme's own pair for every browser.
|
|
92
|
+
|
|
93
|
+
**An event across days is one bar.** In the month view and in the week's all-day row, an event spanning days is one bar across them, broken only where a week row ends, with a square end there so the two pieces read as one; it keeps its lane on each day. Bars come first, longest first; a day with more than its lanes hold says "+N more", counted for that day. A timed event of a calendar day or more is a bar too (midnight to midnight is a day on the day the clocks change, too); an event with no end takes an hour within its own day. A timed event crossing midnight is drawn on each of its days, cut at midnight, its time reading "20:00 – 04:00 (+1)" (a screen reader hears "the next day"); one ending exactly at midnight ends on its own day, where it showed on the next day as well, and reads no "(+1)". The week's overlapping events share the width only with the events they overlap, and short ones back to back sit side by side, where the second was drawn over the first. Bars sit 24 px apart and a timed event is at least 24 px tall, each a target big enough to press (WCAG 2.5.8); one late in the evening grows up from the day's foot, where it ran past it.
|
|
94
|
+
|
|
95
|
+
**Days and hours start an event, by pointer and by keyboard.**
|
|
96
|
+
- **`onDayClick(date, detail)`:** a week's hour gives the time as well, `detail.time`, "HH:mm" (a click in an hour's lower half gives :30); a month's day gives the date alone, as before, and so does a day's cell in the week's all-day row, which takes the click the week's day header took (the header no longer does).
|
|
97
|
+
- **The month and the week are grids with one Tab stop** (the APG grid, DatePicker's keys): the arrows move a day (the week's ↑ and ↓ an hour), Home and End go to the week's ends, PageUp and PageDown change the month or the week, and Enter or Space starts an event there. Each event is still its own Tab stop; focusing one makes its day the grid's place. The compact month is DatePicker's grid too, where every day was a Tab stop.
|
|
98
|
+
- **On a phone** the month view's events are dots, and a tap on a day opens its list: each event opens its card, and New event starts one on that day when the page passes `onDayClick`. So on a phone `onDayClick` runs from the list's New event, not from the tap, and the dots are no longer buttons. The compact month's day list is a dialog too (it holds buttons), opened by a click; it no longer opens on hover.
|
|
99
|
+
|
|
100
|
+
**The week fills the page and opens at the working day.** When the Calendar leads its page in week view (the page's last content, with no height of its own) it takes the rest of the page's height, and only its hours scroll, under its day header and all-day row (the rule the Gantt, the board and the split view follow). The month and year keep their natural height. The week opens scrolled to 8:00, or to the week's earliest event when that's earlier (events a page loads just after move it, until the user scrolls), and keeps the user's scroll when the week changes.
|
|
101
|
+
|
|
102
|
+
**"Now" is the accent.** The week view's now line and its dot are the accent, drawn as borders, so forced colours keep them; they were the error colour. Today's circle is one recipe in every view, the theme's gradient included: the week's header had no gradient, and the year view tinted today's cell.
|
|
103
|
+
|
|
104
|
+
**Dates and times in the page's language.** The Calendar (its month and week headers, its weekday names, its hours, its events' times, the card), DatePicker and DateRangePicker now write dates and times as the rest of the library does since 0.25.0: in the page's language, in the visitor's own formats where they speak it. They wrote US-style English by hand ("Mar 5, 2026", "2 PM"), which 0.25.0's "everything" missed: a British visitor now reads "5 Mar 2026" and "14:00" there too. A date typed in the page's language is read back ("5 mars 2026"), as well as in English. The words around them stay the library's English. In American English too some text changes: an event's time reads "9:00 AM" (it was "9 AM"), a range keeps the language's own dash and spacing ("9:00 – 10:30 AM", thin spaces around the dash), the range picker shows "Mar 5 – 9, 2026" (it showed "Mar 5, 2026 – Mar 9, 2026"), a picker's day is named "Thursday, March 5, 2026" (it was "Mar 5, 2026"), a one-day range reads "Mar 5, 2026" (not "Mar 5, 2026 – Mar 5, 2026"), the range picker's heading reads "March – April 2026" (it read "March 2026 – April 2026"), the weekday headers are the language's own ("Mon" in the month; "S" in the pickers, each named in full), and a day's list is named with its year.
|
|
105
|
+
|
|
106
|
+
**Time zones by today's city names.** TimezoneSelect finds a zone by its city's current name where browsers still write an older one ("Kolkata" finds Asia/Calcutta, "Kyiv" Europe/Kiev, "Ho Chi Minh City" Asia/Saigon, and others); what's stored and shown stays the browser's own name. Combobox options take `keywords`, words a search finds them by that never show.
|
|
107
|
+
|
|
108
|
+
**Also.**
|
|
109
|
+
- A page that fills (the Gantt, the board, the split view, the week) keeps its own scroll positions when the page is measured again; a re-measure, a window resize among them, sent them back to the top.
|
|
110
|
+
- A Drawer, a Sheet, the command palette, a tour and a non-modal panel give focus back to what opened them when the page unmounts them while open, as Dialog already did: a form keyed by its address, or rendered only while open, left focus on the page. And a move up to a path above the one an overlay opened at is the same page, when focus was lost: an overlay's own address closing (`/calendar/events/7` back to `/calendar`, a home page's `/events/7` back to `/`) gives focus back to what opened it, if it's still there; any change of path counted as another page, which owns focus. A move to any other path still leaves focus to the new page.
|
|
111
|
+
- The library's focus helpers read Tab stops in document order wherever they run: in jsdom a selector list came back grouped, so a focus trap's first or last stop could be the wrong element in an app's test. A test that pinned the old order notices.
|
|
112
|
+
- The year view's day preview draws its events, and can't be pressed (it never could); the Calendar's registry description no longer names hover cards.
|
|
113
|
+
- A page that passes both `onEventClick` and `eventActions` is told in development that the buttons never show (`onEventClick` means a surface of the page's own, so no card opens).
|
|
114
|
+
- An event whose start isn't a date is left out (told in development) and an end that isn't a date is no end: one bad record no longer takes the Calendar down. A `date` or `defaultDate` that isn't a date shows today, told in development.
|
|
115
|
+
- Next from the 29th to the 31st of a month showed the month after next (31 January went to March); it goes to the next month, at its last day where it's shorter. The range picker's second month did the same.
|
|
116
|
+
|
|
117
|
+
### Upgrade note for agents (0.25.0 → 0.26.0)
|
|
118
|
+
|
|
119
|
+
Upgrade `gridsmith-ui` to `^0.26.0` (an app pinned to `^0.25.0` never reaches it), then:
|
|
120
|
+
- **A card of your own for events.** Remove a Popover (or a HoverCard) opened from the Calendar's `onEventClick` to show an event, and `onEventClick` itself, and put the card's buttons in `eventActions`: `eventActions={[{ label: "Edit", iconLeft: "edit", onClick: (event) => openEdit(event) }]}`. Anything your card showed that the Calendar's doesn't (a recurrence, a status) goes in the event's `description`. Keep `onEventClick` only for a page that opens a surface of its own for an event (an edit drawer, the event's own page): it replaces the card, and its `eventActions` never show. The engine flags the hand-built card (`gs:calendar-card-hand-built`, an info).
|
|
121
|
+
- **Event colours.** A type's colour written as `var(--ds-palette-N-…)` keeps working as supporting colour N: keep it, above all where the page draws the colour too (a legend's swatch, a type picker), since a number is no CSS colour. A colour of your own (a hex, a status or chart token) now draws on the neutral pair, the colour on its edge only: change it to a number, `"accent"` or `"neutral"` (`{ color: 2, label: "Meetings" }`), or, where the page draws it as well, to the supporting colour's token, `"var(--ds-palette-2-chart)"`, the colour of the events' edges (the accent's is `"var(--ds-chart-1)"`). Type such a map with `satisfies Record<string, EventCategory>`, so its colours stay strings a legend can draw.
|
|
122
|
+
- **The tokens.** Regenerate the theme's tokens (`get_tokens`), every app: they write the accent's pair, which Work events and events with no type draw in. Without it, a browser that can't read relative colours draws them on the accent's paler tint.
|
|
123
|
+
- **Starting events.** Read the time a week's hour gives: `onDayClick={(date, detail) => openNewEvent(date, detail?.time)}`, prefilling the form with it (a month's day gives no time: keep the form's usual start time). When New event has an address of its own, carry the day and time in the navigation's state, `navigate({ pathname, search: location.search }, { state: { date, time } })`, not in a search of its own. On a phone `onDayClick` runs from a day's list's New event, which shows whenever the page passes it: pass it only to start events (a page that used it to pick a day would get a New event button on phones that picks it).
|
|
124
|
+
- **A calendar page.** Leave the Calendar with no height of its own when it's the page's main content: in week view it fills the page. One with content after it keeps its natural height.
|
|
125
|
+
- **Its address.** Carry the page's search whenever it opens or closes an overlay by its route: `navigate({ pathname, search: location.search })`. The engine flags a navigation that drops it (`gs:overlay-drops-url-state`).
|
|
126
|
+
- **Dates.** A test that read the Calendar's or a date picker's dates or times by their text ("Mar 5, 2026", "9 AM") builds the expected text with `Intl` for the page's language, American English included (see "Dates and times in the page's language").
|
|
127
|
+
- **Tests that find the Calendar's parts.** The month and the week are grids now (the week two: its all-day row and its hours), so scope a query to the calendar, `within(calendar).getByRole("grid", …)`, where a date picker's grid may be open too; the compact month's days are grid cells named by their date, where they were buttons named "17"; an event is a button named by its title, when and type, with no `title`; the hover card and the compact month's hover preview (a tooltip) are gone: the card and a day's list are dialogs, and "+N more" says whether its list is open (`aria-expanded`).
|
|
128
|
+
|
|
129
|
+
## 0.25.0 — 2 October 2026
|
|
130
|
+
|
|
131
|
+
New components (Sprint 25.17). The editor does @mentions; a time zone has its select; relative times follow the project's threshold; every icon slot takes an icon name; a treemap's labels stay in their cells and value axes read compact numbers; a danger zone is a component. It adds an entry point (`gridsmith-ui/editor-mentions`), components (`TimezoneSelect`, `RelativeTime`, `DangerZone`), hooks and helpers (`useMentions`, `useRelativeTimeFormat`, `formatRelativeTime`, `formatFullTime`, `browserTimeZone`, `canonicalTimeZone`, `timeZoneOffset`) and props (RichTextEditor's `mentions`, a Timeline item's `time`, the charts' `valueFormat`, Combobox's and MentionInput's `aria-label`, SectionHeading's `titleId`, Icon's `from`), and changes defaults (what an icon slot draws, how an axis reads, which language dates and numbers are written in, where an Escape in the editor goes), so in semver terms it's a minor release, 0.25.0. The upgrade note at the end says what an app on 0.24.0 does.
|
|
132
|
+
|
|
133
|
+
**Icons.**
|
|
134
|
+
- **The everyday icons, in every pack:** `file`, `file-text`, `eye`, `eye-off`, `copy`, `external-link`, `filter` and `lock` are curated names now, so each draws the same thing whichever pack the theme uses (Lucide, Phosphor, Heroicons, Tabler, Feather). Heroicons' `type` is a letter T drawn in the pack's style, where it was Heroicons' translate icon (文A).
|
|
135
|
+
- **Every icon slot takes an icon name.** Button's and Link's `iconLeft` and `iconRight`, an Alert action's `iconLeft` and a Timeline item's `icon` take an icon name (`iconLeft="clock"`), drawn as the theme's icon at the slot's size (a button's follows the button's size), as well as an element. A name was printed as a word before.
|
|
136
|
+
- **An icon the pack can't draw says so.** An Icon whose name the theme's pack (and Lucide, its fallback) doesn't have draws nothing, as before, and in development warns once, naming the slot that drew it (Icon takes `from`, the slot's name, for that warning; Badge's slots name themselves too).
|
|
137
|
+
- **Any string in a slot is safe.** A name only an object's prototype holds ("constructor", "toString", "__proto__") draws nothing, where it drew junk or broke the page, and an empty name (`saved ? "check" : ""`) is no icon, with no warning.
|
|
138
|
+
|
|
139
|
+
**Dates and times.**
|
|
140
|
+
- **TimezoneSelect.** A searchable select of every IANA time zone the browser knows, each with its offset now (which follows daylight saving), and UTC always offered. With no stored zone the field is empty and the browser's zone leads the list, marked as the user's, for one key to pick; Clear leaves the field empty. The list is built once and again each hour, so a page left open across a change to daylight saving shows the new offsets. A stored zone is matched by the browser's own name for it ("Asia/Calcutta" is "Asia/Kolkata"), and one the browser doesn't list still shows, as it is (a stored id of your own, like "us-east", is mapped to its zone first). It is searched by the zone's name as the browser writes it, ignoring case, accents and underscores ("Sao Paulo", "São Paulo" and "New_York" all find theirs), and opens with the stored zone in view. `browserTimeZone()`, `canonicalTimeZone()` and `timeZoneOffset()` come with it.
|
|
141
|
+
- **RelativeTime.** `<RelativeTime time={…} />` draws a time as the project reads times: relative within its threshold ("2 hours ago"), the date and time beyond it, and the full timestamp, with its zone, on hover and on focus. The threshold is the shell's: AppShell's `preferences` carry `relativeThresholdHours` (24 by default), so a page passes nothing. It refreshes as it ages, each minute in its first hour and each hour after, and becomes the date and time once past the threshold; a time that changes (a reused cell) is read against the clock afresh. Days are counted on the calendar, and a time ahead counts down on time, turning relative as it comes within the threshold, however far ahead it is (it looks again each day). Among a collection's rows (a table, a list, a grid, a timeline) or inside a control (a card that is a button, a board's card, a link) it is no Tab stop of its own, so a card stays the button it is (an overlay opened from a row is no row; one record's fields in a DataList are page content); a screen reader hears the full timestamp everywhere. A threshold that isn't a number of hours, or a value that isn't a time, is told about in development (the house's 24 hours is used; nothing is drawn), and a date alone ("2026-09-01") is that day where the reader is. Its `className` is the time's own; the tooltip keeps the library's. Drawn on a server, it shows the browser's reading once the page hydrates, with no hydration error. A Timeline item takes the time itself, `time`, and draws it so. `useRelativeTimeFormat()` gives the same text with the project's threshold, for a slot that takes only text (a toast), read when it is called, so never in a page drawn on a server; `formatRelativeTime(time, { thresholdHours })` gives it outside a component, with the threshold you pass; `formatFullTime()` gives the full timestamp.
|
|
142
|
+
- **Combobox takes `aria-label`** for when no Field labels it (inside a Field the Field's label wins). Before, the attribute type-checked and was dropped. Its list keeps the highlighted option in view as the arrows move it and when it opens, and leaves it where the user scrolls it. Its search ignores case, accents and underscores.
|
|
143
|
+
|
|
144
|
+
**Charts.**
|
|
145
|
+
- **A treemap's labels stay in their cells.** A label wraps onto as many lines as its cell holds, then ends with "…"; a word too long for the cell is hyphenated where the page's language allows, else broken. A cell too small for one line, or narrower than a few characters, shows no label. The tooltip still shows the full name and value. Before, a label was one centred line that spilled into its neighbours. Its colour is the library's on-fill one (white, or a deep tint of the cell where white falls short), so each label reads at 4.5:1 or more over its cell in every theme; labels were always white, under 4.5:1 on the lighter cells of every dark theme.
|
|
146
|
+
- **Value axes read compact numbers.** Every chart's value axis (BarChart, LineChart, AreaChart, CompositeChart, ScatterChart, and a RadarChart's rings) labels its ticks compactly from five digits up (1M, 750K; "1m" in British English; a smaller scale reads 1,234 as it is), in the page's language, each written exactly, however the locale groups its numbers (2.5K, never a rounded 3K; 1.5万 in Japanese; small values keep their digits), and the tooltip shows the full value. A chart whose values carry a unit takes `valueFormat`: "percent" (values out of 100: 45 reads 45%), "milliseconds" or "bytes" (in units of 1,000, as the browser names them), each axis in one unit, or a function. CompositeChart takes `lineValueFormat` for the lines' own axis; ScatterChart `xValueFormat` for its x axis. A stack of positive and negative values spans its positive sum up and its negative sum down. The type is `ValueFormat`, from `gridsmith-ui/charts`.
|
|
147
|
+
- **Table:** a column's `sortValue` may return any of the row's fields as they are (`(row) => row.deployedAt` type-checks); a string, a number or a Date sorts, and a moment written in ISO (`2026-10-01T23:00:00-04:00`) sorts by its time, whatever its offset. Its scroll box keeps text meant for a screen reader inside it, so a wide table never widens the page.
|
|
148
|
+
|
|
149
|
+
**Mentions.**
|
|
150
|
+
- **@mentions in the editor.** A new entry point, `gridsmith-ui/editor-mentions`, exports `useMentions({ people })` (or `useMentions({ search })` for a large team: the people shown stay while the next lookup runs, an answer to an older query is dropped, and Enter replaces everything typed), and RichTextEditor takes its result as `mentions`, from the first render; its type, `RichTextEditorMentions`, comes from `gridsmith-ui/editor`. One result given to two editors draws its list in the one typed in. Typing "@" offers the people whose name or description matches, in MentionInput's list, under the mention being typed (above it at the foot of the screen); the arrows move, Enter or Tab inserts, Escape closes. While the list shows, the editable area names the active option (`aria-activedescendant`). A mention is one token: read by its name, deleted whole, and carried in onChange's HTML as `<span data-type="mention" data-id="…" data-label="…">`, so the page can notify the person. The entry's packages, `@tiptap/extension-mention` and `@tiptap/suggestion`, are installed by the app at its own `@tiptap/core` version; they aren't declared as peers, because npm then resolved them at tiptap's latest and refused the upgrade in an app on an older tiptap. `gridsmith-ui/editor` doesn't import them, so an app with the editor and no mentions builds without them. An editor without `mentions` keeps a stored mention as one token with its id, where it read it as text and its first edit saved the HTML without the id.
|
|
151
|
+
- **An Escape in the editor's text reaches the page.** ProseMirror prevented every Escape, so a Dialog or Drawer around an editor never closed on one while the cursor was in the text. An Escape the editor uses (closing its list of people) stays the editor's; any other reaches the overlay, which closes (asking first when its form has changes).
|
|
152
|
+
- **MentionInput** shares the editor's list. Inside a Field it takes the Field's label, hint, error and required state, where the Field's label named nothing; outside one it takes `aria-label`. While its list shows, the field names the active option. Shift+Tab with the list open goes back and inserts nothing (it inserted the highlighted suggestion). Suggestions come best first: a name starting with what was typed ("@Mar": Marcus Chen), then one with a word that does, then one holding it anywhere, then a description, accents ignored ("@toma" finds Tomás). The list shows the ten best and says how many more match; its rows are Combobox's. The active one stays in view. A suggestion's picture is decoration, so an option is read by its name once, and it shows the name's two initials; a pointer picks on click. `MentionSuggestion` and `MentionInputProps` are exported.
|
|
153
|
+
|
|
154
|
+
**A danger zone is a component.**
|
|
155
|
+
- **DangerZone.** `<DangerZone>` is a page's last section, holding its irreversible actions: its heading ("Danger zone", or a `title` of its own), then a frame drawn as the page's cards are, with the error colour on its edge, holding one ActionPanel per action, divided. The rows sit at a card's inset, so they line up with the cards above. The section is named by its heading. A red Button inside it is in the marked danger zone, so the engine doesn't flag it. A new check, `gs:danger-zone-hand-built`, flags a "Danger zone" heading of the page's own: a SectionHeading's title, a CardTitle, or a heading element.
|
|
156
|
+
- **SectionHeading takes `titleId`,** its heading's id, for a section named by its heading.
|
|
157
|
+
|
|
158
|
+
**Dates, times and numbers in the page's language.**
|
|
159
|
+
- **Everything the library writes as a date, a time or a number** (a RelativeTime, a Timeline's and a chat's times, the Calendar's days, a Gantt's dates and scale, a time zone's offset, a chart's axes and tooltip, the order in which a Table sorts text, and how DatePicker reads a typed "05/03/2026") is in the page's language, its `<html lang>`, in the visitor's own formats where they speak it: an English page shows a British visitor "1 Oct 2026" and an American "Oct 1, 2026", and a visitor whose browser is Japanese sees English dates too. Each was in the browser's language, so that visitor read "5 分前" and "25万" in a page of English, text in another language with no `lang` of its own (WCAG 3.1.2). The visitor's form counts in the page's script only (a page in traditional Chinese stays traditional). A page with no `lang` is written in the browser's language, as before; so is a page drawn on a server, until it hydrates. A Gantt's `locale` still wins.
|
|
160
|
+
|
|
161
|
+
**The package.**
|
|
162
|
+
- Every entry point has a `default` condition, and `gridsmith-ui/package.json` is exported, for tools that read it.
|
|
163
|
+
- `@tiptap/extension-link` and `@tiptap/extension-underline` are no longer peers: nothing imports them (StarterKit has both). An app that installed them keeps them harmlessly.
|
|
164
|
+
- `ResolvedPreferences` has `relativeThresholdHours`: code that builds one itself (a test's mock) adds it.
|
|
165
|
+
|
|
166
|
+
### Upgrade note for agents (0.24.0 → 0.25.0)
|
|
167
|
+
|
|
168
|
+
Upgrade `gridsmith-ui` to `^0.25.0` (an app pinned to `^0.24.0` never reaches it), then:
|
|
169
|
+
- **Charts.** Value axes shorten their numbers themselves now; a chart with a value axis (Bar, Line, Area, Composite, Scatter, Radar) whose values carry a unit (a percentage, a duration, a size) takes `valueFormat` ("percent" takes values out of 100: multiply a fraction by 100), where you may have scaled the data or put the unit in the axis title by hand. A chart title that states the takeaway keeps its articles: the engine no longer flags "the" in a ChartContainer's title. Write the title from the data it shows (`${top.name} uses the most disk`), not as a fixed sentence.
|
|
170
|
+
- **Relative times.** Draw every "… ago" with `<RelativeTime time={…} />`, passing the time itself, and remove the formatting and the threshold of your own. A shell generated before 0.25.0 doesn't carry the threshold: when the project's isn't 24 hours, add `relativeThresholdHours: N` to AppShell's `preferences` literal (the engine's shell check warns and names the value). A sortable column of times passes the time as its `sortValue`. A Timeline item takes the time itself, `time: event.at`, in place of `timestamp` and `dateTime`; where a slot takes only text (a toast), use `useRelativeTimeFormat()`'s formatter. `formatRelativeTime` takes the threshold it uses, `{ thresholdHours }`; in a component, `useRelativeTimeFormat()` passes the project's for you. Past the threshold a time now reads as its date and time.
|
|
171
|
+
- **Time zones.** Replace a hand-built zone list with `<Field label="Time zone"><TimezoneSelect value={zone} onChange={setZone} /></Field>`, its state started from the stored zone, else `browserTimeZone()`. A stored id of your own that isn't an IANA zone ("us-east") shows as it is: map it to its zone (America/New_York) first.
|
|
172
|
+
- **Icon slots.** Text in an icon slot (an arrow "→", a word, an emoji) now draws nothing: put words in the label and give the slot an icon name (`iconRight="arrow-right"`) or an element. A name you passed as text (`iconLeft="clock"`) now draws its icon instead of the word. The engine flags text no icon pack names (`gs:icon-slot-not-an-icon`). A wrapper of your own that hands a `ReactNode` to one of these slots no longer type-checks: type its prop as `IconSlot` (from `gridsmith-ui/icons`).
|
|
173
|
+
- **Mentions.** Replace mentions built by hand (execCommand, a list of your own, a comment box beside the document) with `const mentions = useMentions({ people })` from `gridsmith-ui/editor-mentions` and `<RichTextEditor mentions={mentions} … />`, after installing `@tiptap/extension-mention@<v> @tiptap/suggestion@<v>`, `<v>` being your `@tiptap/core` version (`npm ls @tiptap/core`). Read the mentioned people's ids from onChange's HTML (`[data-type="mention"]`, its `data-id`) to notify them. A MentionInput outside a Field takes `aria-label`.
|
|
174
|
+
- **An editor in a Drawer or a Dialog.** An Escape in its text now reaches the overlay, which closes: put the editor in the overlay's FormWrapper, or pass the overlay `dirty`, so it asks before discarding what was typed.
|
|
175
|
+
- **Time zones, with nothing stored.** The field is empty and offers the browser's zone first; start the page's state from `browserTimeZone()` to show it as chosen.
|
|
176
|
+
- **The page's language.** The library's dates, times and numbers follow `<html lang>` now, no longer the browser's language: check that `index.html` names the app's language (`<html lang="en">`, as Vite's template does). A page with no `lang` keeps the browser's.
|
|
177
|
+
- **Danger zones.** Replace a "Danger zone" heading and the Card or bordered frame under it with `<DangerZone>`, as the page's last section, holding one `<ActionPanel title="…" description="…" action={<Button variant="destructive" …>…</Button>} />` per action; the Button opens the confirmation. The engine flags the hand-built one (`gs:danger-zone-hand-built`).
|
|
178
|
+
|
|
179
|
+
## 0.24.0 — 1 October 2026
|
|
180
|
+
|
|
181
|
+
Navigation, layout and flows (Sprint 25.16). An item's path is its way back and a crumb that is a page goes there; pages led by a chart, a board or a split view fill the screen; a flow takes its step from the library; forms keep a readable width; on a phone a table's cards open their detail when asked. It adds a hook (`useFlowSteps`, with `readFlowProgress`), components (`FlowSaved`, `PageActions`) and props (AppShell's `navigate`, SplitView's `detailPath`, Breadcrumbs' `aria-label`, TreeView's `selected`, FormWrapper's `fullWidth`, a Stepper step's `disabled`), and changes layouts and defaults, so in semver terms it's a minor release, 0.24.0. The upgrade note at the end says what an app on 0.23.0 does.
|
|
182
|
+
|
|
183
|
+
**The item's path is the way back.**
|
|
184
|
+
- **SplitView takes the open item's path** (`detailPath`: breadcrumb items from the top of the list down to the open view's parent, never the view itself, whose own title heads it) and draws it above the detail at every width, where the stacked Back was. Its last crumb, the parent, goes up one level, so there is no separate Back; a last crumb with no address or action of its own closes the detail, as Back did. On a phone a long path keeps its first crumb and its last two, so the parent always shows. Without a path the Back stays as it was.
|
|
185
|
+
- **An item's own header keeps its title readable.** SectionHeading takes an editable title (`onTitleChange`, with `titleLabel` naming the field for a screen reader): the title shows as a field at the title's size, filling the title's line, and the actions stay beside it while it keeps about 20 characters, moving under it when it can't, as they always do on a phone. At the top of a SplitView's detail the pane header insets at the page's gutter, lining up with the item's path above it. A new check, `gs:title-field-beside-controls`, flags a title field squeezed beside three or more controls in a row the app built itself (run 71's document title read "Sys" on a phone).
|
|
186
|
+
- **A crumb that is a page goes there.** AppShell takes the app's router (`navigate`) and Breadcrumbs uses it: a plain click on a crumb with an address and no `onClick` changes the page with no reload; an address on another site stays an ordinary link. A click with a modifier key or the middle button on a crumb with an address keeps the browser's own behaviour (a new tab), whatever handler the crumb has. PageHeader's crumbs, in every header style, follow the same rule (a crumb that goes nowhere is plain text there too, and one at the trail's end is the current page), and a crumb hidden in the overflow menu is a link too, so it opens in a new tab and its address can be copied. With no `navigate` a crumb is an ordinary link. Every crumb's click used to be cancelled, so a crumb with only an address did nothing. A crumb with neither an address nor an action is text, no longer a link to "#". A regenerated shell passes its own router (`navigate`, or Next.js's `router.push`); an existing shell adds `navigate={(href, options) => navigate(href, options)}` to AppShell (the upgrade note has Next.js's).
|
|
187
|
+
- **Breadcrumbs takes `aria-label`** ("Breadcrumb" by default), so a second trail on a page, such as an item's path, names its own landmark ("Path"). Its last crumb is the current page only when it goes nowhere; one with an address or an action is a link, as PageHeader's are, so a trail that ends at the parent ends with its way back.
|
|
188
|
+
- **Link's `component`** takes any component, so react-router's Link and Next.js's Link fit under strict TypeScript with no cast.
|
|
189
|
+
|
|
190
|
+
**A tree marks the folders holding its selection** as the sidebar marks a parent of the current page: the label at medium weight, the icon and chevron in the accent, no fill, open or collapsed. When first drawn it opens the selection's folders, as the sidebar opens the current page's parent, and so it does for a selection that appears later (nodes that load after the first draw, a new selection from the address), once per item, so a folder the user closes stays closed; `defaultExpanded` adds to them. TreeView takes `selected` (with `onSelect`) for an app that holds the selection, so the tree follows the address and a crumb can select a folder.
|
|
191
|
+
|
|
192
|
+
**A page led by a chart, a board or a split view fills the screen.** A SplitView in a page frame, a Gantt (and any view on a TimeViewport) and a KanbanBoard that lead the page (above them only its header, its chrome — notices, tabs, a toolbar, a search or filters row, however tall — and short rows) and are its last content take the height under the page's header, and what they hold scrolls inside them: the page doesn't, and it ends at the page's gutter, as its sides do. With content after them, a height of their own, a place among other content (a board in a dashboard's card), or a screen too short to scroll inside (a phone on its side), they keep their natural height. A board in a SplitView's pane takes the pane's height. Each column of a board with a height of its own scrolls its own cards, with room for a card's focus ring, and the columns stay as tall as each other. A board's column or a split view's pane that scrolls with nothing in it to Tab to becomes a named Tab stop, so a keyboard can scroll it (an item's pane is named by its title); one with focus stays a stop until focus leaves it. The divider's grip stays in the part of the divider on screen, so a split view taller than the screen never hides it. AppShell is as tall as the dynamic viewport (`100dvh` where the browser has it), so nothing hides under a phone browser's toolbar. A plan page needs no flex column of its own: `<PageContainer width="full"><PageHeader …/><SplitView …/></PageContainer>`.
|
|
193
|
+
|
|
194
|
+
**The Gantt's key wraps under its controls.** Where it fits, it sits at the end of the controls' line; where it doesn't, it starts at the left on a line of its own, a row gap below them.
|
|
195
|
+
|
|
196
|
+
**A flow takes its step from the library.** `useFlowSteps` reads a flow's step from its one `:step` route (`/setup/:step`) and returns the step and the total, the caption ("Step 2 of 6"), the title, next, back and go-to, and the Stepper's props. It never goes past the furthest step reached: an address not reached yet, past the last step or not a step at all goes to the furthest step reached, replacing the address, so a fresh `/setup/3` lands on `/setup/1`. A step can be shown only while an answer holds (`when`), so the total follows the answers; changing an answer that changes the steps ahead means walking them again, and `next(answers)` answers and goes on in one move. The progress (the furthest step and the step on screen) and the answers are kept in the browser's storage under the flow's name, so a return resumes, and they go when the flow finishes (`finish`). Every caller with that name — a layout and its step, a page that mounts anew on each step, another tab — shares one flow: its progress (a tab never undoes another's), its finish, the focus after a move and the tab's title. A `storageKey` that is `undefined` (the user still loading) makes the flow wait: nothing drawn, moved or written. After a move the new step's heading takes focus and the browser tab names the step; leaving the flow puts focus on the next page's heading unless that page took it. With sign-in the flow is left through the shell's "Save and log out" and resumed after signing in (`readFlowProgress` gives a gate the step to resume at); with none, through "Save and continue later" to a page drawing `FlowSaved` ("Your progress has been saved"), whose Continue resumes at the step the user was on, going back to it when it was just left; the focus shell draws no exit while it shows, and with nothing saved the page says so. Navigation goes through AppShell's `navigate`, which now takes a `replace` option (a regenerated shell passes it; Next.js: `router.replace`).
|
|
197
|
+
|
|
198
|
+
**Stepper.** A step can be `disabled`: not reached yet, so its circle can't be pressed. Each circle is named for its step ("Step 2: Team size", ", completed" when done); a done step showed only a check, with no name. After going back, the line still joins the done steps, and a step reached but not done answers the pointer.
|
|
199
|
+
|
|
200
|
+
**A segmented control's frame matches a field.** Its corner is a field's (the theme's base radius, where it took one and a half times it, so it now changes with the fields when the radius does), and the segments sit concentric inside it, their corner the frame's less the gap between them, never sharper than the theme's small radius (a theme of square corners stays square). Its frame's edge is a button's, the light border, since each segment is a labelled button; the selected segment carries the 3:1 edge instead (WCAG 1.4.11), so the choice stands out against the frame in every preset and mode, where the selected segment's fill alone measured 1.05–1.46:1. Nothing to change in an app.
|
|
201
|
+
|
|
202
|
+
**A tab page's actions in its area header.** `PageActions`, beside `useContentWidth`: a page under an area layout renders its actions anywhere in itself, and they show in the layout's PageHeader as the page's own live elements, at that header's control size, going when the page does. They go to the page's own header (the first in the page), never to a header inside a pane, a drawer or a dialog; a page that wraps itself in a container with no header of its own hands them to the container around it. The layout passes no actions per tab and keeps no context of its own for them.
|
|
203
|
+
|
|
204
|
+
**Forms keep a readable width.** FormWrapper keeps its fields at the narrow page's reading width wherever it sits — a card, beside a list, under a tab — and its buttons end where its fields end; before, only a settings panel did. In a Drawer or a Dialog a form takes the overlay's width. `fullWidth` is for the rare form whose fields need the width (several columns, line items). The page around it and its lists keep their own width.
|
|
205
|
+
|
|
206
|
+
**On a phone, a table's card opens its row's detail when asked.** A card starts with its fields only; "Show details" at its foot opens the detail below and reads "Hide details" when open (Alert's disclosure, named with the row's name). With no `onRowClick` a tap on the card's fields opens and closes it too; its own controls never do, and neither does a tap in its opened detail. The button is at least 24 px tall, the card around it being a target too. The card and the row share one open state (`expandedRows`, `defaultExpandedRows`, `onExpandedChange`), so a row open on a desktop is open as a card, and a row the page opens through `expandedRows` (an address) opens its card and brings it into view, on the next frame, so a layout that sets its scroll back to the top for a new address no longer undoes it. A desktop row comes into view with its opened panel below it, as much of it as fits. Closing from inside the detail returns focus to the button.
|
|
207
|
+
|
|
208
|
+
### Upgrade note for agents (0.23.0 → 0.24.0)
|
|
209
|
+
|
|
210
|
+
Upgrade `gridsmith-ui` to `^0.24.0` (an app pinned to `^0.23.0` never reaches it), then:
|
|
211
|
+
- **Pass the router to AppShell.** React Router: `navigate={(href, options) => navigate(href, options)}`; Next.js: `navigate={(href, options) => (options?.replace ? router.replace(href) : router.push(href))}`. Crumbs with an address then change the page with no reload; without it they are ordinary links, which reload the page (before, a crumb with only an address did nothing). A crumb whose `onClick` only called `navigate(href)` can keep just its `href`.
|
|
212
|
+
- **A detail's own way back.** In a SplitView detail showing an item from a tree, or a view below an item (its metadata, its history), remove the "Back…" link or arrow you drew and pass the item's path as `detailPath`, ending at the view's parent (for a view below an item, the item), and give the view its own title as a heading. The engine flags a hand-made back there (`gs:breadcrumbs-and-back-button-2`).
|
|
213
|
+
- **An item's title.** Where a view's title is a field you placed in a row beside its buttons, move it into `<SectionHeading variant="pane" title={title} onTitleChange={setTitle} titleLabel="Document title" actions={…} />` at the top of the view (the engine flags the row: `gs:title-field-beside-controls`). A pane header you already draw at the top of a SplitView's detail now insets at the page's gutter, as the item's path and the aside do: if the content under it used the pane's 16 px (`px-ds-4`), inset it at the gutter too (`px-[var(--ds-page-gutter)]`) so the two line up.
|
|
214
|
+
- **Fill pages.** Remove the height or flex column you built around a plan page's SplitView, Gantt or KanbanBoard (`h-[calc(100vh-…)]`, `flex-1 min-h-0` down the tree): the library gives it the height under the header. A height you set yourself still wins, and the page then scrolls as before.
|
|
215
|
+
- **Wizards with an address per step.** Replace hand-rolled step logic with `useFlowSteps` over one `:step` route, with each field writing to the flow as it changes (`setAnswers`), so leaving keeps what is on screen. With sign-in, keep the shell's "Save and log out"; with none, give the flow's focus `exitLabel: 'Save and continue later'` and `exitTo`, a page drawing `FlowSaved` — not the app's home. A regenerated shell's exit goes to `exitTo` when the flow gives one; in an existing shell, change `flowExit` to `focus?.onExit ?? (exitTo ? () => navigate(exitTo) : focus?.kind === 'before-app' ? onLogout : () => navigate(-1))`, with `const exitTo = focus?.exitTo` above it (Next.js: `router.push(exitTo)` and `router.back()`).
|
|
216
|
+
- **Tab pages' actions.** Where an area layout's pages hand their header actions up through a context of your own, render them in the page with `<PageActions>` and delete the context.
|
|
217
|
+
- **Forms.** FormWrapper now keeps every form at the reading width, where it used to fill its box outside a settings panel. Remove a `max-w-` class or wrapper you put on a form (the engine flags both), and don't narrow a page whose form sits beside lists. A form whose fields need the width (several columns side by side, line items) takes `fullWidth`.
|
|
218
|
+
- **Tables on a phone.** Each card shows its row's detail only when opened ("Show details"), where every card showed its detail. To keep a row open from the start, pass it in `defaultExpandedRows` (or `expandedRows`).
|
|
219
|
+
- **TreeView.** Remove a `defaultExpanded` you worked out only to open the selection's folders: the tree opens them. An app that holds the selection passes it as `selected`, with `onSelect`.
|
|
220
|
+
|
|
221
|
+
## 0.23.0 — 30 September 2026
|
|
222
|
+
|
|
223
|
+
Overlays, insets and controls (Sprint 25.14). A container owns its inset and what is inside carries none; drawers share the app's width and keep their content when a phone turns; every control's edge reaches 3:1. It adds props (AppShell's `drawerWidth`) and changes layouts and defaults, so in semver terms it's a minor release, 0.23.0. The upgrade note at the end says what an app on 0.22.0 does.
|
|
224
|
+
|
|
225
|
+
**One inset for the page and its overlays.**
|
|
226
|
+
- **The page's gutter** is named once, `--ds-page-gutter`, the theme's `space-6` (21 px on a phone at the default density, more on a roomier theme). PageContainer's column reads it, and a Drawer's, a Sheet's and a Dialog's header, body and footer are inset by it, as is the unsaved-changes question between the body and the footer: a title, the first field and the buttons line up, on every theme. A theme whose page header floats insets a phone's page less; the overlays keep the gutter. `flush` (Drawer and Sheet) still drops the body's inset for content that draws its own edges.
|
|
227
|
+
- **A Dialog on a phone fills the screen,** edge to edge, with no border, its content at the gutter, and its buttons at the screen's foot, within thumb reach, however short the dialog. On every screen a long dialog scrolls its body with its footer in view. Its title and Close sit in a header row above the body, so scrolled content passes under neither.
|
|
228
|
+
- **Where scrollbars take room** (Windows, Linux, a Mac set to always show them), a scrolling overlay body takes the scrollbar's width from its right inset, so its fields still end where the header's and the footer's content ends.
|
|
229
|
+
- **SplitView's aside** insets its content as the Drawer it collapses into does, so it sits in the same place in both.
|
|
230
|
+
|
|
231
|
+
**An opened row lines up with its table.** The panel's content starts where the row's first column's text starts and ends where its last column's content ends, measured from the header, so it follows the density and the pinned columns; it has no fill of its own, and the row's divider sits under the panel. An overlay's body, an opened row's panel and a split view's pane each start afresh: a list inside lines up with the overlay, not with the Card or pane it was declared in, and an EntityCell in a phone card's panel keeps its own avatar size.
|
|
232
|
+
|
|
233
|
+
**No frame around what frames itself.** A KanbanBoard, a CardGrid, a Table or a DataGrid that is a SplitView's list pane's content tells the view, which then draws no frame or surface behind the list, whatever `bordered` says: the board sits on the canvas at the page's gutter and the open item on the surface, on a phone as on a desktop. A timeline, a chart or a list keeps `bordered`'s hairline, and so does a list beside a card grid; content in a Dialog opened from the list, or in a nested view's other pane, doesn't count.
|
|
234
|
+
|
|
235
|
+
**Drawers.**
|
|
236
|
+
- **One width.** AppShell carries `drawerWidth`, 520 px by default, and a Drawer given no width takes it (400 px outside a shell); a `width` prop still wins. SplitView's aside drawers, which pass none, widen to it. A non-modal side panel (`modal={false}`, such as the shell's assistant) keeps the width it's given, and the width checks leave it alone.
|
|
237
|
+
- **One panel in both forms.** Below `sm` a right or bottom Drawer takes its sheet form in place, so a phone turned sideways and back keeps what's in it: typed text, a running upload, an editor's history, the unsaved-changes question and focus. A non-modal Drawer is fully modal as the sheet: focus moves in as it turns into one, and Escape closes it from anywhere in it. The Sheet is the same panel in its sheet form; its `confirm` prop is gone (its unsaved-changes question is its own: pass `dirty` and `onDiscard`).
|
|
238
|
+
- **A form in a Dialog opened from a Drawer** keeps its buttons in the Dialog; they no longer go to the Drawer's footer.
|
|
239
|
+
|
|
240
|
+
**Controls.**
|
|
241
|
+
- **Edges at 3:1.** `--ds-input-border` is set to reach 3:1 against every ground a control sits on, at the saturation extremes, in every preset and mode: the page and its glows (glassmorphic, modern-dark and claymorphism have them), a card, a muted strip (a table's header) and a raised panel (a popover); hovered, an edge is never fainter. The dark default is lighter than 0.22.0's so a muted strip clears it. The editor's frame, the chat composer, SegmentedControl's track and FileUpload's zone draw their edge from it too. The slider's thumbs are ringed in the checked border and its fill is the checked colour; its track stays light, so the fill stands apart from it.
|
|
242
|
+
- **Checked and on.** A checked box's fill and border, a selected radio's ring and dot, and an on switch's track are the checked colour (`--ds-check-bg`, `--ds-check-border`), set to reach 3:1 against the same grounds; a theme whose accent falls short sets its own step, with a dark tick (`--ds-check-fg`) on a light one, and a checked box's gradient can be turned off per theme (`--ds-check-gradient`). The checked colour follows the accent of the scope it's drawn in: a dark sidebar or a dark portal on a light page, and a project's theme in the dashboard.
|
|
243
|
+
- **The switch.** Off, it is an outline in the edge colour with no fill, and its knob is the lightest shade of the edge that still reaches 3:1 on every ground: the edge itself in most themes, a grey in a theme whose edge is far past 3:1 (neo-brutalist's black, sketch's and playful-geometric's dark edges; `--ds-switch-knob-share`, set per mode by the preset). On, the checked colour with a knob in the surface colour made opaque, which changes colour with its track. The off look comes from tokens only a regenerated `gridsmith-tokens.css` has, so an app that hasn't regenerated keeps the switch as it was. A selectable Card's own checkbox, radio and switch (`selectType`) draw the same looks.
|
|
244
|
+
- **Tabs.** A tab's label sits in the middle of the tab in every style; a tab with an icon is as tall as one without (the icon made its strip 3 px taller).
|
|
245
|
+
- **Compact controls** (SegmentedControl without `fullWidth`, Rating, PinInput) keep their own width in a Field instead of stretching across it, and keep their place in a centred column or a row: they size to their content (`w-fit`) rather than aligning to the start.
|
|
246
|
+
- **Sideways scrollers fade.** The editor's toolbars on a phone and the week view's day strips fade the edge where more waits, as the Tabs strip does.
|
|
247
|
+
|
|
248
|
+
**From the owner's review of the showcase, 30 September 2026.**
|
|
249
|
+
- **A selectable Card's checkbox** (`selectType="checkbox"`) sits at the end of its top row, beside the card's menu, so the icon or title keeps the place it has on a card without one.
|
|
250
|
+
- **AvatarGroup shows no status.** A status dot on overlapping avatars reads as clutter, so the group ignores its items' `status`; a single Avatar still shows one.
|
|
251
|
+
- **Kbd.** A shortcut written in one key ("⌘K", "⇧⌘P") sets its symbols and its letter a little apart.
|
|
252
|
+
- **NavMenu matches NavItem.** Its trigger is as tall as a NavItem beside it in every navbar style, and it takes the Navbar's style (tint, pill, underline, minimal) as NavItem does; a `type` you pass still wins.
|
|
253
|
+
- **StatsCard.** A card with an icon is no taller than one without: the label and the value sit beside the icon, not under it.
|
|
254
|
+
- **Filters on a bold colourful header** sit on a strip that hugs them, drawn as the cards around it are: the theme's surface, border, corner, card shadow and glass (the owner's pick of five looks for a bold header with content overlap). The tab row on a bold band sits on the same strip. Every control keeps a ground it was made for, in every theme, see-through fields included, and a keyboard focus ring shows on it. A see-through surface is never less than 60% opaque there: over the band's colour, glassmorphic's fields fell to 2.98:1.
|
|
255
|
+
- **Under content overlap**, the cards sit as far below the header's last row as the page's own gap (24 px at the default density): the band's bottom edge had added 2 px.
|
|
256
|
+
- **A search field in a header's filter row** is the table toolbar's search width, 280 px, instead of a line of its own.
|
|
257
|
+
|
|
258
|
+
### Upgrade note for agents (0.22.0 → 0.23.0)
|
|
259
|
+
|
|
260
|
+
Upgrade `gridsmith-ui` to `^0.23.0`, then:
|
|
261
|
+
- **Regenerate `gridsmith-tokens.css`** (get_tokens with `css: true`, saved as returned): the new edges, hover edges and checked colours, and the switch's new look, are in it.
|
|
262
|
+
- **Overlays and opened rows.** Remove padding classes (`p-`, `px-`, `pl-`, `pr-`, `ps-`, `pe-`) from the root of what you give a Drawer, a Sheet or a Dialog, or return from `renderExpanded`; keep `space-y` or `gap`. The engine flags it (`gs:container-content-padded`).
|
|
263
|
+
- **A Dialog fills a phone's screen** and scrolls its own body: remove a height or a scroll container you gave its content for that.
|
|
264
|
+
- **Drawers.** Pass a modal Drawer no width; the app's drawers share the shell's (520 px). A `DRAWER_WIDTH` constant your shell exports keeps working. SplitView's aside drawers widen from 400 to 520 px. A non-modal side panel (`modal={false}`) keeps its width.
|
|
265
|
+
- **Sheet.** Its `confirm` prop is gone; pass `dirty` and `onDiscard` for the unsaved-changes question it asks itself.
|
|
266
|
+
- **PageContainer.** A padding class you pass it now wins over its gutter (`px-ds-4` gives 16 px, where it used to lose to 24): remove one you added without meaning it to apply.
|
|
267
|
+
- **Boards and card grids in a SplitView's list pane** have nothing drawn behind them: remove a padded wrapper around the board.
|
|
268
|
+
- **SegmentedControl.** Pass `fullWidth` to one that should stretch across its Field.
|
|
269
|
+
- **Controls.** Don't give a field or a control a border colour of its own; the theme's edge is the one at 3:1.
|
|
270
|
+
- **AvatarGroup.** Remove `status` from its items: it's ignored. Show a person's status on a single Avatar.
|
|
271
|
+
- **NavMenu.** Remove a `type` you passed only to match the Navbar: it now takes the Navbar's.
|
|
272
|
+
|
|
273
|
+
## 0.22.0 — 30 September 2026
|
|
274
|
+
|
|
275
|
+
Lists, tables and cards (Sprint 25.13, with the fixes of its review). Lists take their pager and reach every row, tables scroll with their key columns pinned, and every card has one header row. It adds props (a list's `pager`, a column's `sortValue` and `pin`, Table's `rowKey`, `expandedRows`, `defaultExpandedRows`, `onExpandedChange` and `cardsBelow`, CardGrid's `fill`, usePagination's `pageSizeOptions`, a DataGrid column's `meta.parse` and `meta.editable`), and several layouts and defaults change, so in semver terms it's a minor release, 0.22.0. The upgrade note at the end says what an app on 0.21.0 does.
|
|
276
|
+
|
|
277
|
+
**A list takes its pager.**
|
|
278
|
+
- **One prop.** `<Table data={rows} pager={pager} />`, where `rows` is every row and `pager` comes from `usePagination(rows)`. The table sorts all the rows, draws the pager's part (a page, or every page loaded so far) and puts the control beneath itself in the project's style: page numbers, Load more, or loading as the user scrolls. A new sort starts again at page 1. DataGrid, StackedList, Timeline and VirtualizedList take `pager` too. `<Pagination {...pager.props} />` stays for a list of the page's own, such as a grid of cards.
|
|
279
|
+
- **One element.** A list given the pager and its control are one element, which takes the list's `className` (and a Table's `ref`), so beside a chart in a grid or a flex row they stay one item. The list keeps its element while its items come and go.
|
|
280
|
+
- **One page, no page numbers.** A list that fits on one page shows no page numbers (GOV.UK: don't show pagination for one page); a page-size choice stays, so a user who picked a larger size can go back.
|
|
281
|
+
- **The same list, grown, keeps its place.** A list with items added at its start or its end (a live log's new entry) keeps its page, and a growing list its depth. Any other change (a filter, a search, another order) starts again at page 1.
|
|
282
|
+
- **Development warnings.** A list given other rows than its pager counts, fewer or more, warns: give the list and `usePagination` the same rows. So does a second control for the same pager.
|
|
283
|
+
- **Selection** stays an index into `data`, the whole list, so it survives sorting and paging. The header checkbox selects the rows drawn.
|
|
284
|
+
- **A page-size choice.** `usePagination(rows, { pageSizeOptions: [10, 25, 50] })` puts the choice beside the page numbers, on the list's own control too.
|
|
285
|
+
|
|
286
|
+
**Sorting.**
|
|
287
|
+
- **A column's sort value.** `{ key: "actor", header: "Actor", sortable: true, sortValue: (row) => row.actorName }`. A sortable column sorts by it, else by the cell's own value when that's text, a number or a Date. A column whose cells are elements (an avatar beside a name, a badge, a time) used to sort as "[object Object]" and never reordered; without a sort value it now stays put and warns in development. Empty values go last.
|
|
288
|
+
- **Numbers by size, dates by time.** A number column sorts by its value whatever currency or unit is around it ("$450" before "$1,250", "9.6 KB" before "1.2 MB"), and a date column by its time ("5/9/2026" after "12/1/2025").
|
|
289
|
+
- **Only columns that can sort.** A table-wide `sortable` offers the columns whose cells are values, or which have a `sortValue`; a column's own `sortable` wins. A header with no words is never a button, and a column whose sort was refused doesn't say it's sorted.
|
|
290
|
+
- **Sortable headers are buttons** in Table and DataGrid: Tab reaches them, Enter or Space sorts, and the sorted column carries `aria-sort`. The indicator's room is kept while unsorted, so a header doesn't widen when it sorts.
|
|
291
|
+
|
|
292
|
+
**Wide tables.**
|
|
293
|
+
- **Pinned columns.** A table wider than its frame scrolls sideways with its selection on the left, a column marked `pin: "end"` (the row's actions) on the right, and the row's name beside the selection while the three take no more than about half the frame. A column that names the row and isn't first can be marked `pin: "start"`. Pinned cells are solid, over a see-through (glass) surface too, and a hovered or selected row's pinned cells fade in with the rest of it (`roots.css` registers the row's tint as a colour, `@property --gs-row-tint`). An edge with a pinned group shades that group's inner edge; the other edge still fades. An opened row's panel stays within the visible width.
|
|
294
|
+
- **Cards only where the table doesn't fit.** A table becomes cards on a narrow screen, as before, and now also where it doesn't fit its own width under `cardsBelow` (440 px by default). A table that fits stays a table however narrow its box, and a table in a pane, a drawer or a dialog stays a table above the line and scrolls. It turns back once it has the width it needed, or 40 px past the line. `cardsBelow={0}` switches by the screen alone. Focus on a row stays on that row through the switch.
|
|
295
|
+
- **Values stay whole.** A date ("24 Sept 2026", "Sep 16") or a size ("1.2 MB") stays on one line, including one an element draws, read from the text the column shows. A long identifier (13 digits or more) may wrap.
|
|
296
|
+
|
|
297
|
+
**Rows.**
|
|
298
|
+
- **A row's own controls stay its own.** Its checkbox, its expand toggle and the items of its ⋯ menu no longer run `onRowClick` or open the row, in the table and in the card layout. Selecting a row's text doesn't open it.
|
|
299
|
+
- **An opened row belongs to its row.** Rows open by their key (`rowKey`, default `"id"`), so an open row stays open through sorting and paging. `expandedRows` with `onExpandedChange` controls which are open; open one from the address with `expandedRows={id ? [id] : []}`, and it's brought into view, on another page too. `onExpandedChange` gets every open row, the newest last. `defaultExpandedRows` starts some open, and neither they nor a row the user opens move the page. With no `onRowClick`, a click anywhere on an expandable row opens it. `renderExpanded(row, index)` gets the row's index in `data`, not its place on the screen.
|
|
300
|
+
- **Rows that share an id** open alone and keep keys of their own (the second is known as `${id}~${index}`), with a warning in development.
|
|
301
|
+
- **Named controls.** The header checkbox is "Select all rows shown"; each row's, in the table and in the card layout, is "Select" and the row's name; each expand toggle is "Details" and the row's name.
|
|
302
|
+
- **A focused row's ring** shows over its pinned cells.
|
|
303
|
+
- **Row markers.** A row carries `data-row-key` only when it has a key of its own; a row known only by its place carries `data-row-index`.
|
|
304
|
+
|
|
305
|
+
**DataGrid.**
|
|
306
|
+
- **The project's paging.** It pages at the project's page size and style (`pageSize` has no default now; it was 10) and takes a `pager` as a Table does. The grid and its control are one element, as in 0.21.
|
|
307
|
+
- **Editing by keyboard.** `editable` makes it an APG grid: Tab reaches one cell, the arrow keys move, Enter or F2 opens a labelled editor with the value selected, Enter saves and Escape cancels, both putting focus back on the cell; leaving the cell saves and lets focus go where it went. The Tab stop follows focus, however it got there. A tick shows beside the saved value and "Saved" is announced. The Tab stop, the editor and the tick stay on their row when a save re-sorts the rows. Alt with an arrow stays the browser's (Back, Forward).
|
|
308
|
+
- **Saves.** `onCellEdit` is called only when the value changed. Return a promise and the tick and "Saved" wait for it; a rejection is heard as "Not saved", and the page says why and keeps the old value. A column's `meta.parse` gives the value its type (`(text) => Number(text)`), and `meta.editable: false` keeps a column that shows from editing.
|
|
309
|
+
- **An editable grid stays a grid** at every width, scrolling sideways: its cards have no editor. A read-only grid turns into cards by its own width, as Table does. It takes `aria-label`.
|
|
310
|
+
- **Forms.** The editor never marks a form around the grid as changed; a control in a cell does, as in a Table.
|
|
311
|
+
|
|
312
|
+
**VirtualizedList.** Given a `pager`, it mounts only the rows on screen of the pager's part and draws the control beneath itself. A new page starts at its top, and its rows are known by their place in the whole list, so a row's state stays with it. With infinite scroll, scrolling inside the list loads more; it watched the page before, so it never did. After the list starts again at its first page, scrolling loads more again. `renderItem` gets each item's index in the whole list.
|
|
313
|
+
|
|
314
|
+
**Cards.**
|
|
315
|
+
- **One header row.** CardHeader keeps one row on every card: the checkbox of a checkable card, the title on one line (truncating, the full title on hover and while keyboard focus is in the card), its badges beside it, a selected card's check or its `selectType` indicator, any buttons, and the ⋯ last, at the top right. The title keeps about eight characters: below that the badges and buttons wrap under it, and the ⋯ stays at the top right. Titles line up across a row of cards, with or without a ⋯, a button or a checkbox. The description runs the full width below the row. A badge in `actions` sits beside the title, a button at the end; Badge carries `data-gs-badge` for this. An item that holds a control (a button showing a count, a wrapper around a badge and a menu) stays at the end. On a card with an icon, the checkbox, the check and the ⋯ share the icon tile's line at the top.
|
|
316
|
+
- **What you write stays where you wrote it.** Children before the CardTitle (a logo, a "Most popular" badge) stay above it, and a block of your own in the title's place (a row with its own button) takes the row's free space.
|
|
317
|
+
- **A checkable card.** Its checkbox sits at the ⋯'s inset, on its centre line, and toggles with Space only, once however long it's held. A disabled card's checkbox says so and doesn't toggle. The content, the footer and the loading placeholder lose their left gutter, so their insets are equal. A checkable card with no CardHeader draws its checkbox in a row of its own at the top; a selected or `selectType` card with no CardHeader keeps its check in the corner, as in 0.21. Only the first CardHeader in a card draws the checkbox and the check. Focus on the checkbox stays on it when the card loads or its header arrives.
|
|
318
|
+
- **CardTitle** in a CardHeader defaults to one line; elsewhere it keeps two.
|
|
319
|
+
|
|
320
|
+
**CardGrid.**
|
|
321
|
+
- **The minimum comes from the headers.** A collection with no `minWidth` takes about 20rem when a card's header is given `actions` (a badge or a button besides the ⋯), whether or not they show at the moment, so a badge that comes and goes never reflows the grid, and 14rem otherwise. A comparison (`compare`) keeps 14rem, so plans stay side by side. A loading card's placeholder draws a badge-sized block, so a grid keeps its columns while its cards load.
|
|
322
|
+
- **One row fills.** When every card fits on one row, the cards share it, and a lone card takes at most half, a card among components that draw nothing and a grid inside another grid too. A grid of more rows keeps its columns. `fill={false}` keeps the columns for a collection whose count changes (a filter, a pager's last page).
|
|
323
|
+
|
|
324
|
+
**Tooltip and DropdownMenu.** A Tooltip shows for keyboard focus on a control inside its trigger, as on the trigger. A DropdownMenu's trigger says it opens a menu and whether it's open (`aria-haspopup`, `aria-expanded`).
|
|
325
|
+
|
|
326
|
+
### Upgrade note for agents (0.21.0 → 0.22.0)
|
|
327
|
+
|
|
328
|
+
Upgrade `gridsmith-ui` to `^0.22.0`, then:
|
|
329
|
+
- **Lists.** Pass every row and the pager to the list, `<Table data={rows} pager={pager} />`, and remove the page's own `<Pagination {...pager.props} />` beside it. Build `rows` from the whole list, not from `pager.items`, with `useMemo` for a long one. Move a `pageSizeOptions` from the Pagination to the hook. A `{pager.totalPages > 1 && …}` around a Pagination can go.
|
|
330
|
+
- **Sorting.** Give a sortable column whose cells are elements a `sortValue`.
|
|
331
|
+
- **Wide tables.** Mark the actions column `pin: "end"`.
|
|
332
|
+
- **DataGrid.** Its page size moves from 10 to the project's; pass `pageSize` only when the user asked, with its override note. In an editable grid, give a number column `meta: { parse: (text) => Number(text) }` and a column that only shows `meta: { editable: false }`.
|
|
333
|
+
- **Opened rows.** Give each row an `id` (or name the field with `rowKey`). `renderExpanded`'s second argument is the row's index in `data` now; a page that used it as a place on the screen reads the row instead. A page that opens one record at a time takes `open[open.length - 1]` from `onExpandedChange`.
|
|
334
|
+
- **Cards.** A card's badge sits beside its title with no change to the code. Move a menu written in `actions` to `menuItems`. A checkable card's checkbox toggles with Space, not Enter. A CardTitle in a CardHeader is one line; pass `lines={2}` where a title must wrap. A CardGrid whose loaded cards hold no badge passes `minWidth="14rem"` to keep its columns while loading.
|
|
335
|
+
- **Tests.** Selection boxes have names now ("Select all rows shown", "Select Acme Corp", in the card layout too), and expand toggles are "Details Acme Corp", not "Expand row". A row with no key is `[data-row-index]`, not `[data-row-key]`. The Pagination a list draws is the list's next sibling, inside one element that takes the list's `className`. A one-page list has no Pagination.
|
|
336
|
+
|
|
337
|
+
## 0.21.0 — 30 September 2026
|
|
338
|
+
|
|
339
|
+
A review of 0.20.0 found faults in its toasts, deletions, forms, uploads and editor, and this release fixes them. It adds props (a Field's wiring on Checkbox, RadioGroup and Rating) and changes when FileUpload shows Remove, so in semver terms it's a minor release, 0.21.0. The upgrade note at the end says what an app on 0.20.0 does.
|
|
340
|
+
|
|
341
|
+
**Deletions and their Undo.**
|
|
342
|
+
- **Each Undo restores its own item.** Two deletions in quick succession, each waiting on the network, could hand the second deletion's Undo to the first one's toast: pressing Undo on "Alpha deleted" restored Bravo, and Alpha could no longer be undone. The hook now counts a toast as the page's own only when it's the one confirmation shown as that deletion finished, with no other deletion running at the time.
|
|
343
|
+
- **A page's error stays an error.** In undo mode, a page that caught its own failure and showed `toast.error(…)` saw it turned into a green toast with Undo. In dialog mode, any toast shown meanwhile, even one from another part of the app, silenced "Deleted". An error or warning shown during `onConfirm` now means the deletion didn't happen: no Undo, and no "Deleted". A page's own confirmation takes the Undo and keeps its colour.
|
|
344
|
+
- **The answer to an Undo takes its toast's place.** The restored message goes where the pressed toast was, however long the restore takes and whatever toasts appear meanwhile. A page that confirms its own restore ("Task restored") gets that one toast, not a second "Restored".
|
|
345
|
+
- **`onConfirm` takes any function.** It may return anything. Return the action's promise, so the hook waits for it: `onConfirm: () => api.delete(task.id)`.
|
|
346
|
+
|
|
347
|
+
**Toasts.**
|
|
348
|
+
- **Paused toasts start again.** After the last toast was dismissed with the mouse, or with "Clear all", every later toast stayed paused until the pointer passed over the stack again. Closing a toast with the mouse also no longer moves focus into the stack, which had paused every timer until the user clicked elsewhere.
|
|
349
|
+
- **An action's error is logged.** A toast action that throws, or returns a rejected promise, now reports the error to the console, and the toast closes.
|
|
350
|
+
- **Nothing is lost or renamed.** `toast(…, { id })` called while another toast's action was running returned a different id, so a later `update(id)` did nothing. A toast shown from an effect in the same update as another could be lost.
|
|
351
|
+
- **Announcements are text, and they clear.** The live regions hold only each toast's words (a link or a component in a message was copied into a hidden duplicate), with a full stop before "Press F8…", and they empty after seven seconds. They carry no `role`. In development's StrictMode, a toast shown from a mount effect no longer stops every later announcement.
|
|
352
|
+
- **Focus stays where the user is.** Dismissing one toast from code no longer moves keyboard focus off another toast.
|
|
353
|
+
|
|
354
|
+
**Forms in overlays.**
|
|
355
|
+
- **The library's own buttons never submit a form.** FileUpload's Cancel, Retry and Remove, a delete confirmation's buttons, the unsaved-changes question's buttons, table toolbars, pagination, the bulk action bar, date pickers' popups and the chat composer's buttons are all `type="button"` now. A Button you give a Collapsible, a DropdownMenu or a Popover as its trigger toggles without submitting; one you give `type="submit"` still submits. In 0.20.0 a FileUpload's Cancel inside a form ran the page's save.
|
|
356
|
+
- **A dialog's submit stays in the dialog.** React passes a submit up the component tree, through portals, so a delete confirmation rendered inside an edit form saved that form too when the user pressed Cancel or Delete. FormWrapper now answers only its own form's submit, and a Dialog's own form stops there.
|
|
357
|
+
- **Each form keeps its own changes.** With two forms in one Drawer, saving one no longer discards the other's unsaved change, and a change made while a save is running still counts when the save finishes.
|
|
358
|
+
- **A save is read once the page has answered.** An async `onSubmit` that stopped on its own check (setting a Field's `error`) and returned no longer counts as a save. An error showing at submit that the page clears as the save starts no longer keeps the question. A save that replaces the form with a confirmation ("Invite sent") clears it; a Next that shows other fields still keeps it. If you catch a failed save to show it, throw it again, so the promise rejects: a promise that resolves is a save.
|
|
359
|
+
- **A rejected save isn't swallowed.** Inside a Drawer, Dialog or Sheet, a rejected save that the page doesn't handle reaches the console and your error monitoring, as it does on a page.
|
|
360
|
+
- **What counts as a change.** Moving every item in a TransferList, adding or removing a row in AddAnother, and clearing a prefilled field the page doesn't hold (`defaultValue`) now count. Tabbing through a NumberInput whose value is outside its `min` and `max` no longer counts, and neither do a table toolbar's search and filters, pagination's page size, the phone's tab and settings-section menus, or a dialog opened from inside a form (it answers to its own form, if it has one).
|
|
361
|
+
- **Checkbox, RadioGroup and Rating take their Field's label.** In `<Field hideLabel label={label("Admin")}>` in an AddAnother row they're named for their row ("Admin, person 2"), described by the Field's hint and error, and carry its invalid and required states, as the other controls do.
|
|
362
|
+
|
|
363
|
+
**Uploads.**
|
|
364
|
+
- **A submit while files upload doesn't save without them.** FormWrapper's `errors` carries "Wait for your files to finish uploading" for the zone, the zone shows it, and focus goes there. It goes when the uploads finish. A page that saves only when `errors` is empty waits for them.
|
|
365
|
+
- **Remove shows when the page hears of it.** Without `onRemove`, Remove shows only on a failed file. Before, Remove took a file off the list while the page kept it and saved it.
|
|
366
|
+
- **Focus follows the right row.** After Cancel or Remove, focus waits for that file to leave the list; in 0.20.0 another file's progress moved it to that file's Cancel. When a file's Retry becomes its Cancel, or its upload finishes under a focused Cancel, focus stays on the row.
|
|
367
|
+
- **Smaller fixes.** Two files with the same id no longer crash the page ("Too many re-renders"). Progress that isn't a number shows 0%, and a page's progress is held to 0–100. The same outcome twice is announced twice. When the page takes `upload` away, waiting files fail with a Retry and stop the overlay asking, and files picked meanwhile upload when it's back. A file the zone turned away no longer keeps the overlay asking after a save. In development, FileUpload warns about `files` without `onFiles`, `files` with `upload`, no `onRemove`, and two files sharing an id.
|
|
368
|
+
|
|
369
|
+
**The editor.**
|
|
370
|
+
- **A clear or a reset is applied.** RichTextEditor ignored a new `content` that matched anything it had produced earlier in the session, so a chat composer's `setValue("")` after sending, or a Discard back to the saved text, left the typed text in place, and the next keystroke wrote it back. Only the outputs the page hasn't caught up with count as echoes now.
|
|
371
|
+
- **Undo never takes back a value from outside.** A value the page sets is applied outside the undo history: Ctrl+Z no longer reverts it, and after a record switch without a `key` it no longer writes the last record's text into the new one. Only the part that changed is replaced, so the cursor moves with the text around it.
|
|
372
|
+
- **Less memory.** The editor keeps fewer of its own outputs, within a size limit, and compares a new value with at most ten of them.
|
|
373
|
+
|
|
374
|
+
### Upgrade note for agents (0.20.0 → 0.21.0)
|
|
375
|
+
|
|
376
|
+
Upgrade `gridsmith-ui` to `^0.21.0`, then:
|
|
377
|
+
- **FileUpload.** Pass `onRemove` wherever the page keeps the files (with `onFiles` or `upload`), and take the file out of what the page saves there. Save only when `onSubmit`'s `errors` is empty, so a submit during uploads waits for them.
|
|
378
|
+
- **Saves in an overlay.** Return the save's promise from `onSubmit`. If you catch its failure to show a toast, throw it again.
|
|
379
|
+
- **Deletions.** Return the action's promise from `onConfirm` (`onConfirm: () => api.delete(task.id)`). A delete whose promise is dropped (react-query's `mutate`) is confirmed at once, before the request has finished or failed.
|
|
380
|
+
- **Editor resets.** A clear or a reset through `content` works now. To put back a value the page already holds (a page that stores only the saved text), call `handle.setContent`.
|
|
381
|
+
- **Tests.** A toast's words are in its card and, for about seven seconds, in a hidden live region: query within the stack (`within(screen.getByRole("region", { name: "Notifications" }))`) or use `getAllByText`. The regions carry no role.
|
|
382
|
+
|
|
383
|
+
Changes in 0.20.0 that its notes left out or got wrong:
|
|
384
|
+
- **Standalone `<Toast>`.** Pressing its action also closes it (`onDismiss`), and the action works once per `restartKey`. With `actionWindow`, it closes when the window ends.
|
|
385
|
+
- **Standalone Sheet.** A Sheet without `dirty` routes FormWrapper's Cancel through its close: `onClose` runs, then `onCancel`.
|
|
386
|
+
- **Editor round trips.** The editor follows `content`, so a value stored through a lossy conversion (HTML to Markdown and back, a sanitizer that strips styles or underline) loses that formatting while the user types. Store the editor's HTML as it is.
|
|
387
|
+
- **Avatar.** A presence status is part of the avatar's name ("Priya Nair, online"), so selectors on its alt text need the status.
|
|
388
|
+
- **StatusDot.** The note said the label "is drawn now", but 0.19 drew it too. What's new is that `label` is required.
|
|
389
|
+
- **FileUpload.** The notes said closing "once the user confirms" cancels uploads. Any close of the overlay cancels them, which is why a submit now waits for them.
|
|
390
|
+
|
|
391
|
+
## 0.20.0 — 29 September 2026
|
|
392
|
+
|
|
393
|
+
It adds a component (`AddAnother`) and props (Field's `hideLabel`; FileUpload's `files`, `upload`, `onCancel`, `onRetry` and `onRemove`; RichTextEditor's `handle` and `onImageRequest`; a toast's `key` and `actionWindow`; useDestructiveAction's `doneMessage` and `restoredMessage`), so in semver terms it's a minor release, 0.20.0. Several behaviours change, and two types are stricter: Field's `label` and StatusDot's `label`. The upgrade note for agents, at the end, says what an app on 0.19.x does about each.
|
|
394
|
+
|
|
395
|
+
**Toasts: every confirmation shows (the owner's R28-81).**
|
|
396
|
+
- **They stack.** Up to three show at once by default (`maxVisible`, the project's notifications.toastMaxVisible, which now defaults to 3). The rest wait behind a "+N" count, and each one's time starts when it shows, so nothing expires unseen. A toast with an action (Undo) or an error goes ahead of waiting plain confirmations. "Clear all" appears whenever a toast waits or two or more show. Hovering the stack, or focus inside it, pauses every toast.
|
|
397
|
+
- **Only the same item's toasts merge.** Pass `key` for a toast about one item (a setting that flips): a new toast with the same key takes the place of the one on screen or waiting, and its time starts again, so six flips of one switch give one toast. Toasts without a key never merge, whatever their wording, and toasts with an action and errors never merge. Until now a new toast replaced the waiting one of the same variant, so four of five quick deletions lost their confirmation and their Undo.
|
|
398
|
+
- **An action stays while it can act.** A toast with an action no longer closes on its timer: it stays until the user acts or dismisses it. With `actionWindow` (ms) the action lapses after that long, and the toast stays as a plain message for its time. Pressing the action runs it once, and its answer ("Restored") takes that toast's place, so a full stack never makes the answer wait; an action with no answer closes its toast.
|
|
399
|
+
- **Heard once, and reachable.** The provider announces each toast once, through two live regions (polite, and assertive for errors) that exist from its first render, so the first toast is heard too; the cards themselves stay quiet. The stack is a region named "Notifications" that F8 reaches, and an action toast's announcement says so. When a toast closes from inside the stack, focus returns where it was, else to the next toast, else to `<main>`. On a phone the stack sits above the bottom tab bar. A `<Toast>` used without a provider still announces itself.
|
|
400
|
+
|
|
401
|
+
**Deletions confirm once, and Undo confirms itself.**
|
|
402
|
+
- `useDestructiveAction` confirms a completed action with a toast in dialog mode too, unless the page showed a toast of its own while `onConfirm` ran. The message is `doneMessage` ("Task deleted"), else the confirm label's verb in the past tense ("Deleted"); a verb the hook doesn't know needs `doneMessage`, with a warning in development. `undoMessage` still works, as its older name.
|
|
403
|
+
- In undo mode, a page's own toast shown during `onConfirm` gets the Undo, rather than a second toast appearing.
|
|
404
|
+
- `onUndo` may return a promise. When it resolves the hook says so (`restoredMessage`, else "Restored"); when it rejects, it shows the error. The Undo runs once.
|
|
405
|
+
|
|
406
|
+
**A form in an overlay keeps its changes.** Every control reports a change of its value to the FormWrapper it sits in, and the Drawer, Dialog or Sheet around it asks before discarding: Escape, the backdrop, the close button and a Cancel wired through FormWrapper's `onCancel`. A value the page sets (a record loaded after the overlay opens) doesn't count, and neither does moving through a field without changing it. A submit that passes clears it: return the save's promise from `onSubmit`, and a rejected save keeps it. A `dirty` prop you pass still decides. Before, an overlay asked only when the page passed `dirty`, so a half-filled form closed without a question.
|
|
407
|
+
|
|
408
|
+
**onSubmit gets only the fields that failed.** `errors` has a key for each Field whose check failed, and none for a field that passed: save when `Object.keys(errors).length === 0`. It had kept a key, holding undefined, for a field that had failed once and then passed.
|
|
409
|
+
|
|
410
|
+
**Library buttons no longer submit forms.** Eighty buttons inside components (a Toggle, the "More options" of a Collapsible, tabs, pagination, a Dropdown chip's remove, a TransferList's moves) had no `type`, so inside a form they submitted it: flipping a Toggle ran the page's save. They're all `type="button"` now. Your own `<Button>`s keep HTML's default.
|
|
411
|
+
|
|
412
|
+
**Repeated rows of fields: AddAnother (MOJ's "Add another").** Rows of the same fields that the user adds as many of as they need: people to invite, line items. The labels show once, as column headings, while each field is named for its row ("Email address, person 2"): wrap each control in `<Field hideLabel label={label("Email address")}>`. Add and Remove name their row, and focus moves to the new row after an add and to the previous row after a removal. On a narrow group the rows stack and each field shows its own label. Field's `label` no longer accepts `undefined`, `null` or `false`; a label that shouldn't show takes `hideLabel` (visually hidden, still read).
|
|
413
|
+
|
|
414
|
+
**Uploads show what's really happening.** FileUpload shows each file's real progress, done or failed; the simulated progress is gone. Either pass `upload(file, { onProgress, signal })` and it runs each picked file, three at once while the rest wait, with Cancel (it aborts the signal), Retry after a failure (the rejection's message says why) and Remove; or keep the list yourself and pass `files` with `onCancel`, `onRetry` and `onRemove`. With only `onFiles`, files show as added, with no bar. A running upload makes its overlay ask before closing; closing once the user confirms, or leaving, cancels it. Each outcome is announced.
|
|
415
|
+
|
|
416
|
+
**The editor shows what the page puts in it.** RichTextEditor follows `content`: a value from outside that differs shows at once, the cursor stays where it still exists, and `onChange` isn't called. Its own output coming back (an autosave's echo, a server's sanitised copy) changes nothing, and a value that arrives during an IME composition waits for it to end. `editable` follows its prop. A new `handle` replaces the document on purpose (`setContent`, for a restore or a template; it calls `onChange` like an edit) and inserts at the cursor (`insertContent`, `insertImage`). `onImageRequest` hands the image button to the page's media library. Before, the editor read `content` and `editable` once, when it was created.
|
|
417
|
+
|
|
418
|
+
**Status dots say their status in words.** StatusDot's `label` is required and always drawn beside the dot, so colour never carries the status alone; it's what a screen reader reads, and the dot itself is hidden from it. The dot is no longer a live region (`role="status"`). Avatar's presence has a shape for each status (online a filled dot, away a ring, busy a dot with a bar, offline a ring with a slash) and says the status in the avatar's name ("Priya Nair, online").
|
|
419
|
+
|
|
420
|
+
### Upgrade note for agents (0.19.x → 0.20.0)
|
|
421
|
+
|
|
422
|
+
Upgrade `gridsmith-ui` to `^0.20.0`, then:
|
|
423
|
+
- **The shell's ToastProvider.** Remove `maxVisible={1}` unless the user chose one at a time (check_compliance says which: `gs:shell-toast-old-default` is the old default, fixed without asking; `gs:shell-toast-against-rules` asks the user). Give a toast about one item a `key`; a toggle that saves needs no toast, and if the user asked for one, key it to the setting (`toast.success("Email alerts on", { key: "email-alerts" })`), since an unkeyed toast on a toggle is now an error.
|
|
424
|
+
- **Deletions.** Wire each delete through `useDestructiveAction` with an `onUndo` that restores the item its toast names (in a list, call the hook in the row's own component). A page that showed its own toast in `onConfirm` keeps it: the hook stays quiet. A hand-built Undo toast should go.
|
|
425
|
+
- **Forms.** Return the save's promise from `onSubmit` (`onSubmit={(errors) => { if (Object.keys(errors).length === 0) return save(values) }}`). Take out a `dirty` you passed only to make an overlay ask: the overlay knows now. Code that treated an `errors` key holding undefined as passing can test the keys instead.
|
|
426
|
+
- **Field labels.** A conditional label (`label={first ? "Email" : undefined}`) no longer type-checks: rows of the same fields become AddAnother, and a label that shouldn't show takes `hideLabel`.
|
|
427
|
+
- **StatusDot.** Give every StatusDot a `label`, and remove words written beside a dot that repeat it: the label is drawn now.
|
|
428
|
+
- **FileUpload.** Replace a list kept beside the zone with `upload` or `files`.
|
|
429
|
+
- **RichTextEditor.** Keep `content` in state from `onChange`; send a restore through the handle's `setContent`; key the editor by the record's id when the page switches records.
|
|
430
|
+
|
|
431
|
+
## 0.19.0 — 28 September 2026
|
|
432
|
+
|
|
433
|
+
It adds a component (`CardGrid`) and props (Field's `validate`, SplitView's `detailMinWidth`, CardGrid's `compare` and `gap`, a Timeline item's `dateTime`, and a Field's wiring on six controls), so in semver terms the next release is a minor one, 0.19.0. One behaviour changes: after a failed submit with two or more errors, focus now goes to the error summary.
|
|
434
|
+
|
|
435
|
+
**Tighter line heights for body and large text (the owner, 28 September 2026).** In a theme with a 16px base, as most presets have, body text now has a 22px line (it was 24px) and the large size, 18px, a 24px line (it was 28px). Body text is `text-base` and any text with no size class of its own, such as Markdown and editor text; the large size sets the titles of cards, tables, charts, sections, drawers and empty states. Both are ratios (`--ds-line-height-body` is now 22/16, and the new `--ds-line-height-lg` is 24/18), so a theme with another base keeps the proportions: at a 14px base, 14/19.25 and 15.75/21. `text-2xs` sets no line height, so it takes the one around it and is a little tighter under body text (13.75px for its 10px, from 15px). The other sizes keep Tailwind's line heights. The large controls that use these sizes centre their text in a fixed height, so they don't change.
|
|
436
|
+
|
|
437
|
+
**The fresh-eyes review of rounds 26 and 27 (28 September 2026).** Three reviewers read everything below before release. These are their fixes.
|
|
438
|
+
|
|
439
|
+
- **A field that goes takes its error with it.** When a Field with `validate` unmounts (a section hidden, a field shown only for some answers), its message leaves the form's errors too. It had stayed behind, so `onSubmit` got an error with nothing invalid on the page, and a form that saves only without errors never saved.
|
|
440
|
+
- **The error summary answers a submit.** It appears only after the first submit, as GOV.UK's error summary does. With onBlur timing, two fields left empty had put a summary above the form before the user had tried to save. After a failed submit with two or more errors, focus now moves to the summary, so the user hears what is wrong and can follow its links; with one error, focus goes to that field. A summary link or the focus after a submit reaches a group (a SegmentedControl's radio group) at its chosen option.
|
|
441
|
+
- **Every control takes its Field's label, hint, error and required state.** Dropdown, DatePicker, SegmentedControl, Combobox, TimePicker and DateRangePicker now take the Field's id, are named by its label, are described by its hint and message, and carry its invalid and required states. The summary had read "A field: Needs attention", with no link, and a screen reader heard a Dropdown's placeholder as its name. A control that can't carry `aria-required` (a button, a group) says "Required" in its description instead.
|
|
442
|
+
- **Popups keep focus in Safari.** The panels of Dropdown, DatePicker, TimePicker and DateRangePicker take focus when clicked, so choosing from one is never read as leaving the field, which could flash its error.
|
|
443
|
+
- **CardGrid counts the cards it draws.** A comparison counts the cards on the page, so a card left out by a condition isn't counted and cards from a fragment or a component each are; it had counted React's children, so three plans could stay stacked or never line up. The space between cards is the new `gap` prop, and the columns are worked out from it: a `gap-*` class had moved the cards but not the arithmetic, and a comparison went two and one. A comparison takes all its cards whatever `maxColumns` says, and only Cards share its rows, so a card wrapped in an element of its own no longer stretches the others' headers.
|
|
444
|
+
- **Titles fit by CSS alone.** A title breaks a word only where the word is longer than its line, and its header's floor (its longest word) never exceeds the row, so no name spills off its card however it's written: as parts, inside a fragment, or as a long token among words. The header no longer reads the title's children to decide.
|
|
445
|
+
- **DescriptionList values never reach over their label.** A value that's a row of elements puts its items at the end and wraps them when they don't fit; a long email breaks. Every value is laid out the same way, by CSS, whatever it holds.
|
|
446
|
+
- **SplitView settles.** Its thresholds hold across a band as wide as a classic scrollbar, so an aside whose scrollbar narrowed the layout no longer came and went in a loop. The item's room is read from the panels as the user sized them. On a phone, the open item's aside is a button beside Back that opens it in a Drawer; it couldn't be reached there before.
|
|
447
|
+
- **Modals stay opaque in older browsers.** The raised surface applies only where the browser supports relative colour, with the theme's surface beneath it; elsewhere a modal had turned transparent. A surface with no colour at all stays transparent rather than turning black.
|
|
448
|
+
- **A Timeline date is the day it names.** A `dateTime` that is a day alone ("2026-09-27") is that day wherever the reader is, with no time; it had shown the evening before west of UTC. A screen reader now reads the exact date too, and an item with no timestamp and a date that can't be read draws nothing.
|
|
449
|
+
- **The stylesheets.** The package's own utility and animation carry its prefix (`gs-scrollbar-none`, `gs-indeterminate`), so they meet nothing of an app's. The editor's content styles sit in Tailwind's components layer, so an app can restyle them with utilities, and `roots.css` declares the cascade layers in Tailwind's order before anything else.
|
|
450
|
+
|
|
451
|
+
**Round 27: general layout rules, and styles that now ship.**
|
|
452
|
+
|
|
453
|
+
- **CardGrid: cards always fit.** `<CardGrid>{items.map(…)}</CardGrid>` takes as many columns as fit at the card's minimum width (`minWidth`, default 14rem), up to `maxColumns` (default 4). It reads the width it's given, not the viewport, so the same cards fit a full page, a pane beside a tree and an aside, or a phone: with less room there are fewer per row, and a card is never squeezed. Round 27's file cards had got 130 px each from four breakpoint columns in a pane.
|
|
454
|
+
- **CardGrid `compare`: cards read against each other.** Plans, tiers, options: all of them in one row when each gets its minimum width, or one per row, never two and one, and their header, content and footer line up across the row (the grid shares the cards' rows). The switch happens at exactly the width where they stop fitting, with no breakpoint. The billing recipe uses it now, in place of the hand-shared rows.
|
|
455
|
+
- **Long names truncate or wrap inside their card.**
|
|
456
|
+
- A CardTitle set to one line (`lines={1}`) truncates beside the header's actions, with the full name on hover. Before, the header held the title block at the whole name's width, wider than the card, so a file name ran out of it.
|
|
457
|
+
- A clickable card's one-line title ends in an ellipsis. It was cut bare.
|
|
458
|
+
- A title breaks a word only where the word is longer than its line. Titles of words wrap between words, as before, and keep round 14's floor: the actions move under the title before a word is cut.
|
|
459
|
+
- **SplitView gives the open item room and keeps the page's colour.**
|
|
460
|
+
- When the list and the aside would leave the item narrower than `detailMinWidth` (default 480 px), the aside becomes its toggle and Drawer, as it already did below `asideCollapseBelow`. The aside is the page's secondary content. Pass a larger minimum for an item that holds a wide table or a card grid.
|
|
461
|
+
- Edge to edge (`frame="fill"`), the list now sits on the page's canvas and only the open item, its detail and its aside, on the surface. The whole split had been on the surface, so a page with a folder tree read as a different page from the rest of the app. In a page frame it's still one surface, like a card.
|
|
462
|
+
- **The editor's content styles ship.** RichTextEditor's headings, lists, quotes, code, links and tables are now styled in every app: the rules are in `roots.css`. They lived only in the showcase's own stylesheet, so apps showed them as plain text. The same gap left the scrollbar-hiding utility and the animation that an indeterminate Progress uses out of `tailwind.css` (now `gs-scrollbar-none` and `gs-indeterminate`): calendar and editor scrollbars showed, and an indeterminate Progress bar didn't move. The showcase now imports the stylesheets the package ships, and a test fails when a component needs a style they don't have.
|
|
463
|
+
- **Modals are never see-through.** Dialog, Drawer, Sheet, CommandPalette and GlobalSearch draw the theme's surface at no less than 85% opacity, through CSS relative colour, so an opaque theme is unchanged. A glass theme's half-transparent surface suits a card on the canvas, but over a modal's scrim it read as grey mud, with the page showing through.
|
|
464
|
+
- **A Timeline item can carry its exact time.** Give an item `dateTime` (an ISO string, a day like "2026-09-27", or a Date) with its relative `timestamp`, and the time is a `<time>` element with the exact date on hover and in what a screen reader reads.
|
|
465
|
+
- **A SegmentedControl takes a Field's label.** `<Field label="Status"><SegmentedControl …/></Field>` stacks the label above and names the radio group by it (`aria-labelledby`). Collapsed into its Dropdown, it's named by the Field too. A Label placed beside it ran into the options.
|
|
466
|
+
- **DescriptionList right-aligns every value.** A value that is an element (an Avatar beside a name, a Badge) now ends at the right like a text value. It started at the middle of the row.
|
|
467
|
+
- **BulkActionBar: a destructive action is a secondary button on the bar.** It opens a confirmation, and the confirm carries the red. In the phone menu it's still a destructive item.
|
|
468
|
+
|
|
469
|
+
**Fields check themselves at the project's timing** (round 26).
|
|
470
|
+
- `Field` takes `validate`: the field's check, returning the message for what is wrong with its value, or nothing. It gets the control's `value` (or `checked`), or you can close over your own state.
|
|
471
|
+
- Inside a FormWrapper it runs at the form's validation timing, which is the project's forms.validationTiming:
|
|
472
|
+
- `onBlur`, the house default: when the field is left;
|
|
473
|
+
- `onChange`: as the value changes, once the field has been left;
|
|
474
|
+
- `onSubmit`: on submit only.
|
|
475
|
+
- Whatever the timing, every submit runs every check, and FormWrapper's `onSubmit` gets each message under its Field's `id`.
|
|
476
|
+
- A message that shows is checked again as the value changes, so it goes as soon as the value is right.
|
|
477
|
+
- Focus moving into the control's own popup (a Dropdown's list, a DatePicker's calendar) doesn't count as leaving the field.
|
|
478
|
+
- An `error` you pass still wins, for what only the server can tell.
|
|
479
|
+
- Before this, FormWrapper knew the timing but nothing ran a check at it, so builders checked their fields only on submit. Both of round 26's runs did, as did eight earlier ones in the bench's corpus.
|
|
480
|
+
|
|
481
|
+
**From the owner's review of the showcase, 27 September 2026.**
|
|
482
|
+
|
|
483
|
+
- **A selected card no longer looks focused.** A selectable Card's selected state is its accent border, with the accent fill on a card with a `selectType` (radio, checkbox, toggle). Since Sprint 20.6 it also drew the 2 px accent ring that other controls use for their open state. A ring round the whole card is the focus state, so a selected card looked focused. The ring is gone; focus still draws its own.
|
|
484
|
+
- **AvatarGroup stacks in order.** Each avatar sits above the one before it and below the one after, and the `+N` counter sits on top. An avatar with a status dot had been drawn above every avatar without one, so a photo between two status avatars sat behind both. Every item is now positioned the same way, with no z-index, so nothing in the group can rise above a sticky header or a popover.
|
|
485
|
+
|
|
486
|
+
## 0.18.0 — 27 September 2026
|
|
487
|
+
|
|
488
|
+
**Sprint 25.10: the round 25 fixes. A table that has to scroll sideways now shows it and can be reached by keyboard, and a short card title keeps its badge beside it.**
|
|
489
|
+
|
|
490
|
+
**Tables that scroll show it.**
|
|
491
|
+
- A Table wider than its frame fades the edge it can scroll towards: the right edge at the start, both edges in the middle, the left at the end (left to right, as the library is), none when it fits. The fade is a mask on the scroll area, so it follows the surface the table sits on and takes no clicks. A scrollbar that takes room (Windows, Linux, a Mac set to always show scroll bars) stays whole, and the fade ends where it starts. On a Mac, where the scrollbar hides until you scroll, the fade is the only sign there's more.
|
|
492
|
+
- While it can scroll either way, sideways or up and down, the table's scroll area is a region in the tab order, named by the table's title, or by its new `aria-label` prop, or "Table". Tab reaches it, the arrow keys scroll it, and the table's frame draws the focus ring. A table that fits adds no tab stop, and a region that has focus keeps its tab stop until it loses focus. Give each untitled table on a page its own `aria-label`, so their regions have different names.
|
|
493
|
+
- An untitled Table now has the same scroll area inside its frame as a titled one: one element more between the frame and the `<table>`. The frame keeps `ref`, `className` and its markers, and a height set on it still scrolls the table. Classes that style the scroller itself (its overflow, its scrollbar) and `ref.current.scrollLeft` now reach the frame, not the scroller.
|
|
494
|
+
- The frame can shrink below the table's width (`min-w-0`), so in a grid track or a flex row the table scrolls inside its share instead of widening it. Before, a titled table (Sprint 25.9) wider than its share of a `1fr 2fr` row took the chart's room.
|
|
495
|
+
- The Tabs strip and the Table share one mechanism for these fades.
|
|
496
|
+
|
|
497
|
+
**Card headers.**
|
|
498
|
+
- In a CardHeader that starts with its CardTitle (no icon) and whose actions are badges, the badges share a row with the title alone. The title claims half the row or its own width, whichever is less, so a short title keeps its badge beside it, and a long one still gets at least half the row. The description and anything after the title run the header's full width below that row.
|
|
499
|
+
- A header with a button or a menu keeps its layout as before: the title and the description together, the actions beside them or under both. Beside the title alone, a button taller than the title's line pushed the description down, and cards with and without one stopped lining up. So does a header with an icon, or with anything before its title.
|
|
500
|
+
- A titled Table's band follows the same rule, with CardHeader's classes.
|
|
501
|
+
|
|
502
|
+
**Registry.** EntityCell is filed under data display with the other data components; its category had a name the component lists didn't use.
|
|
503
|
+
|
|
504
|
+
**Sprint 25.9: the round 24 fixes. Charts size and label themselves, blocks side by side can be titled either way, content fits the width chosen for it, and the settings panel is no longer squeezed by its rail.**
|
|
505
|
+
|
|
506
|
+
**Charts size themselves.** A chart with no height set now works one out from its type and its data:
|
|
507
|
+
- sideways bars and funnels take a row per item, never under the small height;
|
|
508
|
+
- charts over dates or numbers keep two wide to one high, between the small and large heights;
|
|
509
|
+
- pie, radar and radial charts are square.
|
|
510
|
+
|
|
511
|
+
A preset or a number still overrides, and a ChartContainer's own height wins over its chart's. `height="fill"` now means the same as the new default, `"auto"`, and ChartContainer takes a number height. New token: `--ds-chart-row` (2rem).
|
|
512
|
+
|
|
513
|
+
**Rows of blocks match.**
|
|
514
|
+
- In a row, every chart's frame matches the tallest block beside it.
|
|
515
|
+
- The chart inside grows only to a bound: square for two-to-one charts, twice its height for row charts, and not at all for round charts. The extra space sits below the legend.
|
|
516
|
+
|
|
517
|
+
**Legends.** The legend sits below the drawing instead of taking height from it, in series order (the stack order) rather than alphabetical. FunnelChart has no legend; its labels name the stages.
|
|
518
|
+
|
|
519
|
+
**Names are never skipped.**
|
|
520
|
+
- Dates and numbers keep the even labelling, now measured in the theme's real axis font (a token written as `calc()` measured at a 12 px fallback before).
|
|
521
|
+
- Names wrap onto two lines, then end with "…", with the full name on hover and in the bar's tooltip.
|
|
522
|
+
- An upright BarChart or CompositeChart of names turns itself sideways when a name would show fewer than six letters, and back when there's room. `horizontal={true}` or `={false}` forces either.
|
|
523
|
+
- New `categoryKind` prop (`"names"` or `"scale"`) on both, and `categoryKind()` is exported from `gridsmith-ui/charts` to say which the chart guessed.
|
|
524
|
+
- On sideways charts, the name column is at most a third of the width, the right margin is 4 px, and bars keep their thickness when the chart grows.
|
|
525
|
+
- CompositeChart gains `horizontal`.
|
|
526
|
+
|
|
527
|
+
**Readable and reachable.**
|
|
528
|
+
- Axis titles are horizontal, with the value axis's title above the axis.
|
|
529
|
+
- Axis text is drawn in the muted text colour; it was Recharts' #666, about 2.6:1 on dark cards.
|
|
530
|
+
- Sideways charts answer Up and Down (Left and Right follow reading order), and the tooltip's category and values are announced.
|
|
531
|
+
- Chart titles are h3 headings in the card-title style, never cut, and they name the chart's graphic. ChartContainer's padding matches CardHeader's inset.
|
|
532
|
+
- Fix: a chart whose rows have a `name` field no longer replaces each series' name with the row's name in the tooltip.
|
|
533
|
+
|
|
534
|
+
**Titles inside frames.**
|
|
535
|
+
- Table gains `title`, `description` and `actions`: a band inside the frame, in the card-title style, that never scrolls sideways with a wide table. The table is named and described by it, and in card mode the heading sits above the cards. An untitled Table is unchanged.
|
|
536
|
+
- SectionHeading titles wrap instead of truncating; the pane variant keeps one line.
|
|
537
|
+
|
|
538
|
+
**Settings.**
|
|
539
|
+
- A narrow settings page with a SettingsLayout rail is wider by the rail and its gap, so the panel keeps a narrow page's full width (720 px at the default density), with or without a PageHeader. It needs CSS `:has()`; without it the page stays as before.
|
|
540
|
+
- The rail becomes the section dropdown below a 560 px panel (was 480), now counting the gap. The panel is a size container, so `@…` classes inside it follow the panel's width.
|
|
541
|
+
- In a settings panel, FormWrapper forms and Fields keep the narrow reading width however wide the page is. A `max-w-*` class overrides this, and dialogs and drawers are unaffected.
|
|
542
|
+
|
|
543
|
+
**Also:**
|
|
544
|
+
- DescriptionList re-flows by its own width: a pair needs 20rem side by side, or 8rem stacked. Two-column horizontal lists in drawers, panes and narrow panels now show one column. `stackBelow="none"` keeps the columns.
|
|
545
|
+
- The minimal PageHeader shows its `description`, including when it snaps.
|
|
546
|
+
- SliderRange gains `formatValue`, which sets each thumb's `aria-valuetext` and the shown value ("Weekly", not "2").
|
|
547
|
+
- Markers for the bench:
|
|
548
|
+
- `data-gs="table"`, `"section-heading"` and `"chart"`;
|
|
549
|
+
- `data-gs-chart`, `data-category-kind`, `data-categories`, `data-labels-shown` and `data-labels-cut` on charts;
|
|
550
|
+
- `data-gs-clip` on the library's deliberate clippers;
|
|
551
|
+
- `data-width` on PageContainer.
|
|
552
|
+
|
|
553
|
+
## 0.17.2 — 26 September 2026
|
|
554
|
+
|
|
555
|
+
**Sprint 25.7: the owner's calls after Part D, and the fresh-eyes review of Part D.**
|
|
556
|
+
|
|
557
|
+
**gridsmith-ui needs React 19.**
|
|
558
|
+
- The peer range said React 18 or 19, but the components render React 19's context providers and take `ref` as a prop. On React 18, AppShell, PageContainer, Card, Dialog, Drawer, Tooltip, Collapsible, Combobox and the Gantt crashed ("render is not a function"), and 21 components lost their refs. That was checked in a React 18.3 install.
|
|
559
|
+
- The range is now `^19.0.0`, so npm refuses a React 18 project with a clear conflict, and `get_app_shell`'s install step says to upgrade React first.
|
|
560
|
+
- An app that forces the install anyway gets a named error from AppShell and PageContainer: "gridsmith-ui needs React 19, and this app has React 18.3.1. Upgrade react, react-dom, @types/react and @types/react-dom to 19…".
|
|
561
|
+
|
|
562
|
+
**KanbanBoard keeps a dropped card where it was dropped while its move saves.**
|
|
563
|
+
- An app that re-rendered while a save was out (a "Saving…" state) with the columns it had, or with an array rebuilt from the same cards, used to see the card jump back, then forward when the save landed. That came from the board comparing `columns` by identity. The board now holds the move, the pattern Atlassian's drag-and-drop guidance sets for Trello and Jira: the move shows at once, and data that arrives meanwhile has it replayed over it.
|
|
564
|
+
- While any save for the card is out (an earlier move's included), nothing `columns` shows ends the hold. After that, the hold ends when `columns` shows the card where it was dropped, or somewhere new (the app's data decided), or, with the card still where it started, when `columns` changes some other way. A late refetch that shows one of the card's own earlier places is treated as behind.
|
|
565
|
+
- **Return the save's promise** from `onCardMove` or `onColumnsChange` (an async function, or a mutation's `mutateAsync`).
|
|
566
|
+
- If it rejects, the card goes back to where `columns` has it.
|
|
567
|
+
- A screen reader hears "Move not saved. Alpha is back in To do, position 1 of 3." Failures that land together are all said.
|
|
568
|
+
- Focus stays on the card, if it was there on this board.
|
|
569
|
+
- A failure during another card's drag leaves that drag too, so the drop cannot carry it.
|
|
570
|
+
- A handler that throws refuses the move the same way, and the error is logged.
|
|
571
|
+
- The save must settle: give a request a timeout.
|
|
572
|
+
- Both callbacks are typed as returning `unknown`, so any handler still fits.
|
|
573
|
+
|
|
574
|
+
**Gantt**
|
|
575
|
+
- **Unscheduled tasks:** `start` and `end` may be `null`, or any date that doesn't parse. The task keeps its row, named "Wireframes, no dates", with a "No dates" mark and no bar. Nothing throws.
|
|
576
|
+
- **Selection:** it stays controlled once `selectedId` has had a value, so `selectedId={task?.id}` works. From outside the chart, Tab lands on the selected task, and a selection made elsewhere is scrolled into view. When the focused task is removed, focus moves to the row before it.
|
|
577
|
+
- **Status icons:** every status tone now carries an icon: neutral a minus sign, info an "i", accent a star, as well as success, warning and error. `GanttStatus.icon` chooses another. The legend stays when `toolbar={false}`.
|
|
578
|
+
- **New, all additive:**
|
|
579
|
+
- `describe(item)` adds words to each item's accessible name (the assignees `renderLabel` shows);
|
|
580
|
+
- `labelHeader` takes any content;
|
|
581
|
+
- `Gantt` is generic over its items.
|
|
582
|
+
- **Smaller changes:**
|
|
583
|
+
- a fixed `zoom` with no `onZoomChange` shows no zoom controls;
|
|
584
|
+
- duplicate ids are dropped, with a warning in development builds;
|
|
585
|
+
- the scroll region is named "{aria-label}, {dates}", apart from the tree;
|
|
586
|
+
- in forced-colours mode, bars, diamonds, summaries and legend swatches keep an edge, and a focused milestone shows its outline.
|
|
587
|
+
|
|
588
|
+
**Time blocks**
|
|
589
|
+
- **Dates:**
|
|
590
|
+
- A `Date` end at local midnight after a time-of-day start now ends there. A 16:00-to-midnight shift was drawn a day too long.
|
|
591
|
+
- A date picker's day is a whole day in time zones whose clocks skip midnight (Chile, Cuba, Egypt, Lebanon).
|
|
592
|
+
- "Today" rolls over at midnight, not 30 seconds before.
|
|
593
|
+
- `dayOf` and `endDayOf` take `DateInput | number`. `null` and other non-dates give `NaN` instead of throwing.
|
|
594
|
+
- **Server-rendered pages:** without `today`, the server render and the first browser render draw no Today line. The browser adds it straight after, and the view still opens on today, so hydration matches.
|
|
595
|
+
- **Scrolling and keys:**
|
|
596
|
+
- Resizing keeps the centre date exactly; small steps, like dragging a divider, used to drift it by hundreds of pixels.
|
|
597
|
+
- Previous and Next always move, by a view's width when a period is wider than the view.
|
|
598
|
+
- Ctrl/⌘ + wheel at the last zoom level zooms the page.
|
|
599
|
+
- Keys from portalled popups (a menu in the toolbar, a dialog opened from an item) no longer reach the viewport or `useGridKeyboard`.
|
|
600
|
+
- **Focus:** TimeBar's and the scroll region's focus rings show in forced-colours mode.
|
|
601
|
+
- **Labels:** tick forms stay one per level (`TimeTick.labels` may repeat a form), so May no longer shows as "M" between "Apr" and "Jun".
|
|
602
|
+
- **Performance:** scrolling does less work. Three years at day zoom went from 28 ms to 3.8 ms per scroll step, and 300 bars from 24–42 ms to 13–18 ms. The scroll variables now sit on a rows holder (`data-time-rows`, `display: contents`).
|
|
603
|
+
|
|
604
|
+
**`useToast()` without a provider** throws an error that says what to do: a page outside the app shell (sign-in, a public page) shows its messages inline, with an Alert above the form and each field's error below the field.
|
|
605
|
+
|
|
606
|
+
**Tokens and generated CSS** (apps get these with their generated tokens):
|
|
607
|
+
- **Generated tokens now match the builder's preview.**
|
|
608
|
+
- A theme switched to its other mode no longer keeps the default mode's preset colours. Neumorphism and Industrial had dark text on a dark page, Precision Minimal a light sidebar in dark mode, and Claymorphism, Cyberpunk and Terminal their other mode's canvas pattern.
|
|
609
|
+
- The chosen status palette wins over a preset's own status colours, as it always did in the preview (Neumorphism, Industrial, Cyberpunk, Terminal), so every status fill reaches AA with its text.
|
|
610
|
+
- Each generated file gains a dark overlay block (`[data-ds-overlay="dark"]`) for a dark sidebar's tooltips and menus.
|
|
611
|
+
- An independent sidebar takes its mode's preset border.
|
|
612
|
+
- A dark default switched to light takes the preset's text colours in the sidebar and tabs.
|
|
613
|
+
- **get_tokens** also lists `--ds-text-on-success`, `-warning`, `-error` and `-info`.
|
|
614
|
+
- **`gridsmith-ui/tokens.css`** is regenerated: the full Precision Minimal output, with its gradient, palette, charts, and its dark and overlay blocks.
|
|
615
|
+
- **Teal and cyan palette Badges and Tags keep white text.** Their bold fill is one step darker (lightness 0.55 → 0.54 at hues 170–216.5) instead of their text turning a near-black tint. All 720 palette fills, checked in Chrome at every half degree of hue, reach 4.5:1 with white.
|
|
616
|
+
- **Text on a gradient accent is decided over every colour the gradient paints,** not only its two ends. Chrome paints it in Oklab, and a point between the ends can be darker than both: up to 5%, which took text chosen on the ends below AA part-way along some gradients.
|
|
617
|
+
- **Presets:**
|
|
618
|
+
- analytical dark: the gradient ends at primary-500, hue 240 (was 600 at 260);
|
|
619
|
+
- precision-minimal light: end saturation 0.55 (was 0.7);
|
|
620
|
+
- precision-minimal dark: accent hue 223, saturation 0.65 (were 220 and 0.7);
|
|
621
|
+
- developer-dark, both modes: end saturation 0.55 (was 0.4);
|
|
622
|
+
- neumorphism: the end is hue 230 (was 240), saturation 0.75 in light mode and 0.7 in dark (was 0.6), and the light accent's saturation is 0.55 (was 0.8);
|
|
623
|
+
- neo-brutalist, both modes: accent saturation 0.7 (was 1), so its accent carries white text again.
|
|
624
|
+
|
|
625
|
+
Every preset gradient reaches 4.5:1 at every pixel in Chrome.
|
|
626
|
+
|
|
627
|
+
## 0.17.1 — 26 September 2026
|
|
628
|
+
|
|
629
|
+
The same build as 0.17.0, published a minute later under the next version number; nothing changed.
|
|
630
|
+
|
|
631
|
+
## 0.17.0 — 26 September 2026
|
|
632
|
+
|
|
633
|
+
**Sprint 25.7, Part D: a Gantt, and the time-axis building blocks it is made from.** Round 23's planner hand-built a Gantt with no zoom, no pan, a clipped Today badge, overprinting month labels and a 100-px phone view. The library now has one, built from public blocks. The next time-axis view (a rota, a booking grid, a roadmap) is assembled from the same blocks, as the new Resource Scheduler example is.
|
|
634
|
+
|
|
635
|
+
**New components** (main entry):
|
|
636
|
+
- **`Gantt`:**
|
|
637
|
+
- tasks with start and end dates, in collapsible groups, with bars coloured by status (success, warning and error carry an icon too) and a legend;
|
|
638
|
+
- milestones as diamonds, and finish-to-start dependency arrows;
|
|
639
|
+
- zoom, pan, previous and next period and Today;
|
|
640
|
+
- one Tab stop (the APG tree view), with accessible names that carry title, dates, status and dependencies;
|
|
641
|
+
- `selectedId` / `onSelect` for a detail beside it. It is built to sit in SplitView with `closedDetail="hidden"`.
|
|
642
|
+
- **`TimeViewport`,** with `TimeRow` and `TimeBar`: the one scrolling area for a time axis.
|
|
643
|
+
- A sticky scale, a label column pinned at every width, zoom by named levels, and pan by scroll, drag or keys.
|
|
644
|
+
- A Today line, or a "now" line for hourly views (`todayAt="now"`), and day shading (`shadeDays`).
|
|
645
|
+
- `TimeBar` places itself by the scale, shows the first of its `labels` that fits the part in view (never an ellipsis), and keeps it clear of the pinned column.
|
|
646
|
+
- **`TimeScaleHeader`:** two rows of labels that shorten ("September" → "Sep" → "S") and thin out, never overlapping and never clipped, with the Today label drawn in the scale.
|
|
647
|
+
|
|
648
|
+
**New hooks and helpers:**
|
|
649
|
+
- **Scale and dates:**
|
|
650
|
+
- `useTimeScale` and `TIME_ZOOMS`;
|
|
651
|
+
- `dayOf`, `dateOfDay` and `endDayOf`, the date model: calendar days, so a clock change never stretches a day; a `"YYYY-MM-DD"` string is a local calendar day; a whole-day end is inclusive;
|
|
652
|
+
- `timeExtent`, and `formatDateRange` for accessible names: dates and date-times, joined the locale's way.
|
|
653
|
+
- **Labels:** `fitLabels`, `fitLabel`, `useFittedLabels` and `useTextMeasure`.
|
|
654
|
+
- **Viewport:** `useTimeViewport` and `useTimeViewportLayout`.
|
|
655
|
+
- **`useGridKeyboard`,** now public: one Tab stop over rows or a grid, with typeahead, Enter and Space, scrolling into view, and opt-in modifier keys.
|
|
656
|
+
|
|
657
|
+
**Tokens:** `--ds-text-on-info`, `--ds-status-neutral` and `--ds-text-on-neutral`.
|
|
658
|
+
|
|
659
|
+
**Behaviour an app may notice: text on a fill is decided as Chrome draws the fill.**
|
|
660
|
+
- The token algorithm modelled an out-of-range colour by reducing its chroma; Chrome clips each channel. Its decisions are now made on the clipped colour and Chrome's 8-bit rounding, measured against 399 colours drawn in Chrome. Every solid fill in every preset now reaches 4.5:1 with its text in the preview. Apps get this with their generated tokens; four presets' status colours only from 0.17.2, which makes the generated tokens follow the preview.
|
|
661
|
+
- **Where the text changes from white to a near-black tint:**
|
|
662
|
+
- palette Badges and Tags in teal and cyan hues (170–216.5) in nine presets;
|
|
663
|
+
- the page accent in neo-brutalist, developer-dark and neumorphism;
|
|
664
|
+
- four independent sidebar and header accents.
|
|
665
|
+
- Some dark tints move by 0.01–0.03 in lightness.
|
|
666
|
+
|
|
667
|
+
## 0.16.0 — 25 September 2026
|
|
668
|
+
|
|
669
|
+
**Sprint 25.7, Part C: round 23's library fixes.** A setup or checkout flow gets a focus mode in the same shell; a Kanban card can go into an empty column; a detail pane can open only on selection; stacked charts; and a slider and a file upload that say what they are once. It also carries the Part A follow-up and B10's markers, written as 0.15.1 before Part C (below).
|
|
670
|
+
|
|
671
|
+
**Behaviour an app may notice:**
|
|
672
|
+
- **PageHeader draws `tabs` in every variant.** Before, only `tabs-integrated` and `snapping` drew them, so `tabs` passed to any other variant was ignored.
|
|
673
|
+
- **KanbanBoard reports a move once, on drop.** `onColumnsChange` no longer fires while a card is being dragged. Escape puts the card back and reports nothing. A new `columns` array replaces what the board shows, where before the board kept its own copy after the first render.
|
|
674
|
+
- **SliderRange has no "Value" default.** A `label` that is passed is now shown above the slider, where before it was an invisible `aria-label`. A slider with no `label`, no `aria-label` and no Field around it has no name, and accessibility checkers report it.
|
|
675
|
+
- **FileUpload turns away a file of the wrong type or over `maxSize` with a message,** and `onFiles` is no longer called with an empty list. `accept` is checked on drop too. With `multiple={false}`, a new file replaces the old, and a drop of several files takes none.
|
|
676
|
+
- **Chart colours change in dark mode, and chart-1 sometimes changes in light mode.** Every series now shows at 3:1 against the surface it sits on (WCAG 1.4.11):
|
|
677
|
+
- in dark mode, charts 2–6 are lighter;
|
|
678
|
+
- chart-1 moves off the accent when the accent is too dark for a dark card (shade 500 or darker) or too light for a light one (shade 400 or lighter), to `primary-400` or `primary-500`.
|
|
679
|
+
|
|
680
|
+
Badges and Tags keep the palette's bold. A project gets the new colours when its tokens are fetched again (`get_tokens`).
|
|
681
|
+
- **Escape and presses outside nested popups** (the sprint's review):
|
|
682
|
+
- One Escape closes only the innermost popup. Before, a Combobox or MentionInput list inside a Dialog, Drawer or Popover closed both.
|
|
683
|
+
- A press outside every popup closes them all; a press inside an outer popup closes only the inner one.
|
|
684
|
+
- An alert dialog (`Dialog alert`) now closes on Escape, through `onClose`. Its backdrop still doesn't close it.
|
|
685
|
+
- **A clickable Card that holds controls is no longer a button around them** (a menu, a checkbox, a button inside).
|
|
686
|
+
- Its CardTitle renders the button, which names the card to screen readers, and a click anywhere else on the card still runs `onClick`. A test that found such a card by all its text as a button finds it by its title now.
|
|
687
|
+
- With controls and no CardTitle, the card can be clicked only by mouse. Give it a CardTitle.
|
|
688
|
+
- **NavMenu and SidebarNav's compact flyout are disclosure navigation, not menus:** a list of links, without menu roles.
|
|
689
|
+
- **DatePicker runs `onChange` after it hands focus back,** so a focus move the app makes in `onChange` wins.
|
|
690
|
+
|
|
691
|
+
**The focus mode (AppShell, PageHeader, Stepper)**
|
|
692
|
+
- **`AppShell` gains `focus`** (`AppShellFocus`: `kind` `"before-app"` | `"in-app"`, `exitLabel`, `onExit`) for a flow the user completes before or outside the app.
|
|
693
|
+
- The sidebar, mobile menu and bottom tabs go, and the Navbar keeps its logo and the assistant but drops its links and search.
|
|
694
|
+
- Before the app, one "Save and log out" button takes the user menu's place. Inside the app, "Exit" sits beside the user menu.
|
|
695
|
+
- **`useUnsavedChanges(dirty)`** (new) tells the shell a page has unsaved work, so Exit asks first, focused on "Keep editing".
|
|
696
|
+
- **`PageHeader` gains `caption`**: the "Step 2 of 6" line above the title.
|
|
697
|
+
- **`onBack` in focus mode is a text Back link at the top** ("‹ Back", GOV.UK's pattern). Elsewhere it is the icon button, as before.
|
|
698
|
+
- **A Stepper in a header's tab row:**
|
|
699
|
+
- its first step lines up with the title;
|
|
700
|
+
- below the header's `sm` width it shows a compact bar, measured on the header, not the viewport;
|
|
701
|
+
- its words are for screen readers only when the header has a caption, so "Step 2 of 6" isn't said twice.
|
|
702
|
+
|
|
703
|
+
A Stepper outside a header is unchanged.
|
|
704
|
+
- **On a bold colourful header band,** the tab row sits on a surface card, so Tabs and the Stepper stay readable.
|
|
705
|
+
- **`chevron-left`** joins the curated icons in all five packs.
|
|
706
|
+
|
|
707
|
+
**KanbanBoard**
|
|
708
|
+
- **An empty column takes cards.** Every column's list is a drop area.
|
|
709
|
+
- **`onCardMove(card, toColumnId, index)`** (new) reports a move, into an empty column or a reorder, once, on drop. A drop where the card started reports nothing.
|
|
710
|
+
- **`readOnly`** (new): no dragging and no grab cursor. A card that opens is a button for Enter and Space.
|
|
711
|
+
- **Keyboard drag:** Left and Right move column to column, to the top of the next column's list. Before, every other arrow press did nothing.
|
|
712
|
+
- The lifted card no longer tilts, and the card left behind no longer grows.
|
|
713
|
+
|
|
714
|
+
**SplitView**
|
|
715
|
+
- **`closedDetail`** (`"shown"`, the default, or `"hidden"`) and **`closeLabel`** (default "Close").
|
|
716
|
+
- With `"hidden"`, the detail pane opens beside the list only while `detailOpen` is true, at every width. Closed, the list takes the full width.
|
|
717
|
+
- The pane reopens at the width the user last gave it. Its close control, or Escape inside it, calls `onDetailClose` and returns focus to the item that opened it.
|
|
718
|
+
- The list stays mounted, so a board or timeline keeps its scroll.
|
|
719
|
+
- The default is unchanged, so an existing app looks the same.
|
|
720
|
+
|
|
721
|
+
**Charts**
|
|
722
|
+
- **One stacking API.** `BarDef` gains `stackId` (bars with the same id stack). BarChart's `stacked`, and a new `stacked` on CompositeChart, put every bar without one into a single stack. CompositeChart's lines draw over its stacks.
|
|
723
|
+
- **Only the segment on top of each column is rounded,** worked out per column, so a series that is zero in one column hands the rounding to the segment below it.
|
|
724
|
+
- **`ChartContainer height="fill"`** fills its row, with the small chart height as its minimum, so a chart beside a table is as tall as the table.
|
|
725
|
+
- **Soft bars get a 1 px outline in their solid colour** (BarChart `soft`, and every CompositeChart bar), because the 30% tints alone measured 1.3–1.4:1 against a dark card (WCAG 1.4.11 asks for 3:1). The legend swatch shows the series' fill inside that outline.
|
|
726
|
+
- **The series colours show at 3:1 on the mode's surfaces:**
|
|
727
|
+
- Each palette colour gains `--ds-palette-{n}-chart`: the bold in light mode, and lightness 0.66 in dark mode, which clears 3:1 at every hue on the card, the page and the raised surface. Before, reds and magentas measured 2.9:1 on a dark card.
|
|
728
|
+
- `--ds-chart-2`…`6` draw in it, and `roots.css`'s defaults fall back through it to the bold.
|
|
729
|
+
- `--ds-chart-1` is the accent when the accent's shade shows (500 or darker on light, 400 or lighter on dark), else that nearest step.
|
|
730
|
+
|
|
731
|
+
**Forms: SliderRange, Field, FileUpload**
|
|
732
|
+
- **Field gives its label an id** and lets a control inside take its label, hint and error, for controls that `<label for>` cannot name.
|
|
733
|
+
- **SliderRange:**
|
|
734
|
+
- inside a Field, it takes the Field's label (`aria-labelledby`) and shows none of its own;
|
|
735
|
+
- `aria-label` (new) names a slider whose label is drawn elsewhere, and shows nothing;
|
|
736
|
+
- `aria-describedby` (new) adds descriptions;
|
|
737
|
+
- a range announces "<name> minimum" and "<name> maximum".
|
|
738
|
+
- **FileUpload:**
|
|
739
|
+
- **Copy:** one action line for any device ("Drop files here or browse", or "Drop a file here or browse" for one file), and one constraints line in words from `accept` and `maxSize` ("CSV files up to 5 MB"). A zone with no limits has no constraints line (was "Any file type").
|
|
740
|
+
- **Naming:** the file input now has a name, which it had none of before. Inside a Field it takes the Field's label, hint and error, and draws no label of its own. Outside one, `label` names it, and `id` (new) is the input's id.
|
|
741
|
+
- **Turned-away files:** the message under the zone uses GOV.UK's wording ("The selected file must be a CSV", "The selected file must be smaller than 5 MB"), and several files name each one turned away. A format matches by extension or exact type, so a CSV that Windows reports as an Excel file still counts.
|
|
742
|
+
- **Look:** the zone's own label uses the form label style. An invalid zone keeps the error focus ring, and the drag highlight shows over it.
|
|
743
|
+
|
|
744
|
+
**The sprint's review** (a fresh-eyes review of all of Sprint 25.7, with every fix tested):
|
|
745
|
+
- **Focus:**
|
|
746
|
+
- **Rows:** focus comes back to the right row after a list changes.
|
|
747
|
+
- Rows with `data-row-key` are told apart by it.
|
|
748
|
+
- An edit is no longer mistaken for a deletion.
|
|
749
|
+
- A row deleted a moment later (a server round trip) sends focus to its neighbour.
|
|
750
|
+
- A new query string or hash is no longer a page change.
|
|
751
|
+
- **SidebarNav's compact flyout:** hovering it no longer moves keyboard focus.
|
|
752
|
+
- **Combobox:** Tab closes its list inside a modal, so the Dialog's trap holds.
|
|
753
|
+
- **MentionInput:** it takes the top layer only while suggestions show.
|
|
754
|
+
- **Pickers:**
|
|
755
|
+
- DateRangePicker's month buttons keep the day grid's Tab stop.
|
|
756
|
+
- A removal the app refused (Dropdown chips, TagInput) no longer steals focus later.
|
|
757
|
+
- **Escape everywhere else:** DropdownMenu, TimePicker, Calendar, Breadcrumbs, ContextMenu, RichTextEditor's popovers, CommandPalette and Tour take an Escape only when they are on top and nothing has handled it yet. The compact calendar's day preview is now a layer.
|
|
758
|
+
- **Listeners and panels:**
|
|
759
|
+
- Popover, NotificationCenter, CommandPalette and Tour register their listeners once.
|
|
760
|
+
- A non-modal panel pulls focus back only when focus was lost or was inside it.
|
|
761
|
+
- NotificationCenter's panel is a dialog named "Notifications", and its bell carries `aria-haspopup` and `aria-expanded`.
|
|
762
|
+
- **SplitView with `closedDetail="hidden"`:**
|
|
763
|
+
- Closing the pane returns focus to the item it showed (the list's selected item, else the one used last), even when the selection lives in the URL.
|
|
764
|
+
- The aside, its toggle and its Drawer show only while the pane is open.
|
|
765
|
+
- **KanbanBoard:**
|
|
766
|
+
- A `columns` change that arrives while a card is held shows at once and is kept on drop.
|
|
767
|
+
- The screen-reader announcements use card and column titles, never ids, and the instructions name the keys.
|
|
768
|
+
- **Navbar:** the unsaved-changes question no longer reopens in the next flow.
|
|
769
|
+
- **Charts:**
|
|
770
|
+
- Stacked bars put negative values below zero (`stackOffset="sign"`) and round each side's end.
|
|
771
|
+
- A stacked CompositeChart draws its lines on the bars' axis by default (`dualAxis` defaults to off once bars stack).
|
|
772
|
+
- **FileUpload:** `accept` takes two-part endings (`.tar.gz`) and `*` or `*/*`, and the words list types and formats in one list ("Images, PDF or DOCX files").
|
|
773
|
+
- **Card:**
|
|
774
|
+
- the `checkable` checkbox is named by the card's title;
|
|
775
|
+
- CardHeader's menu trigger is named "More actions";
|
|
776
|
+
- a disabled clickable card no longer responds.
|
|
777
|
+
- **Skeleton:** a `data-gs="skeleton"` marker. The bench waits for skeletons before it calls a page blank. Nothing changes on screen.
|
|
778
|
+
|
|
779
|
+
**Written as 0.15.1 before Part C:**
|
|
780
|
+
|
|
781
|
+
**A list inside a Popover works with the mouse (found after Sprint 25.7 Part A).** Before, a press on an option in a Dropdown inside a Popover closed the Popover first. That removed the list before the click could choose anything, so a filter Popover with a Dropdown in it could not be used with the mouse. The same was true for a date picker or any other popup opened from inside a Popover.
|
|
782
|
+
|
|
783
|
+
- **Only the topmost popup reacts to a press outside it**, as Escape already does. A press in a popup opened from a Popover belongs to that popup. A press outside everything closes the innermost popup first, and the next press closes the Popover.
|
|
784
|
+
- **Focus comes back after a mouse choice.** After choosing in such a list by mouse, closing the Popover from its own button with the keyboard hands focus back to the Popover's trigger.
|
|
785
|
+
- **PageHeader and Stepper carry markers:** `data-gs="page-header"` and `data-gs="stepper"`. The bench's blank-page check reads them to tell a page's own content from its header and its steps (Sprint 25.7 B10). Nothing changes on screen.
|
|
786
|
+
|
|
787
|
+
## 0.15.0 — 25 September 2026
|
|
788
|
+
|
|
789
|
+
**Sprint 25.7, Part A: the keyboard works in every popup (the owner's report, 24 September 2026).** Choosing from a list, a menu or a date picker by keyboard puts focus back where the user was, and the next Tab carries on from there. Before, focus fell to the page, and the next Tab started at the top of the page or at its end, where popups are rendered. A test suite in real Chrome now covers every popup, and a guard fails when a new popup ships without a keyboard test.
|
|
790
|
+
|
|
791
|
+
- **Focus returns to the control that opened a popup.** Choosing, Escape, and a button inside that closes the popup all return focus to its trigger. This covers Dropdown, DropdownMenu, NavMenu, Popover, Combobox, DatePicker, DateRangePicker, TimePicker, NotificationCenter, Calendar's month and year pickers and "+N more", SidebarNav's compact flyout, and the RichTextEditor's colour, link and image popovers. A focus move the app makes in the same handler still wins, and a choice that opens another page leaves focus to that page.
|
|
792
|
+
- **Tab in an open popup moves on.**
|
|
793
|
+
- **Lists:** in a Dropdown or DropdownMenu, Tab closes the list and moves to the next field.
|
|
794
|
+
- **Picking with Tab:** in a Dropdown or Combobox, Tab picks the option the user moved to with the arrow keys, as GOV.UK's autocomplete and the WAI-ARIA select pattern do. It changes nothing when they didn't move.
|
|
795
|
+
- **Panels:** in a Popover, the pickers and NotificationCenter, Tab moves through the panel's controls and leaves after the last.
|
|
796
|
+
- Shift+Tab from the first control goes back to the trigger. Inside a modal Drawer or Dialog, Tab never escapes to the page behind.
|
|
797
|
+
- **The date pickers follow the standard calendar pattern.**
|
|
798
|
+
- **One Tab stop:** the day grid is a single Tab stop.
|
|
799
|
+
- The arrow keys move by day and week.
|
|
800
|
+
- Home and End go to the ends of the week.
|
|
801
|
+
- Page Up and Page Down move by a month, or by a year with Shift.
|
|
802
|
+
- **Opening and choosing:** opening focuses the chosen day, else today. Choosing a day returns focus to the calendar button.
|
|
803
|
+
- **Announced as a grid:** the grid is announced as a grid with its day names, and days carry `aria-selected` and `aria-current="date"`. Tests that found a day with `getByRole("button", …)` now find it with `getByRole("gridcell", …)`.
|
|
804
|
+
- **TimePicker:** each column is one Tab stop.
|
|
805
|
+
- **Removing something moves focus to its neighbour.** This covers a chip, a tag, or a row deleted by its own menu or by a confirmation the menu opened. Focus goes to the item before, else the item after, else the field or list (the order MOJ's "Add another" uses). It holds for lists that key their rows by position, as the library's Table does, and for paged lists. A row edited in place keeps focus.
|
|
806
|
+
- **Dialog, Drawer, Sheet, CommandPalette and Tour give focus back when they close,** including when a field inside took focus with `autoFocus`.
|
|
807
|
+
- CommandPalette keeps Tab inside it and announces the highlighted command.
|
|
808
|
+
- Tour is a modal dialog, with focus on Next.
|
|
809
|
+
- **An interactive Card with `onClick` works from the keyboard.** It is a Tab stop with a focus ring, and Enter or Space activates it.
|
|
810
|
+
- **SidebarNav's compact rail opens from the keyboard.** ArrowRight on a tile with children opens its flyout. The Up and Down arrows move through the links, and Escape or ArrowLeft goes back to the tile.
|
|
811
|
+
- **Fixed on the way:**
|
|
812
|
+
- **DropdownMenu:** the arrow keys could not reach items after a divider, and Enter chose the wrong item. This affected every shell's user menu (Settings, divider, Log out).
|
|
813
|
+
- **Dropdown:** the list took focus before it was visible, so focus never entered it.
|
|
814
|
+
- **Combobox:** a long value's tooltip no longer rebuilds the field, which lost focus while typing in a narrow field. The highlighted option is announced to screen readers.
|
|
815
|
+
- **Click containment:** a click inside a Dropdown, DropdownMenu or Popover panel no longer also reaches a clickable row or card that the trigger sits in.
|
|
816
|
+
- **Tag:** the remove button is labelled "Remove {label}".
|
|
817
|
+
- **Tooltip:** gains `disabled`.
|
|
818
|
+
- **Slot:** no longer triggers React 19's `element.ref` warning.
|
|
819
|
+
|
|
820
|
+
## 0.14.0 — 24 September 2026
|
|
821
|
+
|
|
822
|
+
**The review of round 22 (23 September 2026).** Every overlay that can hold a form now protects it, a form's own Cancel handler runs inside an overlay, and a few edges the round left are closed.
|
|
823
|
+
|
|
824
|
+
- **`Sheet` gains `dirty` and `onDiscard`.** With unsaved changes, every close path asks first, inside the sheet: Escape, the backdrop, dragging it down, the close control and a FormWrapper's Cancel. Drawer and Dialog already did this. Without `dirty` a Sheet closes at once, as before, and a Drawer's phone sheet keeps the Drawer's own question.
|
|
825
|
+
- **FormWrapper's Cancel runs `onCancel` inside an overlay.** It closes the Drawer, Dialog or guarded Sheet through its unsaved-changes question, then runs `onCancel`, unless that is the overlay's own `onClose`. When the user keeps editing, nothing runs. Before, `onCancel` never ran inside an overlay.
|
|
826
|
+
- **The count line takes the pager's total** when a page passes both `resultCount` and `pagination`, so the range and the total always come from the same list.
|
|
827
|
+
- **Timeline's icon slot holds any icon at 2rem**, so an Avatar left at its default size sits on the line like a small one.
|
|
828
|
+
- **Infinite scroll measures its trigger correctly on pages where the window scrolls.** The window's scroll was counted twice; nothing changed on screen, because a scroll also re-arms the trigger.
|
|
829
|
+
- **The showcase examples place the pager straight after the table**, as the guidance teaches. The Dashboard example, whose list switches between a table, a card grid and a stacked list, gives the pager `className="mt-ds-4"`.
|
|
830
|
+
|
|
831
|
+
## 0.13.0 — 23 September 2026
|
|
832
|
+
|
|
833
|
+
**Round 22 — the owner's review of bench runs 59 and 60 (T3b and T3c).** One hook shows more of a long list in the project's style — page numbers, a Load more button or loading as the user scrolls — read from the shell, so a rule change reaches every list with no page edited. The Timeline shows each item's icon, and a pager keeps its distance from the list above it.
|
|
834
|
+
|
|
835
|
+
- **`AppShell` `preferences` gains `paginationStyle`** (`"pagination"` | `"load-more"` | `"infinite-scroll"`, the project's `dataDisplay.paginationStyle`; page numbers outside a shell and when the shell leaves it out). `usePreferences()` returns it with the other fields.
|
|
836
|
+
- **`usePagination` follows the list style.** Under `load-more` and `infinite-scroll`, `pager.items` holds every row loaded so far and `pager.loadMore()` adds the next page; the pager also gives `style`, `shown` and `hasMore`, and `pager.props` now carries `mode`, `hasMore` and `onLoadMore`, so `<Pagination {...pager.props} />` renders whichever the project uses. `usePagination(list, { style })` sets one list's style over the project's: the override path, with its `// @gridsmith-override dataDisplay.paginationStyle: <reason>` note.
|
|
837
|
+
- **`Pagination` gains `mode`, `hasMore`, `onLoadMore` and `loadMoreLabel`.** `load-more` renders a secondary "Load more" button while rows are left; `infinite-scroll` loads the next page as the end of the list comes within 200 px of view and keeps the button for keyboard users. After a load it loads again only when the list grew or the user has scrolled, so a list shown as a loading skeleton does not load every page at once. A bare `Pagination` with no `mode` keeps its page numbers.
|
|
838
|
+
- **`TableToolbar` counts a growing list.** With the pager, the count line reads "Showing 40 of 120 members" while a list grows, and "120 members" once everything is showing; paged lists keep "Showing 51–100 of 200 events".
|
|
839
|
+
- **`Timeline` draws an item's `icon` where the dot was** — the actor's `Avatar`, typically — in both orientations, with the title centred on it. The icon was accepted and never shown before; at least three earlier bench runs passed avatars that never appeared.
|
|
840
|
+
- **A pager straight after its list sits a step below it.** `Table`, `DataGrid`, `StackedList` and `Timeline` mark their root (`data-ds-list`) and `Pagination` its own (`data-ds-pager`); roots.css puts `var(--ds-space-4)` between the pair. A pager inside a wrapper or a row of its own is spaced by that row, as before.
|
|
841
|
+
|
|
842
|
+
## 0.12.0 — 23 September 2026
|
|
843
|
+
|
|
844
|
+
0.11.0 was already taken on npm, so the round-21 release below shipped as 0.12.0.
|
|
845
|
+
|
|
846
|
+
**Round 21 — the owner's review of bench run 58 (T3).** A paged list pages itself at the project's page size and says which rows are showing; the avatar, the toolbar and the toasts absorb what the run got wrong; Cancel asks before throwing a form away.
|
|
847
|
+
|
|
848
|
+
- **`usePagination` (new).** `usePagination(list, { resetOn })` pages a list at the project's `dataDisplay.itemsPerPage`, read from the shell (the house 25 outside one; `pageSize` fixes it for one list): `pager.items` is the page's slice, `<Pagination {...pager.props} />` takes the rest, and a new list — another sort, a search, a filter — starts again at page 1. A mid-build `update_rule` reaches every paged list with no page edited.
|
|
849
|
+
- **`TableToolbar` gains `pagination`.** Pass the pager and the count says which rows are showing — "Showing 51–100 of 200 events" — instead of a bare total the table does not show (the owner's call); one page reads "12 events". Under tabs that carry the counts pass the pager alone: the line shows only the range, and only while the list spans pages. The toolbar renders no empty controls row when it holds only a count, owns 12 px of space below it (`last:mb-0`), and renders nothing when it has nothing to say.
|
|
850
|
+
- **`Avatar` and `EntityCell`: a name passed as `initials` shows its initials.** "Amara Haddad" → "AH" (run 58 printed the full name across the circle on every row); one to three letters with no space stay as given.
|
|
851
|
+
- **Top toasts clear the app's top bar.** The Navbar of a shell that fills the window claims a new `--ds-toast-inset-top` down to its own bottom edge, the way a Launcher claims its bottom corner; `top-right`, `top-left` and `top-center` read it (`var(--ds-space-4)` otherwise, as before). A toast no longer covers the user menu for its whole duration.
|
|
852
|
+
- **`FormWrapper` gains `onCancel` and `cancelLabel`.** It renders a secondary Cancel before the actions; inside a Drawer, Sheet or Dialog the Cancel closes through the overlay's unsaved-changes question — the one Escape and the close control already ask — and elsewhere `onCancel` runs. Drawer and Dialog provide their guarded close to the content (`OverlayCloseCtx`).
|
|
853
|
+
- **`usePreferences()` fills every field** (the shell's value, else the house default: `ResolvedPreferences`), so app code never writes a fallback of its own; components read only what the shell passed, so a bare component keeps the default it always had.
|
|
854
|
+
|
|
855
|
+
## 0.10.0 — 23 September 2026
|
|
856
|
+
|
|
857
|
+
**Sprint 25.6 — the customer's preferences live in the shell.** The generated shell passes `preferences={…}` to `AppShell` once, the way it passes the theme, and the components read it: a page gets the customer's value by doing nothing, and an explicit prop is the only thing left to check.
|
|
858
|
+
|
|
859
|
+
- **`AppShell` gains `preferences`** (`AppShellPreferences`: validation timing, form actions, items per page, table-to-cards breakpoint, empty style, loading style, destructive mode, the high-severity and type-to-confirm lists, the undo window), exposed on `useAppShell().preferences` and through **`usePreferences()`**, which returns an empty object outside any shell.
|
|
860
|
+
- **`FormWrapper`** defaults `validationMode` and `actionsAlign` from the shell (onSubmit and end outside one, as before). **`Table` and `DataGrid`** default `mobileBreakpoint` from the shell (md outside one). **`Pagination`** defaults the page-size selector's `pageSize` from the shell; app code reads the same number with `usePreferences().itemsPerPage`.
|
|
861
|
+
- **`EmptyState` gains `style`** (`illustration` | `text`), read from the shell's `emptyStyle` when not passed: `text` renders the title, description and action with no icon frame; a `redirect` preference reads as `text` here (the page decides to navigate). The root carries `data-style`.
|
|
862
|
+
- **`useToastOptional` (new).** The toast API when a ToastProvider is above, else null; `useDestructiveAction` uses it, so dialog mode works with no provider and undo mode falls back to the dialog.
|
|
863
|
+
- **`useDestructiveAction` (new).** The destructive safety net as configured: `run()` opens a destructive Dialog whose confirm performs the action (an input the user must match for actions on the type-to-confirm list), or, in undo mode, performs it at once and shows a toast with an Undo action for the configured window; high-severity actions always confirm. Render the returned `element` once on the page; call `run()` from a button or a menu item.
|
|
864
|
+
|
|
865
|
+
## 0.9.1 — 18 September 2026
|
|
866
|
+
|
|
867
|
+
- **`SplitView` moved to `gridsmith-ui/resizable`.** SplitView composes ResizablePanelGroup, so it pulls in `react-resizable-panels` — the same peer the resizable entry point exists to isolate. Import it from `gridsmith-ui/resizable` alongside the panel primitives; `react-resizable-panels` is the only install needed.
|
|
868
|
+
|
|
869
|
+
## 0.9.0 — 16 September 2026
|
|
870
|
+
|
|
871
|
+
**Sprint 25.3 — house rules that just work (the owner's review of bench round 4).** Controls share one height, tables read their own rows, the reading pane knows which page it is, forms put their buttons where the surface expects them, and the colourful overlap is the container's job. No new theme decisions.
|
|
872
|
+
|
|
873
|
+
- **Control heights (breaking for hand-sized controls).** `--ds-control-height-sm/md/lg` are now `calc()` on `--ds-density` with `max()` floors: 32 / 40 / 48 at the default density, 28 / 36 / 44 compact, 36 / 44 / 52 comfortable. Button (including `icon-*`), Input, Textarea (min-height), Dropdown, Combobox, DatePicker, DateRangePicker, TimePicker, NumberInput, PinInput, SegmentedControl (container, segments, thumb inset) and Pagination read them; NumberInput, SegmentedControl's segments and Pagination's "Go" lose their hard-coded heights and move at the default density too. Input, Dropdown, Combobox, DatePicker, DateRangePicker and TimePicker gain `size` (`sm` | `md` | `lg`). `ButtonSizeContext` becomes `ControlSizeContext` (the old name stays as an alias): `TableToolbar` and the PageHeader `filters` slot pass one size to every control inside; an explicit `size` wins. Button's heights leave the spacing scale, so a preset with a non-4 px space unit no longer moves Button on its own.
|
|
874
|
+
- **`Table` and `DataGrid` infer column kinds** from the rows (dates, numbers, emails, short labels, free text; at most fifty rows sampled; a JSX value gives no kind): dates and numbers never wrap, numbers right-align (header included), emails truncate with a title, free text takes the slack. `kind` on a column (DataGrid: `meta.kind`) overrides. Card mode: labels `shrink-0`, values `min-w-0` with `overflow-wrap: anywhere`.
|
|
875
|
+
- **`EntityCell` (new).** Avatar + primary + secondary line for people, companies and accounts in tables and lists; stacks below `sm`.
|
|
876
|
+
- **`StackedList`** stacks the meta under the title below `sm` instead of truncating it. **`SectionHeading`** owns its space below (`--ds-heading-gap`) and, with `actions` on a phone, drops the action under the description — call sites add no margin of their own any more.
|
|
877
|
+
- **`SplitView` gains `frame`.** `fill`: the page root, flush to the shell, `h-full` against `<main>`, never bordered; `page` (default): the 0.8.0 surface inside a PageContainer under a PageHeader. Each pane carries `data-pane`.
|
|
878
|
+
- **`PageContainer` derives the colourful overlap.** `AppShellTheme.pageHeader.overlap` (the existing `pageHeaderOverlap` decision, now plumbed through `resolveShellProps`) turns it on; the container measures the first content block (half its height, floor 24 px, cap 64 px, one level into a bare wrapper), writes `--ds-overlap` on its root, and both the header's bottom padding and the content's pull-up read it. **`overlapHeight` is removed from `PageHeaderProps`** — pages carry no numbers.
|
|
879
|
+
- **`PageContainer` clears the assistant launcher.** With a Launcher in a bottom corner (`AppShell` context `launcherCorner`), the content column's bottom padding becomes `max(its own, the corner's toast-inset variable)`, so the last row clears the bubble at scroll end.
|
|
880
|
+
- **`FormWrapper` follows the surface.** Inside a Card the actions row takes CardFooter's styling (a rule above, right-aligned) — add no CardFooter; inside a Drawer or Sheet the buttons render in the overlay footer, each stamped `form={id}` (fragments flattened) so submission works from there, an explicit `footer` wins; inside a Dialog the row stays inside the form. Card exports `CardSurfaceCtx`; Drawer exports `DrawerFooterCtx`.
|
|
881
|
+
- **`Badge` gains `severity`** (`info` | `low` | `medium` | `high` | `critical` → neutral, success-soft, warning-soft, error-soft, error); `severity` wins over `variant`, `color` wins over both.
|
|
882
|
+
- **Charts.** The shared x-axis tick is a plain `<text>` with no width box: Recharts neither wraps nor truncates it; `interval` with `preserveStartEnd` thins colliding ticks.
|
|
883
|
+
- **Sidebar rail labels** wrap to two lines (`line-clamp-2`, centred) before the hover tooltip; the rail width is fixed. **Vertical Tabs and SettingsLayout** lose the track line (`--ds-nav-v-track-border` is `0px` for the rail style; set it back by CSS to restore it).
|
|
884
|
+
- **PageHeader `filters` slot** lays its children out: one row, wrapping only when they do not fit, every control `sm`.
|
|
885
|
+
- **Showcase.** The inbox example is the `fill` reference, the new tickets example (`TicketsPage`) the `page` reference with the detail closed on load; the control-height gallery shows every control at each density; the table showcase adds a contacts table with inferred kinds.
|
|
886
|
+
|
|
887
|
+
## 0.8.0 — 15 September 2026
|
|
888
|
+
|
|
889
|
+
**Sprint 25.2 — the owner's review of bench round 3.** Two compositions become components; the components own the behaviour the builders kept getting wrong.
|
|
890
|
+
|
|
891
|
+
- **`SettingsLayout` (new).** The settings page: a sticky rail of sections beside the panel or horizontal tabs above it, following the shell theme's `settingsLayout` (the customer's settings.layout preference); below the width the rail needs (measured on the layout, not the viewport) the rail is a Dropdown above the panel. `AppShellTheme` gains `settingsLayout`.
|
|
892
|
+
- **`SplitView` (new).** The reading pane: list, detail and an optional aside on one surface with draggable dividers (it composes `ResizablePanelGroup`), filling the height it is given; below the collapse width the list shows alone and the detail stacks over it with a Back button (`detailOpen`, `onDetailClose`; `detailMode="route"` for an app that navigates instead); the aside collapses to a toggle that opens a Drawer.
|
|
893
|
+
- **`PageContainer` publishes `--ds-sticky-offset`.** A pinned, collapsed snapping `PageHeader` reports its bar's height to the container, which writes it as a CSS variable so sticky content (SettingsLayout's rail) starts below the bar.
|
|
894
|
+
- **`SegmentedControl`** renders a `Dropdown` above `maxOptions` (default 6) with the same value, onChange and options.
|
|
895
|
+
- **`Timeline`** horizontal items never squeeze below `minItemWidth` (200px): the row scrolls sideways and snaps; `orientation="responsive"` measures the timeline and goes horizontal only when the items fit.
|
|
896
|
+
- **`FormWrapper`** gains `actions` (the buttons row with a clear gap above the last field, right-aligned), `focusFirstError` (on) and `errorSummary` (on): after a failed submit the first invalid field is focused and, when two or more fail, a summary above the fields links to each. Both read the Fields' `aria-invalid`, ids and messages from the DOM.
|
|
897
|
+
- **`Drawer` and `Dialog`** gain `dirty` and `onDiscard`: while dirty, Escape, the backdrop and the close control ask inside the overlay before discarding.
|
|
898
|
+
- **`StatsCard`** trend gains `sentiment` ("positive" | "negative" | "neutral"): the arrow is the direction, the colour is the sentiment, so a falling cost can be green.
|
|
899
|
+
- **`StackedList`** gains `hover="title"` for article lists. **`CardTitle`** clamps at two lines by default (`lines`). **`EmptyState`** centres its title. **`DescriptionList`** with more than one column stacks below `sm` by default (`stackBelow="none"` opts out). **`CodeBlock`** wraps a single-line snippet (`wrap`). Chart x-axis ticks are one line and thinned rather than wrapped.
|
|
900
|
+
- **Showcase.** The settings and inbox examples are the compositions above (the builder preview renders the same files); the timeline and form showcases demonstrate the fit test and the failed submit.
|
|
901
|
+
|
|
902
|
+
## 0.7.0 — 14 September 2026
|
|
903
|
+
|
|
904
|
+
**Sprint 25 — the theme in the shell.** A page is `PageContainer` + `PageHeader title` and carries no layout flags.
|
|
905
|
+
|
|
906
|
+
- **`AppShell` gains `theme`, `currentPath` and `homePath`.** The generated shell passes the theme's header decisions once (variant, snapping, floating position, intensity, colourful-on-home-only, breadcrumb position / style / separator) and tells the shell which route is home. `useAppShell()` exposes `theme`, `isHome`, `navbarFloating`, `sidebarFloating` and `registerChrome`.
|
|
907
|
+
- **`Sidebar` and `Navbar` report their position** to the shell, so `navbarFloating` and `sidebarFloating` describe the chrome that is actually rendered. Their props still win where a component is used outside a shell.
|
|
908
|
+
- **`PageHeader` reads the theme.** Variant, behaviour, position, intensity, breadcrumb style and separator, and the floating-chrome flags come from the shell when the page passes none; a page's own prop still wins. A floating sidebar's inset is dropped below md, where the sidebar is a drawer. Inside a shell a pinned collapsed snapping header sticks to the top of the scrolling `main`.
|
|
909
|
+
- **`PageContainer` composes the page.** With a `PageHeader` among its direct children it drops its top padding, keeps the header inside its column for the default, minimal and tabs-integrated variants, lifts colourful, snapping and floating headers above the column and centres them to its width, renders contained breadcrumbs as a strip above the header (inside the header's card when snapping and floating), insets the content under a floating header, and pulls the content up for a colourful overlap. `renderHeader` wraps the header for previews. Without a header nothing changes. `contentMaxWidth`, `navbarFloating` and `sidebarFloating` no longer need passing; `verticalPadding` is for pages without a header.
|
|
910
|
+
- **`AsidePanel`** reads `navbarFloating` / `sidebarFloating` from the shell when not passed.
|
|
911
|
+
- **Existing pages.** A page that already passed the flags renders the same; a page with a header inside a container that relied on the container's default top padding loses that gap, which is what the builder preview always showed.
|
|
912
|
+
|
|
913
|
+
**Sprint 24 — what the bench's Sonnet set found (14 September 2026).**
|
|
914
|
+
|
|
915
|
+
- **`EmptyState.icon` accepts an icon name** (`"filter"`) as well as an element; a name renders through `Icon` at the frame's size, an `Icon` element is resized to it. Builders had passed names into the ReactNode prop and React printed the word. The bordered state gains horizontal padding.
|
|
916
|
+
- **Phosphor weights stop at `bold`.** A numeric stroke weight above 2.25 used to select Phosphor's `fill` variant, whose glyphs are different shapes (a check became a check in a filled square). Themes with a heavy weight on Phosphor will see plain strokes now.
|
|
917
|
+
- **Page titles wrap to two lines** before truncating (with the tooltip on overflow); smaller headings and descriptions are unchanged.
|
|
918
|
+
- **`StatsCard` trend values truncate** with a tooltip instead of stretching the badge.
|
|
919
|
+
- **`AppShell` gains `footer`** (pinned below the content on short pages), **`skipLink`** (a `SkipLink` rendered first, on by default) and **`mainId`** (the `<main>` region's id, default `main`).
|
|
920
|
+
- **New `SkipLink`** and **new `TableToolbar`** — the progressive-disclosure toolbar (search, one inline filter, further filters behind a button, result count, removable chips, utility actions) as a component.
|
|
921
|
+
- **`Tooltip` can be a Slot child**: it forwards a merged ref and handlers to its trigger, so `<DropdownMenu trigger={<Tooltip …><Button/></Tooltip>}>` works.
|
|
922
|
+
|
|
923
|
+
**Toast defaults follow the UX corpus.** `ToastProvider` now shows one toast at a time (`maxVisible` default 1, was 3) for 5 seconds (`duration` default 5000 ms, was 3000; `Toast` alone likewise), matching the corpus rules that one toast is displayed at a time and a toast stays for at least five seconds. The Gridsmith preference defaults (`notifications.toastDuration`, `toastMaxVisible`) and the generated app shell carry the same values; a customer's configured preference still overrides them. Found by the Sprint 22 bench (round 1, 13 September 2026): the judge flagged the old defaults on every run.
|
|
924
|
+
|
|
925
|
+
## 0.6.7 — 13 September 2026
|
|
926
|
+
|
|
927
|
+
**Fix — production builds no longer require every icon pack (Vite 8).** Each icon pack module named its curated icons in a static import from its peer package. On Vite 8 (rolldown), a consumer's `vite build` failed with hundreds of `MISSING_EXPORT` errors for every optional pack that was not installed, because Vite substitutes an export-less stub for an absent optional peer and the bundler treats the missing names as fatal. Dev mode and `tsc` passed, so the failure only appeared at build time. Found by the Sprint 22 test bench's first run (13 September 2026). The pack modules now reach their peer only through a dynamic import and look the curated icons up by name at runtime; the missing-pack error message is unchanged. A consumer build test with the optional packs stubbed out guards the fix (`showcase/src/test/vite8-optional-peers-build.test.ts`). No API change.
|
|
928
|
+
|
|
929
|
+
**Breaking — navigation active styles are theme decisions (Sprint 20.8).** The *shape* of an active navigation item is now decided once per theme, not per page:
|
|
930
|
+
|
|
931
|
+
- `Tabs.variant` is **removed**. Horizontal tabs read `--ds-nav-h-*` and vertical/responsive tabs read `--ds-nav-v-*`; the theme's `navigation.inPageHorizontal` (`underline` · `tint` · `pill`) and `navigation.inPageVertical` (`rail` · `tint` · `pill`) decide the look. Delete the prop from call sites — the compliance engine flags it (`gs:tabs-variant-prop`). The old `variant="pill"` track fill is gone; the tokenised pill sits on a bare track.
|
|
932
|
+
- `Navbar` / `NavItem` / `NavMenu` / `MobileHeaderMenu` `type="default"` is renamed **`tint`**; `default` is accepted as a deprecated alias for one release. New **`pill`** type: solid accent fill with on-primary text.
|
|
933
|
+
- `SidebarNav` reads `--ds-sidebar-active-*` (`sidebar.activeStyle`: `rail` · `tint` · `pill`; minimal and compact sidebars render `tint` for `rail`). **The active root label now uses the sidebar accent** — previously it stayed neutral while its icon took the accent, unlike every other surface. `TreeView` reads `--ds-tree-*`; a real `pill` is a full-width accent bar and the chevron follows `--ds-tree-active-muted`.
|
|
934
|
+
- 24 new tokens (`--ds-nav-h-*` ×9, `--ds-nav-v-*` ×7, `--ds-tree-active-*` ×4, `--ds-sidebar-active-*` ×4) with today's defaults in `roots.css`. Rails scale with the theme border width (`--ds-border-width-thick`): unchanged at a 1px base for the sidebar and tree, and the vertical Tabs rail moves from 2.5× to 2× so the three match, declared for `:root, [data-mode], [data-ds-overlay]` so they resolve inside nested mode wrappers and overlay portals (a `var()` alias is substituted where it is declared). Generated `tokens.css` carries the chosen styles the same way. A consumer with only `roots.css` renders exactly as before.
|
|
935
|
+
- `NavMenu`: under `tint`, the trigger with an active child and the active row inside the open menu use the accent-subtle fill with accent text (previously the neutral muted fill), matching an active `NavItem`.
|
|
936
|
+
- New compliance warning `gs:nav-active-style-handrolled` for active-conditional classNames that paint an accent fill or rail on a non-Button element.
|
|
937
|
+
|
|
938
|
+
**Internal: React Compiler-clean.** No API changes. Overlays and inputs no longer write refs or set state during effects in ways the React Compiler rejects; `useBreakpoint` and `useMediaQuery` now use `useSyncExternalStore`, so components that depend on them render correctly on the first pass instead of after an effect. `Sheet`/`Drawer` mount and close transitions are unchanged.
|
|
939
|
+
|
|
940
|
+
**Drawer becomes a Sheet on phones.** At phone width (≤ the `sm` breakpoint) a right or bottom `Drawer` now renders as a draggable bottom `Sheet` — drag handle, snap points (`snapPoints`, default `[0.5, 0.92]`), same title, footer, children, `container` and `flush`. Left drawers keep the full-height side panel (the navigation convention, so `AppShell`'s mobile menu is unchanged). New `Drawer` props: `mobile="sheet" | "panel"` to override the default per position, and `snapPoints`. `lightBackdrop` and `topOffset` do not apply in sheet mode. `Sheet` gains `container` and `flush`, and its `title` accepts a ReactNode. Consumers no longer need breakpoint code to get a sheet on phones; pass `mobile="panel"` to restore the previous behaviour.
|
|
941
|
+
|
|
942
|
+
**Tabs on mobile.** `Tabs` (array API) gains `orientation="responsive"` — vertical from the md breakpoint up, a `Dropdown` below it — and `mobile="dropdown"` for horizontal sets that shouldn't side-scroll. `Tab.group` renders section headings in the vertical rail and option groups in the Dropdown. The compliance engine now warns on `orientation="vertical"` (`gs:tabs-vertical-not-responsive`).
|
|
943
|
+
|
|
944
|
+
**Active items don't react to hover.** Codified as a navigation rule: Tabs, NavItem, NavMenu, SegmentedControl and Sidebar already exclude hover styling from the active item; the Settings examples' hand-rolled Button navigation (where the ghost variant's hover greyed out the active item) is replaced with responsive Tabs. New compliance warning `gs:nav-item-composed-from-button`.
|
|
945
|
+
|
|
946
|
+
**Avatar colours.** Initials avatars no longer default to the accent colour. `color="auto"` (the new default) derives a stable tint from the name/initials using the decorative palette (`--ds-palette-{n}-bg/-text`, with neutral fallbacks); `"accent"`, `"neutral"` and `1`–`5` are available explicitly. `AvatarGroupItem` accepts `color` too. The Projects example now uses `AvatarGroup` instead of a hand-rolled cluster.
|
|
947
|
+
|
|
948
|
+
**ChartContainer surface.** `ChartContainer` now renders on `--ds-bg-surface` like `Card` (it already had the border, radius and shadow). `variant="ghost"` drops surface, border, shadow and padding for charts embedded in another container.
|
|
949
|
+
|
|
950
|
+
**Active-state halo.** Every stroke-based active state — focused inputs, selected cards/rows/swatches, open picker triggers, the current Stepper step, drag-over FileUpload — now renders the same pair: a solid `--ds-border-focus` stroke plus a soft 2px `--ds-ring-active` halo (30% accent). Previously the input focus ring never rendered outside neumorphic themes (`--ds-input-shadow: none` invalidated Tailwind's composed `box-shadow`) and selection states used a 1px solid accent ring. New token `--ds-ring-active`; `--ds-input-focus-ring` now aliases it. `gridsmith-ui/utils` exports `ACTIVE_RING`, `FOCUS_RING`, `FOCUS_WITHIN_RING` and `HAS_FOCUS_RING` so consumers can apply the same grammar.
|
|
951
|
+
|
|
952
|
+
## 0.6.0 — 5 September 2026
|
|
953
|
+
|
|
954
|
+
**Breaking — five components moved to their own entry points.** The main `gridsmith-ui` entry used to bundle tiptap/ProseMirror, dnd-kit, TanStack Table/Virtual and react-resizable-panels (3.3 MB), and its shared chunks also imported `recharts`, so installing the package without those libraries could fail at import time. Each heavy component now lives in a subpath entry and its libraries are optional peer dependencies:
|
|
955
|
+
|
|
956
|
+
| Component | Old import | New import | Install |
|
|
957
|
+
|---|---|---|---|
|
|
958
|
+
| `RichTextEditor` | `gridsmith-ui` | `gridsmith-ui/editor` | `@tiptap/react @tiptap/pm @tiptap/starter-kit` + the `@tiptap/extension-*` packages listed in `peerDependencies`, `lowlight` |
|
|
959
|
+
| `KanbanBoard` | `gridsmith-ui` | `gridsmith-ui/kanban` | `@dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities` |
|
|
960
|
+
| `DataGrid` | `gridsmith-ui` | `gridsmith-ui/data-grid` | `@tanstack/react-table` |
|
|
961
|
+
| `VirtualizedList` | `gridsmith-ui` | `gridsmith-ui/virtualized` | `@tanstack/react-virtual` |
|
|
962
|
+
| `ResizablePanelGroup`, `ResizablePanel`, `ResizableHandle` | `gridsmith-ui` | `gridsmith-ui/resizable` | `react-resizable-panels` |
|
|
963
|
+
| chart components | `gridsmith-ui` (undocumented) | `gridsmith-ui/charts` (unchanged) | `recharts` |
|
|
964
|
+
|
|
965
|
+
Everything else is unchanged. `import { Button } from 'gridsmith-ui'` now works with only `react`, `react-dom` and `lucide-react` installed.
|
|
966
|
+
|
|
967
|
+
## 0.5.0
|
|
968
|
+
|
|
969
|
+
Unpublished internal build.
|