Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: adminforth-custom-vue
description: "Use when implementing AdminForth custom Vue UI: field components, page injections, login or global injections, meta-driven component declarations, and frontend packages inside custom/."
description: "Use when implementing AdminForth custom Vue UI: AFCL components, theme colors and dark mode, field components, page injections, login or global injections, meta-driven component declarations, and frontend packages inside custom/."
user-invocable: true
---

Expand All @@ -13,6 +13,189 @@ user-invocable: true
- Adding resource page injections, login injections, or global layout injections.
- Passing `meta` into reusable Vue components.
- Installing frontend packages used only by custom AdminForth Vue code.
- Any task that produces visible UI in an AdminForth app, even when the task says nothing about how it should look.

## Non-Negotiable UI Defaults

Apply all of these to every piece of UI you write under `custom/`, including when the user gave no design
instructions at all. These are the defaults, not options — do not ask whether the user wants them, and do
not wait for a follow-up prompt about styling or dark mode.

1. **Build from AFCL first.** AFCL (AdminForth Components Library) is imported from `@/afcl` and is always
available in `custom/` without installing anything. Reach for a raw HTML control only when no AFCL
component covers the case.
2. **Buttons come from AFCL with an explicit intent.** Primary/confirming action is the default filled
accent `<Button>`. Secondary, cancel, and "back" actions are stroked `<Button variant="secondary">`.
Destructive actions are `<Button variant="danger">`. Never hand-roll a `<button>` with your own
background classes.
3. **Form controls come from AFCL.** `Input`, `Textarea`, `Select`, `Checkbox`, `Toggle`, `DatePicker`,
`Dropzone`. Never a bare `<input>`, `<select>`, or `<textarea>` styled by hand — that is the main way
custom pages end up looking foreign.
4. **Accents use `lightPrimary` / `darkPrimary`.** Anything that carries brand or "this is the important
one" meaning — accent fills, highlighted values, active states, links, focus emphasis, the main chart
series — should use `bg-lightPrimary dark:bg-darkPrimary`, `text-lightPrimary dark:text-darkPrimary`,
`text-lightPrimaryContrast dark:text-darkPrimaryContrast` on top of an accent fill, or
`bg-lightPrimaryOpacity dark:bg-darkPrimaryOpacity` for a subtle tint. Hardcoding an accent
(`bg-blue-600`, `text-indigo-500`) breaks apps whose theme sets a different brand color.
5. **Everything else may use Tailwind's stock palette.** `bg-white`, `bg-gray-50`, `text-gray-700`,
`text-red-600`, `border-gray-200`, `bg-pink-500`, and friends are all fine for neutrals, surfaces,
borders, and semantic colors. The theme tokens in the table below are still the better choice when a
block sits directly next to built-in AdminForth chrome and should match it exactly — but they are a
recommendation, not a restriction.
6. **Dark theme is part of writing the class, not a later pass.** Every color utility must be written as a
light/dark pair: `bg-white dark:bg-gray-900`, `text-gray-700 dark:text-gray-300`,
`bg-lightForm dark:bg-darkForm`. This matters most with stock Tailwind colors, which have no built-in
dark behavior — a `bg-gray-50` with no `dark:` counterpart is a defect, fix it before finishing.
`light*`/`dark*` token pairs satisfy this by construction. Dark mode is class-based
(`darkMode: 'class'`), so `dark:` variants work everywhere in `custom/`.
7. **Non-color utilities are unrestricted.** Tailwind spacing, sizing, radius, flex/grid, and font-size
utilities are fine and encouraged. Match AFCL's own rhythm so custom blocks sit naturally next to
built-in ones: `rounded-lg`, `text-sm`, `p-4`, `gap-2`/`gap-4`.
8. **Icons come from the prerendered Iconify packages** already present in the SPA:
`@iconify-prerendered/vue-flowbite` (default), plus `-heroicons`, `-humbleicons`, and `-flag`.
Do not add an icon dependency to `custom/package.json` for these.
9. **Never build Tailwind class names dynamically.** `custom/` is copied into the SPA sources and scanned
statically by Tailwind, so `` `text-${color}-600` `` produces no CSS. Write full class strings and pick
between them.

## AFCL Component Inventory

All of these are imported from `@/afcl` and are already theme-aware and dark-mode-ready:

```ts
import { Button, LinkButton, ButtonGroup, Link, Input, Textarea, Select, Checkbox, Toggle,
DatePicker, Dropzone, Card, Table, Dialog, Modal, Tooltip, VerticalTabs,
ProgressBar, Spinner, Skeleton, JsonViewer, CountryFlag,
AreaChart, BarChart, PieChart, MixedChart, TreeMapChart } from '@/afcl';
```

