0.4.0
The licensed compiler arrives — one Bezalel Core licence installs Core and the compiler, and only the compiler builds — with live lists and presence, analytics, search, and a long list of fields, filters and widgets.
TemplateField: a Word template written in OnlyOffice with what it can say beside it — the variables its records offer, searched, copied with a click — and what it has behind it on its page: each${…}its file uses and what it means, an unknown one marked; «Try it on a record» opens it filled from the latest record to read (forResource(), orvariables()/sample()/values()). See docs/guide/document-templates.md. Runbezalel-core:stubsHasDocuments: a record made into a document from a Word template — a «Document» row action choosing a template, showing what each${…}says for the record (an empty one marked), and downloading it as Word or, through OnlyOffice's ConvertService, PDF. The variables are the Resource's fields unless it names its own (documentVariables(),TemplateVariable); the templates are the app's (documentTemplates(),TemplateFile).Documents\WordTemplatereads and fills.docxvariables with no library. Runbezalel-core:stubsPanel::mediaLibrary(false): a panel kept away from the shared media library — a portal for customers or carriers: none of the library's routes on it, its file fields uploading without it, its rich text with no image from it (panel.mediaLibraryshared). Runbezalel-core:stubsPanel::interstitials([...]): a page held until something's done — anInterstitialof the app's names the pages it covers and, per request, whether they're held and what it says; the layout draws it over the page with no way past it (components/interstitial.tsx, shared asinterstitial). Runbezalel-core:stubsDateField::minDate()/maxDate()(a day ortoday: checked when saved, the calendar offering nothing past them),->withToday()(a «Today» button),->clearable()(a button to clear it). Compiled by both compilers: only a date field with them compiles differently. Runbezalel-core:stubsPhoneField: a phone input (type="tel"), checked as a number, kept as E.164 (->countryCode('998')for a number typed without one,->asTyped()to keep it as written,->requireCountryCode();->unchecked()leaves the checking to its own rules), atel:link in the table; no phone libraryTextField::mask('999 999 999'): typed into a mask (9a digit,aa letter,*either), checked against it, kept without what it put in (MaskedInput). Compiled by both compilers: only a masked field compiles differently. Runbezalel-core:stubsResource::listData(): what a list component draws besides its rows — the page's own settings, a list to choose from — astable.listData, worked out per request. Runbezalel-core:stubsPanel::pageTabs(fn () => [...]): a row of tabs over pages that belong together, drawn by the layout above whichever of them is open (components/page-tabs.tsx, shared aspageTabs), worked out per request. Runbezalel-core:stubs- Realtime, which an app turns on (
bezalel-core.realtime.enabled,BEZALEL_REALTIME; Laravel's broadcasting on Reverb or Pusher;npm install laravel-echo pusher-js), sobezalel-core:stubspublisheslib/realtime-echo.ts. When on: the bell hears notifications as they're sent (PanelNotificationbroadcasts); a table reloads on itsResource::liveUpdates()events;useRealtime()inlib/realtime.tsfor the app's own components. When off, nothing of it is in the build. Runbezalel-core:stubs Action::form([...fields]): an action asks for fields in a dialog the package draws (ActionForm) — text, a list (an async one searched as@{action}.{field}), a date, yes/no, a file — posted with it, files and all, and checked by the fields' own rules; no component to write. Runbezalel-core:stubsResource::$exports: the table's look — its filters, search and sort, every row — downloaded as a spreadsheet from{plural}/export(exportAction()in the toolbar,Action::download()): xlsx written bySupport\Spreadsheetwith no library (CSV without PHP's zip extension); each column's label and what it shows by default,exportRows()for a sheet's own columns. Runbezalel-core:stubsPointField: a point on the map as «lat,lng», typed in or picked on the map (components/forms/LocationPicker, the map'sPickerMap) — while the map's on (bezalel-core.map.enabled, shared asbezalelMap); the text alone otherwise. Runbezalel-core:stubsStep::claims('segments.*.hops'): a wizard step answers for keys besides its fields' — one object edited across steps — so a mistake on them takes the wizard to it (the most specific claim wins); a record's wizard opens any step from its title. Compiled by both compilers: only a step that claims keys compiles differently. Runbezalel-core:stubsResource::$cardColumns: how many cards a row holds on a wide screen, 1–4 (three by default). Runbezalel-core:stubsFilter::defaultValue()on a table: it opens filtered by it — worked out per request (a role's own view), shown chosen — until it's cleared (sent with no condition,defaultFiltersin the payload) or another value is picked. Runbezalel-core:stubsAction::component($name, width: '4xl'): the component's dialog wider than the default (md,lg,xl,2xl,4xl). Runbezalel-core:stubsPage::routeSlug(): a page names its own address, under a section too (queries/query-map, its routequeries.query-map); by default its name, kebab-cased, as beforeMapWidgetandMapLayer: markers in layers on a map — clustered, a coloured pill or an image turned by a heading — a clicked one opening the app's panel, a place searched for (places(),{page}/map-places), the view remembered. Leaflet's, and optional:bezalel-core.map.enabled(BEZALEL_MAP),npm install leaflet leaflet.markercluster, thenbezalel-core:stubspublishescomponents/map/leaflet-map.tsx; off, nothing of it is in the build.bezalel-core:stubspublishes optional stub sets while they're on and removes them (unchanged) when off. Runbezalel-core:stubsSelectFilter::options()andcolors()take a closure: options kept in the database worked out per request, sent in the payload (filterOptions) over the compiled page's. Runbezalel-core:stubsFilter::visibleWhen(): a filter offered only to whom it's for (the bin to a Super Admin) — declared always, so the compiled page knows it; neither applied nor shown to the rest (hiddenFiltersin the payload). Runbezalel-core:stubsSelectFilter::icons(),colors(): an option's icon and colour on its tab.Resource::$defersTabCounts(DataTable::deferTabCounts()): the tabs' counts follow the page, a deferred prop of their own. Runbezalel-core:stubsResource::$listComponent: a component of the app's drawing the whole list in place of the generated table (DataTableList,lib/list-components.ts). Compiled by both compilers: only a Resource that names one compiles differently. Runbezalel-core:stubsResource::views(): a list seen more than one way — cards, the table, a board — switched in the toolbar (DataTableViews,?view=, the last remembered);Resource::$loadsOnScroll: the next page as the list is scrolled to its end (DataTableLoadMore);Resource::rowsFor()(DataTable::rowsOf()): any records as the table's rows;DataTable::sortedQuery(): a look's whole query. Compiled by both compilers: only a Resource with them compiles differently. Runbezalel-core:stubsSelectFilter::dependsOn(): an async filter whose options follow other filters' values (a region's districts), its search getting them asstate, its choice cleared when one changes. Runbezalel-core:stubsResource::board(): the Index as columns of cards (Board,BoardColumn— a label, colours, a query narrowing the table's), each with its count and first cards and «Ещё» ({plural}/board-column); the table's search, filters, sort, card component and row actions apply in every column.DataTable::withoutRows(). Compiled by both compilers: only a Resource that declaresboard()compiles differently. Runbezalel-core:stubsPage::filters(): what every widget on a page follows (a month, a manager), the table's Filter classes drawn as a bar above the widgets (components/widgets/PageFilters.tsx) and kept in the address; each widget'sdata()gets their values (Page::filterValues()), a value picked checked byFilter::pageValue(), elseFilter::defaultValue(); a select markedasTabs()is a row of buttons, andPage::filtersWidget()places the bar among the widgets. Compiled by both compilers: only a page with filters compiles differently. Runbezalel-core:stubsWidget::defer(): a slow widget's data loads after the page shows (an Inertia deferred prop of its own);WidgetGridshows a placeholder until it comes. Runbezalel-core:stubsResource::ownerKey(): who the signed-in person is as an owner-scoped Resource's$ownercolumn holds it — their id by default; a company's TIN, or a list of keys whose rows are all theirs (a new record filed under the one chosen when it's among them). Null or an empty list owns nothing- A wizard step's
checkStep()may answer with aFormPatch: values for the steps after it (recipients counted for a review step), which the form takes as it moves on.useWizard()takes the form'ssetData; rebuild a wizard form and runbezalel-core:stubs - A field in a repeater's rows can react (
afterChange()): the form posts it by its row (segments.2.template_id), its reaction gets the row as a fourth argument and answers with the repeater's rows.fieldReactions()lists it assegments.template_id; runbezalel-core:stubs Resource::$keepsDraft: a new record's page form kept in the browser until it's saved (lib/use-draft.ts), per person; a link with values starts fresh; "Start over" discards it. Compiled by both compilersLayouts\WizardandStep: a page form a step at a time — Back, Next (each step checked at{plural}/step-check,Resource::runStepCheck(): its fields' rules, thenstepRules()andcheckStep()), Save on the last; a save's mistake goes back to its step.components/forms/Wizard.tsxandlib/use-wizard.tsin the stubs. Compiled by both compilersResource::pageDirectory(): the folder its pages compile into, for a resource whose default (its name's plural) is already the app's ownLayout::visibleIf(Condition): a section of the form — a card and everything in it — shown only while its condition holds, as a field's is. Form only; compiled by both compilersResource::savedUrl(Model $record)andlistUrl(): where a page form's save goes, and where the list is when it isn't the Index (which then redirects there).Field::editableFor($rule, onCreate: true)asks the lock on a new record's form too. Compiled by both compilers; only a resource that has them compiles differentlySelectField::createOption($fields, using:, label:, onEdit:): a picker's "+ New" — a small form in a dialog (components/forms/CreateOption.tsx), validated by its fields' rules, made through{plural}/field-create(Resource::runFieldCreate()), and selected. Compiled by both compilers; runbezalel-core:stubs- A reaction can offer choices:
FormPatch::offer($title, $choices, dismissible:)opens a dialog (components/form-offers.tsx, mounted inapp.tsx); a choice picked appliesField::afterPick()'s patch, one dismissed callsafterDismiss().FormPatch::merge()sets several values. The generatedfieldChange()passespick/dismiss— rebuild a resource with a reacting field, and runbezalel-core:stubs HiddenField: a value the form keeps but never shows or saves — what another field'svisibleIf()reads, set by a reaction and worked out from the record (->value())Resource::createFormData(Request $request): what a new record's page form opens with, worked out per request (a default from the app's settings, a value from the link) — sent to the create page asdefaults, over the fields' build-time defaults. Compiled by both compilers; only a resource that has it compiles differently- A
FileFieldin a relationship repeater's rows: attached to the row's own record after it's saved (Repeater::rowAttachments()), read back with the row; a new row starts it asnull. A repeater in the rows gets a row type of its own intypes.ts(OrderLegsStopsRow) - A
Repeaterin a repeater's rows — a leg's stops: a relation of each row synced under it (Repeater::syncRows()), or an array column of the row; its fields bound to the inner row (item2,update2), errors naming both rows, ruleslegs.*.stops.*.place, an async select searched aslegs.stops.place_id. Compiled by both compilers; every other form compiles as before BooleanField::toggleable(using:, when:): a yes/no cell that switches on a click and saves at once, offered per row to whoever may update the record (and never where updates wait for an approval), checked again byPOST {plural}/{id}/toggle/{field}(ToggleController). Compiled by both compilers;BooleanCelltakestoggle,CellExtrasdraws a link beside a switch. Rebuild a page to draw itphp artisan bezalel-core:stubspublishes the React and CSS stubs and, on every later run, brings them up to the package's without losing the app's changes: a file the app never touched is updated, one only the app changed is kept, one both changed is merged (with conflict markers where they changed the same lines), and a file marked@bezalel-keepis never touched. It keeps what it published in.bezalel/stubs— commit it.--dry-runshows what would happen;--base=merges an app published before against the stubs it came from- The shell's spacing as tokens an app sets in its
app.css:--page-gutter,--card-padding,--dialog-padding,--control-height,--header-height,--table-cell-padding-y,--table-head-heightand--nav-item-padding-y(asp-page,p-card,h-control, …) Panel::breadcrumbs(false)hides the pages' breadcrumbs<AppLayout withoutSectionSidebar>gives a wide page the room of a sections panel's sidebar, its pages moving to the strip under the headercomponents/app-header-actions.tsx, shipped empty, for the app's own buttons in the headerResource::prepareRows()— called once with a table page's records before their columns are formatted, to load what aformat()needs in one query instead of one per row- Page dialogs:
<DialogLink href>opens a page — a record's view, edit or money — in a dialog over the list, as a modal would, and stays a plain link for a new tab. Links inside the dialog open there; a save from it reloads the list underneath and shows the page the save led to in the dialog (PageDialogRedirects); "Open as a page" leaves for the page itself components/ui/skeleton.tsx— a pulsing placeholder the shape of what's still loading, forInertia::defer()ed propsNavItem::badge()— a count beside a menu item, in the header, the More menu, the sidebar and the phone menuPanel::headerRows(2)— a sections panel's top-level items and its sections in two header rows, so neither pushes the other into MorePanel::sectionSidebar(false)— a sections panel's pages in the strip under the header at every width, instead of a sidebar beside the pageNavSection::sidebar()— one section keeps its sidebar (or drops it) whatever the panel says- A sections panel's strip under the header no longer runs off the edge: what doesn't fit goes into More, as in the header
bodyClassNameonSheetContentandDialogContent, for a panel that lays itself out edge to edge (p-0);classNamenever reached the padded body- A
selectedcolour (bg-selected,text-selected-foreground, a tint of the accent) and aselectedButton variant, for the chosen tab, section, filter or page Panel::transliterate(): a search term's other ways of writing (Cyrillic ↔ Latin, as the app's alphabet says); every async picker and every table search, of a query or of rows, also matches them (SearchTerms);tables: falsekeeps them to the pickersLocationFieldandPlaceProvider: a place searched through the app's provider (a geocoder, or its own table), stored as its id;fills()copies the chosen place's coordinates or name into other fields of the formFieldset: one related record's fields edited inline with the parent's form — abelongsTo,morphTo,hasOneormorphOne, or a JSON column — and, with->morph('type_field', [...]), one of several kinds (each its own model and fields) following another field of the form. Compiled by both compilers; on Show its fields are entriesField::summarize(): a totals row under the table — sum, average, count, min or max of a column over every row the filters and search keep (one SQL aggregate, or over aListsRowslist's rows), per value of another column withby:(money per currency).DataTableSummaryandTableFooterin the stubs; rebuild a page to draw itMoneyField: an amount shown with its currency in the table and on Show — a fixed one or the record's own (currencyFrom('currency.name'), eager-loaded) — in the app's locale, rounded half up;MoneyField::formatUsing()for an app's own way of writing moneySelectField::dependsOn('carrier_id'): an async select whose options follow other fields — their values reach the query as a third argument (state), and changing one clears the choice; in a repeater's rows, the row's own fieldsDisplayField: a value the form shows but never saves — worked out from the record (->value(fn (?Model $record) => …)) and updated by another field's reaction as the form changes- Fields locked by the record:
Field::readonlyOnEdit()(set when the record is made),editableFor(fn ($record) => bool)(a price locked once signed) anddisabled()— shown read-only, ignored on save.Resource::fieldAccess($record)works per record; an overlay form gets its record's access with the edit data (_fieldAccess). Rebuild an overlay resource with access rules to pick it up DateFilter(on / on or before / on or after / between, with presets for today, this week, this month and last month) andNumberFilter(equals / at least / at most / between);Filter::valueOf()says what a filter means without a query — aListsRowsresource'srows()now gets a yes/no filter as a bool and a range as its endsListsRows: a Resource whose list is rows worked out in PHP (a report) rather than a query —rows($filters, $search), searched, sorted and paged in memory (DataTable::fromRows()), withrowMatches(),sortRows()andfilterValues()to override- An async
SelectFieldinside aRepeater's rows: its combobox searches asrepeater.field(lines.product_id), which the field-search endpoint now finds. Rebuild a resource that has one - The licensed compiler:
php artisan bezalel-core:compiler installdownloads bezalel-compile for the machine (Linux or macOS, Intel or ARM) with the app's Bezalel Core licence — the same one Composer installs bezalel/core with, as its password for packages.bezalel.rocks — runs it only once Bezalel's signature and its checksum match, and gives it the licence;bezalel-core:buildthen compiles with it, and the app's ID is kept inbezalel.json(commit it). An app'spost-install-cmd/post-update-cmdrunbezalel-core:compiler install --optional.bezalel-core:compiler statussays which compiler builds and the licence's state. See docs/guide/compiler.md bezalel-core:build --check: writes nothing, and fails naming every compiled file that differs from a fresh build — for CI- Live lists and record pages, with realtime on: a list reloads when one of its records is saved, deleted or restored, tinting the rows someone else changed (
Resource::live(),liveModels()for a list over a view or several tables); a record's page reloads when someone else changes it, or asks first if this person has typed (LiveRecord::share()for an app's own page); and who's here — the avatars of whoever has the record open, a pencil on whoever is typing, on the page and on its row (usePresence()for an app's own). See docs/guide/realtime.md. Runbezalel-core:stubs Panel::analytics(): a page that groups, splits, totals, filters, charts and forecasts the rows each person may see, worked out in their browser (DuckDB in memory, optional:BEZALEL_ANALYTICS); reports saved under a name and pinned to a dashboard; and, withBEZALEL_ANALYTICS_AI, a question in words answered with a report or a checked, read-only query. See docs/guide/analytics.md. Runbezalel-core:stubsPanel::globalSearch(): a search in the header (⌘K, Ctrl+K,/) over every Resource that sets$globallySearchableand the person may list, and the records they opened lately;Resource::globalSearchQuery()for a search of its own.Panel::listViews(): a list's search, filters, sort and view saved under a name, for oneself or everyone. Runbezalel-core:stubsPanel::appLauncher(): a sections panel's menu in one row — every section as a tile with its first pages and a search over every page, the open section's pages as tabs. Runbezalel-core:stubs- Keyboard shortcuts:
NavItem::shortcut()/NavSection::shortcut()— «g» then its key goes there; «?» lists them. Runbezalel-core:stubs Panel::utilityBar(): tools along the bottom of every page whose windows stay open across pages — «Notes» (each person's own) and «Recently viewed», and the app's own inresources/js/utilities/. Runbezalel-core:stubsRecordPath,RecordHighlightsandRecordHoverCard: a record's stages with the action that moves it on, its key fields pinned on top, and its key fields on hover over anything that names it. Runbezalel-core:stubsStatsWidgetandStat: a row of figures — a line under the value, its change against before, a tone and a small trend line — with no component of the app's.CalendarWidgetandCalendarEvent: a month of events from several tables at once, each opening its address or an overlay, with actions. Runbezalel-core:stubs- Dashboard pieces:
BarList(a ranked bar per figure),Progress,ProgressRingandDelta(a change coloured by what it means), on shared status tones (lib/tones). Runbezalel-core:stubs BulkAction::allMatching(): «Select all N» runs a bulk action on every row the table matches, 500 at a time, queued pastResource::$bulkInBackgroundAboveResource::$pollSeconds: a table that reloads itself while it's looked at, for figures that move with no event to hearField::opens('lead-card', fn ($row) => [...]): a cell that opens the app's own overlay (useOverlayOpener(),openOverlay()). Runbezalel-core:stubsResource::rowTone(): a row tinted by its record's state.Field::sticky(): a wide table keeps its first column and header in view- Small field options:
NumberField::prefix()/suffix()(a unit beside the input and in the table),RadioField::segmented(),ColorField,TextField::revealable()(a password with an eye) Repeater::addLabel()for its add-a-row button, and itshelp()line drawn;Field::audio(): a recording played in its row- Filters on one line:
Filter::asSwitch()(a small button group),Resource::$inlineFiltersandPage::$inlineFilters(past that many, one «Filters» button and chips);Filter::asTabs(all: false); a fixed-listSelectFilter::multiple()picked from checkboxes;DateFilter::range();DataTable::searchPlaceholder() - A list opens as this person last left it on this browser: its sort, and within twenty minutes its search and filters, unless the link says otherwise
- A long list's export is made in the background past
bezalel-core.exports.background_afterrows and downloads by itself when ready Panel::brandNameShown(false): the logo alone in the sidebar and header.Panel::passwordReset(false): no password reset, for a panel whose people get their passwords another wayUserResource::avatarFieldUsing(): an app's own photo field for its usersRecordDocuments: a record's documents opened from a page of the app's own
- **Breaking:**
bezalel-core:buildcompiles only with the licensed compiler — install it withphp artisan bezalel-core:compiler install(it needs a Bezalel Core licence; see docs/guide/compiler.md). A release of bezalel/core no longer carries the PHP compiler, so there is nothing to fall back to: without the compiler, the build stops and says how to install it. Apps keep running and deploying as they are — their compiled pages are committed - Placeholders while things load: a table's rows, cards and tab counts while a new look loads, the next page on scroll, and every panel, sheet and dialog while its content comes. Run
bezalel-core:stubs - A bar along the top while the page waits on the server, for every fetch; core's own screens use the DatePicker for every date and time, in the page's language (month and weekday names, Monday first in Russian). Run
bezalel-core:stubs - «Forgot your password?» says the same whether or not the account exists
- A Resource's save is one transaction: the record and what its fields save after it (a repeater's rows, a fieldset's record, an upload's attachment), so one that fails leaves no half-saved record
- Update the stubs with
bezalel-core:stubsinstead ofvendor:publish --tag=bezalel-core-stubs --force, which overwrote the app's changes - A table's sort, search, filter or page reloads the widgets above it too (
only: ['table', 'widgets']), so a widget that sums what the table lists follows its filters; update the stubs withbezalel-core:stubs - Generated pages give a card without a title its top padding as
pt-card(the card token) instead ofpt-4 sm:pt-6; rebuild withbezalel-core:build --forceonly if you want existing pages to follow it
- A Resource listing rows (
ListsRows) opens on its filters' defaults, hides the filters a person isn't offered, and counts its tab filters — over its rows by each row's value, or by its ownfilterCounts()(deferred with$defersTabCounts) - Two Resources with a card, list, action or widget component of the same name (
OrderLists/Cards/OrderCard.tsx,TrackedOrders/Cards/OrderCard.tsx) drew the same one — whichever was found last. Each is now found in its own pages directory first (lib/component-registry.ts,DataTable::componentScope(),scopeon record actions and widgets). Runbezalel-core:stubs - A Resource under a section (
routeSlug()orders/orders) had its pages, controller and row actions ask for routes by its slug (route('orders/orders.index')), which no route is called — the page threw in the browser.Resource::routeName()(orders.orders) names them, in both compilers. Rebuild such a Resource with--force - A page with a deferred widget (
Widget::defer()) went blank once its data came: Inertia's client put the widget alone in place of the list. With one deferred,widgetsis keyed by widget (Widget::asProp()) andWidgetGridtakes either. Runbezalel-core:stubs - A Resource under a section (
routeSlug()operations/leads) has its index, show, update and destroy routes named by its whole address (operations.leads.index), as its other routes are; they were named by the last segment (leads.index) - A slow page no longer leaves the old one sitting there as if nothing happened: past a moment the page dims under "Loading…", and the progress bar takes the accent colour so it shows on the canvas
- A class passed to a component replaces its spacing token again (
h-autooverh-control,p-0overp-card):cn()didn't know the token names and kept both, so a wrapping tab bar overflowed its box - In light mode the open section, tab, page, settings page and filter now stand out: they were a grey a step off the page's own, the same as hover
- A searchable select (
AsyncCombobox) is now as tall as the inputs and selects beside it - A collapsed sidebar no longer opens and shuts again on every page change
- A sections panel's header no longer scrolls sideways: the sections that don't fit move into the More menu, as a header menu's pages do
- A new app's page no longer stays white under
npm run dev(its layout lacked@viteReactRefresh) - A page error shows a message and a reload instead of a white window, and a page left open across a new build reloads onto it
- Picking an option in a select inside a popover no longer closes the popover
- A toolbar's icon-only action is the height of the buttons beside it
- A filter left at its default isn't counted as applied, and its chip reads «Status: Live»
- Tabs wrap instead of running off the page; a widget that draws nothing takes no room
- History: a record in the bin opens its history; a field the model hides is never shown; a list or object reads as JSON; an app's own activity model is used
- A repeater keeps a row's fields that have no rules of their own
- Downloads and imports say they're working, and a second click doesn't start another; a handled action's file reaches the browser
- The sign-in throttle's message is shown under the panel's own field; a sign-in link works while someone is signed in
- The comments sheet says why a comment was refused
bezalel-core:exportwrites its JSON untouched when an AI agent runs it