> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dezerx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Creating a Theme

> Restyle the customer-facing UI by overriding only the pages and components you care about. Everything you leave alone falls back to the default theme.

A theme is a folder. Drop it in `resources/js/pages/themes/`, pick it in the admin settings, and your
versions of the pages take over. You never have to copy the whole UI.

<Info>
  This page covers the **client theme** system, the one that restyles the dashboard, market, tickets
  and auth screens. It is a different thing from **portal themes**, which change the public landing
  page and are managed as database records under Customization. See
  [Portal themes vs client themes](#portal-themes-vs-client-themes).
</Info>

## How it works

<Steps>
  <Step title="You create a folder">
    `resources/js/pages/themes/aurora/`. The folder name is the theme name.
  </Step>

  <Step title="The admin settings page finds it">
    DezerX scans the themes directory on every load, so there is nothing to register.
  </Step>

  <Step title="An admin activates it">
    That writes the `active_theme` setting, which is shared with the frontend on every request.
  </Step>

  <Step title="Inertia resolves your file first">
    For each page it tries your theme, then falls back to `default`.
  </Step>
</Steps>

```text Resolution order theme={null}
client/dashboard  ->  themes/{active_theme}/UI/dashboard.tsx
                  ->  themes/default/UI/dashboard.tsx

auth/login        ->  themes/{active_theme}/auth/login.tsx
                  ->  themes/default/auth/login.tsx
```

<Check>
  A theme with one file is valid. `quanta` ships five auth pages and nothing else, and every other
  screen quietly uses the default.
</Check>

## Create one

<Steps>
  <Step title="Make the folder">
    ```bash theme={null}
    mkdir -p resources/js/pages/themes/aurora/auth
    ```

    Use lowercase with hyphens. The admin label is derived from the folder name, so `aurora` shows as
    "Aurora" and `neon-dark` shows as "Neon Dark".
  </Step>

  <Step title="Copy a page you want to change">
    ```bash theme={null}
    cp resources/js/pages/themes/default/auth/login.tsx \
       resources/js/pages/themes/aurora/auth/login.tsx
    ```

    Starting from the default copy means you inherit the props, the form wiring and the translation
    keys, and you only have to change markup.
  </Step>

  <Step title="Build and activate">
    ```bash theme={null}
    npm run build
    ```

    Then go to **Admin, Settings, Client Theme** and pick Aurora.
  </Step>
</Steps>

<Warning>
  Vite discovers theme files with a glob at build time, so a new folder needs a rebuild (or a restart
  of `npm run dev`) before it appears. Adding files to a theme that already exists is picked up by the
  dev server automatically.
</Warning>

## What you can override

<Tabs>
  <Tab title="Auth pages">
    Live in `{theme}/auth/`. The filename must match exactly.

    | File                     | Screen               |
    | ------------------------ | -------------------- |
    | `login.tsx`              | Sign in              |
    | `register.tsx`           | Sign up              |
    | `ForgotPassword.tsx`     | Request a reset link |
    | `ResetPassword.tsx`      | Set a new password   |
    | `2fa-verification.tsx`   | Two-factor prompt    |
    | `phone-verification.tsx` | Phone verification   |
  </Tab>

  <Tab title="Client pages">
    Live in `{theme}/UI/`. The path after `UI/` must match the name core renders.

    | File                        | Screen               |
    | --------------------------- | -------------------- |
    | `dashboard.tsx`             | Customer dashboard   |
    | `Profile.tsx`               | Account settings     |
    | `Inbox.tsx`                 | Notifications        |
    | `gift-cards.tsx`            | Gift cards           |
    | `Market/Index.tsx`          | Store front          |
    | `Market/Products.tsx`       | Category listing     |
    | `Market/ProductDetails.tsx` | Single product       |
    | `Market/Checkout.tsx`       | Checkout             |
    | `Invoice/Index.tsx`         | Invoice list         |
    | `Invoice/View.tsx`          | Single invoice       |
    | `Service/manage.tsx`        | Service overview     |
    | `Service/configuration.tsx` | Configurable options |
    | `Service/addons.tsx`        | Addons               |
    | `Service/invoices.tsx`      | Service invoices     |
    | `Service/invite.tsx`        | Sub-user invites     |
    | `Tickets/index.tsx`         | Ticket list          |
    | `Tickets/create.tsx`        | New ticket           |
    | `Tickets/show.tsx`          | Ticket thread        |
  </Tab>

  <Tab title="Components">
    Live in `{theme}/components/`. These replace shared pieces everywhere they are used, without you
    copying a single page. See [Overriding components](#overriding-components).
  </Tab>
</Tabs>

<Note>
  Admin pages are not themeable. They live in `resources/js/pages/admin/` and are shared by every
  theme.
</Note>

## Layouts come for free

A `UI/` page does not need to declare a layout. If yours does not set one, the resolver wraps it in
`AuthenticatedLayout`, which gives you the sidebar, navbar and footer.

```tsx resources/js/pages/themes/aurora/UI/dashboard.tsx theme={null}
export default function Dashboard({ services, invoices }: PageProps) {
    return <div className="space-y-6">{/* your markup */}</div>;
}
```

If you want a page to render without the standard chrome, set a layout explicitly:

```tsx theme={null}
Dashboard.layout = (page: React.ReactNode) => <MyBareLayout>{page}</MyBareLayout>;
```

<Tip>
  Auth pages are never wrapped. They own their whole screen, which is why `quanta` can restyle sign in
  completely without touching anything else.
</Tip>

## Overriding components

Copying pages gets expensive if all you want is a different card or button. Most shared pieces in the
default theme are wrapped in `withThemeOverride`, so a file with a matching path replaces them
everywhere.

<Steps>
  <Step title="Create the file at the same path">
    ```bash theme={null}
    resources/js/pages/themes/aurora/components/SectionCard.tsx
    ```
  </Step>

  <Step title="Export the same name">
    ```tsx theme={null}
    export const SectionCard = ({ title, children }: SectionCardProps) => (
        <section className="border-primary bg-secondary rounded-2xl border p-5">
            <h2 className="text-primary text-lg font-semibold">{title}</h2>
            {children}
        </section>
    );
    ```

    A `default` export works too. The named export is tried first.
  </Step>
</Steps>

<Warning>
  Your override receives the same props as the original, and callers still pass them. Read the default
  implementation before you replace it, or you will silently drop behaviour like loading states or
  permission checks.
</Warning>

<AccordionGroup>
  <Accordion title="Every overridable component" icon="cubes">
    Path is relative to `{theme}/components/`. Export name matters.

    | Path                                 | Export                                   |
    | ------------------------------------ | ---------------------------------------- |
    | `Button`                             | `Button`                                 |
    | `Footer`                             | `Footer`                                 |
    | `Modal`                              | `Modal`                                  |
    | `Navbar`                             | `Navbar`, `ProfileDropdown`              |
    | `Sidebar`                            | `Sidebar`                                |
    | `PageHeader`                         | `PageHeader`                             |
    | `SearchInput`                        | `SearchInput`                            |
    | `SectionCard`                        | `SectionCard`                            |
    | `StatusBadge`                        | `StatusBadge`                            |
    | `TabFilter`                          | `TabFilter`                              |
    | `EmptyState`                         | `EmptyState`                             |
    | `dashboard/banner-carousel`          | `BannerCarousel`                         |
    | `dashboard/metrics-card`             | `MetricsCards`                           |
    | `dashboard/notification`             | `Notification`, `NotificationsContainer` |
    | `dashboard/payment-drawer`           | `PaymentDrawer`                          |
    | `dashboard/payment-gateway-selector` | `PaymentGatewaySelector`                 |
    | `dashboard/services-table`           | `ServicesTable`                          |
    | `dashboard/user-contact`             | `UserContact`                            |
    | `checkout/BillingPaymentTab`         | `BillingPaymentTab`                      |
    | `checkout/GuestAuthSection`          | `GuestAuthSection`                       |
    | `checkout/OrderSummary`              | `OrderSummary`                           |
    | `checkout/ServerConfigurationTab`    | `ServerConfigurationTab`                 |
    | `checkout/TrustpilotBanner`          | `TrustpilotBanner`                       |
    | `service/CancelModal`                | `CancelModal`                            |
    | `service/DowngradeModal`             | `DowngradeModal`                         |
    | `service/RenewModal`                 | `RenewModal`                             |
    | `service/UpgradeModal`               | `UpgradeModal`                           |
  </Accordion>

  <Accordion title="Why the default theme cannot use overrides" icon="circle-info">
    Overrides are only consulted when `active_theme` is not `default`, and the loader deliberately
    excludes `themes/default/components/**` from the on-demand bundle.

    That keeps the default path free of extra work, and means a theme's component files are only
    fetched when that theme is active.
  </Accordion>
</AccordionGroup>

## Colours and spacing

Before writing custom CSS, reach for the existing variables. They are what the admin Colors page
edits, so anything built on them stays in sync when an operator retunes the palette.

<CodeGroup>
  ```tsx Use the classes theme={null}
  <div className="bg-secondary border-primary rounded-xl border p-4">
      <h3 className="text-primary font-medium">Heading</h3>
      <p className="text-secondary text-sm">Supporting copy</p>
      <button className="button-primary rounded-lg px-4 py-2">Save</button>
  </div>
  ```

  ```css Underlying variables theme={null}
  --bg-primary        --text-primary       --border-primary
  --bg-secondary      --text-secondary     --border-secondary
  --bg-hover          --text-button        --border-third
  --bg-success        --text-success       --border-sidebar
  --bg-warning        --text-warning
  --bg-danger         --text-danger
  --icon-primary      --link-active
  --app-font-family
  ```
</CodeGroup>

Button helpers: `button-primary`, `button-secondary`, `button-danger`, `button-warning`,
`button-text`, `button-link`.

<Tip>
  Counting usage across the default theme: `text-primary`, `border-primary`, `text-secondary`,
  `bg-secondary` and `bg-primary` do most of the work. Match those and your theme will respond to
  light and dark mode and to operator colour changes without extra effort.
</Tip>

<Warning>
  Hardcoding hex values is the most common theme mistake. It looks fine in the mode you built in, then
  breaks in the other one and ignores whatever the operator sets under Customization, Colors.
</Warning>

## What your pages receive

Theme pages are ordinary Inertia pages, so props come from the controller that rendered them. Two
things are always available.

```tsx theme={null}
import { usePage } from '@inertiajs/react';

const { props } = usePage();
const user = props.auth?.user;                    // null when signed out
const settings = props.auth?.settings;            // [{ key, value }, ...]
```

`settings` is an array of every non-sensitive row from the settings table, which is also where
`active_theme` itself lives.

<Tip>
  Copy the default page you are replacing and keep its `interface PageProps`. That is the quickest way
  to see exactly what the controller sends without reading the PHP.
</Tip>

## Things worth reusing

You are not limited to your own folder. These are shared and safe to import from any theme.

| Import                       | For                                               |
| ---------------------------- | ------------------------------------------------- |
| `@/contexts/LanguageContext` | `useTranslation()` for translated copy            |
| `@/contexts/ToastContext`    | `useToast()` for notifications                    |
| `@/hooks/use-appearance`     | Light and dark mode state                         |
| `@/components/ui/*`          | Checkbox, select, language selector, theme toggle |
| `@/lib/sanitize`             | `createSanitizedSvg()` for operator-supplied SVG  |
| `lucide-react`               | Icons used across the app                         |
| `framer-motion`              | Animation, already a dependency                   |

<Warning>
  Run any HTML or SVG that came from a database setting through `@/lib/sanitize` before rendering it.
  Operators can paste custom markup into settings, and a theme that renders it raw is an XSS hole.
</Warning>

## Test it

<Steps>
  <Step title="Activate the theme">
    Admin, Settings, Client Theme.
  </Step>

  <Step title="Walk every page you overrode">
    Plus one you did not, to confirm the fallback still works.
  </Step>

  <Step title="Toggle light and dark mode">
    Then change a colour under Customization, Colors and reload.
  </Step>

  <Step title="Check narrow screens">
    The default components are responsive. Overrides often are not.
  </Step>

  <Step title="Sign out and back in">
    Auth pages render without a session, so a prop you assumed exists may be null.
  </Step>

  <Step title="Switch back to Default">
    Confirm nothing you changed leaked into the default theme.
  </Step>
</Steps>

```bash theme={null}
npm run types      # TypeScript
npm run lint       # ESLint with auto-fix
npm run format     # Prettier
```

## Portal themes vs client themes

Two separate systems with similar names.

<CardGroup cols={2}>
  <Card title="Client themes" icon="browser">
    **This page.** Folders in `resources/js/pages/themes/`, selected with the `active_theme` setting.
    Restyle the dashboard, market, tickets and auth screens.
  </Card>

  <Card title="Portal themes" icon="house">
    Database records (`portal_themes`) managed under Admin, Customization. Each points at a component
    path such as `resources/js/pages/Portal/Default/welcome.tsx` and changes the public landing page.
  </Card>
</CardGroup>

Activating one has no effect on the other.

## When it does not work

| What you see                             | Cause                                                                                                                       |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Theme missing from the settings dropdown | Folder is not directly under `resources/js/pages/themes/`, or you have not rebuilt                                          |
| Your page is ignored                     | Filename or path does not match the name core renders. `client/Service/manage` needs `UI/Service/manage.tsx`, case included |
| Component override ignored               | Wrong path under `components/`, wrong export name, or the active theme is `default`                                         |
| Sidebar and navbar vanished              | You set a `layout` on a `UI/` page. Remove it to get `AuthenticatedLayout` back                                             |
| Looks wrong in dark mode                 | Hardcoded colours instead of the variables                                                                                  |
| Works in dev, breaks in production       | A new theme folder needs a build. Run `npm run build`                                                                       |
| Blank page, console error                | Check the `interface PageProps` against the controller. A renamed prop is the usual culprit                                 |
