UI Components
The starter kit includes a small set of reusable UI helpers built on top of PrimeVue. They cover consistent modal behavior, avatar uploads, file previews, tag rendering, centralized toast rendering, and loading states.
Included Components
AppDialogAvatarUploadSkImageUploadImageLightboxFilePreviewModalSkCard- PrimeVue
Tag(SK-themed) - PrimeVue
Button(SK-themed, extended severity) - PrimeVue
Message/InlineMessage(SK-themed, extended severity) ToastComponent(SK custom toast renderer)ConfirmDialogComponentMimePickerFieldToggleFeatureCardSkPageLoaderTurnstileWidget- skeleton helpers:
PageLoading,SkeletonBox,SkeletonCard,SkeletonTable,SkeletonText FileManagercomponent for media-oriented flows
Global Overlays
AdminLayout.vue renders these once for the whole admin area:
@lvntr/components/ui/ConfirmDialogComponent.vue@lvntr/components/ui/ToastComponent.vue@lvntr/components/ui/AppDialog.vue@lvntr/components/ui/ImageLightbox.vue
These components are meant to be triggered from composables and page logic instead of being recreated in every page.
ImageLightbox and FilePreviewModal
File previews now use two different UI paths:
ImageLightboxfor fullscreen image viewingFilePreviewModalfor PDF, video, audio, text, and other non-image previews
Open ImageLightbox through useImageLightbox(). Open FilePreviewModal through useDialog().
AppDialog and useDialog()
AppDialog works with useDialog() to render dynamic Vue components in a single shared dialog.
import { useDialog } from '@/composables/useDialog';
import UserForm from '@/pages/Admin/Users/components/UserForm.vue';
const dialog = useDialog();
dialog.open(UserForm, { inDialog: true }, 'Edit user', {
refreshKey: 'users-table',
width: '640px',
});
Async mode
Use openAsync when the dialog component needs data fetched from the server before it can render:
await dialog.openAsync(UserForm, '/roles/1/data', 'Edit user', {
refreshKey: 'users-table',
mapResponse: (data) => ({ role: data }),
});
ConfirmDialogComponent and useConfirm()
The confirm dialog is globally mounted and styled once. Pages only call useConfirm().
import { router } from '@inertiajs/vue3';
import { useConfirm } from '@/composables/useConfirm';
const { confirmDelete } = useConfirm();
confirmDelete(() => {
router.delete('/users/1');
});
ToastComponent
ToastComponent is the shared PrimeVue toast container. In this project, it is typically fed by:
- flash messages from
AdminLayout.vue useApi()error handling
SkCard
SkCard is the shared wrapper around PrimeVue Card used by SkForm (and intended for SkDatatable and page-level cards). It guarantees a single, consistent caption header: title text on the left, an optional #title-end slot on the right (for action buttons, badges, status indicators), subtitle directly below, and a bottom divider that separates the whole caption block from the content.
<script setup lang="ts">
import SkCard from '@lvntr/components/ui/SkCard.vue';
</script>
<template>
<SkCard title="Users" subtitle="Active accounts">
<template #title-end>
<Button icon="pi pi-plus" :label="$t('users.create')" @click="open" />
</template>
<SkDatatable :config="config" />
</SkCard>
<!-- transparent shell — no background, shadow, padding, or divider visuals -->
<SkCard transparent>
<p>Anything goes here.</p>
</SkCard>
<!-- hide the caption divider when the section is purely decorative -->
<SkCard title="Notes" :divider="false">
<p>Quick note.</p>
</SkCard>
</template>
Props:
title?: string— shorthand for the#titleslot. Pass a translated string (no internal$tcall).subtitle?: string— shorthand for the#subtitleslot.transparent?: boolean(defaultfalse) — whentrue, the card renders without background, border, shadow, or padding. Useful inside dialogs or as an invisible grouping wrapper.divider?: boolean(defaulttrue) — draws a bottom border under the caption block (title + subtitle) so it reads as a distinct header above the content. Themed via--p-surface-200(light) /--p-surface-700(dark).pt?: Record<string, any>— extra PrimeVue Card passthrough. Merged with internal pt; consumer keys win on conflicts.
Slots:
header,title,subtitle,content,footer— pass through to PrimeVue Card. The default slot also maps tocontent, so<SkCard>…</SkCard>is equivalent to<SkCard><template #content>…</template></SkCard>.title-end— rendered to the right of the title in the same flex row.
Notes:
SkCardisinheritAttrs: falseinternally but forwardsclassonto the Card root viauseAttrs, so<SkCard class="my-cls">works as expected (plain class fallthrough is otherwise blocked by PrimeVue Card's owninheritAttrs: false).- The divider only renders when the caption is present and
divideristrue(default). A card without a title and subtitle gets no divider line.
Tag (PrimeVue)
The kit standardizes on PrimeVue's <Tag>, repainted to the SK palette. It is auto-imported (no import needed), and its severity accepts the 6 PrimeVue severities plus the supported SK palette names — so you get the full palette with no component patch (the theme targets the data-p attribute PrimeVue emits). SkDatatable tag columns render through this same <Tag>.
<template>
<Tag value="Active" severity="success" />
<Tag value="Pending" severity="amber" rounded class="p-tag-soft" />
<Tag value="Verified" icon="pi pi-check" severity="emerald" class="p-tag-outlined" />
<Tag value="Indigo" severity="indigo" />
</template>
Severity values:
- the 6 built-ins:
success,info,warn,danger,secondary,contrast - Tailwind families:
red,orange,amber,yellow,lime,green,emerald,teal,cyan,sky,blue,indigo,violet,purple,fuchsia,pink,rose,slate,gray,zinc,neutral,stone - SK custom families:
mauve,olive,mist,taupe
Variants — opt-in via class, since PrimeVue Tag has no variant prop:
p-tag-soft— lighter, tinted fillp-tag-outlined— border onlyp-tag-dot— leading status dotp-tag-sm/p-tag-lg— sizesrounded(native prop) — pill shape
All variants are themed for light and dark mode. See the Components → Tag showcase page (/sk-components) for every variant and color.
Button (PrimeVue)
PrimeVue Button is auto-imported and its severity prop accepts the 8 built-in PrimeVue values plus any Tailwind color family or SK custom color — the same extended palette the Tag uses. The theme targets the data-p-severity attribute that PrimeVue emits on the button root.
Built-in severities (8): primary (no prop, default), secondary, success, info, warn, help, danger, contrast.
Extended color values (same Tailwind + SK families as Tag): red, orange, amber, yellow, lime, green, emerald, teal, cyan, sky, blue, indigo, violet, purple, fuchsia, pink, rose, slate, gray, zinc, neutral, stone, mauve, olive, mist, taupe.
<template>
<Button label="Save" />
<Button label="Delete" severity="danger" outlined />
<Button label="Approve" severity="emerald" />
<Button label="Tag" severity="indigo" rounded />
<Button icon="pi pi-cog" severity="secondary" text />
</template>
Variants are the standard PrimeVue button props (outlined, text, raised, rounded, size, loading, disabled). No extra class is needed for color variants — severity alone drives the full color.
Destructive actions must use severity="danger" (outlined or filled depending on prominence). See the Components → Button showcase page (/sk-components) for the full per-color matrix.
Message and InlineMessage (PrimeVue)
PrimeVue Message and InlineMessage are auto-imported and repainted to the SK palette. Their severity accepts the same extended color set as Button and Tag. The theme targets the data-p attribute on Message and the p-inlinemessage-<severity> class on InlineMessage.
Built-in severities (6): success, info, warn, danger, secondary, contrast.
Extended color values: all Tailwind families and SK custom families (same list as Button/Tag above).
Message supports four layout variants:
- accent (default) — tinted left-border banner; use
:closable="false"for persistent notices - fill — solid-color filled banner, opt in with
class="p-message-fill" - outlined — border-only, opt in with
variant="outlined" - simple — minimal, no border or background, opt in with
variant="simple"
<template>
<!-- accent (default) -->
<Message severity="info" :closable="false">Your changes were saved.</Message>
<!-- fill variant -->
<Message severity="success" icon="pi pi-check-circle" class="p-message-fill">
<div class="font-semibold">Published</div>
<div class="text-[12.5px] opacity-80">All users can see this post.</div>
</Message>
<!-- outlined variant -->
<Message severity="warn" variant="outlined" :closable="false">Review required.</Message>
<!-- InlineMessage — inline, no dismiss -->
<InlineMessage severity="danger">Field is required.</InlineMessage>
<!-- Extended color -->
<Message severity="indigo">Custom severity.</Message>
</template>
See the Components → Message showcase page (/sk-components) for every variant and color.
Toast (SK custom renderer)
ToastComponent wraps PrimeVue's <Toast> with a fully custom #container template. Because the template is custom, PrimeVue's native severity markup does not apply — styling is driven entirely by .sk-toast-<severity> CSS classes in theme/main/components/toast.css. All toast.add() calls go through useToast() from PrimeVue.
Built-in severities: success, info, warn, error, secondary, contrast.
Extended color values: all Tailwind families and SK custom families (same list as Button/Tag above). Pass any family name as severity and the toast picks up the matching sk-toast-<name> color token.
Variants (opt in via styleClass):
- Default (accent) — tinted background, colored accent bar, progress bar
sk-toast-outlined— border-only shellsk-toast-solid— fully filled, inverted text
Extra options beyond the standard ToastMessageOptions:
icon— PrimeVue icon class (e.g.'pi pi-check-circle'); falls back to a per-severity default if omittedstyleClass— variant class ('sk-toast-solid'/'sk-toast-outlined')actions— pill-button array:{ label: string; command?: () => void; primary?: boolean; dismiss?: boolean }
import { useToast } from 'primevue/usetoast';
const toast = useToast();
// Simple info toast
toast.add({
severity: 'info',
summary: 'Saved',
detail: 'Your changes have been applied.',
group: 'bc',
life: 4000,
});
// Custom icon + solid variant
toast.add({
severity: 'success',
summary: 'Published',
icon: 'pi pi-globe',
styleClass: 'sk-toast-solid',
group: 'bc',
life: 3000,
});
// Action buttons (sticky — no life)
toast.add({
severity: 'warn',
summary: 'Delete item?',
detail: 'This cannot be undone.',
icon: 'pi pi-exclamation-triangle',
group: 'bc',
actions: [
{ label: 'Delete', primary: true, command: () => doDelete() },
{ label: 'Cancel' },
],
});
group: 'bc' is required — ToastComponent is registered on the bc group. See the Components → Toast showcase page (/sk-components) for live examples.
AvatarUpload
Avatar upload is a ready-made image picker/uploader component.
<AvatarUpload :avatar-url="user.avatar_url" upload-url="/user/avatar" delete-url="/user/avatar" />
Built-in behavior:
- instant preview with
FileReader - upload via
fetch - delete confirmation through
useConfirm() - Inertia reload after successful upload or delete
PageLoading
PageLoading.vue shows a skeleton overlay during Inertia navigation.
<PageLoading>
<template #skeleton>
<SkeletonTable :rows="6" :columns="4" />
</template>
<YourPageContent />
</PageLoading>
Other skeleton helpers available under @lvntr/components/Skeleton/:
SkeletonBox— generic placeholder boxSkeletonCard— card-shaped loading blockSkeletonTable— table-shaped loading block with configurable rows and columnsSkeletonText— text-line placeholders
SkImageUpload
SkImageUpload is a generic image upload slot for settings-style brand assets (logo, favicon). It mirrors AvatarUpload's optimistic-preview pattern — instant FileReader preview, fetch upload, and an Inertia partial reload on success — but is designed for rectangular/non-avatar contexts.
<SkImageUpload
:preview-url="settings.logo_url"
upload-url="/admin/settings/logo"
field-name="logo"
response-key="logo_url"
accept="image/png,image/svg+xml"
label="Light logo"
hint="Recommended: 300×80 px, PNG or SVG"
upload-label="Upload"
remove-label="Remove"
remove-confirm="Remove the logo?"
variant="logo-light"
layout="stacked"
:reload-only="['settings']"
/>
Props:
previewUrl?: string | null— canonical server URL;nullshows a placeholder iconuploadUrl: string— endpoint for both POST (upload) and DELETE (remove)fieldName: string— FormData key for the file (e.g.logo,favicon)responseKey: string— key underjson.dataholding the new URL after uploadaccept: string— passed to<input accept>label: string,hint: string— already-translated display stringsuploadLabel: string,removeLabel: string,removeConfirm: string— already-translated button/confirm stringsvariant?: 'logo-light' | 'logo-dark' | 'favicon'(default'logo-light') — preview box style;logo-darkuses a dark backdrop;faviconrenders a square boxlayout?: 'stacked' | 'row'(default'row') —stackedstacks preview + buttons vertically (used in logo grids);rowputs everything inlinereloadOnly?: string[](default['settings']) — Inertia partial-reload prop keys
MimePickerField
MimePickerField is a checkbox-group MIME-type picker used in the File Manager settings. It renders categories (Images, Documents, Archive) of MIME options as labeled checkboxes and emits a string[] of selected MIME strings.
<MimePickerField v-model="settings.allowed_mimes" />
Props:
modelValue?: string[] | null— currently selected MIME typescategories?: MimeCategory[]— override the default category list (Images / Documents / Archive). Each entry:{ titleKey: string; options: { label: string; value: string; icon: string }[] }
Default MIME groups: JPEG, PNG, GIF, WebP (images); PDF, DOC/DOCX, XLS/XLSX, plain text, CSV (documents); ZIP (archive).
ToggleFeatureCard
ToggleFeatureCard is a styled card-row with an integrated toggle switch, used for feature-flag settings UIs (e.g. enabling/disabling File Manager modules).
<ToggleFeatureCard
v-model="settings.share_enabled"
label="Share links"
description="Allow users to generate public share links for files."
icon="pi-share-alt"
/>
Props:
modelValue?: boolean(defaultfalse) — bound toggle statelabel: string— feature name, rendered in bolddescription?: string— secondary text below the labelicon?: string— PrimeVue icon name (withoutpiprefix, e.g.'pi-share-alt'); renders a tinted icon badge on the left when provided
The card border and background shift to a primary-tinted color when the toggle is on.
SkPageLoader
SkPageLoader is a full-screen animated loading overlay that appears during Inertia page switches. It replaces the default NProgress top bar with an animated radial grid background and a staggered letter-wave word. The animation and theming are defined in theme/main/components/page-loader.css.
It is opt-in — the shipped scaffold does not mount it. To enable it, add <SkPageLoader/> to the overlays slot of your AdminLayout.vue, alongside the other global overlays, and import it from the component library:
import SkPageLoader from '@lvntr/components/ui/SkPageLoader.vue';
<template #overlays>
<ConfirmDialogComponent />
<ToastComponent />
<AppDialog />
<ImageLightbox />
<SkPageLoader :delay="250" />
</template>
Props:
delay?: number(default250) — milliseconds before the overlay appears; prevents flashing on instant navigations
SkPageLoader uses usePageLoading() internally and reads the localized sk-layout.loading string for the animated word.
TurnstileWidget
TurnstileWidget embeds a Cloudflare Turnstile challenge widget. It is used by the kit's auth pages (login, register, password reset) and can be placed in any form that needs bot protection.
For full configuration and backend setup, see the Authentication documentation.
<TurnstileWidget v-model="form.turnstile_token" />
Props:
modelValue: string— the Turnstile token emitted after a successful challenge. Bind it to your form's token field and include it in the submit payload.
Exposes (via defineExpose):
reset()— reset the widget (useful after a failed form submit)loadFailed: Ref<boolean>—trueif the Cloudflare script failed to load
The widget only renders when page.props.turnstile.enabled is true. If Turnstile is disabled in the app settings, the component renders nothing. The Cloudflare script is loaded lazily on mount and is shared across widget instances on the same page.
Recommendation
Prefer reusing these components before introducing new local variants. That keeps the admin panel visually consistent and makes future kit updates easier.