bert.hausmans 82ca920b81 fix(timetable): canvas layout — sticky-left + sticky-top freeze panes, single canvas scroll (B3)
Restructures the canvas so the spreadsheet-feel works correctly with
the seeder's 14 stages: horizontal scroll moves the rows AND the
TimeAxis together; vertical scroll moves the rows but keeps TimeAxis
pinned; both panes intersect at a fixed corner cell. Diagonal trackpad
scroll behaves naturally because there's only one scroll container.

DOM restructure (E2 — sticky resolves to its nearest scroll ancestor;
fixed by giving sticky elements the right scroll-container parent
instead of patching with absolute positioning):

  .tt-page__canvas           position: relative; overflow: auto
   └ .tt-page__layout        display: grid; grid-template-columns: 200px auto;
                             inline-size: max-content
      ├ .tt-page__corner     sticky top:0 left:0  z=3
      ├ .tt-page__axis       sticky top:0         z=2  (full 1872px wide, no clip)
      └ for each stage:
        ├ .tt-page__header-cell  sticky      left:0  z=2
        │  └ <StageHeaderCell :row-height-px="row.rowHeightPx">
        └ .tt-page__row-cell     normal              z=1  (height = same value)
           └ <StageRow>

Z-index ladder (E1) is documented in the page CSS:
  corner=3, axis row=2, header rail=2, row content=1, blocks=auto.
Popover + AddPerformanceDialog stay above via Teleport-to-body.

Drops the broken pre-stabilization layout:
  - `grid-template: "corner axis" 28px "stages rows" 1fr / 200px 1fr`
    that put ALL stage headers in ONE grid cell (cause of "lanes too tall"
    via headers stretching to 100% of the 570px cell)
  - nested `overflow: auto` on `.tt-page__rows` (cause of horizontal-scroll
    desync — only the rows pane scrolled, axis stayed put)
  - `overflow: hidden` on `.tt-page__axis` (E4 — clipped axis ticks beyond
    the 1fr cell width)
  - `<GridBg :total-height="0" />` which was a no-op anyway; gridlines now
    render directly on each `.tt-page__row-cell` background

`inline-size: max-content` on the layout grid forces it wider than the
canvas viewport, so `overflow: auto` on the canvas actually fires a
horizontal scrollbar. Without this, the `auto` second column shrinks to
viewport and nothing overflows.

The page now passes `:row-height-px` to StageHeaderCell (B2 seam, now
load-bearing). Both header and row cell get the same explicit blockSize
inline so the freeze panes align pixel-for-pixel under whatever
laneCount each stage resolves to.

Visual scroll/alignment proof is deferred to TEST-VISUAL-001 — jsdom
cannot verify position:sticky behavior, scrollbar visibility, or pixel
alignment of the freeze panes. This is a known limitation, not a test
gap. B4 covers the structural assertions jsdom CAN verify.

All 389 existing tests still pass; production build smoke clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 00:33:12 +02:00

Crewli

Multi-tenant SaaS platform for event and festival operations: planning, people, accreditation, artist advancing, volunteer shifts, briefings, and show-day tooling (Mission Control). The backend is a JSON-only Laravel API; all user interfaces are Vue 3 single-page apps.


What Crewli covers

  • Organisations & events — Multi-tenant data with organisation-scoped access; events move through a defined lifecycle (draft → published → registration → buildup → show day → teardown → closed).
  • Festival structure — Sections, time slots, shifts, assignments, claiming/approval flows for volunteers and crew.
  • People & crowds — Crowd types (crew, volunteers, artists, guests, press, partners, suppliers), persons, lists, and (planned) rich accreditation (items, zones, hand-out).
  • Artists & advancing — Booking status, stages/timetable concepts, advance sections and token-based portal access for external artists.
  • Communication — Briefings, campaigns, and operational messaging (see architecture doc for target modules).
  • Portal (external users) — One portal app, two modes: login (Sanctum) for long-term users such as volunteers, and token access for per-event links (e.g. artists, suppliers).

Implementation is phased; the authoritative feature and schema list lives in the architecture and design references below.


