generated from boboko/starter
216 lines
14 KiB
Markdown
216 lines
14 KiB
Markdown
# Front-end Guidelines
|
||
|
||
This is a Laravel 12 e-commerce project using Lunar PHP (headless). These guidelines apply to all front-end work.
|
||
|
||
---
|
||
|
||
## Project Language Settings
|
||
|
||
<!-- CHANGE THESE PER PROJECT -->
|
||
- **Greek register:** `singular` — use the informal second person (εσύ/σου/σε). e.g. "Η κριτική σου", "Το όνομά σου", "Το email σου"
|
||
<!-- singular = informal (εσύ) | plural = formal (εσείς) -->
|
||
|
||
All UI text written for this project must follow the register above.
|
||
|
||
---
|
||
|
||
## Localization
|
||
|
||
Uses classic Laravel localization (https://laravel.com/docs/13.x/localization) — no third-party i18n routing package.
|
||
|
||
**Locales:** Greek (`el`) is primary/default, English (`en`) is secondary. Set in `config('app.available_locales')` (`config/app.php`).
|
||
|
||
**URL structure:** every route is prefixed with `{locale}`, including the default — `3dealer.gr/el/...` and `3dealer.gr/en/...`. Bare root (`/`) 301-redirects to `/el` (see `routes/web.php`). This avoids the ambiguity of an unprefixed default locale (see the reasoning in project memory / past conversation — prefixing every locale keeps hreflang symmetrical and scales cleanly to a third language later).
|
||
|
||
**URL slugs are the same across both locales, permanently** — e.g. `/el/products/{x}` and `/en/products/{x}`, not `/el/proionta/{x}` vs `/en/products/{x}`. This is a deliberate, standing decision for this project, not a placeholder to revisit later. Only the visible content is translated, not the slug. Do not introduce per-locale translated slugs (e.g. a `lang/{locale}/routes.php` segment map) unless the user explicitly asks for that to change.
|
||
|
||
**How it's wired:**
|
||
- All routes live inside `Route::prefix('{locale}')->where('locale', ...)->middleware('setlocale')` in `routes/web.php`.
|
||
- `App\Http\Middleware\SetLocale` validates the `{locale}` segment (404s if not in `available_locales`), calls `App::setLocale()`, and shares `$currentLocale`, `$altLocale`, and `$altLocaleUrl` (the same page in the other locale) to every view — this is what powers both the header's language switcher and the `<head>` hreflang tags in `resources/views/layouts/app.blade.php`. Don't recompute this logic elsewhere; read those shared variables instead.
|
||
- When adding a new route, put it inside that `{locale}` group and give it a route `name()` — the alt-locale URL generation in the middleware depends on the current route being named (falls back to the locale root if unnamed).
|
||
- When linking to a page in Blade, prefer `route('name', [...])` (locale is a normal route param, defaults to `app()->getLocale()` implicitly via the shared route group) over hand-built path strings, except for pages that don't have a controller/route yet (e.g. `/products`, `/contact`, `/cart` placeholders in the header currently use `url('/'.app()->getLocale().'/products')` — replace with `route()` once those pages exist).
|
||
- **Every controller action for a route inside the `{locale}` group must declare `$locale` as its first parameter, even if unused** — e.g. `show(string $locale, Product $product)`. Laravel's `ControllerDispatcher` ultimately calls the controller with `...array_values($parameters)`, i.e. **positionally**. If a route-bound model parameter (like `$product`) isn't preceded by a matching `$locale` parameter in the method signature, the resolved values shift out of position and the wrong value (the locale string) gets passed where the model was expected — a `TypeError` that's easy to misread as a binding failure. Closures with zero declared parameters are unaffected (PHP just ignores the extra positional arg), so this only bites real controller methods.
|
||
|
||
**Where translatable strings go:**
|
||
- Most UI copy is editable by the client/marketing team via **Stoic** (the headless CMS) — don't hardcode it here.
|
||
- Small strings that don't need client editing (nav labels, aria-labels, tab labels, pluralized counts) go in Laravel's own lang files at **`lang/{locale}/*.php`** (project root, per Laravel 12+ convention — not `resources/lang`). Current file: `lang/el/general.php` / `lang/en/general.php`. Use `__('general.key')` or `trans_choice('general.key', $count, [...])` for pluralized strings.
|
||
|
||
---
|
||
|
||
## Tech Stack
|
||
|
||
- **Laravel 12** + **Lunar PHP 1.3** (headless e-commerce)
|
||
- **Blade** for templating (`@extends`, `@section`, `@yield`, components)
|
||
- **Tailwind CSS v4** via `@tailwindcss/vite` — zero config, `@import 'tailwindcss'` only
|
||
- **Stimulus JS** for interactivity — controllers registered in `resources/js/stimulus/index.js`
|
||
- **Popover API** (native browser) for overlays — no JS overlay libraries
|
||
- SQLite database, `npm run dev` for local asset compilation
|
||
|
||
---
|
||
|
||
## Key File Locations
|
||
|
||
| What | Where |
|
||
|---|---|
|
||
| CSS entry point | `resources/css/app.css` |
|
||
| Font definitions | `resources/css/fonts.css` |
|
||
| JS entry point | `resources/js/app.js` |
|
||
| Stimulus controllers | `resources/js/stimulus/` |
|
||
| JS utilities | `resources/js/utils/` |
|
||
| Layout | `resources/views/layouts/app.blade.php` |
|
||
| UI components (atomic) | `resources/views/components/ui/` |
|
||
| Section components | `resources/views/components/` |
|
||
| Page views | `resources/views/{page}/` |
|
||
| Public assets | `public/images/` |
|
||
|
||
---
|
||
|
||
## Design Reference
|
||
|
||
The Bluebeard template (`resources/css/bluebeard.css`) is used as a **reference only** for colors, spacing, typography, and effects. It is never imported or used directly. Everything is translated to Tailwind utilities or custom CSS where unavoidable.
|
||
|
||
The boboko project at `/Users/farenoubi/Projects/boboko-test` can be referenced for CSS architecture patterns.
|
||
|
||
---
|
||
|
||
## Tailwind vs. Custom CSS
|
||
|
||
**Prefer Tailwind utilities by default.** Write custom CSS only when something genuinely cannot be expressed as a utility:
|
||
|
||
- Pseudo-elements (`::before`, `::after`) with dynamic transforms or transitions
|
||
- Complex descendant/sibling selectors (e.g. `.group:hover .nav-dropdown`)
|
||
- Keyframe animations
|
||
- The underline-slide animation on nav links (background-size trick)
|
||
|
||
**Never** write inline CSS (`style="..."`). **Never** write custom CSS for something Tailwind already covers.
|
||
|
||
When a component requires a CSS class (e.g. as a hook for a pseudo-element), still put all properties that can be Tailwind utilities as classes on the element in the component file. The CSS rule should contain only what genuinely cannot be a utility — pseudo-elements, complex selectors, keyframes. Do not default to putting all styles in the CSS class just because the class exists.
|
||
|
||
When custom CSS is needed, add it to `resources/css/app.css` inside `@layer components`. Keep the rule minimal — only what can't be a utility.
|
||
|
||
---
|
||
|
||
## Design Tokens
|
||
|
||
Defined in `@theme {}` in `app.css`:
|
||
|
||
- `--font-sans`: Manrope (body text)
|
||
- `--font-display`: Manrope (headings, nav, buttons) — kept separate from `--font-sans` in case they diverge later
|
||
|
||
Reuse existing colors (`neutral-200`, `black`), font sizes, spacing, and border styles as components are built. Do not invent new values — check what's already in use first.
|
||
|
||
---
|
||
|
||
## Component Structure
|
||
|
||
### `resources/views/components/ui/` — atomic, reusable UI elements
|
||
|
||
Single-purpose, stateless or minimally stateful elements used across many contexts:
|
||
|
||
- `button.blade.php` ✓
|
||
- `input.blade.php`
|
||
- `input-group.blade.php`
|
||
- `icon.blade.php`
|
||
- `image.blade.php`
|
||
- `tabs.blade.php` / `tab.blade.php`
|
||
- `accordion.blade.php`
|
||
- `tooltip.blade.php`
|
||
- `badge.blade.php`
|
||
- etc.
|
||
|
||
### `resources/views/components/` — section-level or composite components
|
||
|
||
Larger reusable blocks made of several elements, often with real content:
|
||
|
||
- `header.blade.php` ✓
|
||
- `footer.blade.php`
|
||
- `cta.blade.php`
|
||
- `product-card.blade.php`
|
||
- `contact-section.blade.php`
|
||
- etc.
|
||
|
||
---
|
||
|
||
## Interactivity
|
||
|
||
### Popover API for overlays
|
||
|
||
For **tooltips, modals, dropdowns, and any overlay**: use the native [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) with CSS — no JS, no libraries.
|
||
|
||
```html
|
||
<button popovertarget="my-popover">Open</button>
|
||
<div id="my-popover" popover>...</div>
|
||
```
|
||
|
||
Style open/closed states with `[popover]` and `:popover-open` selectors in CSS.
|
||
|
||
### Stimulus JS for behavior
|
||
|
||
Use Stimulus when JS is genuinely needed (form handling, cart updates, dynamic state, etc.).
|
||
|
||
- Keep controllers lean: manage state and coordinate the DOM, don't build UI inside them
|
||
- Use Stimulus values, targets, and outlets — avoid reading from the DOM imperatively
|
||
- **Never inject HTML strings or classes from inside a controller.** Instead, drive appearance via data attributes or CSS classes toggled on existing elements, and let CSS handle the visual result
|
||
- Never write inline JS (`onclick="..."`, etc.)
|
||
|
||
### Pure CSS for simple interactions
|
||
|
||
Hover dropdowns, focus states, active states — handle with CSS (`:hover`, `:focus-within`, `.group:hover`) before reaching for Stimulus.
|
||
|
||
---
|
||
|
||
## Accessibility (ARIA)
|
||
|
||
Always add ARIA attributes. This is not optional:
|
||
|
||
- Interactive elements: `aria-label`, `aria-expanded`, `aria-controls`, `aria-haspopup` as appropriate
|
||
- Images: meaningful `alt` text, or `alt=""` + `aria-hidden="true"` for decorative images
|
||
- Icons used as buttons: `aria-label` on the button, `aria-hidden="true"` on the SVG
|
||
- Form fields: always associated `<label>`, `aria-describedby` for hints/errors
|
||
- Dynamic content: `aria-live` regions where content updates without navigation
|
||
- When refactoring or adding interactivity, review ARIA impact — don't break existing roles
|
||
|
||
---
|
||
|
||
## Greek Typography
|
||
|
||
CSS `text-transform: uppercase` leaves Greek tonos accents in place (e.g. Ά instead of Α), which is incorrect. The utility at `resources/js/utils/strip-accents.js` handles this automatically for any element with the `uppercase` Tailwind class. **Always add the `uppercase` class to elements** rather than applying `text-transform: uppercase` in CSS — this keeps the strip-accents utility working without extra configuration.
|
||
|
||
---
|
||
|
||
## Performance & Core Web Vitals
|
||
|
||
Every frontend change should assume it will be measured by PageSpeed/Lighthouse. These rules exist because we hit each of these mistakes in production and had to fix them after the fact — bake them in up front instead.
|
||
|
||
### CSS bundling
|
||
- Before adding a CSS import to a shared/global entry point (e.g. `app.css`), ask whether every page actually needs it. Page- or component-specific styles (date pickers, legal-page typography, admin-only widgets) should not ship on pages that don't use them.
|
||
- Scope non-critical CSS to load only where it's used — either a dedicated build entry for that page, or import the `.css` file inside the same JS module that already lazy-loads that component (e.g. a Stimulus/JS controller), so the bundler emits it as a separate chunk fetched only when that component actually mounts.
|
||
- Exception: never defer CSS for anything the page shows immediately on load (above-the-fold content, or a component that renders as soon as its JS runs — an FAQ accordion, a cookie-consent banner). Even a well-scoped async CSS chunk arrives a beat after the JS that displays the element, causing a visible flash of unstyled/wrong-state content and a layout shift. Keep that CSS in the blocking bundle on purpose.
|
||
- Never import a full icon-font library (Phosphor, Font Awesome, etc.) — it ships every icon regardless of usage. Use inline SVGs for only the icons actually referenced; check the shared icon component for an existing icon before adding a new one.
|
||
- Only declare design tokens / CSS custom properties that are actually used somewhere in the codebase (Tailwind v4 already tree-shakes unused `@theme` tokens from output, so this is for keeping the source legible, not for bytes).
|
||
|
||
### Fonts
|
||
- `font-display: swap` means any text using a font weight will visibly reflow once that weight's file finishes downloading, if the fallback font's metrics differ. Preload every font weight used in above-the-fold text, not just the default body-copy weight — bold/black heading weights are usually the most visually prominent element on the page and cause the most visible reflow if not preloaded.
|
||
- When introducing a new heavy-weight text utility (e.g. a bold/black heading class) in a hero or above-the-fold component, confirm that weight has a matching `<link rel="preload" as="font">` in the layout `<head>`.
|
||
|
||
### Images
|
||
- Any image that could be the Largest Contentful Paint element (hero/banner images) must have: `fetchpriority="high"`, `loading="eager"`, explicit `width`/`height`, a responsive `srcset`/`sizes`, and a `<link rel="preload" as="image">` in `<head>` with matching `imagesrcset`/`imagesizes` — this lets the browser start the fetch as soon as it parses `<head>`, instead of waiting to discover the `<img>` tag in the body.
|
||
- Every other image: `loading="lazy"` with explicit `width`/`height` to reserve its layout space.
|
||
|
||
### Layout shift (CLS)
|
||
- Never animate `top`/`bottom`/`left`/`right`/`width`/`height`/`margin` on any element that appears or moves without direct user interaction (banners, toasts, slide-in modals, accordions). These are layout-affecting properties — the browser recalculates layout on every animation frame, and each frame's position delta counts toward CLS. Animate `transform` (translate/scale) and `opacity` instead — both are compositor-only and fully excluded from CLS scoring, no matter how far or long the animation runs.
|
||
- This applies to vendor/third-party CSS too (cookie-consent widgets, modal libraries) — check its default show/hide mechanism before shipping, and override it if it animates a layout property directly.
|
||
- Any element with a collapsed/expanded default state (accordions, dropdowns) needs its collapsed-state CSS in the blocking stylesheet, never a lazily-loaded chunk — otherwise it renders in its expanded/default state for a frame before the async CSS applies.
|
||
|
||
---
|
||
|
||
## Forms
|
||
|
||
Never add `novalidate` to forms. Always use the browser's default validation.
|
||
|
||
---
|
||
|
||
## Asking Questions
|
||
|
||
When the approach is unclear — layout structure, whether something should be a component, which Tailwind pattern fits — **ask before building**. A short question is cheaper than a refactor.
|