Composables
Kit composables are shipped inside the package and run directly from the vendor library by default — they no longer need to be copied into the consumer app. Imports throughout the application use @/composables/<name> (or the bare @/composables barrel) exactly as before; a Vite customResolver plus a matching tsconfig path entry resolve those paths local-first, then vendor: if a file exists under the consumer's resources/js/composables/ it wins, otherwise the vendor copy is used automatically.
useAdminMenu is the only composable that still ships as an editable stub (resources/js/composables/useAdminMenu.ts). It depends on the consumer's generated @/routes/* files and is the project's own menu definition, so it must remain editable. The @/composables/index.ts barrel also stays as a stub.
Upgrading composables via Composer
Because the kit composables live in the package, they are updated when you run composer update lvntr/laravel-starter-kit. No manual file copying is required.
Publishing composables for customization
To edit a composable, publish it to the consumer app first:
php artisan sk:publish --tag=composables
This copies the current vendor versions into resources/js/composables/. Once a local copy exists the local-first resolver picks it up automatically — no alias changes or build config edits are needed.
Migration note for existing installs
Projects created before this change already have all composables under resources/js/composables/. The local-first resolver means those local copies continue to be used, so nothing breaks. The trade-off is that those projects will not receive upstream composable fixes via composer update until the unmodified files are removed. To opt into vendor-managed upgrades, delete the composables you have not customized from resources/js/composables/ — keeping useAdminMenu.ts, index.ts, and any file you have intentionally edited. The kit never auto-deletes local files.
Commonly Used Composables
useApifor JSON requests using the API response envelopeuseCanfor permission and role checks from Inertia shared propsuseDefinitionfor cached definition loadinguseDialogfor dialog state and remote-loading flowsuseImageLightboxfor fullscreen image preview from FileManager and file-upload fieldsuseConfirmfor confirmation actionsuseFlashfor flash message handlinguseDarkModefor dark mode persistenceuseThemefor applying the active runtime theme (main/aura)usePageLoadingfor Inertia loading stateuseRefreshBusfor forcing table or widget refreshesuseSidebarfor responsive sidebar stateuseUrlTabfor tab state synced to the URLuseAdminMenuanduseMenuBuilderfor admin navigation composition
Core Request and Dialog Helpers
useApi()
Small fetch() wrapper for the project's to_api() / ApiResponse JSON envelope.
- Adds
Accept: application/jsonandX-Requested-With: XMLHttpRequest - Adds
X-XSRF-TOKENwhen available - Unwraps the
datapayload - Throws
ApiErroron failed responses - Can show PrimeVue toast errors unless
toast: falseis passed
const api = useApi();
const user = await api.get<User>('/api/v1/users/1');
await api.post('/api/v1/users', { name: 'John Doe' });
await api.put('/api/v1/users/1', { name: 'Jane Doe' });
await api.patch('/api/v1/users/1', { status: 'active' });
await api.delete('/api/v1/users/1');
useConfirm()
PrimeVue ConfirmationService wrapper with two helpers:
confirmDelete(onAccept, message?, icon?)confirmAction({ message, onAccept, header?, icon?, acceptLabel?, rejectLabel?, acceptClass? })
const { confirmDelete, confirmAction } = useConfirm();
confirmDelete(() => {
console.log('Delete accepted');
});
confirmAction({
message: 'Publish this record now?',
acceptLabel: 'Publish',
onAccept: () => console.log('Confirmed'),
});
useDialog()
Global dialog manager used together with @lvntr/components/ui/AppDialog.vue.
open(component, props?, header?, options?)openAsync(component, url, header?, options?, baseProps?)close()setLoading(val)
If refreshKey is provided in options, onSuccess and onCancel callbacks are injected automatically.
useImageLightbox()
Shared fullscreen image preview state rendered through the global ImageLightbox overlay in AdminLayout.vue.
open(url, name?)close()state.visible,state.url,state.name
Use this for images. For non-image files, keep using useDialog() with FilePreviewModal.
Authorization and Navigation
useCan()
Reads permission and role data from Inertia shared props.
can(permission)canAny(permissions)hasRole(role)
useAdminMenu()
Defines the project's admin sidebar items and delegates visibility and active-state behavior to useMenuBuilder().
useMenuBuilder()
Shared menu helper for sidebar-style navigation.
- Filters top-level items and child items by permission and role
- Removes empty section headers after filtering
- Detects active links for both plain URLs and URLs with query parameters
- Keeps parent groups open when one of their children is active
const allItems: MenuItem[] = [{ title: 'sk-menu.dashboard', href: '/dashboard' }];
return useMenuBuilder(allItems);
useUrlTab()
Keeps a tab selection in sync with a query string key such as ?tab=security. tabs accepts a plain array (a reactive one included — mutations are tracked live, so a tab list that changes after mount, e.g. one filtered by permission, stays in sync), a ref, or a getter (MaybeRefOrGetter<TabDefinition[]>), read through toValue() on every access. Setting the active tab to its current value is a no-op (no navigation fires); setting it to the list's first entry removes the query param instead of writing it; any #hash on the current URL is preserved across the switch. An optional third argument, { history: 'push' | 'replace' }, controls the history entry written on a switch — 'replace' (default) overwrites the current entry, 'push' gives each switch its own. SkTabs no longer builds on this composable directly — it owns an internal, equivalent active-tab state so its behavior never depends on whichever copy of useUrlTab an app has published.
useRefreshBus()
Simple global refresh bus for components such as DataTable. Registered callbacks are automatically cleaned up on component unmount.
on(key, callback)— register a refresh callbackrefresh(...keys)— trigger one or more named refresh callbacksrefreshAll()— trigger all registered callbacks
const bus = useRefreshBus();
bus.on('users-table', () => fetchData());
bus.refresh('users-table');
UI State Helpers
useSidebar()
Handles desktop collapse state and mobile drawer state for the admin sidebar.
useDarkMode()
Persists dark mode in local storage and toggles the .dark class on <html>.
useTheme()
Applies the active runtime theme (main, aura) to <html> via the data-sk-theme attribute, driven by the admin-wide appearance.theme value from Inertia shared props.
theme— the resolved runtime theme name (undefinedduring a partial reload that omits theappearanceprop)runtimeThemes— the set of instantly-switchable themes; falls back to['main', 'aura']when the server omitsruntime_themesapplyTheme(value)— sets or removesdata-sk-themeon<html>
usePageLoading()
Tracks Inertia navigation state using inertia:start and inertia:finish browser events.
useFlash()
Returns reactive flash data from Inertia shared props.
In this project, flash messages are displayed in AdminLayout.vue, not inside the composable itself.
Definition Helpers
useDefinition()
Loads definition records from the authenticated /definitions endpoint and stores them in a shared reactive cache.
load(keys)— loads only the requested keys; deduplicates through shared cacheloadAll()— loads all available definition keyslist(key, filter?)— returns raw definition items, optionally filteredoptions(key, filter?)— returns items as{ label, value }pairs for selectsfind(key, value)— looks up a single item by valueclearCache()— resets the reactive cacheloaded— reactive boolean that becomestrueonce any load completes
Typical keys in this project include userStatus, gender, identityType, and yesNo.
const { load, options, find } = useDefinition();
await load(['userStatus', 'gender']);
const statusOptions = options('userStatus');
const activeStatus = find('userStatus', 'active');
list() and options() accept an optional filter object with only or except arrays when you need a subset of a definition list:
// Only show active and pending statuses
const filteredOptions = options('userStatus', { only: ['active', 'pending'] });
// Exclude a specific status
const filteredOptions = options('userStatus', { except: ['archived'] });
DataTable Selection
useDatatableSelection()
Manages row selection and bulk action submission for SkDatatable. Consumer pages import this directly — it is the recommended way to add checkboxes and bulk actions to any index screen.
Types exported:
type BulkSelectionMode = 'page' | 'all';
interface BulkActionPayload {
action: string;
ids: (string | number)[];
select_all_filtered: boolean;
filter_snapshot: Record<string, unknown>;
[key: string]: unknown;
}
interface BulkActionResult {
processed: number;
skipped: number;
failed: Array<{ id: number | string; reason: string }>;
message: string;
}
Signature:
const selection = useDatatableSelection({
bulkUrl: string; // Absolute URL for the bulk endpoint (use Wayfinder: users.bulk.url())
idKey?: string; // Row property used as ID — default: 'id'
onSuccess?: () => void; // Called after a successful bulk action (refresh the table here)
});
Returns:
| Property / Method | Type | Description |
|---|---|---|
selectedIds |
Ref<Set<string|number>> |
Currently selected row IDs |
selectionMode |
Ref<BulkSelectionMode> |
'page' (current page) or 'all' (cross-page filtered) |
submitting |
Ref<boolean> |
True while the bulk request is in flight |
selectedCount |
ComputedRef<number> |
Number of selected IDs |
hasSelection |
ComputedRef<boolean> |
True when at least one row is selected |
isAllFilteredMode |
ComputedRef<boolean> |
True when cross-page selection is active |
toggleRow(row) |
function | Select or deselect one row |
isRowSelected(row) |
function | Returns true if row is selected |
togglePageSelection(rows, selected) |
function | Select or deselect all rows on the current page |
isPageFullySelected(rows) |
function | True when every row on the page is selected |
isPagePartiallySelected(rows) |
function | True when some (not all) rows on the page are selected |
selectAllFiltered() |
function | Activate cross-page mode — backend recomputes from filters |
clearSelection() |
function | Clear all selected IDs and reset to page mode |
executeBulkAction(action, filterSnapshot?, overrideUrl?) |
function | Post the bulk payload via Inertia router |
Usage:
import { useDatatableSelection } from '@/composables/useDatatableSelection';
import { useRefreshBus } from '@/composables/useRefreshBus';
import users from '@/routes/users';
const bus = useRefreshBus();
const selection = useDatatableSelection({
bulkUrl: users.bulk.url(),
idKey: 'id',
onSuccess: () => bus.refresh('users-table'),
});
// Bind to SkDatatable:
// <SkDatatable :selection="selection" ...>
// Trigger a bulk delete with current filters:
selection.executeBulkAction('delete', activeFilterSnapshot.value);
Pass selection to <SkDatatable :selection="selection"> — this renders the checkbox column. Bulk action buttons go in the #bulk-actions slot; while rows are selected, SkDatatable shows a floating dark action bar at the bottom of the viewport. See docs/datatable.md for the full bulk-action pattern including the backend BulkAction interface.
Internal Composables
The composables below are used by vendor UI and are not intended to be called directly from consumer pages. They are listed for reference.
useFileShare() — Internal
Used by the vendor Files module (ShareLinkModal, MyShareLinksDrawer) to create and revoke signed share links for FileManager media. Consumer pages do not call this directly — share link actions are available through the built-in Files UI. See docs/file-manager.md for the share link API.
createShare(mediaId: number, ttlHours: number): Promise<ShareLinkResult | null>— creates a signed share link (TTL: 1–720 hours); returns{ url, expires_at, token_hash }ornullon errorrevokeShare(mediaId: number, token: string): Promise<boolean>— revokes an existing link by token hash
useAccentColor() — Internal
Used by AdminLayout and the Appearance tab to manage the per-user accent color preference and sidebar surface. Consumers interact with accent color through the header popover, not by calling this composable directly. See docs/theme.md for the accent color system.
useAppearanceDefaults() — Internal
Reads the global appearance defaults (accent color, dark mode, sidebar style, logo and favicon URLs) from Inertia shared props on every page load. Used by useAccentColor, useDarkMode, and layouts to seed initial state before any per-user override is applied.
getXsrfToken() — Internal
Exported from useCsrf.ts. Single source of truth for reading the Laravel XSRF-TOKEN cookie — useApi(), the FileManager upload XHR, and the rich-text editor's image upload all read through this so cookie parsing stays consistent. Returns '' when the cookie is absent (SSR, or not yet set).
getXsrfToken(): string
withBasePath() — Internal
Exported from useBasePath.ts. Used by vendor UI (the rich-text EditorInput, FileManager) for raw fetch/XMLHttpRequest calls that build their own URL and need the app's deploy sub-path prefix; Inertia navigation already honors the base automatically.
withBasePath(path: string): string
Recommendation
When a UI behavior appears in more than one page, move it into a composable before repeating it inline.