fix(ui): mobile + dashboard polish + dev CSRF relaxation

- filter-bar: hide select / multi-select fields when the options list is
  empty (was rendering bare "Tags" / "Status" labels above empty inputs)
- berth-detail-header: show "Berth A1" title on mobile (was hidden via
  `hidden sm:block`)
- dashboard-shell: time-aware greeting (Good morning/afternoon/evening,
  firstName) using the existing ['me'] cache; falls back to
  "Welcome back" when firstName isn't set yet
- mobile-topbar: hide UUID-segment fallback title flash on detail-page
  navigation — when the URL last segment is a UUID, walk up to the
  parent collection name ("Clients", "Yachts") until the page sets the
  real entity title via useMobileChrome
- mobile-bottom-tabs: subtle bg-primary/10 pill behind icon on active
  tab for a clear "you are here" cue
- branded-auth-shell: lock to viewport via fixed/inset-0 so the iOS
  Safari rubber-band bounce doesn't scroll the centered login card
- middleware: skip CSRF origin check in development. LAN testing
  (real iPhone on 192.168.x.x hitting the Mac dev server while a Mac
  browser tab is on localhost) trips the cross-origin defense; prod
  keeps it as-is.
- package.json dev script: -H 0.0.0.0 so the dev server is reachable
  from devices on the LAN

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 17:58:42 +02:00
parent de8726a9b9
commit 979eadae48
6 changed files with 79 additions and 18 deletions

View File

@@ -188,6 +188,10 @@ function FilterField({
// Radix Select forbids empty-string item values (throws at render
// time, crashes the page). Use a sentinel and translate.
const ANY = '__any__';
// Hide the field entirely when there's nothing to pick — avoids a
// bare "Status" / "Stage" label sitting above an empty dropdown
// before the parent has loaded options.
if (!definition.options || definition.options.length === 0) return null;
return (
<div className="space-y-1">
<Label className="text-xs">{definition.label}</Label>
@@ -211,12 +215,17 @@ function FilterField({
);
}
case 'multi-select':
case 'multi-select': {
// Hide the entire field — label + checkbox list — when there's
// nothing to filter by. Tags/segments/etc. that aren't configured
// yet would otherwise show as a "Tags" header with an empty box
// beneath it.
if (!definition.options || definition.options.length === 0) return null;
return (
<div className="space-y-2">
<Label className="text-xs">{definition.label}</Label>
<div className="max-h-32 overflow-y-auto space-y-1">
{definition.options?.map((opt) => {
{definition.options.map((opt) => {
const selected = Array.isArray(value) ? value.includes(opt.value) : false;
return (
<div key={opt.value} className="flex items-center gap-2">
@@ -243,6 +252,7 @@ function FilterField({
</div>
</div>
);
}
case 'boolean':
return (