Skip to content

v0.6.13

Choose a tag to compare

@jaspertey jaspertey released this 30 Oct 03:07
c1d3a46

Upgrading

  • kit v0.6.10–v0.6.12 shipped unresolved conflict markers in Kit/Layout/Page.vue; re-run kit:update apply for core (re-pinned sha256:ce15e8de…e8dd → sha256:c97858e5…41da). The shipped file is a known prior version (history.json records it), so an untouched copy is replaced cleanly. A host that hand-resolved the markers sees its copy as edited: take the release bytes, or keep its resolution if it matches the one below.
  • A host that re-shares the kit's props in its browser test case after pest-plugin-browser's flush (a BrowserCase workaround) can drop that re-share: the kit puts its props back on every web request.
  • Run kit:update plan|apply for core (re-pinned sha256:f3932382…7dbe → sha256:6111274d…1cc8): Kit/Workbench/WorkbenchNavigator.vue, Kit/Workbench/Workbench.vue and Kit/Workbench/workbench.ts take the item menus below. An item without actions renders as before.
  • Run kit:update plan|apply for shell (sha256:56acbf30…3bb0 → sha256:efd6116b…ba5e): the Workspace Layout and its partials take the changes below, and FocusShell is new.
  • Run kit:update plan|apply for core (re-pinned sha256:6111274d…1cc8 → sha256:2646246d…77fe), shell (sha256:efd6116b…ba5e → sha256:6b2a9dac…5077), session-ui (sha256:be81f458…6f4f → sha256:71617f5d…3108) and workspace-ui (sha256:93e9b61a…7bed → sha256:2f226d38…39e7): css/kit.css adds the --destructive-text token, and every kit stub that coloured text with text-destructive now uses text-destructive-text (the menus' destructive items, Kit/Form/Repeater.vue's Remove item, Kit/Overlay/Dialog.vue's danger icon, the form and page error messages, Kit/Action/HeaderStatus.vue, Kit/Search/SearchBox.vue, Kit/Workbench/WorkbenchActions.vue, the login error, the workspace switcher's Leave item and the People page's expired date).
  • A host with its own theme (not importing kit.css) must define --destructive-text for light and dark; the kit's AA values are a starting point: hsl(0 72% 42%) light and hsl(0 91% 71%) dark. Until it does, the kit's theme host step reads unmet. A host theme's value wins over the kit's, as for the other tokens.
  • A host that builds navigator items on the server: send actions on the PHP WorkbenchItem (a list of Action) to get row menus, and regenerate or re-copy types/kit.d.ts, whose WorkbenchItem now has actions. A host that sends plain arrays can switch to WorkbenchItem unchanged.
  • Run kit:update plan|apply for core (re-pinned sha256:2646246d…77fe → sha256:e0090454…0845): Kit/Form/FormStatus.vue and Kit/contract.ts take the FormStatus fix below, and FormStatus takes an optional flashKey. Pages that pass no flashKey keep reading the status page prop.
  • An app that flashes its own confirmation with Inertia::flash() can show it with <FormStatus flash-key="notice" />: Inertia v3 keeps flash data at usePage().flash, not in page.props. Fortify's status stays a shared prop and needs no change.
  • Run kit:update plan|apply for core (re-pinned sha256:e0090454…0845 → sha256:93b89e36…51e0): Kit/Workbench/Workbench.vue takes the slot and the phone layout fix below.
  • Run kit:update plan|apply for core (re-pinned sha256:93b89e36…51e0 → sha256:ca1f9248…7179): Kit/http.ts is new, and Kit/Workbench/useWorkbench.ts, Kit/Workbench/workbenchLayout.ts and Kit/Feedback/passkeyOffer.ts take the fix below. A host whose own code reads response.data.x from Inertia v3's http client can read it through responseJson(response) from @/components/Kit/http instead.
  • Run kit:update plan|apply for core (re-pinned sha256:ca1f9248…7179 → sha256:ce15e8de…e8dd): Kit/Action/actionPlacement.ts, Kit/Action/ActionMenuItems.vue, Kit/Workbench/Workbench.vue, Kit/Workbench/WorkbenchNavigator.vue and Kit/Workbench/workbench.ts take the checkbox menu items and #navigator-item below. ActionMenuItems now imports DropdownMenuCheckboxItem from the host's shadcn-vue dropdown-menu (DataTable already did). A descriptor without checked and a Workbench without #navigator-item render as before.
  • No changes needed. A host that relied on its HandleInertiaRequests' position for the request memo, or that wraps a kit prop in its own share, keeps working. A workaround that moved kit middleware earlier (such as prependToPriorityList for PrepareKitRequest) can be removed.
  • Run kit:update plan|apply for core (re-pinned sha256:c97858e5…41da → sha256:12a237c5…e9f8): Kit/Form/InputCheckbox.vue, Kit/Form/Select.vue, Kit/Form/FileInput.vue, Kit/Form/RadioCardGroup.vue, Kit/Form/InputRichText.vue, Kit/Form/InputSelect.vue, Kit/Form/InputNumber.vue and Kit/contract.ts take the attribute fix below. InputCheckbox's row is now wrapped in a grid (for its description), so a host that styled its row by structure should check it.
  • Run kit:update plan|apply for core (re-pinned sha256:12a237c5…e9f8 → sha256:da818a98…6ef1): Kit/Layout/StickyBar.vue, Kit/Layout/stickyBars.ts, Kit/Workbench/Workbench.vue and Kit/Workbench/WorkbenchActions.vue take the fix below. StickyBar takes an optional pinned.
  • A host with its own home route or its own invitation flow can now keep it. Set kit.home_name (for example app.home) to rename the kit's home route, and set kit.routes.session-ui.invitations => false to leave out the kit's invitation link (transitional; see docs/catalog.md). Nothing changes by default.
  • Run kit:update plan|apply for core (re-pinned sha256:da818a98…6ef1 → sha256:fa5d0296…4815): Kit/Workbench/WorkbenchNavigator.vue, Kit/Workbench/Workbench.vue and Kit/Workbench/workbench.ts take the thumbnails and filmstrip layout below. An item without thumbnail in the default list layout renders as before. A host that copies types/kit.d.ts re-copies it: WorkbenchItem has thumbnail, and WorkbenchThumbnail is new.
  • Run kit:update plan|apply for core (re-pinned sha256:fa5d0296…4815 → sha256:ea1d2e42…3b8c): Kit/Form/SegmentedToggle.vue is new, and Kit/index.ts, Kit/contract.ts and Kit/Layout/PaneTabs.vue take the changes below. PaneTabs looks different: its pressed pane is no longer a primary-filled button but a neutral raised segment in a muted track. Its semantics, props and events are unchanged. A host that styled around the old primary button, or asserted its classes, should check its phone tabs.

Added

  • Workbench navigator thumbnails and a filmstrip layout (docs/workbench.md › Thumbnails and filmstrip). A navigator item takes an optional thumbnail ({ src, alt }, exported as WorkbenchThumbnail; alt is required, '' for a decorative image that repeats the label), rendered as a lazy, async-decoded <img> inside the item's role="option" row in a fixed-aspect box, so the row doesn't shift when it loads. navigator-layout="filmstrip" on the Workbench (layout on WorkbenchNavigator) lists the items as a Slides-like editor's left rail: the item's number, a 16:9 thumbnail (an empty placeholder box without one) and the label; list stays the default. #navigator-thumbnail ({ item, index, selected }, typed as WorkbenchNavigatorThumbnailSlotProps; thumbnail on WorkbenchNavigator) fills each filmstrip box with the host's own render, such as a scaled live slide, ahead of thumbnail.src and the placeholder. Its wrapper is inert and aria-hidden, and the box has contain: strict with overflow hidden, so the render can't take focus, change the option's name or move the layout. List rows take no thumbnail slot. The listbox semantics, keys, Alt+↑/↓ reorder, drag, filter, F6, #navigator-item (the label part) and the row menus beside the listbox are unchanged. The PHP WorkbenchItem takes thumbnail (a new WorkbenchThumbnail data class, default null) and types/kit.d.ts is regenerated; the MCP WorkbenchReadTool still gives exactly { id, label, type, description }, leaving the thumbnail out. composer test:workbench-item-parity covers the new key through the Inertia round trip, and the browser job checks a filmstrip: images inside the options with their alt, nothing focusable in the listbox, Alt+↓ reorder, filter and F6, axe clean with color-contrast, each thumbnail box sized before its image loads, and a live #navigator-thumbnail holding a button and a link: inside the box at the image box's size, never focused or tabbed to, the option named by its label only, axe clean, and Alt+↓ working.
  • Checkbox menu items (docs/workbench.md › Item menus). An action descriptor with checked (true or false) renders in ActionMenuItems, and so in every kit menu built on it including the Workbench row menu, as role="menuitemcheckbox" with aria-checked and the host's check indicator (shadcn-vue's DropdownMenuCheckboxItem). Choosing it emits the action as today; the host flips checked. Without checked, or with null, the row stays a plain menuitem. The PHP Action takes checked (nullable bool, default null), and types/kit.d.ts is regenerated. A row menu opened from the item focuses its first row, checkbox rows included.
  • Workbench #navigator-item slot (docs/workbench.md › Custom rows). The Workbench forwards WorkbenchNavigator's item slot as #navigator-item ({ item, selected, index }, typed as WorkbenchNavigatorItemSlotProps) in every layout: columns, the lg drawer layout, phone tabs and the overlay navigator. A host customises row content without replacing the navigator. The kit keeps the role="option" row, its keys and the row menus outside the listbox; the slot holds content only. WorkbenchNavigator now declares its item slot with defineSlots. The browser job checks a row menu with a checked and an unchecked item and a Workbench using #navigator-item, with axe clean on both.
  • SegmentedToggle, a neutral view switch (docs/segmented-toggle.md). Kit/Form/SegmentedToggle.vue picks one of a few views ("Slides | Outline", grid or list) with v-model, options ({ value, label, icon?, disabled? }), label and size. Its selected segment is a bordered bg-background surface in a bg-muted track, never brand-filled. It's a role="radiogroup" of role="radio" buttons with one tab stop: the arrows move and select, Home and End jump, and disabled options are skipped. The selected border keeps 3:1 against the track and the segment in light and dark. tests/js/segmented-toggle.test.mjs checks the source and the ratios; the browser job drives the keys, compares the selected background with the primary colour and runs axe with colour contrast, light and dark.
  • Workbench navigator item menus (docs/workbench.md › Item menus). A navigator item can carry its own kit action descriptors in actions (Move up, Move down, Duplicate, Delete, as the host decides per item), and gets a row menu: a button named "Actions for ", shown on hover and focus and always on a touch pointer, plus right-click, Shift+F10 or the context-menu key on the item, which open the same menu anchored to the row. A chosen descriptor is emitted as the Workbench's action event with the item as its second argument. Escape returns focus to the item, or to the button the menu opened from. The buttons sit in a column beside the listbox, never inside it, since a listbox may own only options. The listbox's arrows, filter and F6 are unchanged, and so is Alt+↑/↓ reordering. An item without actions has no button and keeps the browser's context menu. The browser job (Playwright with axe-core) checks a three-item Workbench: no axe violations, no focusable element inside the listbox, and the Shift+F10, right-click and Escape paths.
  • Server-side item menus: actions on the PHP WorkbenchItem (docs/workbench.md › Item menus). WorkbenchItem takes actions, a list of the kit's Action (default empty, no menu), and a WorkbenchViewModel sends them to the page unchanged; the generated type has actions: Action[]. The MCP WorkbenchReadTool still gives each item exactly { id, label, type, description }: row-menu actions are UI affordances, and no MCP tool exposes action descriptors. composer test:workbench-item-parity pins the PHP keys to the TS WorkbenchItem, the generated type, the read tool's keys and the Inertia round trip of Move up, Duplicate and Delete.
  • #navigator-actions on the Workbench (docs/workbench.md › Navigator actions). The app's own controls, such as "+ New slide", render first in the navigator, before its filter and list, at every width (columns, lg, phone tabs and the overlay sheet). So the visual and tab order match without order-first. With the slot empty, the navigator renders as before.
  • session-ui route groups and kit.home_name. kit.routes.session-ui.<group> => false switches off one group of session-ui's routes, and their names, at a time: invitations, welcome, passkey-offer or demo-login. Every group is on by default, and the kit's own redirects and props for a group that is off are guarded. An unknown key or a non-boolean value stops the boot with an error that names the groups. kit.home_name (KIT_HOME_NAME, default home) names the kit's home route, so an app that already owns home keeps it; the kit's breadcrumbs follow the new name. Checked by composer test:route-groups.

