Skip to content
Merged
Show file tree
Hide file tree
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
6 changes: 6 additions & 0 deletions bootstrap/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
then: function (Application $app): void {
if ($app->environment('local')) {
Route::middleware('web')->group(base_path('routes/local.php'));
}
},
)
->withMiddleware(function (Middleware $middleware): void {
$middleware->web(append: [
Expand Down
18 changes: 18 additions & 0 deletions docs/engineering/code-quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,24 @@ composer analyse
baseline file, no `@phpstan-ignore` sprinkled to silence a real problem. Fix
the cause. A genuinely unavoidable ignore carries a comment explaining why.

## Styling — design tokens

- **Source of truth:** `resources/css/tokens.css`, plain CSS custom properties
(`--nx-*`) with no Tailwind or Vue syntax, so the Filament admin can reuse
them (ADR-0020). `resources/css/app.css` maps them into Tailwind's `@theme`.
- **Tailwind's defaults are cleared** for colour, radius, shadow, font, type
scale and tracking. Utilities such as `bg-red-500` do not exist; use the
semantic ones (`bg-surface`, `text-fg-muted`, `text-danger`, `rounded-md`).
- **Components use semantic tokens** (`--nx-color-*`), never `--nx-palette-*`.
- **Contrast is tested.** `resources/js/design/contrast-pairs.ts` derives the
required pairs from token names (text 4.5:1, controls and focus 3:1,
NFR-A11Y-03). A new colour token fails the suite until it is either covered
by a pair or listed as decorative with a reason.
- **Gallery:** `/dev/tokens` renders every token. It is registered only when
`APP_ENV=local` (`routes/local.php`) and 404s everywhere else.
- **Arabic** switches to IBM Plex Sans Arabic and taller line heights through
`:lang(ar)`, so it also applies to Arabic content inside other locales.

## Before every pull request

```sh
Expand Down
4 changes: 2 additions & 2 deletions docs/engineering/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ endpoints.

| Advisory | Chain | Why accepted | Last reassessed |
|---|---|---|---|
| [GHSA-3g3m-2fjr-6pw4](https://github.com/advisories/GHSA-3g3m-2fjr-6pw4) (moderate, `glob` 13.0.6) | `@vue/test-utils` → `js-beautify` → `glob` | Test-time only; never bundled or shipped. No fixed `@vue/test-utils` 2.5.x yet; `npm audit fix --force` would downgrade to 2.4.11 | 2026-11-05 |
| [GHSA-g3hv-75wg-m249](https://github.com/advisories/GHSA-g3hv-75wg-m249) (moderate, `minimatch` 10.2.6) | `@vue/test-utils` → `js-beautify` → `editorconfig` / `glob` → `minimatch` | Same as above; inputs are our own repository paths, not untrusted patterns | 2026-11-05 |
| [GHSA-3g3m-2fjr-6pw4](https://github.com/advisories/GHSA-3g3m-2fjr-6pw4) (moderate, `glob` 13.0.6) | `@vue/test-utils` → `js-beautify` → `glob` | Test-time only; never bundled or shipped. No fixed `@vue/test-utils` 2.5.x yet; `npm audit fix --force` would downgrade to 2.4.11 | 2026-11-08 |
| [GHSA-g3hv-75wg-m249](https://github.com/advisories/GHSA-g3hv-75wg-m249) (moderate, `minimatch` 10.2.6) | `@vue/test-utils` → `js-beautify` → `editorconfig` / `glob` → `minimatch` | Same as above; inputs are our own repository paths, not untrusted patterns | 2026-11-08 |

- Add a dependency only when this phase needs it. Prefer a small, maintained
package or a few lines of first-party code over a large transitive tree.
Expand Down
117 changes: 114 additions & 3 deletions resources/css/app.css
Original file line number Diff line number Diff line change
@@ -1,9 +1,120 @@
@import 'tailwindcss';
@import './tokens.css';

@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';

@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
/*
* Tailwind's theme is reduced to the Nexo tokens. The default colours, radii,
* shadows, fonts and type scale are cleared, so utilities such as bg-red-500
* do not exist and every style goes through a token. `inline` makes each
* utility reference the token variable, so the :lang(ar) and reduced-motion
* overrides in tokens.css apply at runtime.
*/
@theme inline {
--color-*: initial;
--radius-*: initial;
--shadow-*: initial;
--font-*: initial;
--text-*: initial;
--tracking-*: initial;
--font-weight-*: initial;

--color-transparent: transparent;
--color-current: currentColor;

--color-canvas: var(--nx-color-bg-canvas);
--color-surface: var(--nx-color-bg-surface);
--color-subtle: var(--nx-color-bg-subtle);
--color-muted: var(--nx-color-bg-muted);

--color-fg: var(--nx-color-fg-default);
--color-fg-muted: var(--nx-color-fg-muted);

--color-accent: var(--nx-color-accent);
--color-accent-hover: var(--nx-color-accent-hover);
--color-on-accent: var(--nx-color-on-accent);

--color-border-subtle: var(--nx-color-border-subtle);
--color-border: var(--nx-color-border-default);
--color-border-control: var(--nx-color-border-control);
--color-focus: var(--nx-color-focus-ring);

--color-success: var(--nx-color-success-fg);
--color-success-bg: var(--nx-color-success-bg);
--color-warning: var(--nx-color-warning-fg);
--color-warning-bg: var(--nx-color-warning-bg);
--color-danger: var(--nx-color-danger-fg);
--color-danger-bg: var(--nx-color-danger-bg);
--color-info: var(--nx-color-info-fg);
--color-info-bg: var(--nx-color-info-bg);

--font-sans: var(--nx-font-sans);
--font-mono: var(--nx-font-mono);

--font-weight-regular: var(--nx-font-weight-regular);
--font-weight-medium: var(--nx-font-weight-medium);
--font-weight-semibold: var(--nx-font-weight-semibold);

--text-xs: var(--nx-text-xs);
--text-xs--line-height: var(--nx-text-xs-leading);
--text-sm: var(--nx-text-sm);
--text-sm--line-height: var(--nx-text-sm-leading);
--text-base: var(--nx-text-base);
--text-base--line-height: var(--nx-text-base-leading);
--text-lg: var(--nx-text-lg);
--text-lg--line-height: var(--nx-text-lg-leading);
--text-xl: var(--nx-text-xl);
--text-xl--line-height: var(--nx-text-xl-leading);
--text-2xl: var(--nx-text-2xl);
--text-2xl--line-height: var(--nx-text-2xl-leading);
--text-3xl: var(--nx-text-3xl);
--text-3xl--line-height: var(--nx-text-3xl-leading);
--text-4xl: var(--nx-text-4xl);
--text-4xl--line-height: var(--nx-text-4xl-leading);
--text-5xl: var(--nx-text-5xl);
--text-5xl--line-height: var(--nx-text-5xl-leading);

--tracking-tight: var(--nx-tracking-tight);
--tracking-normal: var(--nx-tracking-normal);
--tracking-wide: var(--nx-tracking-wide);

--spacing: var(--nx-space-unit);
--spacing-gutter: var(--nx-space-gutter);
--spacing-section: var(--nx-space-section);
--container-content: var(--nx-content-max);

--radius-sm: var(--nx-radius-sm);
--radius-md: var(--nx-radius-md);
--radius-lg: var(--nx-radius-lg);
--radius-full: var(--nx-radius-full);

--shadow-xs: var(--nx-shadow-xs);
--shadow-sm: var(--nx-shadow-sm);
--shadow-md: var(--nx-shadow-md);
--shadow-lg: var(--nx-shadow-lg);

--ease-standard: var(--nx-ease-standard);
--ease-enter: var(--nx-ease-enter);
--ease-exit: var(--nx-ease-exit);
--default-transition-duration: var(--nx-duration-base);
--default-transition-timing-function: var(--nx-ease-standard);
}

@layer base {
html {
background-color: var(--nx-color-bg-canvas);
color: var(--nx-color-fg-default);
}

/* Re-resolve the font stack where the language changes (see :lang(ar)
in tokens.css); inherited font-family would keep the outer stack. */
[lang] {
font-family: var(--nx-font-sans);
}

:focus-visible {
outline: 2px solid var(--nx-color-focus-ring);
outline-offset: 2px;
}
}
164 changes: 164 additions & 0 deletions resources/css/tokens.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Nexo design tokens.
*
* Plain CSS custom properties with no Tailwind or Vue syntax, so the Filament
* admin can import this file unchanged (ADR-0020). app.css maps them into
* Tailwind's @theme.
*
* Direction: premium minimalist on a warm neutral palette. The accent is ink
* (near-black) with no hue; colour is reserved for status.
*
* Layers:
* --nx-palette-* raw values. Components never use these directly.
* everything else semantic tokens, which components and Tailwind use.
*
* Dark mode: not shipped. It will be one block that re-points the semantic
* colour tokens at other palette values; no component changes are needed.
*
* Contrast rules for every colour token live in
* resources/js/design/contrast-pairs.ts and are enforced by Vitest.
*/

:root {
color-scheme: light;

/* ---- Palette: warm neutrals ---- */
--nx-palette-neutral-0: #ffffff;
--nx-palette-neutral-25: #fcfcfa;
--nx-palette-neutral-50: #f7f6f3;
--nx-palette-neutral-100: #efeee9;
--nx-palette-neutral-200: #e3e1db;
--nx-palette-neutral-300: #cfccc4;
--nx-palette-neutral-400: #a8a49b;
--nx-palette-neutral-500: #7b776e;
--nx-palette-neutral-600: #5c5952;
--nx-palette-neutral-700: #45423d;
--nx-palette-neutral-800: #2d2b28;
--nx-palette-neutral-900: #1c1b19;
--nx-palette-neutral-950: #121110;

/* ---- Palette: status ---- */
--nx-palette-green-50: #edf6f0;
--nx-palette-green-700: #1f6a44;
--nx-palette-amber-50: #fbf3e2;
--nx-palette-amber-800: #7a4807;
--nx-palette-red-50: #fcefee;
--nx-palette-red-700: #b0262c;
--nx-palette-blue-50: #eef3fb;
--nx-palette-blue-700: #1f58ad;

/* ---- Colour: backgrounds ---- */
--nx-color-bg-canvas: var(--nx-palette-neutral-25);
--nx-color-bg-surface: var(--nx-palette-neutral-0);
--nx-color-bg-subtle: var(--nx-palette-neutral-50);
--nx-color-bg-muted: var(--nx-palette-neutral-100);

/* ---- Colour: text ---- */
--nx-color-fg-default: var(--nx-palette-neutral-900);
--nx-color-fg-muted: var(--nx-palette-neutral-600);

/* ---- Colour: accent (ink) ---- */
--nx-color-accent: var(--nx-palette-neutral-900);
--nx-color-accent-hover: var(--nx-palette-neutral-700);
--nx-color-on-accent: var(--nx-palette-neutral-0);

/* ---- Colour: borders and focus ---- */
--nx-color-border-subtle: var(--nx-palette-neutral-100);
--nx-color-border-default: var(--nx-palette-neutral-200);
--nx-color-border-control: var(--nx-palette-neutral-500);
--nx-color-focus-ring: var(--nx-palette-neutral-900);

/* ---- Colour: status ---- */
--nx-color-success-fg: var(--nx-palette-green-700);
--nx-color-success-bg: var(--nx-palette-green-50);
--nx-color-warning-fg: var(--nx-palette-amber-800);
--nx-color-warning-bg: var(--nx-palette-amber-50);
--nx-color-danger-fg: var(--nx-palette-red-700);
--nx-color-danger-bg: var(--nx-palette-red-50);
--nx-color-info-fg: var(--nx-palette-blue-700);
--nx-color-info-bg: var(--nx-palette-blue-50);

/* ---- Typography ----
* The Latin face also covers Malay. Arabic switches the stack below; the
* Latin face stays second so Latin words and digits in Arabic text match
* the rest of the brand (digits are Latin, ADR-0018 §3).
* --font-instrument-sans and --font-ibm-plex-sans-arabic are emitted by the
* laravel-vite-plugin fonts integration with metric-matched fallbacks. */
--nx-font-sans: var(--font-instrument-sans, 'Instrument Sans'), ui-sans-serif, system-ui, sans-serif, var(--nx-font-emoji);
--nx-font-emoji: 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--nx-font-arabic:
var(--font-ibm-plex-sans-arabic, 'IBM Plex Sans Arabic'), var(--font-instrument-sans, 'Instrument Sans'),
ui-sans-serif, system-ui, sans-serif, var(--nx-font-emoji);
--nx-font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

--nx-font-weight-regular: 400;
--nx-font-weight-medium: 500;
--nx-font-weight-semibold: 600;

--nx-text-xs: 0.75rem;
--nx-text-xs-leading: 1rem;
--nx-text-sm: 0.875rem;
--nx-text-sm-leading: 1.25rem;
--nx-text-base: 1rem;
--nx-text-base-leading: 1.5rem;
--nx-text-lg: 1.125rem;
--nx-text-lg-leading: 1.75rem;
--nx-text-xl: 1.25rem;
--nx-text-xl-leading: 1.75rem;
--nx-text-2xl: 1.5rem;
--nx-text-2xl-leading: 2rem;
--nx-text-3xl: 1.875rem;
--nx-text-3xl-leading: 2.25rem;
--nx-text-4xl: 2.25rem;
--nx-text-4xl-leading: 2.5rem;
--nx-text-5xl: 3rem;
--nx-text-5xl-leading: 1.1;

--nx-tracking-tight: -0.015em;
--nx-tracking-normal: 0em;
--nx-tracking-wide: 0.04em;

/* ---- Spacing: a 4px base unit; Tailwind multiplies it (p-4 = 16px) ---- */
--nx-space-unit: 0.25rem;
--nx-space-gutter: 1.5rem;
--nx-space-section: 4rem;
--nx-content-max: 80rem;

/* ---- Radius ---- */
--nx-radius-sm: 0.25rem;
--nx-radius-md: 0.5rem;
--nx-radius-lg: 0.75rem;
--nx-radius-full: 9999px;

/* ---- Elevation: ink-tinted, soft ---- */
--nx-shadow-xs: 0 1px 2px 0 rgb(28 27 25 / 0.05);
--nx-shadow-sm: 0 1px 3px 0 rgb(28 27 25 / 0.08), 0 1px 2px -1px rgb(28 27 25 / 0.06);
--nx-shadow-md: 0 4px 12px -2px rgb(28 27 25 / 0.08), 0 2px 4px -2px rgb(28 27 25 / 0.05);
--nx-shadow-lg: 0 16px 32px -8px rgb(28 27 25 / 0.12), 0 4px 8px -4px rgb(28 27 25 / 0.06);

/* ---- Motion ---- */
--nx-duration-fast: 120ms;
--nx-duration-base: 200ms;
--nx-duration-slow: 320ms;
--nx-ease-standard: cubic-bezier(0.2, 0, 0, 1);
--nx-ease-enter: cubic-bezier(0, 0, 0, 1);
--nx-ease-exit: cubic-bezier(0.3, 0, 1, 1);
}

/* Arabic: its own face and taller body lines. Scoped by language, not just
* the page locale, so Arabic content inside an English page (a vendor or
* product name) gets it too; app.css re-applies font-family on [lang]. */
:lang(ar) {
--nx-font-sans: var(--nx-font-arabic);
--nx-text-base-leading: 1.75rem;
--nx-text-sm-leading: 1.5rem;
--nx-tracking-tight: 0em;
}

@media (prefers-reduced-motion: reduce) {
:root {
--nx-duration-fast: 0ms;
--nx-duration-base: 0ms;
--nx-duration-slow: 0ms;
}
}
Loading
Loading