Applications

App Path Port Role
SPA apps/app/ 5174 Single-SPA product covering organizers, volunteers, crew, super admins (context-routed in-app), plus token-based access for artists, suppliers, press. Includes Platform Admin section for super admins (/platform/*).

The SPA talks to the API over CORS with Laravel Sanctum tokens.


Tech stack

Layer Technology
API PHP 8.2+, Laravel 12, Sanctum, Spatie Permission (and Activity Log / Media Library where used)
Data MySQL 8, Redis (cache/queues)
Frontends Vue 3, TypeScript, Vite, Vuexy + Vuetify, Pinia, TanStack Query, VeeValidate + Zod
Local services Docker Compose (MySQL, Redis, Mailpit)

Rule of thumb: business tables use ULID primary keys; event-related data is scoped by organisation (global scopes + policies), not ad hoc where clauses in controllers.


Project structure

crewli/
├── api/                 # Laravel 12 REST API (JSON only)
├── apps/
│   ├── app/             # Organizer SPA (primary UI + platform admin)
│   └── portal/          # External portal SPA
├── docker/              # Docker / Compose assets
├── docs/                # SETUP, API notes, schema notes
├── resources/
│   ├── design/          # Product source of truth (design docs, see table below)
│   └── vuexy-admin-*/   # Vuexy template reference (bundled kit)
├── .cursor/             # ARCHITECTURE.md, instructions.md, rules for AI/helpers
└── Makefile             # Dev commands

Vuexy @core/ and @layouts/ in each app should stay untouched; customize via app config, navigation, and app-level components.


Quick start

# 1. Infrastructure
make services

# 2. API env, dependencies, database (see docs/SETUP.md)
cd api && cp .env.example .env && composer install && php artisan key:generate && php artisan migrate

# 3. Run API + the SPAs you need (separate terminals)
make api
make app
make portal

Detailed setup: docs/SETUP.md.


Development URLs

Service Development Env / notes
API http://localhost:8000/api/v1 Base path /api/v1
Organizer http://localhost:5174 FRONTEND_APP_URL
Portal http://localhost:5175 FRONTEND_PORTAL_URL
Mailpit http://localhost:8025 Local mail capture

Production (crewli.app)

Domains: crewli.app is this product (API + organizer + portal SPAs, transactional email from the app, seeds, etc.). crewli.nl is reserved for a future public marketing site only — do not point this codebase's APP_URL, CORS, Sanctum, or app mail at crewli.nl.

Typical layout (configure the same values in api/.env — see api/.env.example):

Service URL Env variable
API https://api.crewli.app APP_URL
Organizer https://crewli.app FRONTEND_APP_URL
Portal https://portal.crewli.app FRONTEND_PORTAL_URL

Frontends: set VITE_API_URL=https://api.crewli.app/api/v1 in each app's env for production builds. SANCTUM_STATEFUL_DOMAINS must list the hostnames only of the two SPAs (e.g. crewli.app,portal.crewli.app).


Makefile commands

make services       # MySQL, Redis, Mailpit
make services-stop
make api            # Laravel on :8000
make app            # Organizer on :5174
make portal         # Portal on :5175
make migrate
make fresh          # migrate:fresh --seed
make db-shell

Documentation

Resource Contents
CLAUDE.md Project conventions, vibe-coding principles, Vuexy-first decision tree (auto-loaded by Claude Code).
.cursor/rules/ Workspace, Laravel, Vue, testing conventions.
dev-docs/SETUP.md Environment and local setup.
dev-docs/SCHEMA.md Database schema (kept in sync with migrations).
dev-docs/API.md API contract.
dev-docs/design-document.md Product specification.

Testing

cd api && php artisan test

Feature tests should cover happy paths plus 401 (unauthenticated), 403 (wrong organisation), and 422 (validation) where applicable.

Description
No description provided
Readme 329 MiB
Languages
HTML 54.4%
JavaScript 15.2%
Vue 11.8%
TypeScript 7.2%
SCSS 3.7%
Other 7.6%