- Actions: `Button` (`variant`: `primary` | `secondary` | `danger`; also `loader`, `disabled`, `active`,
`shadow`), `LinkButton` (same variants, navigates via `to`), `ButtonGroup`, `Link`.
- Inputs: `Input` (requires `type`, supports `v-model`, `fullWidth`, `readonly`, `prefix`/`suffix` props
or slots), `Textarea`, `Select` (`:options="[{ label, value }]"`, `multiple`, `placeholder`),
`Checkbox`, `Toggle`, `DatePicker`, `Dropzone`.
- Layout and data: `Card`, `Table` (client or server-side data, sorting, pagination), `VerticalTabs`,
`Modal`, `Dialog`, `Tooltip`.
- Feedback: `Spinner`, `Skeleton`, `ProgressBar`.
- Charts: `AreaChart`, `BarChart`, `PieChart`, `MixedChart`, `TreeMapChart` — prefer these over pulling in
a new charting library, they already follow theme colors.

Before writing a bespoke widget, check this list. Reusing an AFCL component is always the preferred
answer to "make a button / input / table / modal / chart".

## Theme Color Tokens

These light/dark pairs follow the app's configured theme automatically. The accent rows are the ones you
should actually reach for by default (rule 4); the rest are available when you want a custom block to
match built-in AdminForth chrome pixel-for-pixel instead of approximating it with stock grays. Every
`light*` token has a `dark*` twin.

| Need | Classes |
| --- | --- |
| **Accent (brand) text** | `text-lightPrimary dark:text-darkPrimary` |
| **Accent fill** | `bg-lightPrimary dark:bg-darkPrimary` |
| **Text on accent fill** | `text-lightPrimaryContrast dark:text-darkPrimaryContrast` |
| **Subtle accent tint** | `bg-lightPrimaryOpacity dark:bg-darkPrimaryOpacity` |
| Panel / form surface | `bg-lightForm dark:bg-darkForm` |
| Panel border | `border-lightFormBorder dark:border-darkFormBorder` |
| Section heading strip | `bg-lightFormHeading dark:bg-darkFormHeading` |
| Card surface | `bg-lightCardBackground dark:bg-darkCardBackground` |
| Card border | `border-lightCardBorder dark:border-darkCardBorder` |
| Strong / title text | `text-lightCardTitle dark:text-darkCardTitle` |
| Muted / secondary text | `text-lightCardDescription dark:text-darkCardDescription` |
| Body and table text | `text-lightListTableText dark:text-darkListTableText` |
| Table heading text | `text-lightListTableHeadingText dark:text-darkListTableHeadingText` |
| Divider / separator | `border-lightListBorder dark:border-darkListBorder` |
| Error / invalid text | `text-lightInputErrorColor dark:text-darkInputErrorColor` |
| Required marker | `text-lightRequiredIconColor dark:text-darkRequiredIconColor` |
| Focus ring | `focus:ring-lightFocusRing dark:focus:ring-darkFocusRing` |

The complete token list lives in `node_modules/adminforth/dist/modules/styles.js`. Look a name up there
instead of inventing one — an unknown token silently produces no CSS.

## Default Panel Recipe

When a task asks for "a panel", "a summary block", "a small form", or any custom page area with no visual
spec, start from this shape. It is theme-correct and dark-ready with no extra work:

```vue
<template>
<div class="rounded-lg border border-lightFormBorder dark:border-darkFormBorder
bg-lightForm dark:bg-darkForm p-4">
<h3 class="text-lg font-semibold text-lightCardTitle dark:text-darkCardTitle">
\{{ meta?.title || 'Orders overview' }}
</h3>
<p class="mt-1 text-sm text-lightCardDescription dark:text-darkCardDescription">
Totals for the current filter
</p>

<div class="mt-4 grid gap-3 sm:grid-cols-2">
<Input type="text" v-model="query" full-width placeholder="Search orders" />
<Select v-model="period" :options="periodOptions" placeholder="Period" />
</div>

<div class="mt-4 flex items-center gap-2">
<Button :loader="loading" @click="apply">Apply</Button>
<Button variant="secondary" @click="reset">Reset</Button>
</div>

<p v-if="error" class="mt-2 text-sm text-lightInputErrorColor dark:text-darkInputErrorColor">
\{{ error }}
</p>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { Button, Input, Select } from '@/afcl';

defineProps<{ meta?: { title?: string } }>();

const query = ref('');
const period = ref(null);
const loading = ref(false);
const error = ref('');
const periodOptions = [
{ label: 'Last 7 days', value: '7' },
{ label: 'Last 30 days', value: '30' },
];

function apply() { /* ... */ }
function reset() { query.value = ''; period.value = null; }
</script>
```

