14 KiB
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
- Greek register:
singular— use the informal second person (εσύ/σου/σε). e.g. "Η κριτική σου", "Το όνομά σου", "Το email σου"
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')inroutes/web.php. App\Http\Middleware\SetLocalevalidates the{locale}segment (404s if not inavailable_locales), callsApp::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 inresources/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 routename()— 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 toapp()->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,/cartplaceholders in the header currently useurl('/'.app()->getLocale().'/products')— replace withroute()once those pages exist). - Every controller action for a route inside the
{locale}group must declare$localeas its first parameter, even if unused — e.g.show(string $locale, Product $product). Laravel'sControllerDispatcherultimately calls the controller with...array_values($parameters), i.e. positionally. If a route-bound model parameter (like$product) isn't preceded by a matching$localeparameter 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 — aTypeErrorthat'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 — notresources/lang). Current file:lang/el/general.php/lang/en/general.php. Use__('general.key')ortrans_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 devfor 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-sansin 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.phpinput-group.blade.phpicon.blade.phpimage.blade.phptabs.blade.php/tab.blade.phpaccordion.blade.phptooltip.blade.phpbadge.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.phpcta.blade.phpproduct-card.blade.phpcontact-section.blade.php- etc.
Interactivity
Popover API for overlays
For tooltips, modals, dropdowns, and any overlay: use the native Popover API with CSS — no JS, no libraries.
<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-haspopupas appropriate - Images: meaningful
alttext, oralt=""+aria-hidden="true"for decorative images - Icons used as buttons:
aria-labelon the button,aria-hidden="true"on the SVG - Form fields: always associated
<label>,aria-describedbyfor hints/errors - Dynamic content:
aria-liveregions 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
.cssfile 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
@themetokens from output, so this is for keeping the source legible, not for bytes).
Fonts
font-display: swapmeans 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", explicitwidth/height, a responsivesrcset/sizes, and a<link rel="preload" as="image">in<head>with matchingimagesrcset/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 explicitwidth/heightto reserve its layout space.
Layout shift (CLS)
- Never animate
top/bottom/left/right/width/height/marginon 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. Animatetransform(translate/scale) andopacityinstead — 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.