feat(v5): add shadcn/ui with button, select, separator, and navbar

This commit is contained in:
Andras Bacsai committed 2026-06-16 19:38:21 +02:00
1 parent bc6a12d614
commit c178dfdb39
32 files changed
+8788 -74

No files matched your search

+157 -8
View File
@@ -1,11 +1,160 @@
@font-face {
font-display: swap;
font-family: 'Geist Sans';
font-style: normal;
font-weight: 100 900;
src: url('../../fonts/geist-sans-variable.woff2') format('woff2');
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@fontsource-variable/geist";
@source '../../js/v5/**/*.{js,jsx}';
@source '../../../storage/framework/views/*.php';
@custom-variant dark (&:where(.dark, .dark *));
@theme inline {
--font-sans: 'Geist Variable', sans-serif;
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--font-heading: var(--font-sans);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
body {
font-family: 'Geist Sans', sans-serif;
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
--primary: oklch(0.21 0.006 285.885);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.21 0.006 285.885);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.871 0.006 286.286);
--chart-2: oklch(0.552 0.016 285.938);
--chart-3: oklch(0.442 0.017 285.786);
--chart-4: oklch(0.37 0.013 285.805);
--chart-5: oklch(0.274 0.006 286.033);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
--sidebar-primary: oklch(0.21 0.006 285.885);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
}
.dark {
--background: #101010;
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.92 0.004 286.32);
--primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.552 0.016 285.938);
--chart-1: oklch(0.871 0.006 286.286);
--chart-2: oklch(0.552 0.016 285.938);
--chart-3: oklch(0.442 0.017 285.786);
--chart-4: oklch(0.37 0.013 285.805);
--chart-5: oklch(0.274 0.006 286.033);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.552 0.016 285.938);
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
@apply border-border outline-ring/50;
}
html,
body,
#v5-app {
min-height: 100%;
}
html {
@apply font-sans;
}
body {
@apply bg-background text-foreground;
min-height: 100vh;
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button:not(:disabled),
[role="button"]:not(:disabled) {
cursor: pointer;
}
}
+25 -38
View File
@@ -1,47 +1,34 @@
import { Head } from '@inertiajs/react';
export default function Home({ flux, clusters }) {
import { AppNavbar } from '@/components/app-navbar';
export default function Home({
flux,
clusters = [],
projects = [],
selectedProjectUuid = null,
selectedEnvironmentUuid = null,
}) {
return (
<>
<Head title="V5" />
<Head title="Magic" />
<main>
<h1>Coolify v5</h1>
<div className="h-dvh overflow-hidden bg-background text-foreground">
<AppNavbar
flux={flux}
clusters={clusters}
projects={projects}
selectedProjectUuid={selectedProjectUuid}
selectedEnvironmentUuid={selectedEnvironmentUuid}
/>
<section aria-label="Flux status">
<p>
<strong>Flux:</strong> {flux.label} — {flux.socket ? flux.socket : flux.message}
</p>
</section>
<section aria-labelledby="clusters-heading">
<h2 id="clusters-heading">Clusters</h2>
{clusters.length === 0 ? (
<p>No clusters have been added yet.</p>
) : (
<ul>
{clusters.map((cluster) => (
<li key={cluster.id}>
<strong>{cluster.name}</strong> — {cluster.serversCount}{' '}
{cluster.serversCount === 1 ? 'server' : 'servers'}
{cluster.description ? <p>{cluster.description}</p> : null}
{cluster.servers.length > 0 ? (
<ul>
{cluster.servers.map((server) => (
<li key={server.id}>
{server.name} ({server.status}) — {server.host};{' '}
{server.capabilities.join(', ')}
</li>
))}
</ul>
) : null}
</li>
))}
</ul>
)}
</section>
</main>
<main className="flex h-full min-h-0 items-center justify-center overflow-hidden px-6 pt-16">
<section className="flex w-full max-w-5xl flex-col items-center justify-center gap-3 rounded-lg border border-dashed border-border bg-card px-8 py-24 text-center">
<p className="text-sm font-medium text-foreground">Magic</p>
<p className="max-w-md text-sm text-muted-foreground">This is where the magic happens.</p>
</section>
</main>
</div>
</>
);
}
+119
View File
@@ -0,0 +1,119 @@
import { useMemo, useState } from 'react';
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
function csrfToken() {
return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') ?? '';
}
function persistSelection(projectUuid, environmentUuid) {
void fetch('/v5/selection', {
method: 'POST',
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken(),
},
body: JSON.stringify({
project_uuid: projectUuid,
environment_uuid: environmentUuid,
}),
});
}
export function AppNavbar({
flux,
clusters = [],
projects = [],
selectedProjectUuid = null,
selectedEnvironmentUuid = null,
}) {
const firstProject = projects[0] ?? null;
const [projectUuid, setProjectUuid] = useState(selectedProjectUuid ?? firstProject?.uuid ?? '');
const selectedProject = useMemo(
() => projects.find((project) => project.uuid === projectUuid) ?? firstProject,
[firstProject, projectUuid, projects],
);
const firstEnvironment = selectedProject?.environments?.[0] ?? null;
const [environmentUuid, setEnvironmentUuid] = useState(selectedEnvironmentUuid ?? firstEnvironment?.uuid ?? '');
const selectedEnvironment = useMemo(
() => selectedProject?.environments?.find((environment) => environment.uuid === environmentUuid) ?? firstEnvironment,
[environmentUuid, firstEnvironment, selectedProject],
);
function selectProject(nextProjectUuid) {
const nextProject = projects.find((project) => project.uuid === nextProjectUuid);
const nextEnvironmentUuid = nextProject?.environments?.[0]?.uuid ?? '';
setProjectUuid(nextProjectUuid);
setEnvironmentUuid(nextEnvironmentUuid);
persistSelection(nextProjectUuid, nextEnvironmentUuid);
}
function selectEnvironment(nextEnvironmentUuid) {
setEnvironmentUuid(nextEnvironmentUuid);
persistSelection(projectUuid, nextEnvironmentUuid);
}
return (
<header className="fixed inset-x-0 top-0 z-40 border-b border-border bg-background">
<nav className="flex h-16 items-center gap-4 px-6" aria-label="Main navigation">
<a
href="/v5"
className="flex shrink-0 items-center rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
aria-label="Coolify home"
>
<img src="/coolify-logo.svg" alt="Coolify" className="size-8" />
</a>
<div className="flex min-w-0 items-center gap-2">
<Select value={selectedProject?.uuid ?? ''} onValueChange={selectProject} disabled={projects.length === 0}>
<SelectTrigger aria-label="Select a project" variant="ghost" className="max-w-[10rem]">
<SelectValue placeholder="Select a project" />
</SelectTrigger>
<SelectContent position="popper" align="start" sideOffset={4}>
<SelectGroup>
{projects.map((project) => (
<SelectItem key={project.uuid} value={project.uuid}>
{project.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<span className="text-muted-foreground">/</span>
<Select
value={selectedEnvironment?.uuid ?? ''}
onValueChange={selectEnvironment}
disabled={!selectedProject || (selectedProject.environments ?? []).length === 0}
>
<SelectTrigger aria-label="Select an environment" variant="ghost" className="max-w-[10rem]">
<SelectValue placeholder="Select an environment" />
</SelectTrigger>
<SelectContent position="popper" align="start" sideOffset={4}>
<SelectGroup>
{(selectedProject?.environments ?? []).map((environment) => (
<SelectItem key={environment.uuid} value={environment.uuid}>
{environment.name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="ml-auto flex items-center gap-3">
<div
className="hidden rounded-md border border-border bg-muted/40 px-3 py-1 text-xs text-muted-foreground lg:block"
title={flux?.socket ?? flux?.message}
>
Flux: {flux?.label ?? 'Unknown'} · {clusters.length} clusters
</div>
</div>
</nav>
</header>
);
}
+61
View File
@@ -0,0 +1,61 @@
import * as React from "react"
import { cva } from "class-variance-authority";
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
icon: "size-8",
"icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3",
"icon-sm": "size-7 rounded-none",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
asChild = false,
...props
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props} />
);
}
export { Button, buttonVariants }
+185
View File
@@ -0,0 +1,185 @@
import * as React from "react"
import { Select as SelectPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { CaretDownIcon, CheckIcon, CaretUpIcon } from "@phosphor-icons/react"
function Select({
...props
}) {
return <SelectPrimitive.Root data-slot="select" {...props} />;
}
function SelectGroup({
className,
...props
}) {
return (
<SelectPrimitive.Group
data-slot="select-group"
className={cn("scroll-my-1", className)}
{...props} />
);
}
function SelectValue({
...props
}) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}
function SelectTrigger({
className,
size = "default",
variant = "default",
children,
...props
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"flex w-fit items-center justify-between gap-1.5 rounded-none whitespace-nowrap transition-colors outline-none select-none focus-visible:ring-1 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-none *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variant === "default" && "border border-input bg-transparent py-2 pr-2 pl-2.5 text-xs focus-visible:border-ring dark:bg-input/30 dark:hover:bg-input/50",
variant === "ghost" && "h-auto border border-transparent bg-transparent px-0 py-0 text-sm font-medium text-foreground hover:bg-transparent focus-visible:border-transparent dark:bg-transparent dark:hover:bg-transparent",
className
)}
{...props}>
{children}
<SelectPrimitive.Icon asChild>
<CaretDownIcon className="pointer-events-none size-4 text-muted-foreground" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
);
}
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
...props
}) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
data-align-trigger={position === "item-aligned"}
className={cn(
"relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-none bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
align={align}
{...props}>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
data-position={position}
className={cn(
"data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
position === "popper" && ""
)}>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
);
}
function SelectLabel({
className,
...props
}) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("px-2 py-2 text-xs text-muted-foreground", className)}
{...props} />
);
}
function SelectItem({
className,
children,
...props
}) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"relative flex w-full cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}>
<span
className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="pointer-events-none" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
);
}
function SelectSeparator({
className,
...props
}) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("pointer-events-none -mx-1 h-px bg-border", className)}
{...props} />
);
}
function SelectScrollUpButton({
className,
...props
}) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}>
<CaretUpIcon />
</SelectPrimitive.ScrollUpButton>
);
}
function SelectScrollDownButton({
className,
...props
}) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}>
<CaretDownIcon />
</SelectPrimitive.ScrollDownButton>
);
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}
@@ -0,0 +1,27 @@
"use client"
import * as React from "react"
import { Separator as SeparatorPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
className
)}
{...props} />
);
}
export { Separator }
+6
View File
@@ -0,0 +1,6 @@
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
+28 -3
View File
@@ -1,13 +1,38 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title inertia>{{ config('app.name') }} v5</title>
@viteReactRefresh
@vite('resources/js/v5/app.jsx')
@php
$viteHotFile = public_path('hot');
$viteDevServerUrl = null;
if (file_exists($viteHotFile)) {
$viteHotUrl = trim(file_get_contents($viteHotFile));
$viteScheme = parse_url($viteHotUrl, PHP_URL_SCHEME) ?: request()->getScheme();
$vitePort = parse_url($viteHotUrl, PHP_URL_PORT) ?: 5173;
$viteHost = request()->getHost();
$viteDevServerUrl = "{$viteScheme}://{$viteHost}:{$vitePort}";
}
@endphp
@if ($viteDevServerUrl)
<script type="module">
import RefreshRuntime from '{{ $viteDevServerUrl }}/@react-refresh'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
<script type="module" src="{{ $viteDevServerUrl }}/@@vite/client"></script>
<script type="module" src="{{ $viteDevServerUrl }}/resources/js/v5/app.jsx"></script>
@else
@viteReactRefresh
@vite('resources/js/v5/app.jsx')
@endif
<x-inertia::head />
</head>