Appearance
Frontend (Vue 3 + Inertia.js v3)
Stack Summary
- Vue 3 (Composition API,
<script setup lang="ts">) - Inertia.js v3 (
@inertiajs/vue3,@inertiajs/vite) - TypeScript
- Vite 8 build pipeline
- Tailwind CSS v4 (with forms & typography plugins) + Preline component library
- Pinia for state management
- Vue I18n for localization
- Sentry SDK for error & performance monitoring
Directory Conventions
resources/js/
Pages/ # Page components mapped to routes (Auth/, Client/, Shop/, Profile/, Public/)
Components/ # Reusable presentational or functional components
Elements/ # Domain UI building blocks (Agenda, TableComponent, Form, …)
Directives/ # Custom Vue directives (clickoutside)
Helper/ # Shared helper modules
Hooks/ # Composables (useSearch, useModal, useTable, useShortcuts, …)
Layouts/ # AppLayout, ClientLayout, GuestLayout (+ Partials)
Locales/ # vue-i18n catalogs (en, fr)
Modals/ # Modal components, resolved lazily by the modal plugin
Plugins/ # auth, broadcasting, i18n, modal, switchDark, eventBus, …
Stores/ # Pinia stores
Utils/ # FormatNumber, FormatString, StringDate, UrlParams, HydrateEnums, …
types/ # Domain TS definitions (generated.ts, enumRegistry.ts, Props.ts, forms.ts)
app.ts # Inertia app bootstrap- Do NOT edit
types/generated.tsortypes/enumRegistry.ts; both are regenerated from PHP. - Component tests live in
__tests__/folders next to the source.
Layout Resolution
Handled in app.ts:
Pages/Auth/*and the top-levelErrorpage →GuestLayoutPages/Client/*→ClientLayout- everything else →
AppLayout
Override with defineOptions({ layout: MyLayout }); opt out with noLayout: true.
Enum Hydration
Backend enums are serialized as { __enum, value } envelopes. hydrateEnums() (from @/Utils/HydrateEnums.ts) runs in the Inertia resolve hook on every page swap — initial load, visits, partial reloads and history restores — so props are already hydrated by the time a component renders. Read the hydrated value (or the helpers in @/types/enumRegistry.ts), never the raw envelope.
Routing & Navigation
- Server-generated routes via
tightenco/ziggy; always reference routes with theroute()helper (or theuseRoutecomposable). - Navigate with Inertia's
<Link>rather than<a>to preserve SPA state. Import it asInertiaLinkso it doesn't collide with the HTML<link>element.
HTTP & Forms
Two complementary APIs, both from @inertiajs/vue3:
useForm— classic Inertia form state (processing,errors,reset, visit callbacks). Still the default for page forms.useHttp<TForm, TResponse>(data)— for requests that return data rather than a page visit. Use it instead of axios; the codebase has no axios dependency.
useHttp rules:
- Bind
http.<field>directly in templates — do NOT mirror fields into separate refs. - Type the payload with an explicit object type or
Record<string, string | number | boolean | null | undefined>— notRecord<string, unknown>.
Common Patterns
- Disable the submit button while
processing. - Show inline errors near fields.
- Use
preserveScrollto avoid scroll jumps on validation failure.
Gotchas
- When mixing file + JSON data, rely on
forceFormDatato prevent ambiguous encoding. - Do not manually append CSRF tokens; Inertia handles this via Laravel middleware.
State Management (Pinia)
- Stores live under
Stores/. - Prefer explicit actions over directly mutating state for traceability.
- Use stores for cross-page state only (ephemeral local state stays in components).
Internationalization
vue-i18nwith catalogs inresources/js/Locales/{en,fr}, compiled by@intlify/unplugin-vue-i18n.- Server-side strings live in
lang/; backend translatable model attributes use Spatie Translatable. - The active locale is shared through Inertia props and applied by
setupI18n()inapp.ts.
Styling (Tailwind v4)
- Import Tailwind with
@import "tailwindcss";in theresources/cssentry. - Use the semantic theme tokens (
bg-primary,text-foreground,border-line-2,bg-card, …). They handle light and dark mode automatically — seetheme.mdfor the full token reference. Do not hard-code hex values or raw palette colors. - Prefer
gap-*over margin hacks for spacing in flex/grid containers. dark:variants are for the rare override a token can't express; dark mode is toggled via@/Plugins/switchDark.ts.
Design System (Preline + Streamline Icons)
The UI builds on Tailwind v4 plus the Preline component library for accessible primitives. Preline is initialized in app.ts (HSStaticMethods.autoInit), with a MutationObserver re-initializing it after DOM updates — dynamically inserted Preline markup works without manual wiring.
Icons come from StreamlineHQ. Drop the SVG into resources/icons/; the Vite watcher runs npm run generate:icons and the icon becomes available through the generated barrel. Do not inline raw external SVGs in pages.
Components & Conventions
- Single root element per Vue component.
- Kebab-case component names in templates (ESLint
vue/component-name-in-template-casing); PascalCase filenames. - One attribute per line (
vue/max-attributes-per-linesingleline: 1, plus Prettier'ssingleAttributePerLine). - 4-space indentation in both
<template>and<script>(vueIndentScriptAndStyle). - Floating UI & tooltips via
@headlessui-float/vueandvue-tippy(registered globally as thev-tooltipdirective).
Realtime & Events
- WebSockets via
laravel-echo+pusher-js, connecting to the Reverb server; wiring lives in@/Plugins/broadcasting.ts. - Model broadcasts arrive on
models.{morphClass}.{id}channels.
Error & Performance Monitoring
- Sentry Vue integration initialized in
app.ts(@sentry/vuewith browser tracing and the Pinia plugin). - DSN comes from
VITE_SENTRY_LARAVEL_DSN; source maps are uploaded by@sentry/vite-pluginat build time.
Testing (Vitest)
- Unit & component testing via
vitest+@testing-library/vue/@vue/test-utils. - DOM environment simulated using
happy-dom(seevitest.config.tsandvitestSetupFile.js). npm run test(watch),npm run test:coverage.
Imports & Aliases
- Path alias
@→resources/js(defined intsconfig.jsonand resolved by Vite). - Linting uses ESLint flat config (
eslint.config.js) witheslint-plugin-import-x. The ruleimport-x/extensionsis set toalwayswithignorePackages: true— relative imports need their.ts/.vueextension, bare package specifiers do not.
Performance Guidelines
- Pages are code-split automatically by Inertia's dynamic
resolvePageComponentimports;Pages/Auth/*is pinned to its own chunk and vendor code to avendorchunk (seevite.config.js). - Lazy-load heavy modules (e.g., TipTap) with dynamic import.
- Avoid large reactive objects in global stores; prefer granular refs.
- Use
watchEffectsparingly; prefercomputedfor derived state.
Common Patterns
Example page component:
vue
<script setup lang="ts">
import { Link as InertiaLink } from '@inertiajs/vue3';
import { ref } from 'vue';
import CarCard from '@/Components/Car/CarCard.vue';
const filter = ref('');
</script>
<template>
<div class="space-y-4">
<div class="flex items-center gap-2">
<input
v-model="filter"
type="text"
class="bg-layer border-line-2 rounded-lg border px-3 py-2"
placeholder="Search"
/>
<inertia-link
:href="route('shop.car.create')"
class="bg-primary text-primary-foreground hover:bg-primary-hover rounded-lg px-4 py-2"
>
New car
</inertia-link>
</div>
<car-card
v-for="car in cars"
:key="car.id"
:car="car"
/>
</div>
</template>Proceed to dependencies.md for the package landscape.