anousss007/blatui
BlatUI brings shadcn/ui-style, WCAG AA accessible components to the BLAT stack (Blade, Laravel, Alpine, Tailwind). A CLI copies fully themeable, light/dark UI components, blocks, and charts directly into your app so you own and customize the code.
calendar controlled mode. The root now carries x-modelable="value", so a parent can
drive and observe the whole selection with plain Alpine two-way binding:
<x-ui.calendar mode="range" x-model="stay" />. The parent's value wins on mount and the
binding stays live in both directions — a popover no longer has to stay mounted just so it can
be re-seeded on every open. value is 'Y-m-d'|null (single), ['Y-m-d', …] (multiple) or
{ from, to } (range). See the new Controlled calendar example.calendar instance handle. New calendar-id prop (defaults to the element id). It
renders as data-calendar-id, aims the incoming calendar:* hooks at one instance when they
are broadcast on window (detail.id), and is echoed on every outgoing calendar:updated.
Pages that run several calendars at once — a range picker in a sticky sidebar plus one in a
mobile sheet — can finally address them individually instead of hitting all of them.calendar:updated event. Bubbling, composed, fired on every change with
{ id, mode, value, source } where source is select (a user pick), set, set-range,
today, clear or value (a controlled write). This is the event to listen to when you need
to mirror programmatic changes; calendar-change remains the "the user picked a day" event.
It is named updated, not change, on purpose: calendar:change would have differed from the
existing calendar-change by one character — indistinguishable in review and inseparable in a
grep. calendar:* is now the structured API (in: set / set-range / today / goto /
clear, out: updated); calendar-change is the historical user-pick event.calendar:goto and calendar:clear hooks. calendar:goto moves the visible month(s)
without selecting ('Y-m', 'Y-m-d', a Date or { month, id? }) and is the one hook that
works in every mode — it replaces the incidental view-scrolling the selection hooks used to do
in the wrong mode. calendar:clear empties the selection in any mode.apps/demo/tests/js/calendar.test.mjs locks the calendar's event and
targeting contract. Zero-dependency: node --test "apps/demo/tests/js/*.test.mjs", also run
in CI.prevMonthLabel / nextMonthLabel and added an Events table to the API
reference generator (docs-api files may now declare an events key).calendar:* hook no longer emits calendar-change.
calendar:set, calendar:set-range and calendar:today used to re-emit the very same event a
day click emits, so any popover that seeded itself on open closed again on the click that
opened it as soon as the seeded value was complete — and every consumer had to carry a
syncing flag to work around it. Seeding is not a pick: those hooks now emit calendar:updated
(with the source that caused it) and nothing else. If you were relying on the old behaviour,
listen for calendar:updated and check $event.detail.source. date-picker's internal
_keepOpen flag is gone as a result.calendar:set / calendar:today previously moved the visible month in every mode and only
then checked that the calendar was in single mode — so a birthday picker seeding 1991
dragged an unrelated range calendar 35 years back. They are now a complete no-op outside their
mode. Use calendar:goto when you want to move the view regardless of mode.date-picker and datetime-picker now listen for calendar:updated, so their label and hidden
inputs stay in step when the calendar is driven from outside.calendar leaked its window listeners. The three incoming hooks were bound to window
and never unbound, so every Livewire re-render or SPA navigation left another live listener
behind, and destroyed calendars kept reacting to broadcasts. The component now unbinds on
destroy().calendar arrow keys did not mirror under dir="rtl". The grid is built from logical
properties, so RTL renders it mirrored — the next day sits to the left of the current one —
but ArrowLeft still moved to the previous day, i.e. visually to the right. Arrow keys are
visual in the APG grid pattern, so they now follow the rendered direction. Home / End are
unchanged: "first/last day of the week" is a logical position and must not flip.calendar day aria-labels were hardcoded English. "Today, …" and ", selected" ignored
the app locale; they are now the localisable today-label / selected-label props (defaults
__('Today, :date') / __('selected')). The month and year dropdown aria-labels in
caption-layout="dropdown" go through __() as well.nav-user, nav-main,
nav-projects, nav-secondary, team-switcher, version-switcher, search-form,
file-tree). These are the <x-block.*> pieces the dashboard/sidebar blocks compose. They
ship with the package (stubs/block/), install to resources/views/components/block/, and are
reachable via php artisan blatui:add nav-user, the registry index, and /r/nav-user.json.
Fixes #10 — 14 blocks referenced these
components, but none were shipped or installable, so a copied block threw
Unable to locate ... component [block.nav-user] on render.registryDependencies were incomplete. The dependency scanner matched <x-ui.*>
only, silently dropping every <x-block.*> reference — so sidebar-07 advertised
breadcrumb, separator, sidebar but not the four block components it actually needs. It now
scans both namespaces, in the manifest, the HTTP registry, and the MCP client.install_command no longer emits commands it knows will fail — unknown names are
reported separately instead of being folded into the blatui:add line.__() with an override prop (falling back to the translation key). pagination-previous
/ pagination-next gained label + aria-label; sidebar-trigger gained label; calendar
gained prev-month-label / next-month-label. Existing usage is unchanged (English defaults).ui/ components moved from physical
direction utilities to logical ones (pl/pr→ps/pe, ml/mr→ms/me, left-/right-→start-/end-,
border-l/r→border-s/e, rounded-l/r→rounded-s/e, text-left/right→text-start/end), with
rtl:rotate-180 on directional chevrons. Indentation, check/radio indicators, sub-menu chevrons,
sidebar affordances and table sticky-action columns now flip correctly under dir="rtl".
Genuinely physical mechanics (carousel scroll gutter, symmetric slider handle, LTR diff gutter)
and explicit side/position-prop APIs (sheet/drawer/sidebar side, dialog/sonner position) are
intentionally left physical.calendar selection not always repainting. A programmatic calendar:set updated state
reliably, but the day's data-selected highlight only appeared after some month navigations —
the grid's x-for used positional keys, so navigated cells were reused and their selection
bindings went stale. The month/week/day loops are now keyed by date (fmt(m) / fmt(week[0]) /
fmt(day)), so a navigation mounts fresh cells that re-evaluate the selection deterministically.qr-code scaling. The SVG bound its viewBox via a plain :viewBox, which the HTML
parser lowercases to viewbox — silently ignored since SVG's viewBox is case-sensitive.
With no valid viewBox the code rendered at ~1px per module instead of filling size. Now
bound with Alpine's .camel modifier (:view-box.camel="viewBox") so it scales to any
size, crisp and scannable.server-table component. New server-driven table for Livewire-backed data — sorting,
search, and pagination are handled on the server rather than client-side, so it scales past
the in-memory limits of data-table. Pulls in button, dropdown-menu, and input.data-table row actions. New actions slot renders a trailing actions column. It sits
inside the Alpine x-for, so the current row is available as item.r (row data) and item.i
(index) — wire buttons straight to Livewire, e.g.
x-on:click="$wire.edit(item.r.id)". Accompanied by three new props: rowKey (row-data key
used for stable :keys and passed through to the actions slot, default id), actionsLabel
(sr-only header for the actions column, default Actions), and stickyActions (freeze the
actions column to the right edge on horizontal scroll).data-table accessibility. Header cells now carry scope="col", sortable headers expose
aria-sort, decorative sort/check icons are marked aria-hidden, and the select/sort controls
gain visible focus-visible rings. Row keys are now stable via rowKey instead of the paged
index.date-picker presets. New presets prop renders a quick-pick panel beside the calendar.
Pass true for sensible defaults per mode, or an array mixing named keys (today, yesterday,
tomorrow, thisWeek, lastWeek, last7Days, last14Days, last30Days, thisMonth,
lastMonth, thisYear, yearToDate, allTime) with fully custom entries
('My label' => ['from' => 'Y-m-d', 'to' => 'Y-m-d'] or ['date' => 'Y-m-d']). Dates resolve
client-side relative to today, so a cached view never serves a stale range. Applying a preset
keeps the popover open so the selection stays visible and adjustable.select / combobox indicator variants. New indicator prop (check | checkbox |
radio) controls how a selected option is marked in the list — a trailing check (default,
unchanged), a checkbox box (pairs with multiple), or a radio dot (pairs with single-select).
On the compositional select API, set indicator on <x-ui.select-content> and it cascades to
every <x-ui.select-item> via [@aware](https://github.com/aware).calendar:set /
calendar:set-range / calendar:today bind to both window (unchanged, backward-compatible)
and each calendar's own root element. A non-bubbling dispatch on one calendar targets only that
instance — so the new date-picker presets work correctly with several pickers on one page.darkMode: false no longer strips the dark class (#4).
The theme store's apply() ran classList.toggle('dark', false) on every page load even when dark
mode was disabled, removing a dark class set by the host app (e.g. Flux) and causing a dark→light
flash on full refresh. darkMode: false now means hands off — BlatUI never touches the dark
class, so it coexists with apps that drive their own dark mode. ('class'/'system' unchanged.)[@import](https://github.com/import) 'tw-animate-css' (#4).
tw-animate-css was dropped as a dependency (components animate via Alpine, not CSS keyframes), but
the "Copy theme CSS" scaffold still imported it — so a pasted theme failed to build with a missing
package. The scaffold now matches the shipped app.css (Tailwind import only).<dialog> so it stopped rendering behind it — but it then rendered at the
dialog's top-left, detached from its trigger, and a tall popover (a calendar, a long
select/dropdown) overflowed off-screen. Two causes, both fixed:
x-anchor defaults to position: absolute, whose
offsetParent math is wrong for an element in the browser's top layer. Popovers now position
with position: fixed (viewport-relative — correct in a top-layer <dialog> and in <body>).x-blat-anchor directive (floating-ui flip + shift + size)
used by the tall popovers (datetime-picker, date-picker, select, dropdown-menu) caps the
popover to the height actually available and lets it scroll, so it can never overflow the
viewport — while never growing past the component's own max-h. The remaining popovers gained
the fixed strategy.[@floating-ui](https://github.com/floating-ui)/dom package (already a transitive dependency of
[@alpinejs](https://github.com/alpinejs)/anchor; now listed explicitly in the install instructions and blatui:init).datetime-picker, date-picker, combobox, select,
dropdown-menu, context-menu, menubar, popover, hover-card, tooltip) when used inside a
modal:
<flux:modal> is a native <dialog> opened with
showModal(), which lives in the browser's top layer — it paints above everything in
<body> regardless of z-index, so a popover teleported to <body> was hidden behind it (and
inert). New shared x-blat-dialog-layer directive relocates the popover into the nearest
ancestor native <dialog> when there is one (top layer + interactive); otherwise it stays in
<body> as before, still escaping overflow-clipping ancestors.open/errors/invalid) resolved
against window — throwing 'open' called on … Window and … is not defined, killing the
widget until a full page refresh. wire:ignore on the teleport template keeps morph from
touching the popover; Alpine reactivity and wire:model/[@entangle](https://github.com/entangle) keep working.wire:model on every form control, with full two-way binding. Components are
Blade + Alpine, so they already render inside Livewire; this wires wire:model through to their value:
input, textarea, native select/checkbox) bind directly.[@entangle](https://github.com/entangle)($attributes->wire('model')) — select,
combobox, autocomplete, switch, toggle, toggle-group, radio-group, checkbox, slider,
rating, knob, number-input, color-picker, date-picker, datetime-picker, time-field,
input-otp, tags-input, editable, markdown-editor.wire:model to their real input — file-upload (upload target), phone-input,
segmented-control, mention-input, rich-text-editor, signature-pad..live / .blur / .debounce modifiers. The bridge is fully inert (and stripped)
when Livewire isn't installed, so plain Blade/Alpine projects are unaffected.brand — your app's logo/wordmark, optionally linked (parity with Flux's <flux:brand>).profile — an avatar paired with a name/description, for account buttons and dropdown triggers
(parity with Flux's <flux:profile>).slider, date-picker and datetime-picker bind a single value with wire:model in their default
mode; their range modes submit via standard name[...] form fields.wire:model on every form control, with full two-way binding. Components are
Blade + Alpine, so they already render inside Livewire; this wires wire:model through to their value:
input, textarea, native select/checkbox) bind directly.[@entangle](https://github.com/entangle)($attributes->wire('model')) — select,
combobox, autocomplete, switch, toggle, toggle-group, radio-group, checkbox, slider,
rating, knob, number-input, color-picker, date-picker, datetime-picker, time-field,
input-otp, tags-input, editable, markdown-editor.wire:model to their real input — file-upload (upload target), phone-input,
segmented-control, mention-input, rich-text-editor, signature-pad..live / .blur / .debounce modifiers. The bridge is fully inert (and stripped)
when Livewire isn't installed, so plain Blade/Alpine projects are unaffected.brand — your app's logo/wordmark, optionally linked (parity with Flux's <flux:brand>).profile — an avatar paired with a name/description, for account buttons and dropdown triggers
(parity with Flux's <flux:profile>).slider, date-picker and datetime-picker bind a single value with wire:model in their default
mode; their range modes submit via standard name[...] form fields.context-menu closes on window scroll/resize instead of staying pinned to stale fixed
coordinates. Scrolling inside the menu keeps it open.accordion trigger shows cursor-pointer again (Tailwind v4 dropped the default cursor).tree-table copyable prop — copies the hierarchy as a markdown tree (├──/└──/│).confetti direction/spreadArc (aimed bursts) and fullscreen (top-edge rain).add-to-cart composes <x-ui.button>; mention-input composes <x-ui.textarea>.dropdown-menu / context-menu / menubar share new menu-* leaf primitives (identical output).autocomplete + combobox share one Alpine listbox engine; combobox gained trigger="input".autosize-textarea → use <x-ui.textarea :max-rows="…"> (textarea gained rows/maxRows).
Ships as a thin alias; existing installs are unaffected.autocomplete → use <x-ui.combobox trigger="input">. Ships as a thin alias.quantity-selector removed — it was the same control as number-input with different
defaults (integer-only, min/value of 1, a tighter footprint), which duplicated the API
surface and the underlying code (#3). Build a
cart/product stepper with number-input instead: <x-ui.number-input :min="1" size="sm" />.
A "Quantity selector" usage example now lives on the number-input docs page. Projects that
already ran blatui:add quantity-selector keep their copied component — only the registry
entry is gone.blatui/blatui → anousss007/blatui — the Composer vendor now matches
the GitHub owner. The product (BlatUI), the BlatUI\ namespace and the component prefix are
unchanged. This release declares replace: { "blatui/blatui": "self.version" }, and the old
package is marked abandoned on Packagist pointing here, so existing composer require blatui/blatui installs keep resolving their pinned versions and only see a one-time rename
notice. Switch your composer.json to anousss007/blatui to receive new releases.audio-player no longer overflows narrow screens — the seek bar
shrinks and the fine volume slider hides below sm (the mute button stays). The horizontal
stepper rail now scrolls instead of overflowing. Verified across the component set at 320–390 px.chat, prompt-input, streaming-text, reasoning, tool-call, citation.gradient-text, number-ticker, border-beam, spotlight-card,
tilt-card, flip-card, confetti, meteors, animated-beam, parallax,
dot-pattern, grid-pattern, aurora.product-card, price, quantity-selector, variant-selector,
add-to-cart, mini-cart.audio-player, image, qr-code (dependency-free SVG), map (keyless OSM).file-upload, color-picker,
password-strength, autosize-textarea, editable, rich-text-editor, markdown-editor,
signature-pad, mention-input, segmented-control, knob, repeater), Data display
(stat, tree, json-viewer, description-list, avatar-group, meter, heatmap,
comparison-slider, masonry, diff-viewer, kanban, tree-table, gantt, scheduler,
org-chart, presence), Navigation (scrollspy, bottom-navigation, dock,
speed-dial, back-to-top, infinite-scroll), Layout (container, stack,
bento-grid, page-header, visually-hidden) and Feedback/Overlays
(cookie-consent, top-progress, loading-overlay, notification-center,
onboarding-tour).php artisan blatui:add <name>.Submenus no longer clipped (dropdown-menu, context-menu, menubar) — the sub-content
flyout lived inside the parent menu panel, which is fixed max-h-96 overflow-y-auto; x-anchor
positioned the flyout but the parent's clip/scroll swallowed it, so it was cut off or pushed
into a scrollbar instead of opening to the side. The flyout now teleports to <body> with
fixed positioning — mirroring how the top-level panel already escapes its container — so it
flies out cleanly. A short, cancellable close delay (closeSoon / cancelClose on the menu
engine) lets the pointer cross the gap from the trigger to the teleported flyout without it
snapping shut. (#2)
Re-run blatui:add dropdown-menu context-menu menubar and re-copy foundations/blatui-core.js
to pick up the fix.
Synced from a full axe-core (WCAG 2.1 A/AA) audit of the demo — 0 critical, 0 serious violations across every component and variant.
foundations/app.css :root): --destructive,
--success, --warning and --info nudged darker so text-* clears WCAG AA 4.5:1 on white and on
the /10 soft tint. Solid-fill *-foreground pairs unchanged. Re-copy foundations/app.css (or
re-apply the token block) to pick up the contrast fix.alert — dropped the /90 opacity on alert-description so it meets AA on the tinted background.kbd — a passed aria-label (invalid on <kbd>) now renders as visually-hidden sr-only text.time-field — an author aria-label is forwarded onto the real <input type="time">.select — the :options shorthand trigger now carries an accessible name (placeholder or
"Select option") for its combobox role.data-table — select-all and per-row checkbox buttons now have aria-labels.item-group — removed role="list" (children aren't listitems).Re-run blatui:add for any of these components to pull the fixes.
comparison-table — a data-driven feature-comparison table (:tiers × :rows, check / dash /
text values, highlight column). blatui:add comparison-table.accent — <x-ui.accent color="#7c3aed">…</x-ui.accent> recolours every BlatUI component in
its subtree from one token override (display:contents, no layout impact). blatui:add accent.color prop on input, textarea and select — brands the focus ring + selection locally,
matching the button color prop; use accent to recolour a whole form or section at once.blatui:add countdown timeline terminal sparkline:
countdown — a live, timezone-safe countdown to a target date with an expired state.timeline (+ timeline-item) — a vertical timeline with dots, connectors, icons and timestamps.terminal — a terminal / console window for command output and code demos (dark in both themes).sparkline — a server-rendered inline trend line from a data array, theme-token coloured.progress circular / ring variant (circular + size / thickness / show-value) — linear behaviour unchanged.button color prop — recolours a button by overriding the primary token locally; the same
style="--primary: …" wrapper trick recolours any subtree of BlatUI components.combobox disabled prop (1.9.1) and
multi-select :multiple on select / combobox / autocomplete (1.9.2, with the updated
blatui-core.js blatSelect engine). No new component code since 1.9.2 and no breaking changes —
installing anousss007/blatui:^1.10 gives you exactly the 1.9.2 component set under a minor version.:multiple) on select, combobox and autocomplete (synced from the demo) —
opt in with multiple: selected entries render as removable chips, picking toggles without closing
the list, and it submits as name[] (binds to a Laravel array field). Pre-select via
:value="['a', 'b']". autocomplete becomes a tag input.blatui-core.js engine gains multi-value support in blatSelect (chips, toggle,
isSelected/remove). The select multi-select needs this updated engine — after bumping, run
blatui:init so the foundation-skew check flags an out-of-date installed blatui-core.js.
combobox/autocomplete multi-select are self-contained Blade (Alpine inline), no engine bump
required.combobox disabled (synced from the demo) — the installable combobox stub now accepts a
disabled prop, matching select/autocomplete: it renders the disabled attribute on the
trigger and dims it, so the listbox can no longer be opened.stepper (multi-step flow, horizontal/vertical, with
completed-step checks), typography (prose styles via one variant prop), data-table,
autocomplete (type-ahead input), phone-input, input-mask, and code-block — plus
menubar sub-menus and an alert-action slot. blatui:add <name> ships them all.tabs variant
(segmented | underline | pills), table variant="card" + striping, select :options
shorthand, combobox :searchable="false", switch size, toggle-group vertical + group
size/variant, textarea character-count/no-resize/read-only, tooltip optional arrow,
calendar calendar:set-range + minDays/maxDays, and more.blatui:init foundation-skew check — after a package bump, blatui:add copies Blade stubs
but not the JS engine. blatui:init now compares the helpers your installed blatui-core.js
registers against the bundled engine and warns about any it is missing (so "the prop exists but
does nothing" skew is caught). Robust to intentional customisation (compares by capability).blatui:doctor false positives — <x-ui.*> mentioned only inside a comment (Blade, HTML or
PHP) is no longer flagged. Comment bodies are masked before scanning (line numbers preserved)..blat-select sets -webkit-appearance: none
(iOS/Safari double-arrow); sonner-flash resolves status strings through __() (no leaked
slug); select items read their own label; toggle-group items inherit the group's
size/variant. See the demo changelog for the full component-level list.marquee (seamless infinite scroll), copy-button (clipboard copy
with a copied state + live announcement), banner (dismissible announcement bar with semantic
tones), typewriter (cycling typed words with a caret), text-reveal (scroll-linked word-by-word
reveal), gallery (thumbnail grid → full-screen lightbox with keyboard nav + focus trap), and
video (styled HTML5 player with poster + custom play overlay). All ARIA-complete, token-driven
and reduced-motion aware.input password & icon affordances: type="password" gains a built-in show/hide eye toggle
(opt out with :toggle="false"); new leading / trailing slots for prefix/suffix icons, with
RTL-safe padding (ps/pe).sonner collapsed stack + expand: toasts collapse into a stack and fan out on hover/focus
(Sonner-style); the new expand prop keeps them always expanded.dialog fullscreen variant: an edge-to-edge takeover instead of the centered box.swipe prop, on by default; mouse unaffected).blatui:doctor now also scans compiled views for literal <x-ui.*> tags that leaked into the
HTML (a tag that failed to compile — e.g. nested as the slot content of an [@aware](https://github.com/aware) component) and
points at the foundations utilities as the fix..blat-input / .blat-textarea / .blat-select / .blat-checkbox /
.blat-radio) documented as the recommended primitive for server-rendered DX layers that re-wrap a
slot, with a getting-started callout on the [@aware](https://github.com/aware)-slot compile footgun.h-full (vertical carousels need a height on <x-ui.carousel-content>).<x-ui.sonner> was mounted more than once on a page.sonner is now a singleton per page (the first-mounted toaster stays active; extras go inert),
so mounting it twice no longer renders duplicate overlapping toasts. The window.toast API is
unchanged.php artisan vendor:publish --tag=blatui-foundations) to pick up the
marquee keyframes added to app.css.out-of-range mode on date-picker / datetime-picker / calendar — disable (default:
out-of-range dates are struck-through and unselectable) or flag (selectable but shown red, and
selecting one flags the field invalid via aria-invalid + an error). min / max now act as
real date bounds on the calendar — previously they were interpreted as range span counts
and didn't gate single-date selection. (Requires re-publishing foundations: the gating logic is
in blatui-core.js.) Verified in-browser.date-picker / datetime-picker selection wasn't saved after the 1.6.2 teleport. The
calendar now lives in a <body> portal, so its calendar-change (and the time-field's
time-change) bubbled to <body>, never reaching the [@calendar-change](https://github.com/calendar-change) listener on the
picker root — the hidden input and trigger label stayed empty. Moved the listeners inside the
teleported popover (which contains the calendar and shares the picker's scope). Verified
in-browser: clicking a day now updates the input and label. Affects single and range.data-outside was bound
to isOutside(day, m), but the outer-loop m goes stale in the nested per-cell bindings
after a prev/next navigation, so prev/next-month days were mislabeled. The panel month is now
stamped onto each day in weeksFor and read directly. Latent for the muted-day styling; it
made show-outside-days="false" render a near-empty grid after navigating. Verified across
months in-browser. (Requires re-publishing foundations: the fix is in blatui-core.js.)show-outside-days="false" now actually works. The 1.6.3 implementation relied on a
runtime Alpine expression that broke the grid (only the first week rendered). Reworked to a
pure-CSS approach (a data-hide-outside-days flag + :has() rules) that hides prev/next-month
filler days and collapses fully-outside week rows — no runtime expression to fail.min/max set, disabled days
were only slightly muted; they now render struck-through and fainter so valid vs unavailable
dates read at a glance.week-start accepts a day name (week-start="monday") in addition to 0–6 (0 = Sunday),
on calendar / date-picker / datetime-picker.show-outside-days on calendar / date-picker / datetime-picker (default true).
Set false to hide the greyed-out days from the previous/next month — outside days render as
empty, non-interactive cells and any week row that is entirely outside the month collapses.overflow-hidden ancestor. date-picker,
datetime-picker and combobox now teleport their popover/listbox to <body> (like
popover / select / dropdown-menu already do), so placing one inside a card, table cell,
or any clipping container shows the full popover. x-anchor still positions it at the trigger.
(navigation-menu is intentionally left inline — it opens on hover and is not used inside
clipping containers.)registerBlatUI defaulted
the theme store to mode: 'system', so on a dark-OS machine it added .dark to <html> at
boot — silently flipping light-only apps to an unreadable dark (invisible in dev/CI; only on
a real dark-OS machine). The default is now light-until-toggled.registerBlatUI(Alpine, { darkMode }) — 'class' (default: light until an explicit
toggle, never auto-OS-dark), 'system' (follow prefers-color-scheme), or false (hard
light-only). To keep the previous OS-following behavior, pass { darkMode: 'system' }.card is now a simple padded box by default. The base <x-ui.card> renders
bg-card rounded-xl border p-6 shadow-sm (no flex / gap / py) — the dominant "just a box"
case is now the cheapest default. Composed cards (using card-header / card-content /
card-footer) must opt into the old layout with variant="sectioned":
<x-ui.card variant="sectioned">…</x-ui.card>. After php artisan blatui:add card, add
variant="sectioned" to every card that composes header/content/footer.--success / --warning / --info
(+ -foreground) plus a reusable tone axis on badge and alert
(tone="success|warning|danger|info|neutral"; badges add variant="soft|solid|outline").
danger reuses the existing --destructive. Status badges are finally first-class.link — an inline, prose text link (default / muted / subtle variants, external).rating — a star rating input (hover preview, keyboard, hidden field for forms,
readonly, sm|default|lg).icon — a thin Lucide wrapper that auto-mirrors directional arrows/chevrons under RTL,
plus a .blat-rtl-flip utility.dialog / sheet / alert-dialog accept an id to open/close
from anywhere via $dispatch('open-dialog-{id}') / $dispatch('close-dialog-{id}'); their
triggers accept for="{id}". A single shared modal now works from triggers inside a
[@foreach](https://github.com/foreach) / server-rendered table — no per-row modal markup.size="sm|default|lg" on input, textarea and select (trigger).native prop on select and checkbox renders a
BlatUI-styled native <select> / <input type=checkbox> (submits without JS, name-bound),
plus [@apply](https://github.com/apply)-able .blat-input / .blat-textarea / .blat-select / .blat-checkbox /
.blat-radio / .blat-label utilities for hand-rolled controls.before / after named icon slots and an as (element
polymorphism) prop.sonner-flash — a server-flash → toast bridge mapping
session('success'|'error'|'warning'|'info'|'status') to sonner toasts (incl. a Fortify
status map). Ships with the sonner family.date-picker range — mode="range" (was single-only) with name[from]/name[to], a
two-month calendar, and a defaultMonth prop.datetime-picker accepts a full-datetime min/max
(Y-m-d\TH:i): the date part bounds the calendar, the time part bounds the edge day
(validated for the input and select time variants). Both pickers gain
min-nights / max-nights (range length) and a hard end ≥ start check — invalid
selections show inline errors, mark aria-invalid, and disable confirming / closing.blatui:doctor — scans Blade views for <x-ui.button> inside a <form> with no type
(renders type=button, silently won't submit) and reports each file:line.datetime-picker range now shows a two-month calendar by default (new numberOfMonths
/ defaultMonth props) for a proper date-range-with-times UX.<x-dynamic-component :component="'lucide-…'"> (e.g.
icon) now correctly declare the mallardduck/blade-lucide-icons dependency in the registry.Charts are now opt-in. ApexCharts (~140kb) and the chart engine are no
longer part of the base foundation, so apps that only use components never
install or bundle them — and npm run build no longer requires apexcharts.
The base blatui.js / blatui-core.js ship the components engine only. To
use <x-ui.chart>:
php artisan blatui:add chart
npm install -D apexcharts
php artisan vendor:publish --tag=blatui-charts
then register it in app.js, before Alpine.start():
import { registerCharts } from './blatui-charts.js';
registerCharts(Alpine);
The chart component now declares apexcharts as its npm dependency, and a
new blatui-charts publish tag ships the opt-in engine. Existing installs are
unaffected until you re-publish the foundations.
php artisan blatui:mcp runs a stdio Model Context Protocol
server so AI editors (Claude Code, Cursor, VS Code…) can discover and install
components in conversation. Tools: search_registry, list_components,
get_component, get_example, install_command; resources
(blatui://component|block|chart/{name}) and prompts (use-component,
scaffold-page). Works offline from the bundled stubs. No new dependencies.blatui:add can now install from third-party,
shadcn-compatible registries by namespace ([@vendor](https://github.com/vendor)/name) or full URL,
resolving registryDependencies recursively with a local-stub fallback.
Configure namespaces in the publishable config/blatui.php.resources/boost/guidelines/core.blade.php and a blatui-development agent
skill, auto-discovered by Boost so AI agents learn BlatUI's conventions.
blatui:init detects Boost and offers to run boost:update --discover.tw-animate-css npm dependency. Components animate via Alpine
x-transition, so the published foundation (blatui.css) no longer imports
it; it is dropped from the install docs and the blatui:init checks.blatui:add now only lists the peer packages (composer / npm) that aren't
already installed, checking composer.json / package.json the same way
blatui:init does. Following the README's up-front install no longer leaves
blatui:add suggesting composer require for packages you already have.Two new Forms & Input components.
datetime-picker: date and time in one popover, modes single and
range — composes calendar and time-field. Timezone-naive
Y-m-d\TH:i value, hidden inputs (name, or name[from] / name[to]),
and a locale-aware trigger label honoring hour-cycle (auto / 12 / 24).time-field: a time control with a native <input type=time> variant and a
dropdown (select) variant (hour / minute / second + AM-PM), 12/24-hour,
seconds, and stepped minutes.select-item: option icons now sit inline with their label instead of
stacking above the text.Theme-foundation and component enhancements shipped to consumers.
button: xs size and an icon size scale (icon-xs / icon-sm / icon-lg).app.css): 9 base-color presets (added slate + gray), a new
input-style dimension ([data-input-style]: outline / fill / inset), a
heading font token (--font-heading) decoupled from the body font, and
additional [data-font] webfont families.blatui-core.js): theme store now persists inputStyle and
fontHeading and applies the matching data-* attributes.Accessibility overhaul of every shipped component (blatui:add) and the
foundations — WAI-ARIA / Base-UI parity, full keyboard + focus management, and
WCAG AA color contrast.
blatui-core.js foundation: x-blat-trigger,
x-blat-labelledby, x-blat-field, the blatMenu / blatMenubar /
blatSelect / blatCommand Alpine components, and $blatNav / $blatType.field-error accepts a :messages array (single → text, multiple → list).role="grid" with keyboard navigation.registry.json regenerated.aria-orientation, nested
interactive elements, listbox child roles, and missing accessible names.blatui-core.js (exports registerBlatUI(Alpine)) alongside the greenfield
blatui.js bootstrap — apps that already run their own Alpine register BlatUI
into it instead of booting a second instance. The CSS is additive: add
[@import](https://github.com/import) "./blatui.css"; to an existing app.css rather than replacing it.blatui:init now detects the Tailwind major version (v4 required; v3 →
npx [@tailwindcss](https://github.com/tailwindcss)/upgrade) and, when an app already runs Alpine, points to the
registerBlatUI path instead of blatui.js.[@import](https://github.com/import) "./blatui.css" + import "./blatui.js"), list tw-animate-css as a required peer (the theme CSS imports
it), and fix the card example to add the missing input component.blatui:init now also checks for tw-animate-css and apexcharts, and verifies
the foundations are actually imported into app.css / app.js — publishing
alone is no longer reported as "present".[@theme](https://github.com/theme) inline mapping + every token), so a pasted
theme actually renders styled instead of producing no utilities.Registry now reads the generated stubs/registry.json manifest (single source
of truth synced from the demo) instead of re-deriving families and dependencies.blatui:add <component> — copies a component family (and its transitive
component dependencies) into resources/views/components/ui.blatui:list — lists the 55 available component families, or details for one.blatui:init — doctor that checks Composer packages, npm/Alpine plugins,
theme tokens and the Alpine bootstrap.vendor:publish --tag=blatui-foundations — publishes the theme tokens (CSS)
and the Alpine + chart + calendar engine (JS).How can I help you explore Laravel packages today?