## Dark Theme Self-Check

Run this over every file you touched before reporting the work as done:

- Search the file for `bg-`, `text-`, `border-`, `ring-`, `fill-`, `stroke-`, `divide-`, `placeholder-`,
and `shadow-` color utilities.
- Each one either has a `dark:` counterpart, comes from a `light*`/`dark*` token pair (which already is
one), or belongs to an AFCL component that handles theming itself. This is the check that actually
matters — a stock Tailwind color with no `dark:` twin is the single most common way custom UI breaks in
dark mode.
- Accents are `lightPrimary`/`darkPrimary`, not a hardcoded blue or indigo.
- No raw `#hex` or `rgb()` in templates or `<style>` blocks.
- Any raw `<button>`, `<input>`, `<select>`, or `<textarea>` has a justification; otherwise replace it with
the AFCL equivalent.

```
❌ <button class="bg-blue-600 text-white rounded px-4 py-2">Save</button>
✅ <Button @click="save">Save</Button>

❌ <div class="bg-white border border-gray-200 text-gray-800">
✅ <div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700
text-gray-800 dark:text-gray-200">
✅ <div class="bg-lightForm dark:bg-darkForm border border-lightFormBorder
dark:border-darkFormBorder text-lightListTableText dark:text-darkListTableText">

❌ <p class="text-red-600">\{{ error }}</p>
✅ <p class="text-red-600 dark:text-red-400">\{{ error }}</p>

❌ <span class="font-semibold text-blue-600 dark:text-blue-400">\{{ total }}</span>
✅ <span class="font-semibold text-lightPrimary dark:text-darkPrimary">\{{ total }}</span>
```

## `custom/` Directory and `@@/`

Expand All @@ -23,6 +206,10 @@ user-invocable: true

## Frontend Packages in `custom/`

- First check whether you need a package at all. These are already available to `custom/` components with
no install step: `@/afcl` (AFCL components), `@/types/Common` (AdminForth types), `@/adminforth`
(`useAdminforth`), `@/stores/core` (`useCoreStore`), `@/websocket`, Vue, Tailwind, and the
`@iconify-prerendered/vue-*` icon sets. AFCL charts already wrap ApexCharts.
- Install frontend-only dependencies inside `custom/`, not in the app root.

```bash
Expand Down Expand Up @@ -127,25 +314,27 @@ show: {
<template>
<div class="grid gap-2">
<Input
type="text"
full-width
:model-value="localValue"
:readonly="readonly"
:placeholder="meta?.placeholder || column.label"
@update:model-value="onInput"
/>

<p v-if="errorMessage" class="text-sm text-red-600">
<p v-if="errorMessage" class="text-sm text-lightInputErrorColor dark:text-darkInputErrorColor">
\{{ errorMessage }}
</p>

<p v-else-if="isEmpty" class="text-sm text-amber-600">
<p v-else-if="isEmpty" class="text-sm text-lightCardDescription dark:text-darkCardDescription">
Value is currently empty
</p>
</div>
</template>

<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import Input from '@/afcl/Input.vue';
import { Input } from '@/afcl';
import type {
AdminForthResourceColumnCommon,
AdminForthResourceCommon,
Expand Down Expand Up @@ -225,7 +414,7 @@ function syncState() {

```vue
<template>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 text-lightListTableText dark:text-darkListTableText">
<span>
\{{ meta?.filler?.repeat(record.number_of_rooms || 0) }}
</span>
Expand Down Expand Up @@ -337,4 +526,10 @@ options: {
- Prefer simple string declarations until you actually need `meta`.
- Reuse one component with multiple full declarations instead of cloning similar files.
- Keep page injections small unless the layout intentionally becomes page-scrolling.
- Keep custom edit and create components explicit about validity and emptiness if the default input heuristics are not enough.
- Keep custom edit and create components explicit about validity and emptiness if the default input heuristics are not enough.
- Reach for an AFCL component before writing markup; use `lightPrimary`/`darkPrimary` for accents; write
the `dark:` variant in the same edit as the light one. These are defaults for every UI task, not polish
to be added when someone asks for it.
- Stock Tailwind colors are fine for neutrals and semantic states — just never leave one without its
`dark:` counterpart.
- When you are done, re-read your diff against the Dark Theme Self-Check above before reporting completion.