diff --git a/CLAUDE.md b/CLAUDE.md index dec71db3..44406501 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,21 +47,60 @@ Three guards: `web` (session), `api` (Sanctum tokens for `/api/v1/`), `customer` - **Web**: `routes/web.php` serves PDF endpoints, auth pages, and catch-all SPA routes (`/admin/{vue?}`, `/{company:slug}/customer/{vue?}`) ### Frontend -- Entry point: `resources/scripts/main.js` -- Vue Router: `resources/scripts/admin/admin-router.js` (admin), `resources/scripts/customer/customer-router.js` (customer portal) -- State: Pinia stores in `resources/scripts/admin/stores/` -- Path aliases: `@` = `resources/`, `$fonts`, `$images` for static assets -- Vite dev server expects `invoiceshelf.test` hostname +- Vue 3 + TypeScript + Pinia + vue-router + Tailwind v4 (`@tailwindcss/vite`) +- Entry point: `resources/scripts/main.ts` (single Vite input) +- Feature-folder layout under `resources/scripts/features/{admin,auth,company,customer-portal,...}` — each feature owns its own `routes.ts`, `views/`, `components/` +- Shared layers: `resources/scripts/{api,stores,components,composables,layouts,plugins,utils,types,config}` +- Path aliases: `@` → `resources/` (so most imports look like `@/scripts/api/client`, `@/scripts/stores/global.store`); `$fonts` → `resources/static/fonts`; `$images` → `resources/static/img`. There is no `@v2` alias — that was retired when the legacy v1 SPA was deleted. +- i18n: `lang/*.json` are dynamically imported by `resources/scripts/plugins/i18n.ts`. Locale-code → filename mismatches (e.g. `pt_BR` → `pt-br.json`) live in `LOCALE_FILE_MAP`. English is statically bundled; other locales lazy-load. Only edit `lang/en.json` directly — other locales are Crowdin-sourced. +- Vite dev server expects the `invoiceshelf.test` hostname (configured in `vite.config.js`) + +### CSS Theme Tokens +The styling system uses **Tailwind v4 with CSS custom properties as the source of truth** — colors are not configured in JS, they live in CSS and are exposed to Tailwind via the `@theme` directive. Two files own this: + +1. **`resources/css/themes.css`** — defines every color as a CSS custom property on `:root` (light) and `[data-theme="dark"]` (dark). This is where you change actual values. +2. **`resources/css/invoiceshelf.css`** — has an `@theme inline { ... }` block that **registers** each custom property as a Tailwind theme token (e.g. `--color-heading: var(--color-heading);`), making it available as utility classes (`bg-heading`, `text-heading`, `border-heading`, etc.). The block also uses the legacy `@theme { --spacing-88: 22rem; --font-base: Poppins, sans-serif; }` for non-color tokens. + +**Token categories defined today:** +- `primary-{50…950}` — brand color scale +- `surface`, `surface-secondary`, `surface-tertiary`, `surface-muted` — background depth tiers +- `heading`, `body`, `muted`, `subtle` — text emphasis tiers +- `line-{light,default,strong}` — borders +- `hover`, `hover-strong` — hover backgrounds +- `header-from`, `header-to` — fixed header gradient stops (not dark-mode-aware) +- `btn-primary`, `btn-primary-hover` — button colors (fixed, always bold) +- `status-{yellow,green,blue,red,purple}` — status badge text colors +- `alert-{warning,error,success}-{bg,text}` — alert variants + +**Dark mode** is toggled via the `[data-theme="dark"]` attribute on the `` element. The same custom-property names get redefined under that selector — components do **not** need `dark:` variants or conditional logic, they just reference the semantic tokens and the right value is picked up automatically. + +**Adding a new color token is a two-step ritual:** +1. Add the custom property to **both** `:root` and `[data-theme="dark"]` in `themes.css` +2. Add a matching `--color-X: var(--color-X);` line inside the `@theme inline` block in `invoiceshelf.css` + +After that the token is usable as `bg-X` / `text-X` / `border-X` in Vue templates and as `var(--color-X)` in raw CSS. Skip step 2 and the value exists at the CSS level but Tailwind utility classes won't be generated. + +**Convention — never hardcode hex/rgb values in components.** Use the semantic tokens: `text-heading` not `text-gray-900`, `bg-surface` not `bg-white`, `border-line-default` not `border-gray-300`. Hardcoded values won't follow dark-mode flips and will diverge from the rest of the app over time. There are **no exceptions** in the project — even the auth pages (which sit outside the admin chrome) use the same `bg-surface` / `text-heading` / `border-line-default` vocabulary as `BaseCard`, just composed differently. ### Backend Patterns - **Authorization**: Silber/Bouncer with policies in `app/Policies/`. Controllers use `$this->authorize()`. - **Validation**: Form Request classes, never inline validation - **API responses**: Eloquent API Resources in `app/Http/Resources/` -- **PDF generation**: DomPDF (`GeneratesPdfTrait`) or Gotenberg -- **Email**: Mailable classes with `EmailLog` tracking -- **File storage**: Spatie MediaLibrary, supports local/S3/Dropbox -- **Serial numbers**: `SerialNumberService` service +- **PDF generation**: Pluggable driver — `dompdf` (default, via `GeneratesPdfTrait`) or `gotenberg` (headless Chromium). Driver chosen per company through the **PDF Generation** admin settings page. +- **Email**: Mailable classes with `EmailLog` tracking. Mail driver is configurable globally and may be overridden per-company. +- **File storage**: Spatie MediaLibrary backed by the **FileDisk** model — admins create named disk entries (local / S3 / Dropbox / DigitalOcean Spaces) and assign them to purposes (`media_storage`, `pdf_storage`, `backup_storage`) in **Admin → File Disks → Disk Assignments**. New uploads go to the assigned disk; existing files stay where they were and require `php artisan media:secure` to migrate. +- **Serial numbers**: `SerialNumberService` - **Company settings**: `CompanySetting` model (key-value per company) +- **User settings**: User-level preferences (notably `language`) stored as JSON via `setSettings()`. The sentinel value `'default'` means "inherit the company-level setting" — used for the per-user language preference so promoting/inviting members doesn't freeze a copy of the inviter's language. + +### PDF Font System +PDFs ship with bundled **Noto Sans** (Latin / Greek / Cyrillic) as the default face. Non-Latin scripts come from on-demand **Font Packages** managed in **Admin → Font Packages** and defined in `FontService::FONT_PACKAGES` (`app/Services/FontService.php`). Currently shipped packages: `noto-sans` (bundled, marker only), `noto-sans-{sc,tc,jp,kr}` (CJK), `noto-sans-hebrew`, `noto-naskh-arabic` (covers `ar`/`fa`/`ur`), `noto-sans-devanagari` (`hi`), `sarabun` (Thai). `GeneratesPdfTrait::ensureFontsForLocale()` synchronously installs the matching package on the first PDF render for a given company language. + +Two non-obvious constraints when extending the font system: +1. **dompdf's PHP-Font-Lib does not parse variable fonts** (`fvar`/`gvar` tables). Any new package must source **static TTF** files — Google Fonts' main repo ships variable fonts and produces empty boxes. Reliable static-TTF sources used today: `openmaptiles/fonts` for non-CJK Noto scripts, `life888888/cjk-fonts-ttf` for the CJK packages, `google/fonts/ofl/sarabun` for Thai. +2. **dompdf does not glyph-fall-back through the `font-family` chain** — it uses the *first* font for ALL characters. So locale-specific packages must be the **primary** font for that locale, not a fallback. Selection happens in `FontService::getFontFamilyForLocale()`. This is also why a Latin-locale company with a Hebrew customer name will still render boxes for the Hebrew text — solving that needs Gotenberg or a custom mid-render font-switching pass. + +The bundled NotoSans is also surfaced as a `bundled: true` package entry (no download URL, files served from `resources/static/fonts/` instead of `storage/fonts/`) so it appears alongside the on-demand packages in the admin UI with a "Bundled" pill instead of an Install button. ### Database Supports MySQL, PostgreSQL, and SQLite. Prefer Eloquent over raw queries. Use `Model::query()` instead of `DB::`. Use eager loading to prevent N+1 queries. @@ -78,11 +117,12 @@ InvoiceShelf follows TDD development style: ## Code Conventions - PHP: snake_case, constructor property promotion, explicit return types, PHPDoc blocks over inline comments -- JS: camelCase +- TS / Vue: camelCase, `