Files

14 KiB
Raw Permalink Blame History

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') 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 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-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.