Fixed

  • laravel/mcp 1.0.0 and 1.0.1 broke every route load. The kit calls Mcp::oauthRoutes(registration: false), whose registration parameter arrived in laravel/mcp 1.0.2; on an older install every request failed with "Unknown named parameter $registration". The requirement is now laravel/mcp: ^1.0.2, the first release with every laravel/mcp API the kit uses.
  • The Workbench reorder kept a null version token after Inertia v3 returned a string body, so the next write got a 409. Inertia v3's HTTP client resolves data as the raw response text, and sendOrder read response.data.version from it, so after every reorder the token was lost and the next stale-safe write (another move, an add, a duplicate, a delete) was refused as stale_write. Bodies now go through core's new responseJson, which returns an object data as it is and parses a string as JSON, and throws on an empty or non-JSON body instead of handing back null (allowEmpty returns null where a caller expects no body); errorJson reads a refused request's body, as null when it isn't JSON, since the status has already decided. The same string-body bug lost the stored layout and version on a 409 from the Workbench layout endpoint (sendLayout), and the creation options in the passkey offer (registerPasskey); both read through it now. A 2xx reorder answer that isn't JSON is a failed save, so the old order comes back. tests/js/response-json.test.mjs covers the helper, workbench.test.mjs and onboarding.test.mjs string bodies, and a browser check makes two moves and a Duplicate against a stand-in server that refuses a stale or null version.
  • The kit's Inertia props survive Inertia::flushShared(). kitUi, kitToasts, shell, environment, kitPassword and passkeyOffer were shared once at provider boot, so anything that flushed Inertia's shared props afterwards dropped them: pest-plugin-browser flushes before each request, so a host's browser tests rendered blank pages, and an Octane-style reset or a host's own flush would too. Each provider now hands its closures to KitShares, which shares them at boot and again at the start of every web request (now first in web, see below). They stay lazy closures, one producer each. The host still wins: the kit puts back only the keys Inertia's factory doesn't already hold, and the host's own HandleInertiaRequests merges over them, so a key the host shares (in its middleware, or on the factory before the request) is never overwritten. The kit still never flushes. test:inertia-shares and test:request-hooks (through the real HTTP kernel) flush between requests and check every kit prop, the host's own shell and environment, and two people in turn.
  • Kit/Layout/Page.vue is a valid component again. A merge in v0.6.10 left <<<<<<<, ======= and >>>>>>> lines around the page header, and they shipped in v0.6.10, v0.6.11 and v0.6.12. The header is now the status and actions version (HeaderStatus, status, actions) with the font-display heading face, as both sides intended. A new CI check, composer test:conflict-markers, fails on any conflict marker (indented ones included) under resources/ or src/.
  • The kit's request start now runs first in web. PrepareKitRequest was appended, so it ran last: a host's own middleware earlier in web (a tenant resolver) read memberships before the request memo began, and repeated their queries; after an Inertia::flushShared() a host share wrapping a kit prop (Inertia::getShared('shell')) captured null, so the page fell back to the guest layout; and a route-model-binding 404 never reached it, so the 404 page had no kit props. session-ui now prepends a new StartKitRequest to web and to the middleware priority list. It begins the request's memos (the registry's new startRequestUsing hooks: the request memo and access's permission, capability and entitlement memos) and puts the kit's props back (KitShares::restore()), neither of which needs the session or the signed-in person. The session-dependent hook (the local brand switch) stays late in PrepareKitRequest, which also restores once more. The host still wins: restore only fills keys Inertia's factory doesn't hold, and the host's own HandleInertiaRequests merges over them. Each memo is scoped to its request, so nothing carries over. composer test:request-order (new, in CI) runs a host through the real HTTP kernel and checks that the host's second membership lookup adds no query, that the wrapped shell and a binding 404 after a flush both have the kit's shell, that a host shell share still wins, and that a membership removed between two requests isn't answered from the first's memo.
  • Kit form controls put your attributes on the control itself, not a wrapper. aria-describedby, aria-invalid, required, aria-label and data-* passed to InputCheckbox landed on its wrapper <div>; on Select they went nowhere (Reka's SelectRoot renders no element); FileInput dropped them; RadioCardGroup and InputNumber replaced or ignored your aria-describedby; InputRichText put them on its frame; and InputSelect dropped a passed required. Each now forwards them to its focusable control (the checkbox button, the select trigger, the file input, the radiogroup, the editable textbox, the combobox input), joins your aria-describedby with its own ids, de-duplicated, and sets aria-required where a native required doesn't apply. class and a style's custom properties stay on the wrapper. InputCheckbox, FileInput, RadioCardGroup and InputRichText take invalid (aria-invalid), and InputCheckbox takes description, shown under the label as <id>-description and joined to its aria-describedby.
  • A toast no longer covers a filling Workbench's actions bar. The bar is a StickyBar at the viewport's foot by layout, not by scroll, so it never counted as pinned and never registered with useStickyBars: a bottom toast sat on top of its buttons (Save) and took their clicks. StickyBar takes pinned for a bar the layout holds at the foot, and the Workbench passes it while it fills. useStickyBars().track() also re-measures when its enabled state changes, not only on resize, so a bar that becomes pinned by scrolling registers at once. FloatingToolbar (and BulkBar on it) already registered while floating.
  • Mail text is escaped exactly once. FluentMailMessage HTML-encoded user text and the markdown render encoded it again, so an apostrophe showed as a literal &#039; ("Ada's talk") in the HTML part, and the text part showed the entities and markdown backslashes (\*x\*). escape() now only backslash-escapes markdown punctuation and leaves HTML encoding to the render, and each block carries its text as given for the text part. <script> and <b> still show as inert text in every builder (headline, paragraph, facts, table, panel, callout, note, bullets, quote, section, button labels) in both layouts. A host that passed FluentMailMessage::escape() output to markdown() gets the same HTML; its text part shows that block's backslashes, as before.

Changed

  • layout.page reads hand-rolled pages in apps on the kit shell again. The ui.kit-shell rule is back beside the PageHeader file-name signal: the rule reads an app that adopted the kit shell, the file name a legacy app. Where the rule can't run (no Pest beside standards), plain standards adoption reports it as skipped and the other signals decide the row; --ci and --compliance still fail closed. Kit's fixture check now asserts each case's expected skips, and fails on one it doesn't list.
  • PaneTabs' pressed pane is neutral. The Workbench's phone tabs (Kit/Layout/PaneTabs.vue) took the primary button for the pressed pane, so it read as a second primary action. They now take SegmentedToggle's look: a bordered raised segment in a muted track. They stay a single-choice toggle group (role="group", aria-pressed).
  • The adoption inventory is calibrated against legacy apps. A *Panel, *History or *Chip file, a drag-and-drop library on its own, and a <table> with no header row no longer count as hand-rolled; cards count only outside public pages. layout.page reads hand-rolled from an app's own page header without the kit shell, shell.workspace from a starter-kit AppLayout, and page actions in an app layout's #header slot count. overlay.modal replaces @inertiaui/modal-vue. Each change has an adoption fixture, and the fixtures run on tey/standards 0.11.47.
  • The adoption inventory no longer reads the app's shadcn primitives or content renderers as hand-rolled. Every resources/js/** file-name signal skips resources/js/components/ui/**, the primitives the kit builds on, so ui/dropdown-menu/* no longer marks action.menu hand-rolled. The *Card.vue signal for layout.blocks and has-content-panels also skips body/, content/, render/ and renderers/ folders, where a card draws content rather than framing a page. New fixtures cards-content-renderer and ui-primitives-only cover both. A ui/button/Button.vue still reads hand-rolled through standards' generic-name scan of replaces.components, which has no path filter yet.

Fixed

  • The adoption schema accepts what tey/standards 0.11.50 accepts. resources/adoption/surfaces.schema.json gains the class-reference signal kind (classes, optional paths, min and scope), file-name except and template-tag in (vue, blade or both; default vue), so packages that copy the schema can use them. A new CI check, composer test:adoption-schema, compares the schema with the installed standards' signal kinds, fields, required fields, groups and analyses, and fails on any drift. The kit's tey/standards dev dependency is now ^0.11.50.
    From teylabs.com's adoption of the Workspace Layout:
  • A host without workspaces gets its navigation. PageShell sent an empty navigation and account menu on a core and shell host. It now lists the pages registered with Registry::navigation() at root-relative URLs, current by the same active rules, each shown while the viewer passes its ability as a Laravel Gate ability. An undefined or denied ability hides the link (fail closed). user-menu links, such as a Settings item, join the account menu.
  • The Layout no longer crashes on a focus shell, and FocusShell ships. For a kind: 'focus' shell (welcome steps, no workspace yet), the Workspace Layout renders the new FocusShell: the product, an optional "Step n of m", and the account cluster with Switch account and Sign out. It's also exported from @kit/shell, and takes :toaster.
  • The home crumb is recognised by path. gretel's breadcrumb URLs are absolute (https://host/@slug), so the root crumb never matched and showed as text. It now shows as the house icon.
  • The extra slot says where it's rendered. It's rendered in both the account dropdown and the phone drawer, now with a place slot prop (menu or drawer), so an app can render a DropdownMenuItem in one and a plain link in the other. A new drawer-extra slot replaces it in the drawer.
  • The current page stands out in PrimaryNav. It now gets a tinted pill and a 3px marker beside full-strength text, not only an underline in the brand marker's colour.
  • Destructive menu items and error text meet WCAG AA. They used --destructive as text, 3.76:1 on the white popover in light mode and 4.00:1 on a focused item in dark mode (AA is 4.5:1). The new --destructive-text token is destructive text on a surface; --destructive-foreground keeps its meaning, text on a destructive background, and solid destructive backgrounds are unchanged. Its defaults are hsl(0 72% 42%) light (6.48:1 on background, card and popover, 5.94:1 on accent and muted) and hsl(0 91% 71%) dark (7.20:1 on background, card and popover, 5.51:1 on accent, 5.29:1 on muted). tests/js/destructive-text.test.mjs computes those ratios from kit.css and refuses a raw text-destructive in any stub, and the browser job's open Workbench row menu now runs axe's color-contrast with the destructive Delete item showing (browser/README.md says why contrast runs use a non-modal surface).
  • FormStatus mounted before a visit now shows the message the visit brings back. It kept a hidden flag set by the router's start and by a watcher on form.processing, and cleared only by finish, so a form whose processing changed without a router visit left it hidden for good. Visibility is now derived from state: it shows when there is a message and no visit is in flight (the router between start and finish, or form.processing), so after each visit it shows exactly when the new page carries a message. The browser job mounts it before the visit and drives visits through the Inertia stub in v3's order (start, the page swap, flash, success, finish).
  • The Workbench's phone navigator is a flex column, as on wider screens. It was only min-h-0, so a host's order-first child sat under the actions bar on phones.

What's Changed

  • Workbench navigator item menus, with the buttons outside the listbox by @jaspertey in #370
  • Calibrate the adoption inventory against legacy apps by @jaspertey in #366
  • Shell gaps from a Workspace Layout adoption: navigation without workspaces, FocusShell, home crumb, extra's place, nav active state (2732) by @jaspertey in #367
  • Destructive text meets AA on surfaces: a --destructive-text token by @jaspertey in #372
  • Send row-menu actions on server-built Workbench items by @jaspertey in #376
  • FormStatus: visibility from state, and a flashKey for Inertia v3's page.flash by @jaspertey in #379
  • Restore the kit's Inertia shares after a flush, host shares still winning by @jaspertey in #381
  • Workbench #navigator-actions, first in the navigator; the phone navigator is a flex column (2697) by @jaspertey in #385
  • Keep the Workbench reorder's version token: parse Inertia v3's string bodies by @jaspertey in #392
  • Checkbox menu items and a #navigator-item slot for the Workbench by @jaspertey in #390
  • Sync the adoption schema with tey/standards 0.11.49 and guard against drift by @jaspertey in #382
  • Put ui.kit-shell back beside PageHeader and assert its skip by @jaspertey in #383
  • Skip shadcn primitives and content renderers in adoption signals by @jaspertey in #402
  • Resolve the conflict markers shipped in Kit/Layout/Page.vue (v0.6.10–v0.6.12); CI guard by @jaspertey in #411
  • Run the kit's request start first in web by @jaspertey in #413
  • Forward caller attributes to the focusable control in kit form controls by @jaspertey in #409
  • StickyBar pinned by layout registers with useStickyBars (toasts clear a filling Workbench's actions bar) by @jaspertey in #397
  • Restore the browser suite cut to one test, and fail CI below a test floor by @jaspertey in #416
  • FluentMailMessage: escape user text exactly once (no ' in mail) by @jaspertey in #399
  • session-ui route groups (kit.routes.session-ui.) and kit.home_name by @jaspertey in #417
  • Workbench navigator thumbnails and a filmstrip layout by @jaspertey in #407
  • Require laravel/mcp ^1.0.2 (oauthRoutes registration parameter) by @jaspertey in #403
  • A neutral SegmentedToggle, and PaneTabs without a primary fill by @jaspertey in #396
  • Release v0.6.13 by @jaspertey in #424

Full Changelog: v0.6.12...v0.6.13